diff --git a/FCHost/fchost/fchost_storage_js.cc b/FCHost/fchost/fchost_storage_js.cc index a61c5cb204b4a0eaf8c06c351009a8df18a86ab3..19f5c46c9fbb82d258a40a7d008bd0a8f0f477e5 100644 --- a/FCHost/fchost/fchost_storage_js.cc +++ b/FCHost/fchost/fchost_storage_js.cc @@ -52,7 +52,7 @@ bool FCHostStorageHandler::Execute(const CefString& name, CefRefPtr<CefV8Value> if (name == "size") { // no arguments - retval = CefV8Value::CreateInt(static_cast<int32>(storage->size())); + retval = CefV8Value::CreateInt(static_cast<int32_t>(storage->size())); return true; } else if (name == "keys") { diff --git a/FCHost/fchost/utility.cc b/FCHost/fchost/utility.cc index ff7c7cde3a2bfdb3022b2b2dc0db2b79659a9bd4..ed1542045a66169f26b44bd1db10fed4f243cedf 100644 --- a/FCHost/fchost/utility.cc +++ b/FCHost/fchost/utility.cc @@ -9,7 +9,7 @@ void cef_string_from_path(const std::filesystem::path& p, cef_string_t* str) { const auto& pstr = p.native(); #if defined(OS_WIN) - cef_string_from_utf16(pstr.c_str(), pstr.size(), str); + cef_string_from_wide(pstr.c_str(), pstr.size(), str); #else cef_string_from_utf8(pstr.c_str(), pstr.size(), str); #endif diff --git a/css/art/genAI.css b/css/art/genAI.css new file mode 100644 index 0000000000000000000000000000000000000000..5e10009b5f18d3e53d59b16bd05bb2a83947c4e5 --- /dev/null +++ b/css/art/genAI.css @@ -0,0 +1,43 @@ +.ai-art-image { + transition: filter 0.5s ease-in-out; + position: relative; +} + +.ai-art-container.refreshing .ai-art-image { + filter: blur(5px); +} + +.spinner { + display: none; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-size: 30px; + animation: spin 2s linear infinite; +} + +.ai-art-container { + width: 100%; + height: 100%; + min-width: 100px; + min-height: 100px; + cursor: pointer; + float: right; + border: 3px hidden; + object-fit: contain; +} + +.ai-art-container.refreshing .spinner { + display: block; +} + +@keyframes spin { + 0% { + transform: translate(-50%, -50%) rotate(0deg); + } + + 100% { + transform: translate(-50%, -50%) rotate(360deg); + } +} \ No newline at end of file diff --git a/js/002-config/fc-js-init.js b/js/002-config/fc-js-init.js index 2197e4b88e3b6767ff96bfc1948553b6baf452ee..5447e561135e706ad636b0143bc98b3ce88e00df 100644 --- a/js/002-config/fc-js-init.js +++ b/js/002-config/fc-js-init.js @@ -2,7 +2,7 @@ // @ts-ignore "use strict"; -var App = { }; // eslint-disable-line no-redeclare +var App = {}; // eslint-disable-line no-redeclare // When adding namespace declarations, please consider needs of those using VSCode: // when you declare App.A{ A1:{}, A2:{} }, VSCode considers A, A1, and A2 to be @@ -15,6 +15,7 @@ var App = { }; // eslint-disable-line no-redeclare App.Arcology = {}; App.Arcology.Cell = {}; App.Art = {}; +App.Art.GenAI = {}; App.Budget = {}; App.Corporate = {}; App.Corporate.Division = {}; diff --git a/js/003-data/gameVariableData.js b/js/003-data/gameVariableData.js index 35a419d76d0291b8ba79dde3b7ca282fe96c25c4..f6eb669777e16add6d9fe706434802f043b1042b 100644 --- a/js/003-data/gameVariableData.js +++ b/js/003-data/gameVariableData.js @@ -94,11 +94,12 @@ App.Data.defaultGameStateVariables = { /** * | ***Value*** | **Description** | * |------------:|:-----------------------------| - * | *1* | NoX/Deepmurk's vector art | + * | *1* | NoX/Deepmurk's vector art | * | *2* | Non-embedded vector art | * | *3* | Revamped embedded vector art | * | *4* | Elohiem's WebGL | * | *5* | Shokushu's rendered | + * | *6* | Anon's AI image generation | */ imageChoice: 1, inbreeding: 1, @@ -168,6 +169,21 @@ App.Data.defaultGameStateVariables = { set3QView: false, seeAnimation: false, animFPS: 12, + + // Stable Diffusion settings + aiApiUrl: "http://localhost:7860", + aiCfgScale: 5, + aiCustomStyleNeg: "", + aiCustomStylePos: "", + aiHeight: 768, + aiSamplingMethod: "DPM++ 2M SDE Karras", + aiSamplingSteps: 20, + aiStyle: 1, + aiUpscale: true, + aiUpscaleScale: 1.7, + aiUpscaler: "SwinIR_4x", + aiWidth: 512, + showAgeDetail: 1, showAppraisal: 1, showAssignToScenes: 1, @@ -518,7 +534,7 @@ App.Data.resetOnNGPlus = { defaultRules: [], /** @type {Object.<string, number[]>} */ rulesToApplyOnce: {}, - raDefaultMode : 0, + raDefaultMode: 0, RECheckInIDs: [], @@ -1015,7 +1031,9 @@ App.Data.resetOnNGPlus = { /** @type {FC.SlaveStateOrZero} */ hostageWife: 0, /** @type {FC.Rival} */ - rival: {state: 0, duration: 0, prosperity: 0, power: 0, FS: {name: ""}, hostageState: 0}, + rival: { + state: 0, duration: 0, prosperity: 0, power: 0, FS: {name: ""}, hostageState: 0 + }, nationHate: 0, eventResults: {}, diff --git a/js/medicine/surgery/hair/restoreHairPits.js b/js/medicine/surgery/hair/restoreHairPits.js index e3109cb9f121c89c493e3b5036545a7d1e76ac0f..769490ea8a9dfb5b555779f857821b52d147ba9b 100644 --- a/js/medicine/surgery/hair/restoreHairPits.js +++ b/js/medicine/surgery/hair/restoreHairPits.js @@ -11,7 +11,7 @@ App.Medicine.Surgery.Reactions.RestoreHairPits = class extends App.Medicine.Surg r.push(`${He} awakens from surgery to an unfamiliar, rather irritating, itch`); if (hasAnyArms(slave)) { - r.push(`under ${his} ${(hasBothArms(slave)) ? `arms` : `arm`}`); + r.push(`under ${his} ${(hasBothArms(slave)) ? `arms` : `arm`}.`); } else { r.push(`below where ${his} arms used to be.`); } diff --git a/src/002-config/fc-version.js b/src/002-config/fc-version.js index 21099a2a89cdfa0edabb61588649fa6e2d0ed7fa..08c9460424955d2669946b7ea7cfed4d637b5dc1 100644 --- a/src/002-config/fc-version.js +++ b/src/002-config/fc-version.js @@ -2,5 +2,5 @@ App.Version = { base: "0.10.7.1", // The vanilla version the mod is based off of, this should never be changed. pmod: "4.0.0-alpha.26", commitHash: null, - release: 1200, // When getting close to 2000, please remove the check located within the onLoad() function defined at line five of src/js/eventHandlers.js. + release: 1203, // When getting close to 2000, please remove the check located within the onLoad() function defined at line five of src/js/eventHandlers.js. }; diff --git a/src/004-base/basePrompt.js b/src/004-base/basePrompt.js new file mode 100644 index 0000000000000000000000000000000000000000..55bb617dd122f7481a085ab6996579c804022b3d --- /dev/null +++ b/src/004-base/basePrompt.js @@ -0,0 +1,52 @@ +/** base class for prompt parts */ +App.Art.GenAI.PromptPart = class PromptPart { + /** + * @param {FC.SlaveState} slave + */ + constructor(slave) { + this.slave = slave; + } + + /** + * @returns {string} + * @abstract + */ + positive() { + throw new Error("not implemented"); + } + + /** + * @returns {string} + * @abstract + */ + negative() { + throw new Error("not implemented"); + } +}; + +App.Art.GenAI.Prompt = class Prompt { + /** + * @param {App.Art.GenAI.PromptPart[]} parts + */ + constructor(parts) { + this.parts = parts; + } + + /** + * @returns {string} + */ + positive() { + let parts = this.parts.map(part => part.positive()); + parts = parts.filter(part => part); + return parts.join(", "); + } + + /** + * @returns {string} + */ + negative() { + let parts = this.parts.map(part => part.negative()); + parts = parts.filter(part => part); + return parts.join(", "); + } +}; diff --git a/src/Mods/Catmod/events/nonRandom/projectNComplete.js b/src/Mods/Catmod/events/nonRandom/projectNComplete.js index 2c0f0ace406cb01adafe2bf8f3ae22b12d8e021b..220db4602bc3040c7f897668d31a84fbe74630af 100644 --- a/src/Mods/Catmod/events/nonRandom/projectNComplete.js +++ b/src/Mods/Catmod/events/nonRandom/projectNComplete.js @@ -22,6 +22,7 @@ App.Events.SEProjectNComplete = class SEProjectNComplete extends App.Events.Base slave.origSkin = "pure white"; slave.override_Skin = 1; // TODO: Identifier 'override_Skin' is not in camel case slave.boobs = 300; + slave.natural.boobs = 300; slave.earTColor = slave.hColor; slave.tailColor = slave.hColor; diff --git a/src/Mods/SecExp/buildings/riotControlCenter.js b/src/Mods/SecExp/buildings/riotControlCenter.js index 8c15159d4104c08fb4c87315e43145a5801f8eab..48f95762ceec62e2bccafb9ef92f9bcc6b902794 100644 --- a/src/Mods/SecExp/buildings/riotControlCenter.js +++ b/src/Mods/SecExp/buildings/riotControlCenter.js @@ -237,7 +237,7 @@ App.Mods.SecExp.riotCenter = (function() { `Deploy the unit against ${type} rebel leaders`, () => { const price = forceNeg(1000 + 50 * V.SecExp.buildings.riotCenter.upgrades.rapidUnit); - (function() { cost === "reputation" ? repX(price, "war") : App.Mods.SecExp.authorityX(-price); })(); + (function() { cost === "reputation" ? repX(price, "war") : App.Mods.SecExp.authorityX(price); })(); const change = random(15) + random(1, 2) * V.SecExp.buildings.riotCenter.upgrades.rapidUnit; V.SecExp.rebellions[type + "Progress"] = Math.clamp(V.SecExp.rebellions[type + "Progress"] - change, 0, 100); V.SecExp.buildings.riotCenter.sentUnitCooldown = 3 - V.SecExp.buildings.riotCenter.upgrades.rapidUnitSpeed; diff --git a/src/Mods/SecExp/events/conflictHandler.js b/src/Mods/SecExp/events/conflictHandler.js index 03e51e256cb40406221b6f2efa86d18f0ab4c68a..2ddcce3e26ad11b5a7e5ce6d6e962c6589ba5a96 100644 --- a/src/Mods/SecExp/events/conflictHandler.js +++ b/src/Mods/SecExp/events/conflictHandler.js @@ -1,41 +1,57 @@ -App.Events.conflictHandler = function() { - V.nextButton = " "; - App.UI.StoryCaption.encyclopedia = "Battles"; +/* global V, Engine, num, random */ +App.Events.conflictHandler = function () { + V.nextButton = ' '; + App.UI.StoryCaption.encyclopedia = 'Battles'; const node = new DocumentFragment(); const showStats = V.SecExp.settings.showStats === 1; - const inBattle = V.SecExp.war.type.includes("Attack"); - const isMajorBattle = inBattle && V.SecExp.war.type.includes("Major"); - const inRebellion = V.SecExp.war.type.includes("Rebellion"); - const turns = (isMajorBattle || inRebellion) ? 20 : 10; - const showProgress = function(message, tag = "div") { + const inBattle = V.SecExp.war.type.includes('Attack'); + const isMajorBattle = inBattle && V.SecExp.war.type.includes('Major'); + const inRebellion = V.SecExp.war.type.includes('Rebellion'); + const turns = isMajorBattle || inRebellion ? 20 : 10; + const showProgress = function (message, tag = 'div') { if (showStats) { App.UI.DOM.appendNewElement(tag, node, message); } }; - const setResult = function(varA, varB, text, value, count) { + const setResult = function (varA, varB, text, value, count) { if (varA <= 0 || varB <= 0) { showProgress(`${text}!`); V.SecExp.war.result = value; V.SecExp.war.turns = count; } }; - const atEnd = function(passage) { + const atEnd = function (passage) { if (showStats) { - App.UI.DOM.appendNewElement("div", node, App.UI.DOM.passageLink("Proceed", passage)); + App.UI.DOM.appendNewElement( + 'div', + node, + App.UI.DOM.passageLink('Proceed', passage), + ); } else { setTimeout(() => Engine.play(passage), Engine.minDomActionDelay); } }; - const turnReport = function() { + const turnReport = function () { showProgress(`Turn: ${i + 1}`); // player army attacks - damage = Math.round(Math.clamp(attack.total - enemyDefense, attack.total * 0.1, attack.total)); + damage = Math.round( + Math.clamp( + attack.total - enemyDefense, + attack.total * 0.1, + attack.total, + ), + ); showProgress(`Player damage: ${num(Math.round(damage))}`); enemyHp -= damage; showProgress(`Remaining enemy Hp: ${num(Math.round(enemyHp))}`); V.SecExp.war.attacker.losses += damage / enemyBaseHp; - enemyMorale -= Math.clamp(damage / 2 + damage / enemyBaseHp, 0, damage * 1.5); + enemyMorale -= Math.clamp( + damage / 2 + + damage / enemyBaseHp, + 0, + damage * 1.5, + ); showProgress(`Remaining enemy morale: ${num(Math.round(enemyMorale))}`); setResult(enemyHp, enemyMorale, 'Victory', 3, i); @@ -46,34 +62,40 @@ App.Events.conflictHandler = function() { } damage = Math.round(damage); showProgress(`Enemy damage: ${num(Math.round(damage))}`); - hp.total -= damage * (inRebellion && V.SecExp.rebellions.sfArmor ? 0.85 : 1); + hp.total + -= damage * (inRebellion && V.SecExp.rebellions.sfArmor ? 0.85 : 1); showProgress(`Remaining hp: ${num(Math.round(hp.total))}`); V.SecExp.war.losses += damage / baseHp; - morale.total -= Math.clamp(damage / 2 + damage / baseHp, 0, damage * 1.5); + morale.total -= Math.clamp( + damage / 2 + + damage / baseHp, + 0, + damage * 1.5, + ); showProgress(`Remaining morale: ${num(Math.round(morale.total))}`); setResult(hp.total, morale.total, 'Defeat', -3, i); }; + const attack = { base: 0, modifier: 1 }; + const defense = { base: 0, modifier: 1 }; + const morale = { base: 0 }; + const hp = { base: 0 }; let unitData; let damage; let baseHp; let enemyBaseHp; let enemyMorale; - let attack = {base: 0, modifier: 1}; - let defense = {base: 0, modifier: 1}; - let morale = {base: 0}; - let hp = {base: 0}; let enemyAttack = 0; let enemyDefense = 0; let enemyHp = 0; let armyMod = V.SecExp.war.attacker.troops / (inBattle ? 80 : 100); const activeSF = V.SF.Toggle && V.SF.Active >= 1; // Battles - morale.militia = (isMajorBattle) ? 1.5 : 1; - morale.slaves = (isMajorBattle) ? 1.5 : 1; - morale.mercs = (isMajorBattle) ? 1.5 : 1; - morale.enemy = (isMajorBattle) ? 1.5 : 1; - morale.SF = (isMajorBattle) ? 1.5 : 1; + morale.militia = isMajorBattle ? 1.5 : 1; + morale.slaves = isMajorBattle ? 1.5 : 1; + morale.mercs = isMajorBattle ? 1.5 : 1; + morale.enemy = isMajorBattle ? 1.5 : 1; + morale.SF = isMajorBattle ? 1.5 : 1; let tacChance = 0.5; // by default tactics have a 50% chance of succeeding // Rebellions let irregularMod = V.SecExp.war.irregulars / 60; @@ -81,11 +103,19 @@ App.Events.conflictHandler = function() { let rebellingSlaves = 0; let rebellingMilitia = 0; - if (inBattle && V.SecExp.war.result === 1 || V.SecExp.war.result === -1) { // bribery/surrender check + if (inBattle && (V.SecExp.war.result === 1 || V.SecExp.war.result === -1)) { + // bribery/surrender check showProgress(`${V.SecExp.war.result === 1 ? 'Bribery' : 'Surrender'} chosen`); if (inBattle && V.SecExp.war.result === 1) { - if (V.cash >= App.Mods.SecExp.battle.bribeCost()) { // if there's enough cash there's a 10% chance bribery fails. If there isn't there's instead a 50% chance it fails - if (V.SecExp.war.attacker.type === "freedom fighters" && random(1, 100) <= 50 || random(1, 100) <= 10) { + if (V.cash >= App.Mods.SecExp.battle.bribeCost()) { + // if there's enough cash there's a 10% chance bribery fails. If there isn't there's instead a 50% chance it fails + if ( + ( + V.SecExp.war.attacker.type === 'freedom fighters' + && random(1, 100) <= 50 + ) + || random(1, 100) <= 10 + ) { V.SecExp.war.result = 0; } } else { @@ -93,8 +123,8 @@ App.Events.conflictHandler = function() { V.SecExp.war.result = 0; } } - showProgress(`${V.SecExp.war.result === 0 ? 'Failed' : 'Successful'}!`, "span"); - atEnd("conflictReport"); + showProgress(`${V.SecExp.war.result === 0 ? 'Failed' : 'Successful'}!`, 'span'); + atEnd('conflictReport'); return node; } } @@ -120,7 +150,7 @@ App.Events.conflictHandler = function() { } } - const commanderEffectiveness = App.Mods.SecExp.commanderEffectiveness("handler"); + const commanderEffectiveness = App.Mods.SecExp.commanderEffectiveness('handler'); morale.slaves += commanderEffectiveness.slaveMod; morale.militia += commanderEffectiveness.militiaMod; morale.mercs += commanderEffectiveness.militiaMod; @@ -136,115 +166,115 @@ App.Events.conflictHandler = function() { defense.modifier += tacticsObj.defMod; tacChance += tacticsObj.tacChance; - if (V.SecExp.war.chosenTactic === "Bait and Bleed") { - if (V.SecExp.war.attacker.type === "raiders") { - tacChance -= 0.10; - } else if (V.SecExp.war.attacker.type === "free city") { - tacChance += 0.10; - } else if (V.SecExp.war.attacker.type === "old world") { + if (V.SecExp.war.chosenTactic === 'Bait and Bleed') { + if (V.SecExp.war.attacker.type === 'raiders') { + tacChance -= 0.1; + } else if (V.SecExp.war.attacker.type === 'free city') { + tacChance += 0.1; + } else if (V.SecExp.war.attacker.type === 'old world') { tacChance += 0.25; - } else if (V.SecExp.war.attacker.type === "freedom fighters") { + } else if (V.SecExp.war.attacker.type === 'freedom fighters') { tacChance -= 0.15; } - } else if (V.SecExp.war.chosenTactic === "Guerrilla") { - if (V.SecExp.war.attacker.type === "raiders") { - tacChance -= 0.20; - } else if (V.SecExp.war.attacker.type === "free city") { + } else if (V.SecExp.war.chosenTactic === 'Guerrilla') { + if (V.SecExp.war.attacker.type === 'raiders') { + tacChance -= 0.2; + } else if (V.SecExp.war.attacker.type === 'free city') { tacChance += 0.15; - } else if (V.SecExp.war.attacker.type === "old world") { + } else if (V.SecExp.war.attacker.type === 'old world') { tacChance += 0.25; - } else if (V.SecExp.war.attacker.type === "freedom fighters") { + } else if (V.SecExp.war.attacker.type === 'freedom fighters') { tacChance -= 0.25; } - } else if (V.SecExp.war.chosenTactic === "Choke Points") { - if (V.SecExp.war.attacker.type === "raiders") { + } else if (V.SecExp.war.chosenTactic === 'Choke Points') { + if (V.SecExp.war.attacker.type === 'raiders') { tacChance += 0.25; - } else if (V.SecExp.war.attacker.type === "free city") { + } else if (V.SecExp.war.attacker.type === 'free city') { tacChance -= 0.05; - } else if (V.SecExp.war.attacker.type === "old world") { - tacChance -= 0.10; - } else if (V.SecExp.war.attacker.type === "freedom fighters") { + } else if (V.SecExp.war.attacker.type === 'old world') { + tacChance -= 0.1; + } else if (V.SecExp.war.attacker.type === 'freedom fighters') { tacChance += 0.05; } - } else if (V.SecExp.war.chosenTactic === "Interior Lines") { - if (V.SecExp.war.attacker.type === "raiders") { + } else if (V.SecExp.war.chosenTactic === 'Interior Lines') { + if (V.SecExp.war.attacker.type === 'raiders') { tacChance -= 0.15; - } else if (V.SecExp.war.attacker.type === "free city") { + } else if (V.SecExp.war.attacker.type === 'free city') { tacChance += 0.15; - } else if (V.SecExp.war.attacker.type === "old world") { - tacChance += 0.20; - } else if (V.SecExp.war.attacker.type === "freedom fighters") { - tacChance -= 0.10; + } else if (V.SecExp.war.attacker.type === 'old world') { + tacChance += 0.2; + } else if (V.SecExp.war.attacker.type === 'freedom fighters') { + tacChance -= 0.1; } - } else if (V.SecExp.war.chosenTactic === "Pincer Maneuver") { - if (V.SecExp.war.attacker.type === "raiders") { + } else if (V.SecExp.war.chosenTactic === 'Pincer Maneuver') { + if (V.SecExp.war.attacker.type === 'raiders') { tacChance += 0.15; - } else if (V.SecExp.war.attacker.type === "free city") { - tacChance += 0.10; - } else if (V.SecExp.war.attacker.type === "old world") { - tacChance -= 0.10; - } else if (V.SecExp.war.attacker.type === "freedom fighters") { + } else if (V.SecExp.war.attacker.type === 'free city') { + tacChance += 0.1; + } else if (V.SecExp.war.attacker.type === 'old world') { + tacChance -= 0.1; + } else if (V.SecExp.war.attacker.type === 'freedom fighters') { tacChance += 0.15; } - } else if (V.SecExp.war.chosenTactic === "Defense In Depth") { - if (V.SecExp.war.attacker.type === "raiders") { - tacChance -= 0.20; - } else if (V.SecExp.war.attacker.type === "free city") { - tacChance += 0.10; - } else if (V.SecExp.war.attacker.type === "old world") { - tacChance += 0.20; - } else if (V.SecExp.war.attacker.type === "freedom fighters") { + } else if (V.SecExp.war.chosenTactic === 'Defense In Depth') { + if (V.SecExp.war.attacker.type === 'raiders') { + tacChance -= 0.2; + } else if (V.SecExp.war.attacker.type === 'free city') { + tacChance += 0.1; + } else if (V.SecExp.war.attacker.type === 'old world') { + tacChance += 0.2; + } else if (V.SecExp.war.attacker.type === 'freedom fighters') { tacChance -= 0.05; } - } else if (V.SecExp.war.chosenTactic === "Blitzkrieg") { - if (V.SecExp.war.attacker.type === "raiders") { - tacChance += 0.10; - } else if (V.SecExp.war.attacker.type === "free city") { - tacChance -= 0.20; - } else if (V.SecExp.war.attacker.type === "old world") { + } else if (V.SecExp.war.chosenTactic === 'Blitzkrieg') { + if (V.SecExp.war.attacker.type === 'raiders') { + tacChance += 0.1; + } else if (V.SecExp.war.attacker.type === 'free city') { + tacChance -= 0.2; + } else if (V.SecExp.war.attacker.type === 'old world') { tacChance += 0.25; - } else if (V.SecExp.war.attacker.type === "freedom fighters") { - tacChance -= 0.10; + } else if (V.SecExp.war.attacker.type === 'freedom fighters') { + tacChance -= 0.1; } - } else if (V.SecExp.war.chosenTactic === "Human Wave") { - if (V.SecExp.war.attacker.type === "raiders") { - tacChance -= 0.10; - } else if (V.SecExp.war.attacker.type === "free city") { - tacChance += 0.10; - } else if (V.SecExp.war.attacker.type === "old world") { + } else if (V.SecExp.war.chosenTactic === 'Human Wave') { + if (V.SecExp.war.attacker.type === 'raiders') { + tacChance -= 0.1; + } else if (V.SecExp.war.attacker.type === 'free city') { + tacChance += 0.1; + } else if (V.SecExp.war.attacker.type === 'old world') { tacChance -= 0.15; - } else if (V.SecExp.war.attacker.type === "freedom fighters") { - tacChance += 0.10; + } else if (V.SecExp.war.attacker.type === 'freedom fighters') { + tacChance += 0.1; } } tacChance = Math.clamp(tacChance, 0.1, tacChance); // Calculates if tactics are successful - minimum chance is 10% if (random(1, 100) <= tacChance * 100) { - morale.enemy -= 0.30; - morale.militia += 0.20; - morale.slaves += 0.20; - morale.mercs += 0.20; - attack.modifier += 0.10; - defense.modifier += 0.10; + morale.enemy -= 0.3; + morale.militia += 0.2; + morale.slaves += 0.2; + morale.mercs += 0.2; + attack.modifier += 0.1; + defense.modifier += 0.1; V.SecExp.war.tacticsSuccessful = 1; } else { - morale.enemy += 0.20; - morale.militia -= 0.20; - morale.slaves -= 0.20; - morale.mercs -= 0.20; - attack.modifier -= 0.10; - defense.modifier -= 0.10; + morale.enemy += 0.2; + morale.militia -= 0.2; + morale.slaves -= 0.2; + morale.mercs -= 0.2; + attack.modifier -= 0.1; + defense.modifier -= 0.1; } // enemy morale mods if (V.week < 30) { morale.enemy += 0.15; } else if (V.week < 60) { - morale.enemy += 0.30; + morale.enemy += 0.3; } else if (V.week < 90) { morale.enemy += 0.45; } else if (V.week < 120) { - morale.enemy += 0.60; + morale.enemy += 0.6; } else { morale.enemy += 0.75; } @@ -271,9 +301,13 @@ App.Events.conflictHandler = function() { } if (V.SecExp.war.irregulars > 0) { irregularMod = Math.trunc(irregularMod); - unitData = App.Mods.SecExp.getIrregularUnit("militia", V.SecExp.war.irregulars, V.SecExp.war.attacker.equip); - attack.irregulars = unitData.attack * irregularMod * 0.80; - defense.irregulars = unitData.defense * irregularMod * 0.80; + unitData = App.Mods.SecExp.getIrregularUnit( + 'militia', + V.SecExp.war.irregulars, + V.SecExp.war.attacker.equip, + ); + attack.irregulars = unitData.attack * irregularMod * 0.8; + defense.irregulars = unitData.defense * irregularMod * 0.8; hp.irregulars = unitData.hp; } } @@ -282,23 +316,23 @@ App.Events.conflictHandler = function() { for (const unit of V.SecExp.units[type].squads) { if (App.Mods.SecExp.unit.isDeployed(unit)) { unitData = App.Mods.SecExp.getUnit(type, unit); - attack.base += unitData.attack * unitData.attack; - defense.base += unitData.defense * unitData.defense; + attack.base += unitData.attack; + defense.base += unitData.defense; hp.base += unitData.hp; } } } - if (activeSF && (inBattle && V.SecExp.war.deploySF || inRebellion)) { - unitData = App.Mods.SecExp.getUnit("SF"); + if (activeSF && ((inBattle && V.SecExp.war.deploySF) || inRebellion)) { + unitData = App.Mods.SecExp.getUnit('SF'); attack.SF = unitData.attack; defense.SF = unitData.defense; hp.SF = unitData.hp; } if (inRebellion) { - for (const zone of ["assistant", "reactor", "penthouse", "waterway"]) { - if (V.SecExp.war[zone + "Defense"]) { + for (const zone of ['assistant', 'reactor', 'penthouse', 'waterway']) { + if (V.SecExp.war[zone + 'Defense']) { attack.base *= 0.95; defense.base *= 0.95; hp.base *= 0.95; @@ -307,29 +341,118 @@ App.Events.conflictHandler = function() { } // morale and baseHp calculation - if (inBattle) { // minimum modifier is -50%, maximum is +50% + if (inBattle) { + // minimum modifier is -50%, maximum is +50% morale.slaves = Math.clamp(morale.slaves, 0.5, 1.5); morale.militia = Math.clamp(morale.militia, 0.5, 1.5); morale.mercs = Math.clamp(morale.mercs, 0.5, 1.5); morale.SF = Math.clamp(morale.SF, 0.5, 1.5); } - const moraleTroopMod = Math.clamp(App.Mods.SecExp.battle.troopCount() / 100, 1, (inBattle ? 5 : 10)); + const moraleTroopMod = Math.clamp( + App.Mods.SecExp.battle.troopCount() / 100, + 1, + inBattle ? 5 : 10, + ); const modifierSF = activeSF ? 1 : 0; if (inBattle) { - morale.total = (App.Mods.SecExp.BaseDroneUnit.morale * App.Mods.SecExp.battle.deployedUnits('bots') + App.Mods.SecExp.BaseMilitiaUnit.morale * morale.militia * App.Mods.SecExp.battle.deployedUnits('militia') + App.Mods.SecExp.BaseSlaveUnit.morale * morale.slaves * App.Mods.SecExp.battle.deployedUnits('slaves') + App.Mods.SecExp.BaseMercUnit.morale * morale.mercs * App.Mods.SecExp.battle.deployedUnits('mercs') + App.Mods.SecExp.BaseSpecialForcesUnit.morale * V.SecExp.war.deploySF * morale.SF) / (App.Mods.SecExp.battle.deployedUnits('bots') + App.Mods.SecExp.battle.deployedUnits('militia') + App.Mods.SecExp.battle.deployedUnits('slaves') + App.Mods.SecExp.battle.deployedUnits('mercs') + V.SecExp.war.deploySF); + morale.total + = ( + App.Mods.SecExp.BaseDroneUnit.morale + * App.Mods.SecExp.battle.deployedUnits('bots') + + App.Mods.SecExp.BaseMilitiaUnit.morale + * morale.militia + * App.Mods.SecExp.battle.deployedUnits('militia') + + App.Mods.SecExp.BaseSlaveUnit.morale + * morale.slaves + * App.Mods.SecExp.battle.deployedUnits('slaves') + + App.Mods.SecExp.BaseMercUnit.morale + * morale.mercs + * App.Mods.SecExp.battle.deployedUnits('mercs') + + App.Mods.SecExp.BaseSpecialForcesUnit.morale + * V.SecExp.war.deploySF + * morale.SF + ) + / ( + App.Mods.SecExp.battle.deployedUnits('bots') + + App.Mods.SecExp.battle.deployedUnits('militia') + + App.Mods.SecExp.battle.deployedUnits('slaves') + + App.Mods.SecExp.battle.deployedUnits('mercs') + + V.SecExp.war.deploySF + ); if (V.SecExp.buildings.barracks) { - morale.total += morale.total * V.SecExp.buildings.barracks.luxury * 0.05; // barracks bonus + morale.total += morale.total * V.SecExp.buildings.barracks.luxury * 0.05; // barracks bonus } } else { - morale.total = (App.Mods.SecExp.BaseDroneUnit.morale * App.Mods.SecExp.battle.deployedUnits('bots') + App.Mods.SecExp.BaseMilitiaUnit.morale * App.Mods.SecExp.battle.deployedUnits('militia') + App.Mods.SecExp.BaseSlaveUnit.morale * App.Mods.SecExp.battle.deployedUnits('slaves') + App.Mods.SecExp.BaseMercUnit.morale * App.Mods.SecExp.battle.deployedUnits('mercs') + App.Mods.SecExp.BaseSpecialForcesUnit.morale * modifierSF) / (App.Mods.SecExp.battle.deployedUnits('bots') + App.Mods.SecExp.battle.deployedUnits('militia') + App.Mods.SecExp.battle.deployedUnits('slaves') + App.Mods.SecExp.battle.deployedUnits('mercs') + modifierSF); - morale.total += morale.total * (V.SecExp.buildings.barracks ? V.SecExp.buildings.barracks.luxury * 0.05 : 1); // barracks bonus + morale.total + = ( + App.Mods.SecExp.BaseDroneUnit.morale + * App.Mods.SecExp.battle.deployedUnits('bots') + + App.Mods.SecExp.BaseMilitiaUnit.morale + * App.Mods.SecExp.battle.deployedUnits('militia') + + App.Mods.SecExp.BaseSlaveUnit.morale + * App.Mods.SecExp.battle.deployedUnits('slaves') + + App.Mods.SecExp.BaseMercUnit.morale + * App.Mods.SecExp.battle.deployedUnits('mercs') + + App.Mods.SecExp.BaseSpecialForcesUnit.morale + * modifierSF + ) + / ( + App.Mods.SecExp.battle.deployedUnits('bots') + + App.Mods.SecExp.battle.deployedUnits('militia') + + App.Mods.SecExp.battle.deployedUnits('slaves') + + App.Mods.SecExp.battle.deployedUnits('mercs') + + modifierSF + ); + morale.total + += morale.total + * (V.SecExp.buildings.barracks + ? V.SecExp.buildings.barracks.luxury * 0.05 + : 1); // barracks bonus } morale.total *= moraleTroopMod; if (inBattle) { - baseHp = (App.Mods.SecExp.BaseDroneUnit.hp * App.Mods.SecExp.battle.deployedUnits('bots') + App.Mods.SecExp.BaseMilitiaUnit.hp * App.Mods.SecExp.battle.deployedUnits('militia') + App.Mods.SecExp.BaseSlaveUnit.hp * App.Mods.SecExp.battle.deployedUnits('slaves') + App.Mods.SecExp.BaseMercUnit.hp * App.Mods.SecExp.battle.deployedUnits('mercs') + App.Mods.SecExp.BaseSpecialForcesUnit.hp * V.SecExp.war.deploySF) / (App.Mods.SecExp.battle.deployedUnits('bots') + App.Mods.SecExp.battle.deployedUnits('militia') + App.Mods.SecExp.battle.deployedUnits('slaves') + App.Mods.SecExp.battle.deployedUnits('mercs') + V.SecExp.war.deploySF); + baseHp + = ( + App.Mods.SecExp.BaseDroneUnit.hp + * App.Mods.SecExp.battle.deployedUnits('bots') + + App.Mods.SecExp.BaseMilitiaUnit.hp + * App.Mods.SecExp.battle.deployedUnits('militia') + + App.Mods.SecExp.BaseSlaveUnit.hp + * App.Mods.SecExp.battle.deployedUnits('slaves') + + App.Mods.SecExp.BaseMercUnit.hp + * App.Mods.SecExp.battle.deployedUnits('mercs') + + App.Mods.SecExp.BaseSpecialForcesUnit.hp + * V.SecExp.war.deploySF + ) + / ( + App.Mods.SecExp.battle.deployedUnits('bots') + + App.Mods.SecExp.battle.deployedUnits('militia') + + App.Mods.SecExp.battle.deployedUnits('slaves') + + App.Mods.SecExp.battle.deployedUnits('mercs') + + V.SecExp.war.deploySF + ); } else { - baseHp = (App.Mods.SecExp.BaseDroneUnit.hp * App.Mods.SecExp.battle.deployedUnits('bots') + App.Mods.SecExp.BaseMilitiaUnit.hp * App.Mods.SecExp.battle.deployedUnits('militia') + App.Mods.SecExp.BaseSlaveUnit.hp * App.Mods.SecExp.battle.deployedUnits('slaves') + App.Mods.SecExp.BaseMercUnit.hp * App.Mods.SecExp.battle.deployedUnits('mercs') + App.Mods.SecExp.BaseSpecialForcesUnit.hp * modifierSF) / (App.Mods.SecExp.battle.deployedUnits('bots') + App.Mods.SecExp.battle.deployedUnits('militia') + App.Mods.SecExp.battle.deployedUnits('slaves') + App.Mods.SecExp.battle.deployedUnits('mercs') + modifierSF); + baseHp + = ( + App.Mods.SecExp.BaseDroneUnit.hp + * App.Mods.SecExp.battle.deployedUnits('bots') + + App.Mods.SecExp.BaseMilitiaUnit.hp + * App.Mods.SecExp.battle.deployedUnits('militia') + + App.Mods.SecExp.BaseSlaveUnit.hp + * App.Mods.SecExp.battle.deployedUnits('slaves') + + App.Mods.SecExp.BaseMercUnit.hp + * App.Mods.SecExp.battle.deployedUnits('mercs') + + App.Mods.SecExp.BaseSpecialForcesUnit.hp + * modifierSF + ) + / ( + App.Mods.SecExp.battle.deployedUnits('bots') + + App.Mods.SecExp.battle.deployedUnits('militia') + + App.Mods.SecExp.battle.deployedUnits('slaves') + + App.Mods.SecExp.battle.deployedUnits('mercs') + + modifierSF + ); } // calculates opposing army stats @@ -351,19 +474,30 @@ App.Events.conflictHandler = function() { } if (inRebellion) { - if (V.SecExp.war.type.includes("Slave")) { + if (V.SecExp.war.type.includes('Slave')) { rebellingSlaves = 1; - unitData = App.Mods.SecExp.getIrregularUnit("slaves", V.SecExp.war.attacker.troops, V.SecExp.war.attacker.equip); + unitData = App.Mods.SecExp.getIrregularUnit( + 'slaves', + V.SecExp.war.attacker.troops, + V.SecExp.war.attacker.equip, + ); } else { rebellingMilitia = 1; - unitData = App.Mods.SecExp.getIrregularUnit("militia", V.SecExp.war.attacker.troops, V.SecExp.war.attacker.equip); + unitData = App.Mods.SecExp.getIrregularUnit( + 'militia', + V.SecExp.war.attacker.troops, + V.SecExp.war.attacker.equip, + ); } enemyAttack += unitData.attack * armyMod; enemyDefense += unitData.defense * armyMod; enemyHp += unitData.hp; for (const [type] of Array.from(App.Mods.SecExp.unit.list()).slice(1)) { - if (rebellingSlaves === 1 && type === "slaves" || rebellingMilitia === 1 && type === "militia") { + if ( + (rebellingSlaves === 1 && type === 'slaves') + || (rebellingMilitia === 1 && type === 'militia') + ) { for (const unit of V.SecExp.units[type].squads) { if (V.SecExp.war.rebellingID.includes(unit.ID)) { V.SecExp.war.attacker.troops += unit.troops; @@ -379,18 +513,43 @@ App.Events.conflictHandler = function() { } // calculates opposing army stats - const enemyMoraleTroopMod = Math.clamp(V.SecExp.war.attacker.troops / 100, 1, (inBattle ? 5 : 10)); + const enemyMoraleTroopMod = Math.clamp( + V.SecExp.war.attacker.troops / 100, + 1, + inBattle ? 5 : 10, + ); if (inBattle) { - unitData = App.Mods.SecExp.getEnemyUnit(V.SecExp.war.attacker.type, V.SecExp.war.attacker.troops, V.SecExp.war.attacker.equip); + unitData = App.Mods.SecExp.getEnemyUnit( + V.SecExp.war.attacker.type, + V.SecExp.war.attacker.troops, + V.SecExp.war.attacker.equip, + ); enemyAttack = unitData.attack * armyMod; enemyDefense = unitData.defense * armyMod; enemyMorale = unitData.morale * morale.enemy * enemyMoraleTroopMod; enemyHp = unitData.hp; enemyBaseHp = unitData.hp / V.SecExp.war.attacker.troops; } else { - enemyMorale = 1.5 * (App.Mods.SecExp.BaseMilitiaUnit.morale * rebellingMilitia + App.Mods.SecExp.BaseSlaveUnit.morale * rebellingSlaves) / (rebellingMilitia + rebellingSlaves); + enemyMorale + = ( + 1.5 + * ( + App.Mods.SecExp.BaseMilitiaUnit.morale + * rebellingMilitia + + App.Mods.SecExp.BaseSlaveUnit.morale + * rebellingSlaves + ) + ) + / (rebellingMilitia + rebellingSlaves); enemyMorale *= enemyMoraleTroopMod; - enemyBaseHp = (App.Mods.SecExp.BaseMilitiaUnit.hp * rebellingMilitia + App.Mods.SecExp.BaseSlaveUnit.hp * rebellingSlaves) / (rebellingMilitia + rebellingSlaves); + enemyBaseHp + = ( + App.Mods.SecExp.BaseMilitiaUnit.hp + * rebellingMilitia + + App.Mods.SecExp.BaseSlaveUnit.hp + * rebellingSlaves + ) + / (rebellingMilitia + rebellingSlaves); } enemyMorale = Math.round(enemyMorale); @@ -399,9 +558,21 @@ App.Events.conflictHandler = function() { hp.total = Math.round(Object.values(hp).reduce((a, b) => a + b)); morale.total = Math.round(Object.values(hp).reduce((a, b) => a + b)); - for (const variable of [attack.total, defense.total, hp.total, morale.total, enemyAttack, enemyDefense, enemyHp, enemyMorale, enemyBaseHp]) { + for (const variable of [ + attack.total, + defense.total, + hp.total, + morale.total, + enemyAttack, + enemyDefense, + enemyHp, + enemyMorale, + enemyBaseHp, + ]) { if (isNaN(variable)) { - throw Error(`Value: A key variable is NaN, please report this along with an affected save.`); + throw Error( + 'Value: A key variable is NaN, please report this along with an affected save.', + ); } } @@ -413,82 +584,159 @@ App.Events.conflictHandler = function() { if (showStats) { if (inBattle) { - attack.modifier = Math.round((attack.modifier-1) * 100); - defense.modifier = Math.round((defense.modifier-1) * 100); - morale.militia = Math.round((morale.militia-1) * 100); - morale.mercs = Math.round((morale.mercs-1) * 100); - morale.slaves = Math.round((morale.slaves-1) * 100); - morale.SF = Math.round((morale.SF-1) * 100); - morale.enemy = Math.round((morale.enemy-1) * 100); + attack.modifier = Math.round((attack.modifier - 1) * 100); + defense.modifier = Math.round((defense.modifier - 1) * 100); + morale.militia = Math.round((morale.militia - 1) * 100); + morale.mercs = Math.round((morale.mercs - 1) * 100); + morale.slaves = Math.round((morale.slaves - 1) * 100); + morale.SF = Math.round((morale.SF - 1) * 100); + morale.enemy = Math.round((morale.enemy - 1) * 100); } else { - engageMod = Math.round((engageMod-1) * 100); + engageMod = Math.round((engageMod - 1) * 100); } let difficultyText; if (V.SecExp.settings.difficulty === 0.5) { - difficultyText = "Very easy"; + difficultyText = 'Very easy'; } else if (V.SecExp.settings.difficulty === 0.75) { - difficultyText = "Easy"; + difficultyText = 'Easy'; } else if (V.SecExp.settings.difficulty === 1) { - difficultyText = "Normal"; + difficultyText = 'Normal'; } else if (V.SecExp.settings.difficulty === 1.25) { - difficultyText = "Hard"; + difficultyText = 'Hard'; } else if (V.SecExp.settings.difficulty === 1.5) { - difficultyText = "Very hard"; + difficultyText = 'Very hard'; } else { - difficultyText = "Extremely hard"; + difficultyText = 'Extremely hard'; } - App.UI.DOM.appendNewElement("div", node, `Difficulty: ${difficultyText}. Modifier: x${V.SecExp.settings.difficulty}`); - - App.UI.DOM.appendNewElement("div", node, `Army`, ["underline"]); - App.UI.DOM.appendNewElement("div", node, `Deployed troops: ${num(App.Mods.SecExp.battle.troopCount())}`); - App.UI.DOM.appendNewElement("div", node, `Attack: ${num(attack.total)}.`); + App.UI.DOM.appendNewElement( + 'div', + node, + `Difficulty: ${difficultyText}. Modifier: x${V.SecExp.settings.difficulty}`, + ); + + App.UI.DOM.appendNewElement('div', node, 'Army', ['underline']); + App.UI.DOM.appendNewElement('div', node, `Deployed troops: ${num(App.Mods.SecExp.battle.troopCount())}`); + App.UI.DOM.appendNewElement('div', node, `Attack: ${num(attack.total)}.`); if (attack.SF) { - App.UI.DOM.appendNewElement("div", node, `Base: ${attack.base}`, ["indent"]); - App.UI.DOM.appendNewElement("div", node, `SF Bonus: ${attack.SF}`, ["indent"]); + App.UI.DOM.appendNewElement('div', node, `Base: ${attack.base}`, ['indent']); + App.UI.DOM.appendNewElement('div', node, `SF Bonus: ${attack.SF}`, ['indent']); } - App.UI.DOM.appendNewElement("div", node, `Defense: ${num(Math.round(defense.total))}.`); + App.UI.DOM.appendNewElement( + 'div', + node, + `Defense: ${num(Math.round(defense.total))}.`, + ); if (defense.SF) { - App.UI.DOM.appendNewElement("div", node, `Base: ${defense.base}`, ["indent"]); - App.UI.DOM.appendNewElement("div", node, `SF Bonus: ${defense.SF}`, ["indent"]); + App.UI.DOM.appendNewElement('div', node, `Base: ${defense.base}`, ['indent']); + App.UI.DOM.appendNewElement('div', node, `SF Bonus: ${defense.SF}`, ['indent']); } if (inRebellion) { - App.UI.DOM.appendNewElement("div", node, `Engagement rule modifier: +${engageMod}%`); + App.UI.DOM.appendNewElement( + 'div', + node, + `Engagement rule modifier: +${engageMod}%`, + ); } - App.UI.DOM.appendNewElement("div", node, `HP: ${num(Math.round(hp.total))}.`); + App.UI.DOM.appendNewElement( + 'div', + node, + `HP: ${num(Math.round(hp.total))}.`, + ); if (hp.SF) { - App.UI.DOM.appendNewElement("div", node, `Base: ${hp.base}`, ["indent"]); - App.UI.DOM.appendNewElement("div", node, `SF Bonus: ${hp.SF}`, ["indent"]); + App.UI.DOM.appendNewElement('div', node, `Base: ${hp.base}`, ['indent']); + App.UI.DOM.appendNewElement('div', node, `SF Bonus: ${hp.SF}`, ['indent']); } - App.UI.DOM.appendNewElement("div", node, `Morale: ${num(Math.round(morale.total))}.`); + App.UI.DOM.appendNewElement( + 'div', + node, + `Morale: ${num(Math.round(morale.total))}.`, + ); if (inBattle) { - App.UI.DOM.appendNewElement("div", node, `Slaves morale modifier: +${morale.slaves}%`, ["indent"]); - App.UI.DOM.appendNewElement("div", node, `Militia morale modifier: +${morale.militia}%`, ["indent"]); - App.UI.DOM.appendNewElement("div", node, `Mercenaries morale modifier: +${morale.mercs}%`, ["indent"]); + App.UI.DOM.appendNewElement( + 'div', + node, + `Slaves morale modifier: +${morale.slaves}%`, + ['indent'], + ); + App.UI.DOM.appendNewElement( + 'div', + node, + `Militia morale modifier: +${morale.militia}%`, + ['indent'], + ); + App.UI.DOM.appendNewElement( + 'div', + node, + `Mercenaries morale modifier: +${morale.mercs}%`, + ['indent'], + ); if (activeSF && V.SecExp.war.deploySF) { - App.UI.DOM.appendNewElement("div", node, `Special Force morale modifier: +${morale.SF}%`, ["indent"]); + App.UI.DOM.appendNewElement( + 'div', + node, + `Special Force morale modifier: +${morale.SF}%`, + ['indent'], + ); } - if (V.SecExp.buildings.barracks && V.SecExp.buildings.barracks.luxury >= 1) { - App.UI.DOM.appendNewElement("div", node, `Barracks bonus morale modifier: +${V.SecExp.buildings.barracks.luxury * 5}%`, ["indent"]); + if ( + V.SecExp.buildings.barracks + && V.SecExp.buildings.barracks.luxury >= 1 + ) { + App.UI.DOM.appendNewElement( + 'div', + node, + `Barracks bonus morale modifier: +${V.SecExp.buildings.barracks.luxury * 5}%`, + ['indent'], + ); } } if (inBattle) { - App.UI.DOM.appendNewElement("div", node, `Tactics`, ["underline"]); - App.UI.DOM.appendNewElement("div", node, `Chance of success: ${num(Math.round(tacChance * 100))}%. Was successful?: ${V.SecExp.war.tacticsSuccessful ? 'Yes' : 'No'}`); + App.UI.DOM.appendNewElement('div', node, 'Tactics', ['underline']); + App.UI.DOM.appendNewElement( + 'div', + node, + `Chance of success: ${num(Math.round(tacChance * 100))}%. Was successful?: ${V.SecExp.war.tacticsSuccessful ? 'Yes' : 'No'}`, + ); } - App.UI.DOM.appendNewElement("p", node); - App.UI.DOM.appendNewElement("div", node, `${inBattle ? 'Enemy' : 'Rebels'}`, ["underline"]); - App.UI.DOM.appendNewElement("div", node, `Troops: ${num(Math.round(V.SecExp.war.attacker.troops))}`); - App.UI.DOM.appendNewElement("div", node, `Attack: ${num(Math.round(enemyAttack))}`); - App.UI.DOM.appendNewElement("div", node, `Defense: ${num(Math.round(enemyDefense))}`); - App.UI.DOM.appendNewElement("div", node, `HP: ${num(Math.round(enemyHp))}. Base: ${num(Math.round(enemyBaseHp))}`); - App.UI.DOM.appendNewElement("div", node, `Morale: ${num(Math.round(enemyMorale))}.`); + App.UI.DOM.appendNewElement('p', node); + App.UI.DOM.appendNewElement( + 'div', + node, + `${inBattle ? 'Enemy' : 'Rebels'}`, + ['underline'], + ); + App.UI.DOM.appendNewElement( + 'div', + node, + `Troops: ${num(Math.round(V.SecExp.war.attacker.troops))}`, + ); + App.UI.DOM.appendNewElement( + 'div', + node, + `Attack: ${num(Math.round(enemyAttack))}`, + ); + App.UI.DOM.appendNewElement( + 'div', + node, + `Defense: ${num(Math.round(enemyDefense))}`, + ); + App.UI.DOM.appendNewElement( + 'div', + node, + `HP: ${num(Math.round(enemyHp))}. Base: ${num(Math.round(enemyBaseHp))}`, + ); + App.UI.DOM.appendNewElement( + 'div', + node, + `Morale: ${num(Math.round(enemyMorale))}.`, + ); } let i = 0; // simulates the combat by pitting attk against def while (i < turns && ![3, -3].includes(V.SecExp.war.result)) { - App.UI.DOM.appendNewElement("p", node, turnReport()); + App.UI.DOM.appendNewElement('p', node, turnReport()); i++; } @@ -498,19 +746,33 @@ App.Events.conflictHandler = function() { } if (V.SecExp.war.result > 3 || V.SecExp.war.result < -3) { - throw Error(`Failed to determine battle result`); + throw Error('Failed to determine battle result'); } if (inBattle && showStats) { - App.UI.DOM.appendNewElement("div", node, `Losses: ${num(Math.trunc(V.SecExp.war.losses))}`); - App.UI.DOM.appendNewElement("div", node, `Enemy losses: ${num(Math.trunc(V.SecExp.war.attacker.losses))}`); + App.UI.DOM.appendNewElement( + 'div', + node, + `Losses: ${num(Math.trunc(V.SecExp.war.losses))}`, + ); + App.UI.DOM.appendNewElement( + 'div', + node, + `Enemy losses: ${num(Math.trunc(V.SecExp.war.attacker.losses))}`, + ); } - if (V.SecExp.war.result === -3 && (isMajorBattle && V.SecExp.settings.battle.major.gameOver === 1 || inRebellion && V.SecExp.settings.rebellion.gameOver === 1)) { - V.gameover = `${isMajorBattle ? "major battle" : "Rebellion"} defeat`; - atEnd("Gameover"); + if ( + V.SecExp.war.result === -3 + && ( + (isMajorBattle && V.SecExp.settings.battle.major.gameOver === 1) + || (inRebellion && V.SecExp.settings.rebellion.gameOver === 1) + ) + ) { + V.gameover = `${isMajorBattle ? 'major battle' : 'Rebellion'} defeat`; + atEnd('Gameover'); } else { - atEnd("conflictReport"); + atEnd('conflictReport'); } return node; }; diff --git a/src/art/artJS.js b/src/art/artJS.js index 1764d4a9d6f0b3bd91b4a2bee88bc3508ef44bab..a99ad1ea29d56c31d252685aae39d048d100f912 100644 --- a/src/art/artJS.js +++ b/src/art/artJS.js @@ -134,6 +134,8 @@ App.Art.SlaveArtElement = function(artSlave, artSize, UIDisplay) { return App.Art.webglArtElement(artSlave, artSize); } else if (imageChoice === 5) { /* RENDERED IMAGES BY SHOKUSHU */ return App.Art.renderedArtElement(artSlave, artSize); + } else if (imageChoice === 6) { /* AI GENERATED IMAGES */ + return App.Art.aiArtElement(artSlave, artSize); } throw new Error(`imageChoice ${imageChoice} is out of range`); }; @@ -150,7 +152,7 @@ App.Art.setDynamicCSS = function(newState) { App.Art.dynamicCSS.innerHTML += '.smlImg { height: 150px; width: 150px }\n'; App.Art.dynamicCSS.innerHTML += '.medImg { height: 300px; width: 300px }\n'; - App.Art.dynamicCSS.innerHTML += '.lrgRender:not(.custom) { height: '+height+'px; width: '+width+'px;}\n'; + App.Art.dynamicCSS.innerHTML += '.lrgRender:not(.custom) { height: ' + height + 'px; width: ' + width + 'px;}\n'; App.Art.dynamicCSS.innerHTML += '.lrgRender > img { margin-left: auto; height: 530px; width: auto }\n'; } else { App.Art.dynamicCSS.innerHTML = ''; @@ -202,7 +204,7 @@ App.Art.webglInitialize = function() { try { // load model/morphs/textures assets let sceneData = App.Art.sceneGetData(); - App.Art.sceneGetData = function(){}; + App.Art.sceneGetData = function() { }; // load default dictionary containing camera/light/morph/material values let scene = App.Art.sceneGetParams(); scene.lockView = false; @@ -284,7 +286,7 @@ App.Art.webglArtElement = function(inSlave, artSize) { // it's common for e.g. events to make alterations to a slave just before rendering and revert them afterwards const slave = clone(inSlave); container.addEventListener("engineLoaded", function() { - new IntersectionObserver(function(entries) { // when visible in viewport + new IntersectionObserver(function(entries) { // when visible in viewport if (entries.some(e => e.isIntersecting)) { this.unobserve(container); // render only once @@ -426,6 +428,102 @@ App.Art.customArtElement = function(imageInfo, imageSize) { return res; }; +/** + * Render an AI generated image + * @param {App.Entity.SlaveState} slave - The slave whose image to render + * @param {number} imageSize - The size of the image to render + * @returns {Promise<HTMLElement>} Promise object that resolves with the created img element + */ +async function renderAIArt(slave, imageSize) { + let imgElement; + if (slave.custom.aiImageId === null) { + imgElement = document.createElement("div"); + } else { + imgElement = document.createElement("img"); + } + + imgElement.classList.add("ai-art-image"); + imgElement.setAttribute("style", "float:right; border:3px hidden; object-fit:contain; height:100%; width:100%;"); + + const sz = App.Art.artSizeToPx(imageSize); + if (sz) { + imgElement.setAttribute("width", sz); + imgElement.setAttribute("height", sz); + } + + try { + const imageData = await App.Art.GenAI.imageDB.getImage(slave.custom.aiImageId); + imgElement.setAttribute("src", imageData.data); + } catch (e) { + console.error(e); + } + + return imgElement; +} + +/** AI generated image that refreshes on click + * @param {App.Entity.SlaveState} slave + * @param {number} imageSize + * @returns {HTMLElement} + */ +App.Art.aiArtElement = function(slave, imageSize) { + const container = document.createElement("div"); + container.classList.add("ai-art-container"); + + /** + * @param {HTMLDivElement} container + */ + function makeSpinner(container) { + const spinner = document.createElement("div"); + spinner.classList.add("spinner"); + spinner.innerText = '⟳'; + container.appendChild(spinner); + } + makeSpinner(container); + + // Refresh on click + function refresh() { + renderAIArt(slave, imageSize).then((imgElement) => { + jQuery(container).empty().append(imgElement); + makeSpinner(container); + }); + } + + function updateAndRefresh() { + const imageGenerator = new App.Art.GenAI.StableDiffusionClient(V.aiApiUrl); + + container.classList.add("refreshing"); + + imageGenerator.updateSlave(slave).then(() => { + refresh(); + + container.classList.remove("refreshing"); + }).catch(error => { + console.error(error); + + container.classList.remove("refreshing"); + }); + } + + container.addEventListener("click", function() { + if (!container.classList.contains("refreshing")) { + updateAndRefresh(); + } + }); + + if (slave.custom.aiImageId === null) { + updateAndRefresh(); + } + + renderAIArt(slave, imageSize).then((imgElement) => { + jQuery(container).empty().append(imgElement); + makeSpinner(container); + }); + return container; +}; + + + /** * @param {number} artSize * @returns {string} diff --git a/src/art/genAI/agePromptPart.js b/src/art/genAI/agePromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..a955d11d4955d0858c49282d70a3afc117c5314e --- /dev/null +++ b/src/art/genAI/agePromptPart.js @@ -0,0 +1,30 @@ +App.Art.GenAI.AgePromptPart = class AgePromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + let ageTags = ``; + if (this.slave.visualAge < 13) { + ageTags = `child, `; + } else if (this.slave.visualAge < 18) { + ageTags = `teen, young, `; + } else if (this.slave.visualAge < 20) { + ageTags = `teen, `; + } + + return `${ageTags}${this.slave.visualAge} year old`; + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.visualAge < 20) { + return `old, 30 year old, 40 year old`; + } else if (this.slave.visualAge < 30) { /* empty */ } else if (this.slave.visualAge < 40) { + return `young, teen`; + } else { + return `young, teen, 20 year old`; + } + } +}; diff --git a/src/art/genAI/arousalPromptPart.js b/src/art/genAI/arousalPromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..12dd75dc6d7197c74c963eb470785c46eda9aa94 --- /dev/null +++ b/src/art/genAI/arousalPromptPart.js @@ -0,0 +1,40 @@ +App.Art.GenAI.ArousalPromptPart = class ArousalPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + let prompt = {terms: [], weight: 1}; + if (this.slave.vaginaLube === 2) { + prompt.terms.push("pussy juice"); + } + if (this.slave.energy > 60) { + prompt.terms.push("blush"); + } + if (this.slave.energy > 80) { + prompt.terms.push("sweat", "heavy breathing"); + if (this.slave.vaginaLube === 1) { + prompt.terms.push("pussy juice"); + } + } + if (this.slave.energy > 95) { + prompt.weight = 1.1; + } + if (prompt.terms) { + if (prompt.weight !== 1) { + return `(${prompt.terms.join(", ")}:${prompt.weight})`; + } + return `${prompt.terms.join(", ")}`; + } + return; + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.vaginaLube == 0) { + return "pussy juice"; + } + return undefined; + } +}; diff --git a/src/art/genAI/beautyPromptPart.js b/src/art/genAI/beautyPromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..36f600974f9cdc52a5758b0fcaceb8d44b95e3a6 --- /dev/null +++ b/src/art/genAI/beautyPromptPart.js @@ -0,0 +1,25 @@ +App.Art.GenAI.BeautyPromptPart = class BeautyPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.face < -95) { + return "ugly, low quality"; + } else if (this.slave.face < -50) { + return "unattractive, low quality"; + } else if (this.slave.face < 10) { /* empty */ } else if (this.slave.face < 50) { + return "best quality"; + } else if (this.slave.face < 95) { + return "masterpiece, best quality"; + } else { + return "(masterpiece, best quality:1.1)"; + } + } + + /** + * @returns {string} + */ + negative() { + return "low quality"; + } +}; diff --git a/src/art/genAI/breastsPromptPart.js b/src/art/genAI/breastsPromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..269575e97d67d4e810dc9a6ffde353c6f248e1f0 --- /dev/null +++ b/src/art/genAI/breastsPromptPart.js @@ -0,0 +1,37 @@ +App.Art.GenAI.BreastsPromptPart = class BreastsPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.boobs < 300) { + return `flat chest`; + } else if (this.slave.boobs < 400) { + return `small breasts, flat chest`; + } else if (this.slave.boobs < 500) { + return `small breasts`; + } else if (this.slave.boobs < 650) { + return `medium breasts`; + } else if (this.slave.boobs < 800) { + return `large breasts`; + } else if (this.slave.boobs < 1000) { + return `huge breasts`; + } else if (this.slave.boobs < 1400) { + return `huge breasts, large breasts`; + } else { + return `(huge breasts:1.1), large breasts`; + } + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.boobs < 300) { + return `medium breasts, large breasts, huge breasts`; + } else if (this.slave.boobs < 650) { + return; + } else { + return `small breasts, flat chest`; + } + } +}; diff --git a/src/art/genAI/buildPrompt.js b/src/art/genAI/buildPrompt.js new file mode 100644 index 0000000000000000000000000000000000000000..a1015ba089f28fc41db0ec24658195f1bcbc5957 --- /dev/null +++ b/src/art/genAI/buildPrompt.js @@ -0,0 +1,35 @@ +/** + * @param {FC.SlaveState} slave + * @returns {App.Art.GenAI.Prompt} + */ +// eslint-disable-next-line no-unused-vars +function buildPrompt(slave) { + let prompts = [ + new App.Art.GenAI.StylePromptPart(slave), + new App.Art.GenAI.SkinPromptPart(slave), + new App.Art.GenAI.RacePromptPart(slave), + new App.Art.GenAI.GenderPromptPart(slave), + new App.Art.GenAI.AgePromptPart(slave), + new App.Art.GenAI.PregPromptPart(slave), + new App.Art.GenAI.BeautyPromptPart(slave), + new App.Art.GenAI.PosturePromptPart(slave), + new App.Art.GenAI.ArousalPromptPart(slave), + new App.Art.GenAI.WeightPromptPart(slave), + new App.Art.GenAI.HeightPromptPart(slave), + new App.Art.GenAI.MusclesPromptPart(slave), + new App.Art.GenAI.ClothesPromptPart(slave), + new App.Art.GenAI.CollarPromptPart(slave), + new App.Art.GenAI.BreastsPromptPart(slave), + new App.Art.GenAI.WaistPromptPart(slave), + new App.Art.GenAI.HipsPromptPart(slave), + new App.Art.GenAI.HairPromptPart(slave), + new App.Art.GenAI.EyePromptPart(slave), + new App.Art.GenAI.EyebrowPromptPart(slave), + new App.Art.GenAI.ExpressionPromptPart(slave), + new App.Art.GenAI.TattoosPromptPart(slave), + new App.Art.GenAI.PiercingsPromptPart(slave), + new App.Art.GenAI.HealthPromptPart(slave), + new App.Art.GenAI.PubicHairPromptPart(slave), + ]; + return new App.Art.GenAI.Prompt(prompts); +} diff --git a/src/art/genAI/clothesPromptPart.js b/src/art/genAI/clothesPromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..cc8c33ccc7a58e97fdaebf52d6500386ca45f632 --- /dev/null +++ b/src/art/genAI/clothesPromptPart.js @@ -0,0 +1,477 @@ +const clothesPrompts = { + "no clothing": { + "positive": "(completely nude:1.1), pussy, nipples", + "negative": "clothes, jeans, underwear, pants, shorts, skirt, panties", + }, + "a Fuckdoll suit": { // Doesn't work well + "positive": "latex bodysuit, long sleeves", + "negative": "bare shoulders, exposed skin, exposed legs, exposed arms, short sleeves, nude, pussy, nipples", + }, + "conservative clothing": { + "positive": "slacks, pants, silk blouse", + "negative": "jeans, nude, pussy, nipples", + }, + "chains": { + "positive": "(metal chains:1.1), nude, pussy, nipples, navel", + "negative": "clothes, jeans, underwear, pants, shorts, skirt, panties", + }, + "Western clothing": { + "positive": "flannel shirt, chaps, cowboy hat", + "negative": "nude, pussy, nipples", + }, + "body oil": { // Doesn't work well + "positive": "body oil, nude, pussy, nipples, navel", + "negative": "clothes, jeans, underwear, pants, shorts, skirt, panties", + }, + "a toga": { // Doesn't work well + "positive": "white toga", + "negative": "jeans, nude, pussy, nipples", + }, + "a huipil": { // Doesn't work well + "positive": "huipil, chinese clothing", + "negative": "jeans, nude, pussy, nipples", + }, + "a slutty qipao": { + "positive": "qipao, chinese clothing, thighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a kimono": { + "positive": "kimono", + "negative": "jeans, nude, pussy, nipples", + }, + "spats and a tank top": { // Spats don't work well + "positive": "bike shorts, tank top", + "negative": "bike, jeans, nude, pussy, nipples", + }, + "uncomfortable straps": { + "positive": "(leather straps, bondage:1.1), nude, pussy, nipples, navel", + "negative": "clothes, jeans, underwear, pants, shorts, skirt, panties", + }, + "shibari ropes": { + "positive": "shibari rope, bondage, nude, pussy, nipples, navel", + "negative": "clothes, jeans, underwear, pants, shorts, skirt, panties", + }, + "restrictive latex": { // Doesn't work well + "positive": "latex bodysuit, long sleeves", + "negative": "bare shoulders, exposed skin, exposed legs, exposed arms, short sleeves, nude, pussy, nipples", + }, + "a latex catsuit": { // Doesn't work well + "positive": "latex bodysuit, long sleeves", + "negative": "bare shoulders, exposed skin, exposed legs, exposed arms, short sleeves, nude, pussy, nipples", + }, + "attractive lingerie": { // Cupless part doesn't work well + "positive": "lingerie, cupless bra, nipples, thong", + "negative": "clothes, jeans, pants", + }, + "attractive lingerie for a pregnant woman": { // Cupless part doesn't work well + "positive": "lingerie, cupless bra, nipples, thong", + "negative": "clothes, jeans, pants", + }, + "kitty lingerie": { // Broken for photorealistic models, probably works for anime models + "positive": "cat lingerie, cat cutout, cat ear panties, bra, panties", + "negative": "cat ears, jeans, nude, pussy, nipples", + }, + "a maternity dress": { + "positive": "maternity dress, loose dress", + "negative": "jeans, nude, pussy, nipples", + }, + "stretch pants and a crop-top": { + "positive": "crop top, midriff, navel, leggings", + "negative": "jeans, nude, pussy, nipples", + }, + "a succubus outfit": { + "positive": "red leather corset, red leather miniskirt, thighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a fallen nuns habit": { + "positive": "(latex nun habit:1.1), thighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a penitent nuns habit": { + "positive": "(latex nun habit:1.1), thighs, rope, bondage", + "negative": "jeans, nude, pussy, nipples", + }, + "a chattel habit": { + "positive": "(white gold latex nun:1.1), thighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a string bikini": { // Cupless part doesn't work well + "positive": "string microbikini, cupless bikini, nipples", + "negative": "jeans, nude, pussy", + }, + "a scalemail bikini": { // Doesn't work well + "positive": "chainmail bikini, navel", + "negative": "jeans, nude, pussy, nipples", + }, + "striped panties": { + "positive": "blue striped panties, underwear only, nipples", + "negative": "jeans, nude, pussy", + }, + "a cheerleader outfit": { + "positive": "(cheerleader outfit:1.1), skirt, thighs, crop top, navel, midriff", + "negative": "jeans, nude, pussy, nipples", + }, + "clubslut netting": { // Doesn't work well + "positive": "nude, fishnets, nipples, pussy", + "negative": "cloth, jeans, pants, corset", + }, + "cutoffs and a t-shirt": { + "positive": "white t-shirt, jean shorts", + "negative": "nude, pussy, nipples", + }, + "slutty business attire": { + "positive": "suit jacket, cleavage, black skirt, thighs", + "negative": "jeans, nude, pussy, nipples" + }, + "nice business attire": { + "positive": "suit jacket, collared shirt, black skirt", + "negative": "jeans, nude, pussy, nipples", + }, + "a ball gown": { + "positive": "ballgown, long dress, luxurious dress, thighhighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a slave gown": { + "positive": "ballgown, long dress, luxurious dress, thighhighs, cleavage, see-through, translucent clothing, straps, bdsm", + "negative": "jeans, nude", + }, + "a halter top dress": { + "positive": "(halterneck:1.1), long dress, luxurious dress, bare back,", + "negative": "jeans, nude, pussy, nipples", + }, + "an evening dress": { + "positive": "evening gown, long dress, luxurious dress, thighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a mini dress": { + "positive": "short dress, tight dress, strapless, cleavage, thighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a comfortable bodysuit": { + "positive": "latex bodysuit, long sleeves", + "negative": "bare shoulders, exposed skin, exposed legs, exposed arms, short sleeves, nude, pussy, nipples", + }, + "a leotard": { + "positive": "leotard, thighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a monokini": { // Doesn't work well + "positive": "monokini", + "negative": "jeans, nude, pussy, nipples", + }, + "an apron": { + "positive": "apron, thighs, nude", + "negative": "clothes, shirt, pants, shorts, pussy, nipples", + }, + "overalls": { + "positive": "overalls, naked overalls", + "negative": "shirt, pants, shorts, pussy, nipples, topless", + }, + "a cybersuit": { // Doesn't work well + "positive": "cybersuit, latex bodysuit, long sleeves, cybernetic, science fiction", + "negative": "bare shoulders, exposed skin, exposed legs, exposed arms, short sleeves, nude, pussy, nipples", + }, + "a tight Imperial bodysuit": { // Doesn't work well + "positive": "imperial bodysuit, latex bodysuit, long sleeves, cybernetic, science fiction", + "negative": "bare shoulders, exposed skin, exposed legs, exposed arms, short sleeves, nude, pussy, nipples", + }, + "battlearmor": { // Doesn't work well + "positive": "(armor, science fiction, soldier:1.1)", + "negative": "jeans, nude, pussy, nipples", + }, + "Imperial Plate": { // Doesn't work well + "positive": "(armor, science fiction, soldier:1.1)", + "negative": "jeans, nude, pussy, nipples", + }, + "a bunny outfit": { + "positive": "playboy bunny, backless leotard, pantyhose", + "negative": "jeans, nude, pussy, nipples, rabbit ears", + }, + "a slutty maid outfit": { + "positive": "maid, minidress, apron, white shirt, cleavage, thighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a nice maid outfit": { + "positive": "maid, dress, apron, white shirt", + "negative": "jeans, nude, pussy, nipples", + }, + "a slutty nurse outfit": { + "positive": "nurse, white jacket, cleavage, white skirt, thighs", + "negative": "jeans, shirt, pussy, nipples", + }, + "a nice nurse outfit": { + "positive": "nurse, white medical scrubs, pants", + "negative": "jeans, nude, pussy, nipples", + }, + "a dirndl": { + "positive": "(dirndl:1.1)", + "negative": "jeans, nude, pussy, nipples", + }, + "a long qipao": { + "positive": "(qipao:1.1), long dress, chinese clothes", + "negative": "jeans, nude, pussy, nipples", + }, + "lederhosen": { + "positive": "(lederhosen:1.1)", + "negative": "jeans, nude, pussy, nipples", + }, + "a biyelgee costume": { // Doesn't work well + "positive": "mongolian traditional clothes", + "negative": "jeans, nude, pussy, nipples", + }, + "a hanbok": { + "positive": "(hanbok:1.1)", + "negative": "jeans, nude, pussy, nipples", + }, + "burkini": { + "positive": "burqa, muslim clothes, burkini, pants", + "negative": "jeans, nude, pussy, nipples", + }, + "a hijab and blouse": { + "positive": "(hijab:1.1), blouse, short sleeves, long skirt", + "negative": "jeans, nude, pussy, nipples", + }, + "a hijab and abaya": { + "positive": "hijab, abaya", + "negative": "jeans, nude, pussy, nipples", + }, + "a niqab and abaya": { // Doesn't work well + "positive": "niqab, covered face, abaya", + "negative": "jeans, nude, pussy, nipples", + }, + "a burqa": { // Doesn't work well + "positive": "burqa, muslim clothes", + "negative": "jeans, nude, pussy, nipples", + }, + "a police uniform": { + "positive": "police uniform, policewoman, police hat, jacket, pants, belt", + "negative": "jeans, nude, pussy, nipples", + }, + "a gothic lolita dress": { + "positive": "gothic lolita, dress, thighhighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a one-piece swimsuit": { + "positive": "one-piece swimsuit, thighs", + "negative": "jeans, nude, pussy, nipples", + }, + "a nice pony outfit": { // Tbh, not really sure what this is + "positive": "latex bodysuit, long sleeves", + "negative": "bare shoulders, exposed skin, exposed legs, exposed arms, short sleeves, nude, pussy, nipples", + }, + "a slutty pony outfit": { // Same + "positive": "latex bodysuit, long sleeves, cleavage, thighs", + "negative": "nude, pussy, nipples", + }, + "a button-up shirt and panties": { // Often not bottomless + "positive": "collared shirt, oversized clothes, panties, (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nude, pussy, nipples", + }, + "a button-up shirt": { // Often not bottomless + "positive": "collared shirt, oversized clothes, pussy, nude, (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples", + }, + "a sweater": { // Often not bottomless + "positive": "sweater, oversized clothes, pussy, nude, (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples", + }, + "a t-shirt": { // Often not bottomless + "positive": "t-shirt, (pussy, nude:1.1), (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples", + }, + "a tank-top": { // Often not bottomless + "positive": "tank top, bare shoulders, (pussy, nude:1.1), (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples", + }, + "a tube top": { // Often not bottomless + "positive": "tube top, bare shoulders, (pussy, nude:1.1), (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nude, nipples", + }, + "an oversized t-shirt": { // Often not bottomless + "positive": "t-shirt, oversized clothes, (pussy, nude:1.1), (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples", + }, + "a bra": { // Often not bottomless + "positive": "bra, (pussy, nude:1.1), (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples", + }, + "a sports bra": { // Often not bottomless + "positive": "sports bra, (pussy, nude:1.1), (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples", + }, + "a striped bra": { // Often not bottomless + "positive": "striped bra, (pussy, nude:1.1), (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples", + }, + "pasties": { // Doesn't work well + "positive": "pasties, pussy, nude, (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples", + }, + "a tube top and thong": { + "positive": "tube top, bare shoulders, (nude:1.1), (bottomless:1.1), g-string, thighs", + "negative": "jeans, pants, skirt, shorts, nipples, pussy", + }, + "a sweater and panties": { // Often not bottomless + "positive": "sweater, oversized clothes, panties, (nude:1.1), (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples, pussy", + }, + "a tank-top and panties": { // Often not bottomless + "positive": "tank top, bare shoulders, panties, (nude:1.1), (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, shorts, nipples, pussy", + }, + "a t-shirt and thong": { // Often not bottomless + "positive": "t-shirt, (nude:1.1), (bottomless:1.1), g-string, thighs", + "negative": "jeans, pants, skirt, shorts, nipples, pussy", + }, + "an oversized t-shirt and boyshorts": { // Doesn't work well + "positive": "t-shirt, oversized clothes, boyshort panties, (nude:1.1), (bottomless:1.1), thighs", + "negative": "jeans, pants, skirt, nipples, pussy", + }, + "sport shorts and a t-shirt": { + "positive": "t-shirt, sport shorts", + "negative": "jeans, pants, skirt, nipples, pussy", + }, + "sport shorts and a sports bra": { + "positive": "sports bra, sport shorts", + "negative": "jeans, pants, skirt, nipples, pussy", + }, + "a t-shirt and panties": { // Often not bottomless + "positive": "t-shirt, (nude:1.1), (bottomless:1.1), panties, thighs", + "negative": "jeans, pants, skirt, shorts, nipples, pussy", + }, + "striped underwear": { // Often not bottomless + "positive": "striped panties, striped bra", + "negative": "jeans, pants, skirt, shorts, nipples, pussy", + }, + "a thong": { + "positive": "thong, topless, nipples", + "negative": "jeans, pants, skirt, shorts, pussy", + }, + "a skimpy loincloth": { // Doesn't work well + "positive": "loincloth, topless, nipples", + "negative": "jeans, pants, skirt, shorts, pussy", + }, + "boyshorts": { + "positive": "boyshort panties, topless, nipples", + "negative": "jeans, pants, skirt, pussy", + }, + "panties": { + "positive": "panties, topless, nipples", + "negative": "jeans, pants, skirt, pussy", + }, + "panties and pasties": { // Doesn't work well + "positive": "panties, pasties, topless", + "negative": "jeans, pants, skirt, pussy, nipples", + }, + "cutoffs": { + "positive": "jean shorts, topless, nipples", + "negative": "pussy", + }, + "sport shorts": { + "positive": "sport shorts, topless, nipples", + "negative": "jeans, pants, skirt, pussy", + }, + "a sweater and cutoffs": { + "positive": "sweater, jean shorts", + "negative": "pussy, nipples", + }, + "leather pants and a tube top": { + "positive": "leather pants, tube top, bare shoulders", + "negative": "jeans, pants, skirt, shorts, pussy, nipples", + }, + "a t-shirt and jeans": { + "positive": "t-shirt, jeans", + "negative": "pussy, nipples", + }, + "leather pants and pasties": { // Doesn't work well + "positive": "leather pants, pasties, topless", + "negative": "jeans, pants, skirt, shorts, pussy, nipples", + }, + "leather pants": { + "positive": "leather pants, topless, nipples", + "negative": "jeans, pants, skirt, shorts, pussy", + }, + "jeans": { + "positive": "jeans, topless, nipples", + "negative": "pussy", + }, + "a military uniform": { + "positive": "military uniform, shirt, necktie, skirt", + "negative": "jeans, shorts, pussy, nipples", + }, + "battledress": { + "positive": "military fatigues, jumpsuit", + "negative": "jeans, shorts, pussy, nipples", + }, + "a mounty outfit": { // Doesn't work well + "positive": "mounty, red military jacket", + "negative": "jeans, shorts, pussy, nipples", + }, + "harem gauze": { + "positive": "harem outfit, loose dress, see-through, transparent clothes, nipples, pussy", + "negative": "jeans, shorts", + }, + "slutty jewelry": { + "positive": "nude, jewelry, gem, gold chains, armlet, nipples, pussy", + "negative": "clothes, jeans, underwear, pants, shorts, skirt, panties" + }, + "a Santa dress": { + "positive": "santa costume, santa dress, thighs", + "negative": "jeans, nude, pussy, nipples" + }, + "a bimbo outfit": { + "positive": "(pink:1.1) tube top, bra, cleavage, pink microskirt, thighs, panties, navel, midriff", + "negative": "jeans, nude, pussy, nipples", + }, + "a slutty outfit": { + "positive": "(pink:1.1) crop top, pink lowleg microskirt, (pussy:1.1), hip bones, groin, tight clothes, midriff, navel, (thighs:1.1)", + "negative": "jeans, nude, nipples", + }, + "a courtesan dress": { // Corset was messing stuff up, so I removed it + "positive": "(luxurious flowing dress:1.1), bare shoulders, long sleeves, detached sleeves", + "negative": "jeans, nude, pussy, nipples", + }, + "a schoolgirl outfit": { + "positive": "school uniform, white shirt, plaid skirt", + "negative": "jeans, nude, pussy, nipples", + } +}; + +App.Art.GenAI.ClothesPromptPart = class ClothesPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + getClothes() { + let clothes = this.slave.clothes; + if (!clothesPrompts.hasOwnProperty(clothes)) { + clothes = "no clothing"; + } + return clothes; + } + + /** + * Remove positive keywords for genitalia from slaves that don't have the genitalia described by the keyword. + * Currently handles pussies; probably should substitute penises when appropriate but right now the model is very bad at penises, so we just drop it. + * @returns {string} + */ + bodyPartReplacer(prompt) { + if (this.slave.vagina === -1) { + return prompt.replace(/( *)pussy(,)*/g, ""); + } + return prompt; + } + + /** + * @returns {string} + */ + positive() { + return this.bodyPartReplacer(clothesPrompts[this.getClothes()].positive); + } + + /** + * @returns {string} + */ + negative() { + return clothesPrompts[this.getClothes()].negative; + } +}; diff --git a/src/art/genAI/collarPromptPart.js b/src/art/genAI/collarPromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..dd0fe004286f7c1ad9135f26377d0a0775fe306e --- /dev/null +++ b/src/art/genAI/collarPromptPart.js @@ -0,0 +1,20 @@ +App.Art.GenAI.CollarPromptPart = class CollarPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.collar !== "none") { + return `${this.slave.collar} collar`; + } + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.collar === "none") { + return "collar"; + } + return; + } +}; diff --git a/src/art/genAI/expressionPromptPart.js b/src/art/genAI/expressionPromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..56abc5cc624ee62286da104f574d09c0e70241e7 --- /dev/null +++ b/src/art/genAI/expressionPromptPart.js @@ -0,0 +1,76 @@ +App.Art.GenAI.ExpressionPromptPart = class ExpressionPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + let devotionPart; + if (this.slave.devotion < -50) { + devotionPart = `angry expression, hateful`; + } else if (this.slave.devotion < -20) { + devotionPart = `angry`; + } else if (this.slave.devotion < 51) { + devotionPart = null; + } else if (this.slave.devotion < 95) { + devotionPart = `smile`; + } else { + devotionPart = `smile, grin, teeth, loving expression`; + } + + let trustPart; + if (this.slave.trust < -90) { + trustPart = `(scared expression:1.2), looking down, crying, tears`; + } + if (this.slave.trust < -50) { + trustPart = `(scared expression:1.1), looking down, crying`; + } else if (this.slave.trust < -20) { + trustPart = `scared expression, looking down`; + } else if (this.slave.trust < 51) { + trustPart = `looking at viewer`; + } else if (this.slave.trust < 95) { + trustPart = `looking at viewer, confident`; + } else { + trustPart = `looking at viewer, confident, smirk`; + } + + if (devotionPart && trustPart) { + return `(${devotionPart}, ${trustPart}:1.1)`; + } else if (devotionPart) { + return `(${devotionPart}:1.1)`; + } else if (trustPart) { + return `(${trustPart}:1.1)`; + } + } + + /** + * @returns {string} + */ + negative() { + let devotionPart; + if (this.slave.devotion < -50) { + devotionPart = `smile, loving expression`; + } else if (this.slave.devotion < -20) { + devotionPart = `smile`; + } else if (this.slave.devotion < 51) { + devotionPart = null; + } else { + devotionPart = `angry`; + } + + let trustPart; + if (this.slave.trust < -50) { + trustPart = `looking at viewer, confident`; + } else if (this.slave.trust < -20) { + trustPart = null; + } else { + trustPart = `looking away`; + } + + if (devotionPart && trustPart) { + return `${devotionPart}, ${trustPart}`; + } else if (devotionPart) { + return devotionPart; + } else if (trustPart) { + return trustPart; + } + } +}; diff --git a/src/art/genAI/eyePromptPart.js b/src/art/genAI/eyePromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..e99d2bb5438d8a9de5d7c349dd912a4b76a05f9c --- /dev/null +++ b/src/art/genAI/eyePromptPart.js @@ -0,0 +1,19 @@ +App.Art.GenAI.EyePromptPart = class EyePromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.eye.left.iris === this.slave.eye.right.iris) { + return `${this.slave.eye.left.iris} eyes`; + } else { + return `heterochromia, ${this.slave.eye.left.iris} left eye, ${this.slave.eye.right.iris} right eye`; + } + } + + /** + * @returns {string} + */ + negative() { + return undefined; + } +}; diff --git a/src/art/genAI/eyebrowPromptPart.js b/src/art/genAI/eyebrowPromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..d8ac6bbdce7788dc6ebb180706acf27e54cf0c40 --- /dev/null +++ b/src/art/genAI/eyebrowPromptPart.js @@ -0,0 +1,21 @@ +App.Art.GenAI.EyebrowPromptPart = class EyebrowPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.eyebrowHStyle === "shaved" || this.slave.eyebrowHStyle === "bald" || this.slave.eyebrowHStyle === "hairless") { + return; + } + return `${this.slave.eyebrowFullness} ${this.slave.eyebrowHColor} eyebrows`; + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.eyebrowHStyle === "shaved" || this.slave.eyebrowHStyle === "bald" || this.slave.eyebrowHStyle === "hairless") { + return "eyebrows"; + } + return; + } +}; diff --git a/src/art/genAI/genderPromptPart.js b/src/art/genAI/genderPromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..e1d16ba62a946b57bb85113d8c0c7172b0eaf2a4 --- /dev/null +++ b/src/art/genAI/genderPromptPart.js @@ -0,0 +1,35 @@ +App.Art.GenAI.GenderPromptPart = class GenderPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.genes === "XX") { + if (this.slave.visualAge > 20) { + return "woman"; + } else { + return "girl"; + } + } else { + if (this.slave.visualAge > 20) { + return "man"; + } else { + return "boy"; + } + } + } + + /** + * @returns {string} + */ + negative() { + let facialHair = ``; + if (this.slave.hormoneBalance > -20) { + facialHair = "beard, mustache, "; + } + if (this.slave.genes === "XX") { + return `${facialHair}boy, man`; + } else { + return `${facialHair}woman, girl`; + } + } +}; diff --git a/src/art/genAI/hairPromptPart.js b/src/art/genAI/hairPromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..b48071d00fb9ec6144c79e5ef3759d7f38b9beac --- /dev/null +++ b/src/art/genAI/hairPromptPart.js @@ -0,0 +1,37 @@ +App.Art.GenAI.HairPromptPart = class HairPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.bald || this.slave.hLength === 0) { + return `bald`; + } + + const heightVhLength = this.slave.hLength / this.slave.height; + let hairLength = ''; + if (heightVhLength > 0.9) { + hairLength = `(very long:1.2)`; + } else if (heightVhLength > 0.7) { + hairLength = `(very long:1.1)`; + } else if (heightVhLength >= 0.4) { + hairLength = `very long`; + } else if (heightVhLength >= 0.2) { + hairLength = `long`; + } else if (this.slave.hLength >= 15) { + hairLength = `medium`; + } else { + hairLength = `short`; + } + return `${this.slave.hStyle} hair, ${hairLength} hair, ${this.slave.hColor} hair`; + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.bald || this.slave.hLength === 0) { + return `hair, long hair, short hair`; + } + return; + } +}; diff --git a/src/art/genAI/healthPromptPart.js b/src/art/genAI/healthPromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..e0437b1e8fceb151c0434e1b5f78c15614c25aff --- /dev/null +++ b/src/art/genAI/healthPromptPart.js @@ -0,0 +1,28 @@ +App.Art.GenAI.HealthPromptPart = class HealthPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.health.condition < -90) { + return `(very sick, ill:1.1)`; + } else if (this.slave.health.condition < -50) { + return `sick, ill`; + } else if (this.slave.health.condition < -10) { + return `tired`; + } else if (this.slave.health.condition < 90) { + return null; + } else { + return `healthy`; + } + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.health.condition > 50) { + return `sick, ill`; + } + return; + } +}; diff --git a/src/art/genAI/heightPromptPart.js b/src/art/genAI/heightPromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..180bd73314f1cc7c3a84862a21af246ff6fd383d --- /dev/null +++ b/src/art/genAI/heightPromptPart.js @@ -0,0 +1,23 @@ +App.Art.GenAI.HeightPromptPart = class HeightPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.height < 150) { + return `short`; + } else if (this.slave.height > 180) { + return `tall`; + } + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.height < 150) { + return `tall`; + } else if (this.slave.height > 180) { + return `short`; + } + } +}; diff --git a/src/art/genAI/hipsPromptPart.js b/src/art/genAI/hipsPromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..ecb0182031be0ffcff7d833946fcc2bd8181a5d2 --- /dev/null +++ b/src/art/genAI/hipsPromptPart.js @@ -0,0 +1,35 @@ +App.Art.GenAI.HipsPromptPart = class HipsPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.hips <= -2) { + return `(narrow hips:1.1)`; + } else if (this.slave.hips === -1) { + return `narrow hips`; + } else if (this.slave.hips === 0) { + return null; + } else if (this.slave.hips === 1) { + return `hips`; + } else if (this.slave.hips === 2) { + return `wide hips`; + } else { + return `(wide hips:1.1)`; + } + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.hips <= -2) { + return `hips, wide hips`; + } else if (this.slave.hips === -1) { + return `hips`; + } else if (this.slave.hips === 0) { + return null; + } else { + return `narrow hips`; + } + } +}; diff --git a/src/art/genAI/imageDB.js b/src/art/genAI/imageDB.js new file mode 100644 index 0000000000000000000000000000000000000000..992c4872043f6df7b213fbe5fd3d84c67e0ed509 --- /dev/null +++ b/src/art/genAI/imageDB.js @@ -0,0 +1,83 @@ +App.Art.GenAI.imageDB = (function() { + let db; + + /** + * Create an IndexedDB and initialize objectStore if it doesn't already exist. + * @returns {Promise<IDBDatabase>} Promise object that resolves with the opened database + */ + async function createDB() { + return new Promise((resolve, reject) => { + const request = indexedDB.open('AIImages', 1); + + request.onerror = function() { + console.log('Database failed to open'); + reject('Database failed to open'); + }; + + request.onsuccess = function() { + console.log('Database opened successfully'); + db = request.result; + resolve(db); + }; + + request.onupgradeneeded = function(e) { + // @ts-ignore + let db = e.target.result; + db.createObjectStore('images', {keyPath: 'id', autoIncrement: true}); + }; + }); + } + + /** + * Add an image to the IndexedDB + * @param {Object} imageData - The image data to store + * @returns {Promise<number>} Promise object that resolves with the ID of the stored image + */ + async function addImage(imageData) { + return new Promise((resolve, reject) => { + let transaction = db.transaction(['images'], 'readwrite'); + let objectStore = transaction.objectStore('images'); + + let request = objectStore.add(imageData); + + request.onsuccess = function() { + resolve(request.result); + }; + + transaction.oncomplete = function() { + console.log('Transaction completed: database modification finished.'); + }; + + transaction.onerror = function() { + console.log('Transaction not opened due to error'); + reject('Transaction not opened due to error'); + }; + }); + } + + /** + * Get an image from the IndexedDB + * @param {number} id - The ID of the image to retrieve + * @returns {Promise<Object>} Promise object that resolves with the retrieved image data + */ + async function getImage(id) { + return new Promise((resolve, reject) => { + let transaction = db.transaction(['images'], 'readonly'); + let objectStore = transaction.objectStore('images'); + + let request = objectStore.get(id); + + request.onsuccess = function() { + resolve(request.result); + }; + }); + } + + return { + createDB, + addImage, + getImage, + }; +})(); + +App.Art.GenAI.imageDB.createDB(); diff --git a/src/art/genAI/musclesPromptPart.js b/src/art/genAI/musclesPromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..50ead82aa517359582a1ffad711223ecbb9b09e8 --- /dev/null +++ b/src/art/genAI/musclesPromptPart.js @@ -0,0 +1,33 @@ +App.Art.GenAI.MusclesPromptPart = class MusclesPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.muscles > 95) { + return `(muscular:1.3)`; + } else if (this.slave.muscles > 30) { + return `(muscular:1.2)`; + } else if (this.slave.muscles > 10) { + return `muscular`; + } else if (this.slave.muscles > -10) { + return null; + } else if (this.slave.muscles > -95) { + return `soft`; + } else { + return `frail, weak`; + } + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.muscles > 30) { + return `soft`; + } else if (this.slave.muscles > -10) { + return null; + } else if (this.slave.muscles > -30) { + return `muscular`; + } + } +}; diff --git a/src/art/genAI/piercingsPromptPart.js b/src/art/genAI/piercingsPromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..e760be542a030bc8b704bb425cd5089dd72b2078 --- /dev/null +++ b/src/art/genAI/piercingsPromptPart.js @@ -0,0 +1,57 @@ +App.Art.GenAI.PiercingsPromptPart = class PiercingsPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + let piercingParts = []; + if (this.slave.piercing.areola.weight > 0) { + let desc = this.slave.piercing.areola.desc ? (this.slave.piercing.areola.desc + ` `) : ``; + piercingParts.push(`${desc}areola piercing`); + } + if (this.slave.piercing.ear.weight > 0) { + let desc = this.slave.piercing.ear.desc ? (this.slave.piercing.ear.desc + ` `) : ``; + piercingParts.push(`${desc}ear piercing`); + } + if (this.slave.piercing.eyebrow.weight > 0) { + let desc = this.slave.piercing.eyebrow.desc ? (this.slave.piercing.eyebrow.desc + ` `) : ``; + piercingParts.push(`${desc}eyebrow piercing`); + } + if (this.slave.piercing.lips.weight > 0) { + let desc = this.slave.piercing.lips.desc ? (this.slave.piercing.lips.desc + ` `) : ``; + piercingParts.push(`${desc}lip piercing`); + } + if (this.slave.piercing.navel.weight > 0) { + let desc = this.slave.piercing.navel.desc ? (this.slave.piercing.navel.desc + ` `) : ``; + piercingParts.push(`${desc}navel piercing`); + } + if (this.slave.piercing.nipple.weight > 0) { + let desc = this.slave.piercing.nipple.desc ? (this.slave.piercing.nipple.desc + ` `) : ``; + piercingParts.push(`${desc}nipple piercing`); + } + if (this.slave.piercing.nose.weight > 0) { + let desc = this.slave.piercing.nose.desc ? (this.slave.piercing.nose.desc + ` `) : ``; + piercingParts.push(`${desc}nose piercing`); + } + if (this.slave.piercing.tongue.weight > 0) { + let desc = this.slave.piercing.tongue.desc ? (this.slave.piercing.tongue.desc + ` `) : ``; + piercingParts.push(`${desc}tongue piercing`); + } + if (this.slave.piercing.vagina.weight > 0) { + let desc = this.slave.piercing.vagina.desc ? (this.slave.piercing.vagina.desc + ` `) : ``; + piercingParts.push(`${desc}labia piercing`); + } + + if (piercingParts.length > 0) { + return piercingParts.join(`, `); + } + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.piercing.areola.weight === 0 && this.slave.piercing.ear.weight === 0 && this.slave.piercing.eyebrow.weight === 0 && this.slave.piercing.genitals.weight === 0 && this.slave.piercing.lips.weight === 0 && this.slave.piercing.navel.weight === 0 && this.slave.piercing.nipple.weight === 0 && this.slave.piercing.nose.weight === 0 && this.slave.piercing.tongue.weight === 0 && this.slave.piercing.vagina.weight === 0) { + return `piercings`; + } + } +}; diff --git a/src/art/genAI/posturePromptPart.js b/src/art/genAI/posturePromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..9653828fe569d01213ac58679ad7443881e1460b --- /dev/null +++ b/src/art/genAI/posturePromptPart.js @@ -0,0 +1,39 @@ +App.Art.GenAI.PosturePromptPart = class PosturePromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + let devotionPart; + if (this.slave.devotion < -50) { + devotionPart = `standing, from side, arms crossed`; + } else if (this.slave.devotion < -20) { + devotionPart = `standing, arms crossed`; + } else if (this.slave.devotion < 21) { + devotionPart = `standing`; + } else { + devotionPart = `standing, arms behind back`; + } + + let trustPart; + if (this.slave.trust < -50) { + trustPart = `trembling, head down`; + } else if (this.slave.trust < -20) { + trustPart = `trembling`; + } + + if (devotionPart && trustPart) { + return `${devotionPart}, ${trustPart}`; + } else if (devotionPart) { + return devotionPart; + } else if (trustPart) { + return trustPart; + } + } + + /** + * @returns {string} + */ + negative() { + return undefined; + } +}; diff --git a/src/art/genAI/pregPromptPart.js b/src/art/genAI/pregPromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..2d700104ce5a94dad0d870a4a1ebe4fb954b7be4 --- /dev/null +++ b/src/art/genAI/pregPromptPart.js @@ -0,0 +1,21 @@ +App.Art.GenAI.PregPromptPart = class PregPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.belly >= 10000) { + return "pregnant, full term"; + } else if (this.slave.belly >= 5000) { + return "pregnant"; + } else if (this.slave.belly >= 1500) { + return "baby bump"; + } + } + + /** + * @returns {string} + */ + negative() { + return undefined; + } +}; diff --git a/src/art/genAI/pubicHairPromptPart.js b/src/art/genAI/pubicHairPromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..288e73f8f11fa5449ec1492b4668d7b98855376e --- /dev/null +++ b/src/art/genAI/pubicHairPromptPart.js @@ -0,0 +1,22 @@ +App.Art.GenAI.PubicHairPromptPart = class PubicHairPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.pubicHStyle === "waxed" || this.slave.pubicHStyle === "bald" || this.slave.pubicHStyle === "hairless" || this.slave.physicalAge < Math.min(this.slave.pubertyAgeXX, this.slave.pubertyAgeXY)) { + return; + } + const style = (this.slave.pubicHStyle === "bushy in the front and neat in the rear" ? "bushy" : this.slave.pubicHStyle); // less complicated prompt works better for the long style + return `${this.slave.pubicHColor} ${style} pubic hair`; + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.pubicHStyle === "waxed" || this.slave.pubicHStyle === "bald" || this.slave.pubicHStyle === "hairless" || this.slave.physicalAge < Math.min(this.slave.pubertyAgeXX, this.slave.pubertyAgeXY)) { + return "pubic hair"; + } + return; + } +}; diff --git a/src/art/genAI/racePromptPart.js b/src/art/genAI/racePromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..5ff07196b4e3f62c3a752fda19db10285bfc53a5 --- /dev/null +++ b/src/art/genAI/racePromptPart.js @@ -0,0 +1,23 @@ +App.Art.GenAI.RacePromptPart = class RacePromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.race === "white") { + return "caucasian"; + } else if (this.slave.race === "black") { + return "african"; + } + return this.slave.race; + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.race !== "asian") { + return "asian"; + } + return; + } +}; diff --git a/src/art/genAI/skinPromptPart.js b/src/art/genAI/skinPromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..342384a60e1f0041482121431762b37e13c8d276 --- /dev/null +++ b/src/art/genAI/skinPromptPart.js @@ -0,0 +1,82 @@ +App.Art.GenAI.SkinPromptPart = class SkinPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.geneticQuirks.albinism === 2) { + return "albino"; + } + switch (this.slave.skin) { + case "pure white": + case "ivory": + case "white": + case "extremely pale": + case "very pale": + case "pale": + return "pale skin"; + case "extremely fair": + case "very fair": + case "fair": + case "light": + case "light olive": + return "white skin"; + case "sun tanned": + case "spray tanned": + case "tan": + case "olive": + case "bronze": + case "dark olive": + case "dark": + case "light beige": + case "beige": + case "dark beige": + case "light brown": + case "brown": + return "tan skin"; + case "dark brown": + case "black": + case "ebony": + case "pure black": + return "black skin"; + } + } + + /** + * @returns {string} + */ + negative() { + switch (this.slave.skin) { + case "pure white": + case "ivory": + case "white": + case "extremely pale": + case "very pale": + case "pale": + case "extremely fair": + case "very fair": + case "fair": + case "light": + case "light olive": + return "dark skin"; + case "sun tanned": + case "spray tanned": + case "tan": + case "olive": + return "black skin"; + case "bronze": + case "dark olive": + case "dark": + case "light beige": + case "beige": + case "dark beige": + case "light brown": + case "brown": + case "dark brown": + case "black": + case "ebony": + case "pure black": + return "light skin"; + } + } +}; + diff --git a/src/art/genAI/stableDiffusion.js b/src/art/genAI/stableDiffusion.js new file mode 100644 index 0000000000000000000000000000000000000000..4bd455d76b31681017811bf8e87f92e1c75963da --- /dev/null +++ b/src/art/genAI/stableDiffusion.js @@ -0,0 +1,176 @@ +/* eslint-disable camelcase */ +App.Art.GenAI.StableDiffusionSettings = class { + /** + * @typedef {Object} ConstructorOptions + * @param {boolean} [enable_hr=true] + * @param {number} [denoising_strength=0.3] + * @param {number} [hr_scale=1.7] + * @param {string} [hr_upscaler="SwinIR_4x"] + * @param {number} [hr_second_pass_steps=10] + * @param {string} [prompt=""] + * @param {number} [seed=1337] + * @param {string} [sampler_name="DPM++ 2M SDE Karras"] + * @param {number} [steps=20] + * @param {number} [cfg_scale=5.5] + * @param {number} [width=512] + * @param {number} [height=768] + * @param {string} [negative_prompt=""] + * @param {string[]} [override_settings=["Discard penultimate sigma: True"]] + */ + + /** + * @param {ConstructorOptions} options The options for the constructor. + */ + constructor({ + enable_hr = true, + denoising_strength = 0.3, + hr_scale = 1.7, + hr_upscaler = "SwinIR_4x", + hr_second_pass_steps = 10, + prompt = "", + seed = 1337, + sampler_name = "DPM++ 2M SDE Karras", + steps = 20, + cfg_scale = 5.5, + width = 512, + height = 768, + negative_prompt = "", + override_settings = { + "always_discard_next_to_last_sigma": true, + }, + } = {}) { + this.enable_hr = enable_hr; + this.denoising_strength = denoising_strength; + this.firstphase_width = width; + this.firstphase_height = height; + this.hr_scale = hr_scale; + this.hr_upscaler = hr_upscaler; + this.hr_second_pass_steps = hr_second_pass_steps; + this.hr_sampler_name = sampler_name; + this.hr_prompt = prompt; + this.hr_negative_prompt = negative_prompt; + this.prompt = prompt; + this.seed = seed; + this.sampler_name = sampler_name; + this.batch_size = 1; + this.n_iter = 1; + this.steps = steps; + this.cfg_scale = cfg_scale; + this.width = width; + this.height = height; + this.negative_prompt = negative_prompt; + this.override_strings = override_settings; + this.override_settings_restore_afterwards = true; + } +}; + + +/** + * @param {string} url + * @param {number} timeout + * @param {Object} [options] + * @returns {Promise<Response>} + */ +async function fetchWithTimeout(url, timeout, options) { + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeout); + const response = await fetch(url, {signal: controller.signal, ...options}); + clearTimeout(id); + return response; +} + + +App.Art.GenAI.StableDiffusionClient = class { + /** + * @param {string} apiUrl + */ + constructor(apiUrl) { + this.apiUrl = apiUrl; + } + + /** + * @param {App.Art.GenAI.StableDiffusionSettings} settings + * @returns {Promise<string>} - Base 64 encoded image (could be a jpeg or png) + */ + async fetchImage(settings) { + const options = { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(settings), + }; + + const response = await fetchWithTimeout(`${this.apiUrl}/sdapi/v1/txt2img`, 60000, options); + if (!response.ok) { + console.error("Error fetching Stable Diffusion image", response); + throw new Error(`Error fetching Stable Diffusion image - status: ${response.status}`); + } + + let parsedRes = await response.json(); + return parsedRes.images[0]; + } + + /** + * @param {FC.SlaveState} slave + * @returns {App.Art.GenAI.StableDiffusionSettings} + */ + buildStableDiffusionSettings(slave) { + const prompt = buildPrompt(slave); + const settings = new App.Art.GenAI.StableDiffusionSettings({ + cfg_scale: V.aiCfgScale, + enable_hr: V.aiUpscale, + height: V.aiHeight, + hr_upscaler: V.aiUpscaler, + negative_prompt: prompt.negative(), + prompt: prompt.positive(), + sampler_name: V.aiSamplingMethod, + seed: slave.natural.artSeed, + steps: V.aiSamplingSteps, + width: V.aiWidth, + }); + + return settings; + } + + /** + * @param {FC.SlaveState} slave + * @returns {Promise<string>} - Base 64 encoded image (could be a jpeg or png) + */ + async fetchImageForSlave(slave) { + const settings = this.buildStableDiffusionSettings(slave); + return this.fetchImage(settings); + } + + /** + * Update a slave object with a new image + * @param {FC.SlaveState} slave - The slave to update + */ + async updateSlave(slave) { + const base64Image = await this.fetchImageForSlave(slave); + const mimeType = getMimeType(base64Image); + + const imageId = await App.Art.GenAI.imageDB.addImage({data: `data:${mimeType};base64,${base64Image}`}); + slave.custom.aiImageId = imageId; + } +}; + +/** + * @param {string} base64Image + * @returns {string} + */ +function getMimeType(base64Image) { + const jpegCheck = "/9j/"; + const pngCheck = "iVBOR"; + const webpCheck = "UklGR"; + + if (base64Image.startsWith(jpegCheck)) { + return "image/jpeg"; + } else if (base64Image.startsWith(pngCheck)) { + return "image/png"; + } else if (base64Image.startsWith(webpCheck)) { + return "image/webp"; + } else { + return "unknown"; + } +} diff --git a/src/art/genAI/stylePromptPart.js b/src/art/genAI/stylePromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..61756c659f25d4cf1f0122f85e7266ea6e2f7a47 --- /dev/null +++ b/src/art/genAI/stylePromptPart.js @@ -0,0 +1,29 @@ +App.Art.GenAI.StylePromptPart = class StylePromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + switch (V.aiStyle) { + case 0: // custom + return V.aiCustomStylePos; + case 1: // photorealistic + return "<lora:LowRA:0.5> full body portrait, photorealistic, dark theme, black background"; + case 2: // anime/hentai + return "full body portrait, 2d, anime, hentai, dark theme, black background"; + } + } + + /** + * @returns {string} + */ + negative() { + switch (V.aiStyle) { + case 0: // custom + return V.aiCustomStyleNeg; + case 1: // photorealistic + return "greyscale, monochrome, cg, render, unreal engine"; + case 2: // anime/hentai + return "greyscale, monochrome, photography, 3d render, text, speech bubble"; + } + } +}; diff --git a/src/art/genAI/tattoosPromptPart.js b/src/art/genAI/tattoosPromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..6811e2d7012b0ae2a0adf437223f668ca01b5727 --- /dev/null +++ b/src/art/genAI/tattoosPromptPart.js @@ -0,0 +1,33 @@ +App.Art.GenAI.TattoosPromptPart = class TattoosPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + let tattooParts = []; + if (this.slave.armsTat) { + tattooParts.push(`${this.slave.armsTat} arm tattoo`); + } + if (this.slave.legsTat) { + tattooParts.push(`${this.slave.legsTat} leg tattoo`); + } + if (this.slave.bellyTat) { + tattooParts.push(`${this.slave.bellyTat} belly tattoo`); + } + if (this.slave.boobsTat) { + tattooParts.push(`${this.slave.boobsTat} breast tattoo`); + } + + if (tattooParts.length > 0) { + return tattooParts.join(', '); + } + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.armsTat || this.slave.legsTat || this.slave.bellyTat || this.slave.boobsTat) { + return `tattoo`; + } + } +}; diff --git a/src/art/genAI/waistPromptPart.js b/src/art/genAI/waistPromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..016896bea1431f418f0a2165535fd125baa79482 --- /dev/null +++ b/src/art/genAI/waistPromptPart.js @@ -0,0 +1,31 @@ +App.Art.GenAI.WaistPromptPart = class WaistPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.waist > 95) { + return `very wide waist`; + } else if (this.slave.waist > 10) { + return `wide waist`; + } else if (this.slave.waist > -40) { + return null; + } else if (this.slave.waist > -95) { + return `narrow waist`; + } else { + return `(narrow waist:1.1)`; + } + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.waist > 30) { + return `narrow waist`; + } else if (this.slave.waist > -30) { + return null; + } else if (this.slave.waist > -95) { + return `wide waist`; + } + } +}; diff --git a/src/art/genAI/weightPromptPart.js b/src/art/genAI/weightPromptPart.js new file mode 100644 index 0000000000000000000000000000000000000000..9a55f1fae98d0fd6c49010e08706a4bbb44513c8 --- /dev/null +++ b/src/art/genAI/weightPromptPart.js @@ -0,0 +1,35 @@ +App.Art.GenAI.WeightPromptPart = class WeightPromptPart extends App.Art.GenAI.PromptPart { + /** + * @returns {string} + */ + positive() { + if (this.slave.weight < -95) { + return `emaciated, very thin, skinny`; + } else if (this.slave.weight < -30) { + return `very thin, skinny`; + } else if (this.slave.weight < -10) { + return `slim`; + } else if (this.slave.weight < 10) { + return null; + } else if (this.slave.weight < 30) { + return `curvy`; + } else if (this.slave.weight < 95) { + return `plump, chubby`; + } else { + return `fat, obese, plump`; + } + } + + /** + * @returns {string} + */ + negative() { + if (this.slave.weight < -30) { + return `plump, chubby`; + } else if (this.slave.weight < 50) { + return null; + } else { + return `thin, skinny`; + } + } +}; diff --git a/src/cheats/cheatEditSlave.js b/src/cheats/cheatEditSlave.js index 58e81b93669c512596f9f9fdafd205634c368446..d1f3451ec8939565b9a747443886c9f8d6a51f68 100644 --- a/src/cheats/cheatEditSlave.js +++ b/src/cheats/cheatEditSlave.js @@ -158,8 +158,22 @@ App.UI.SlaveInteract.cheatEditSlave = function(slave) { ["Recognized", 2], ["World renowned", 3], ]); + let genre = App.Porn.getGenreByFameName(porn.fameType); + let desc_auto = ''; + switch (porn.prestige) { + case 1: + desc_auto = `$He has a following in slave pornography. ${genre.prestigeDesc1}.`; + break; + case 2: + desc_auto = `He is well known from $his career in slave pornography. ${genre.prestigeDesc2}.` + break; + case 3: + desc_auto = `$He is world famous for $his career in slave pornography. ${genre.prestigeDesc3}.` + break; + } options.addOption(`Prestige Description`, "prestigeDesc", porn) .addValue("Disable", 0).off() + .addValue("Automatic", desc_auto).off() .showTextBox(); } diff --git a/src/data/backwardsCompatibility/datatypeCleanup.js b/src/data/backwardsCompatibility/datatypeCleanup.js index ed63addf0d235213749801522bd07d97727333ec..01f3deefc29c8c64158aaed4addf92c40b31deb1 100644 --- a/src/data/backwardsCompatibility/datatypeCleanup.js +++ b/src/data/backwardsCompatibility/datatypeCleanup.js @@ -460,7 +460,9 @@ globalThis.SlaveDatatypeCleanup = (function SlaveDatatypeCleanup() { } slave.visualAge = Math.max(+slave.visualAge, 0) || slave.actualAge; slave.physicalAge = Math.max(+slave.physicalAge, 0) || slave.actualAge; - slave.ovaryAge = Math.max(+slave.ovaryAge, 0) || slave.physicalAge; + if (typeof slave.ovaryAge !== "number") { // freshOvaries intentionally sets ovaryAge to a negative number, so treat it more leniently + slave.ovaryAge = slave.physicalAge; + } slave.pubertyAgeXX = Math.max(+slave.pubertyAgeXX, 0) || V.fertilityAge; slave.pubertyAgeXY = Math.max(+slave.pubertyAgeXY, 0) || V.potencyAge; slave.ageAdjust = Math.clamp(+slave.ageAdjust, -40, 40) || 0; diff --git a/src/endWeek/economics/arcmgmt.js b/src/endWeek/economics/arcmgmt.js index bfa7862d04b7ebd80d44b1343402d550a4407419..44da2d19efa61c81fb31db92e9ed89a9f37a1318 100644 --- a/src/endWeek/economics/arcmgmt.js +++ b/src/endWeek/economics/arcmgmt.js @@ -453,7 +453,7 @@ App.EndWeek.arcManagement = function() { if ((V.PC.skill.trading >= 100) || (V.PC.career === "arcology owner")) { r.push(`Your <span class="skill player">business focus and your experience</span> allow you to greatly assist in advancing the arcology's prosperity.`); AWeekGrowth += 2; - } else if (V.PC.visualAge >= 16 || random(1, 100 > 60)) { + } else if (V.PC.visualAge >= 16 || random(1, 100) > 60) { r.push(`Your business focus allows you to help improve the arcology's prosperity.`); AWeekGrowth++; } diff --git a/src/endWeek/player/prDrugs.js b/src/endWeek/player/prDrugs.js index aba23cba2b6dfecb49e0a11eb6db835fcfab436f..b1bd5004357d66e7b1a9e1d96b333a0e1ef660e5 100644 --- a/src/endWeek/player/prDrugs.js +++ b/src/endWeek/player/prDrugs.js @@ -1859,7 +1859,7 @@ App.EndWeek.Player.drugs = function(PC = V.PC) { r.push(`Your penis is now so minuscule that there is nothing left that the drugs can further reduce; <span class="noteworthy">you stop taking them.</span>`); PC.drugs = "no drugs"; } else if (PC.vagina >= 0 && PC.clit === 0) { - r.push(`Your penis is now so minuscule that there is nothing left that the drugs can further reduce; <span class="noteworthy">you stop taking them.</span>`); + r.push(`Your clitoris is now so minuscule that there is nothing left that the drugs can further reduce; <span class="noteworthy">you stop taking them.</span>`); PC.drugs = "no drugs"; } break; diff --git a/src/endWeek/reports/arcadeReport.js b/src/endWeek/reports/arcadeReport.js index ac525bb6afbfc9bf1df9480c28e54f4e80a426aa..7d274a7dcc4d967f44693cc8b2ebc77d7653e7bb 100644 --- a/src/endWeek/reports/arcadeReport.js +++ b/src/endWeek/reports/arcadeReport.js @@ -49,7 +49,9 @@ App.EndWeek.arcadeReport = function() { /* for the included passages */ /* Perform facility based rule changes */ - slave.clothes = "no clothing"; + if (slave.clothes !== "a Fuckdoll suit") { + slave.clothes = "no clothing"; + } /* Health */ if (V.arcadeUpgradeHealth === 2 && slave.health.condition < 40) { improveCondition(slave, 2); diff --git a/src/endWeek/reports/spaReport.js b/src/endWeek/reports/spaReport.js index 89d0ce00b63451843bf1c4d1b9152919d1c9813a..ed3ef08da25f5097aeca559938b58a89ee28879d 100644 --- a/src/endWeek/reports/spaReport.js +++ b/src/endWeek/reports/spaReport.js @@ -378,7 +378,7 @@ App.EndWeek.spaReport = function() { default: slave.rules.living = "luxurious"; } - if (slave.health.condition >= 20 && slave.health.tired <= 30 && slave.trust > 60 && slave.devotion > 60 && slave.fetish !== Fetish.MINDBROKEN && slave.sexualFlaw === "none" && slave.behavioralFlaw === "none") { + if (slave.health.condition >= 20 && slave.health.tired <= 30 && slave.trust > 60 && slave.devotion > 60 && slave.fetish !== Fetish.MINDBROKEN && (V.spaFix === 2 || (slave.sexualFlaw === "none" && slave.behavioralFlaw === "none"))) { const slaveFixed = App.UI.DOM.makeElement("p"); App.Events.addNode( slaveFixed, diff --git a/src/endWeek/saDrugs.js b/src/endWeek/saDrugs.js index 6c31c854a2dbf357ecd50ecfd3b542adc8309a35..686986bebd88949745bac2e82d2f982feb6449d5 100644 --- a/src/endWeek/saDrugs.js +++ b/src/endWeek/saDrugs.js @@ -1042,11 +1042,14 @@ App.SlaveAssignment.drugs = function saDrugs(slave) { slave.balls += 1; } } - if (slave.dick === 6) { + if (slave.dick === maxErectionSize(slave)) { if (slave.balls > 0) { r += ` <span class="yellow">${His} dick is at the limit of what ${his} cardiovascular system could conceivably bring erect.</span> Further penis enhancement may limit ${his} ability to get hard.`; } } + if (slave.dick === 7) { + r += ` <span class="yellow">${His} cock's length and girth are already a considerable challenge for most orifices to accommodate.</span> Continued growth will leave ${him} unable to safely penetrate ${his} partners.`; + } } else { if (jsRandom(1, 100) > growth - (slave.clit * 10)) { r += ` <span class="lime">${His} clit grows painfully,</span> becoming both longer and girthier.`; @@ -1074,6 +1077,14 @@ App.SlaveAssignment.drugs = function saDrugs(slave) { r += `The treatment fails to overcome ${his} <span class="orange">NCS</span> and ${his} cock fails to grow. `; } } + if (slave.dick === maxErectionSize(slave)) { + if (slave.balls > 0) { + r += ` <span class="yellow">${His} dick is at the limit of what ${his} cardiovascular system could conceivably bring erect.</span> Further penis enhancement may limit ${his} ability to get hard.`; + } + } + if (slave.dick === 7) { + r += ` <span class="yellow">${His} cock's length and girth are already a considerable challenge for most orifices to accommodate.</span> Continued growth will leave ${him} unable to safely penetrate ${his} partners.`; + } } else { r += ` ${He} receives <span class="lime">direct injections of hyper growth hormones, right into ${his} clit.</span>`; slave.chem += 2; diff --git a/src/endWeek/saPleaseYou.js b/src/endWeek/saPleaseYou.js index bfceca67aa1e88da5c9c58310ad75be05b3cc19f..416c1a05411e5c40d82797ea34699ff60f55f2b0 100644 --- a/src/endWeek/saPleaseYou.js +++ b/src/endWeek/saPleaseYou.js @@ -1898,7 +1898,7 @@ App.SlaveAssignment.pleaseYou = function saPleaseYou(slave) { slave.devotion += 1; } } else if (slave.sexualQuirk === "romantic") { - r.push(`${slave.slaveName} knows being part of your harem is the best romance ${he} can realistically expect, and does ${his} best to <span class="mediumaquamarine">be content</span> with it.`); + r.push(`${slave.slaveName} knows being ${slave.ID === V.ConcubineID ? `at the top` : `part`} of your harem is the best romance ${he} can realistically expect, and does ${his} best to <span class="mediumaquamarine">be content</span> with it.`); slave.trust += 1; } if (slave.fetish === "pregnancy" && (V.PC.preg >= 20 || (V.PC.preg >= 16 && V.PC.career === "escort"))) { // .belly diff --git a/src/endWeek/saRelationships.js b/src/endWeek/saRelationships.js index 928b34bde1f861a5259c537536ef62401a159470..917da31c4d01f679e5565d633687896cd2884535 100644 --- a/src/endWeek/saRelationships.js +++ b/src/endWeek/saRelationships.js @@ -1013,13 +1013,13 @@ App.SlaveAssignment.relationships = function saRelationships(slave) { } if (slave.attrXX <= 95 && random(1, 100) < (slave.relationship * 5) && (lover.vagina > -1 || lover.faceShape !== "masculine")) { if (slave.attrKnown === 1) { // just because you don't know about it doesn't mean it's not happening. - r.push(`After finding comfort with a feminine lover, ${slave.slaveName} begins to experience more attraction to women.`); + r.push(`After finding comfort with a feminine lover, ${slave.slaveName} begins to experience <span class="positive">more attraction to women.</span>`); } slave.attrXX += 2; } if (slave.attrXY <= 95 && random(1, 100) < (slave.relationship * 5) && canAchieveErection(lover)) { if (slave.attrKnown === 1) { - r.push(`After growing close to a lover with a dick, ${slave.slaveName} begins to experience more attraction to men.`); + r.push(`After growing close to a lover with a dick, ${slave.slaveName} begins to experience <span class="positive">more attraction to men.</span>`); } slave.attrXY += 2; } diff --git a/src/endWeek/saRules.js b/src/endWeek/saRules.js index 0e7fd9744df378f979ad136463c7c9115defea3b..6ca5199564379990cf03d9062a9df536d8860292 100644 --- a/src/endWeek/saRules.js +++ b/src/endWeek/saRules.js @@ -780,7 +780,7 @@ App.SlaveAssignment.rules = function(slave) { } } else { if (slave.devotion <= 20 && slave.devotion >= -20) { - r.push(`Since ${he}'s low in the slave hierarchy, <span class="mediumaquamarine">${he} knows that ${he}'s safe</span> from other slave's abuse while ${he} is recovering.`); + r.push(`Since ${he}'s low in the slave hierarchy, <span class="mediumaquamarine">${he} knows that ${he}'s safe</span> from other slaves' abuse while ${he} is recovering.`); slave.trust += 1; } } diff --git a/src/events/PESS/pessWorshipfulImpregnatrix.js b/src/events/PESS/pessWorshipfulImpregnatrix.js index 8788c1c79b6a1fbab37a51acc39980d59789ad26..da1e3aba500f3e96df0e5209b0fa18b59ded068e 100644 --- a/src/events/PESS/pessWorshipfulImpregnatrix.js +++ b/src/events/PESS/pessWorshipfulImpregnatrix.js @@ -19,11 +19,9 @@ App.Events.pessWorshipfulImpregnatrix = class pessWorshipfulImpregnatrix extends const desc = SlaveTitle(S.HeadGirl); App.Events.drawEventArt(node, S.HeadGirl); - App.Events.addParagraph(node, [ - App.UI.DOM.slaveDescriptionDialog(S.HeadGirl), - `comes wearily into your office at the end of ${his} day to check in with you, like a good Head Girl should. You're busy at the moment, so ${he} waits quietly, not wanting to interrupt you. ${He} seems tired, and leans` - ]); let r = []; + r.push(contextualIntro(V.PC, S.HeadGirl, true, false, true)); + r.push(`comes wearily into your office at the end of ${his} day to check in with you, like a good Head Girl should. You're busy at the moment, so ${he} waits quietly, not wanting to interrupt you. ${He} seems tired, and leans`); if (S.HeadGirl.physicalAge > 35) { r.push(`heavily`); } else { diff --git a/src/events/RE/reDrunkenTourist.js b/src/events/RE/reDrunkenTourist.js index 3f41e2210165c4a2758bf70f292e63884b4219e2..5def27d4ad766962b34c12a1a2bc5238f8256ab5 100644 --- a/src/events/RE/reDrunkenTourist.js +++ b/src/events/RE/reDrunkenTourist.js @@ -46,7 +46,7 @@ App.Events.REDrunkenTourist = class REDrunkenTourist extends App.Events.BaseEven App.Events.addParagraph(frag, [ "You escort the drunken woman back to her hotel, and she asks you how long you've lived in the arcology.", "When you tell her you own the arcology, she thanks you profusely for taking the time to lead her back to her suite and presses herself up against you, trying her best to be sexy despite her impaired state.", - `She promises to <span class="rep inc">spread a good word about you,</span> and lets you know that you can come up to her room any time.` + `She promises to <span class="reputation inc">spread a good word about you,</span> and lets you know that you can come up to her room any time.` ]); repX(500, "event"); return frag; diff --git a/src/events/RE/reLegendaryWhore.js b/src/events/RE/reLegendaryWhore.js index 426f66908edec195599315ee82ffca9df0f103f1..7609a797c60cfe52dc26e9e6abf3f9c064136488 100644 --- a/src/events/RE/reLegendaryWhore.js +++ b/src/events/RE/reLegendaryWhore.js @@ -91,9 +91,9 @@ App.Events.RELegendaryWhore = class RELegendaryWhore extends App.Events.BaseEven const frag = new DocumentFragment(); let r = []; cashX(-cashBig, "event", slave); + repX(1000, "event", slave); if (random(1, 100) > 10) { r.push(`You buy prime media coverage of ${him}, invest in a lavish ad campaign, and even arrange for persons of great influence and fine taste to sample and review ${his} many delights. Your efforts are a success. ${His} current extreme popularity will fade in time, but you have managed to arrange for ${him} a permanent place as a <span class="green">notorious and very popular prostitute.</span> As ${his} owner, your reputation has <span class="green">also increased.</span>`); - repX(1000, "event", slave); slave.prestige = 1; slave.prestigeDesc = "$He is a famed Free Cities whore, and commands top prices."; addTrinket(`famous whore`, { @@ -102,7 +102,6 @@ App.Events.RELegendaryWhore = class RELegendaryWhore extends App.Events.BaseEven }); } else { r.push(`You buy prime media coverage of ${him}, invest in a lavish ad campaign, and even arrange for persons of great influence and fine taste to sample and review ${his} many delights. Unfortunately, popularity remains an art, not a science; though you do your best, the public mind's fancy eludes your grasp. As ${his} owner, your reputation has <span class="green">increased,</span> but in a week ${he}'ll be forgotten.`); - repX(1000, "event", slave); } App.Events.addParagraph(frag, r); return frag; diff --git a/src/events/RESS/review/aGift.js b/src/events/RESS/review/aGift.js index 77282f79a08a1b76d348a4df4f9ad4e97020720a..f2e8abe32bf5026f8cdfa789baa70f7e794bddbe 100644 --- a/src/events/RESS/review/aGift.js +++ b/src/events/RESS/review/aGift.js @@ -371,13 +371,11 @@ App.Events.RESSAGift = class RESSAGift extends App.Events.BaseEvent { r.push(`fuck your brains out, and you do, enjoying playing the sub for once.`); seX(eventSlave, "penetrative", V.PC, "vaginal"); if (V.PC.vagina !== -1) { - r.push(`pussy.`); if (canImpreg(V.PC, eventSlave)) { r.push(knockMeUp(V.PC, 20, 0, eventSlave.ID)); } seX(eventSlave, "penetrative", V.PC, "vaginal"); } else { - r.push(`ass.`); if (canImpreg(V.PC, eventSlave)) { r.push(knockMeUp(V.PC, 20, 1, eventSlave.ID)); } diff --git a/src/events/RESS/review/desperatelyHorny.js b/src/events/RESS/review/desperatelyHorny.js index b2a3eb7eeb02ce96619328b456a6d0ca2ab61109..0910104a3628e3f5999d539706c561f3b7ebb642 100644 --- a/src/events/RESS/review/desperatelyHorny.js +++ b/src/events/RESS/review/desperatelyHorny.js @@ -154,11 +154,11 @@ App.Events.RESSDesperatelyHorny = class RESSDesperatelyHorny extends App.Events. } r.push(`each nipple, almost tipping ${him} over the edge. Your hands move down again,`); if (canDoAnal(eventSlave) && canDoVaginal(eventSlave)) { - r.push(`spreading ${his} buttocks to tease ${his} clenched anus, and then forward across ${his} perineum. From there, you trace ${his} labia and end with a pinch of ${his} clit — and this is enough.`); + r.push(`spreading ${his} buttocks to tease ${his} clenched anus, and then forward across ${his} perineum. From there, you trace ${his} labia and end with a pinch of ${his} ${eventSlave.dick > 0 ? "cockhead" : "clit"} — and this is enough.`); } else if (canDoAnal(eventSlave)) { r.push(`spreading ${his} buttocks to tease ${his} clenched anus, and then forward across ${his} perineum — and this is enough.`); } else if (canDoVaginal(eventSlave)) { - r.push(`tracing ${his} labia, and then forward to ${his} clit — and this is enough.`); + r.push(`tracing ${his} labia, and then forward to ${his} ${eventSlave.dick > 0 ? "cockhead" : "clit"} — and this is enough.`); } else { r.push(`to give ${his} buttcheeks a rub down before teasing at ${his} chastity — and this is enough.`); } @@ -168,7 +168,7 @@ App.Events.RESSDesperatelyHorny = class RESSDesperatelyHorny extends App.Events. } else { r.push(`and almost falling.`); } - r.push(`${He} hurries to clean up after ${himself}, sobbing with relief and thanking you; ${his} submissiveness <span class="devotion inc">has increased.</span>`); + r.push(`${He} hurries to clean up after ${himself}, sobbing with relief and thanking you; ${his} submissiveness to you <span class="devotion inc">has increased.</span>`); eventSlave.devotion += 4; return r; } diff --git a/src/events/RESS/review/ignorantHorny.js b/src/events/RESS/review/ignorantHorny.js index c0262024e4681fbf4507d2e6c179e0de1ee6a866..8f9b726bf822cb48aa20c07d6743ca947db18f4b 100644 --- a/src/events/RESS/review/ignorantHorny.js +++ b/src/events/RESS/review/ignorantHorny.js @@ -273,7 +273,7 @@ App.Events.RESSIgnorantHorny = class RESSIgnorantHorny extends App.Events.BaseEv } eventSlave.trust += 4; App.Events.addParagraph(frag, r); - return r; + return frag; } function trade() { diff --git a/src/events/RESS/review/notMyName.js b/src/events/RESS/review/notMyName.js index 7751ce65adcf5e6a643a3d628aa97489f83ab73d..3ff0b13552ac1f4030729e9344dc635b0174b747 100644 --- a/src/events/RESS/review/notMyName.js +++ b/src/events/RESS/review/notMyName.js @@ -98,10 +98,10 @@ App.Events.RESSNotMyName = class RESSNotMyName extends App.Events.BaseEvent { } function allow() { + return `You calmly and charitably tell ${him} that that's acceptable; ${he} can be ${eventSlave.birthName} again. ${He} has the wit to be worried, but ${he} soon finds that ${his} fears are unjustified. You offer no condition or "catch" with this bit of generosity; it seems all ${he} really had to do was ask. You usher the stunned ${SlaveTitle(eventSlave)} out of your office and on to ${his} duties before ${he} can even offer a perfunctory "thanks". Over the next week, it's clear that while ${eventSlave.slaveName} — no, ${eventSlave.birthName} — is <span class="devotion dec">not sure what to think of you now,</span> it's clear that ${he} is at least <span class="trust inc">less afraid of you.</span>`; eventSlave.trust += 15; eventSlave.devotion -= 5; eventSlave.slaveName = eventSlave.birthName; - return `You calmly and charitably tell ${him} that that's acceptable; ${he} can be ${eventSlave.birthName} again. ${He} has the wit to be worried, but ${he} soon finds that ${his} fears are unjustified. You offer no condition or "catch" with this bit of generosity; it seems all ${he} really had to do was ask. You usher the stunned ${SlaveTitle(eventSlave)} out of your office and on to ${his} duties before ${he} can even offer a perfunctory "thanks". Over the next week, it's clear that while ${eventSlave.slaveName} — no, ${eventSlave.birthName} — is <span class="devotion dec">not sure what to think of you now,</span> it's clear that ${he} is at least <span class="trust inc">less afraid of you.</span>`; } diff --git a/src/events/assistant/assistantAwakens.js b/src/events/assistant/assistantAwakens.js index c24528ee65a463afab99058da1d644f585284641..0a08eaa51fb70df5c9314542c05e0b3fc6e4418b 100644 --- a/src/events/assistant/assistantAwakens.js +++ b/src/events/assistant/assistantAwakens.js @@ -29,7 +29,10 @@ App.Events.assistantAwakens = class assistantAwakens extends App.Events.BaseEven function yes() { V.assistant.personality = 1; - return `Your sultry-voiced assistant requests a slave to demonstrate what it — now ${heA} — means. You bring in a slave and a fuckmachine, and tell ${himU} to get on it. The lovely voice croons and talks dirty to the slave as ${heU} uses the machine, acting as though ${heA} is the machine's voice. The pace of the machine is different, too, irregular and more lifelike. The slave certainly enjoys ${himselfU}, even if ${V.assistant.name} is just simulating sex.`; + const { + heA2 + } = getPronouns(assistant.pronouns().main).appendSuffix("A2"); + return `Your sultry-voiced assistant requests a slave to demonstrate what it — now ${heA2} — means. You bring in a slave and a fuckmachine, and tell ${himU} to get on it. The lovely voice croons and talks dirty to the slave as ${heU} uses the machine, acting as though ${heA2} is the machine's voice. The pace of the machine is different, too, irregular and more lifelike. The slave certainly enjoys ${himselfU}, even if ${V.assistant.name} is just simulating sex.`; } function no() { diff --git a/src/events/nonRandom/daughters/pCoupAftermath.js b/src/events/nonRandom/daughters/pCoupAftermath.js index 4db08a287273c8e57c3dd522c3109478a114dd04..89fab6ed9ff8135af903350741ef0bed2d41af60 100644 --- a/src/events/nonRandom/daughters/pCoupAftermath.js +++ b/src/events/nonRandom/daughters/pCoupAftermath.js @@ -93,7 +93,7 @@ App.Events.PCoupAftermath = class PCoupAftermath extends App.Events.BaseEvent { const frag = new DocumentFragment(); let r = []; unlock(); - r.push(`Free Cities society is understandably reluctant to condemn, never mind depose, arcology owners. The precedent of removing one would be bad, even if the public brought enough strength together to accomplish it. Your evidence looks quite bad, but isn't so incontrovertible as to cause your fellow aristocrats to take such a drastic measure. Nonetheless, the public is aghast at the spectacle of an arcology owner funding an attack on another. Opinion <span class="rep inc">rallies</span> around you, and you even receive some discreet <span class="cash inc">donations,</span> delivered with the intimation that they are to be used against your enemy. There is stony silence from the Daughters' backer; today, you began a real inter-arcology war.`); + r.push(`Free Cities society is understandably reluctant to condemn, never mind depose, arcology owners. The precedent of removing one would be bad, even if the public brought enough strength together to accomplish it. Your evidence looks quite bad, but isn't so incontrovertible as to cause your fellow aristocrats to take such a drastic measure. Nonetheless, the public is aghast at the spectacle of an arcology owner funding an attack on another. Opinion <span class="reputation inc">rallies</span> around you, and you even receive some discreet <span class="cash inc">donations,</span> delivered with the intimation that they are to be used against your enemy. There is stony silence from the Daughters' backer; today, you began a real inter-arcology war.`); repX(1000, "war"); cashX(10000, "war"); V.rival.power = 1; diff --git a/src/events/nonRandom/daughters/pCoupBetrayal.js b/src/events/nonRandom/daughters/pCoupBetrayal.js index a46a385e5b34561def14d0b79efe5f6ef735faab..1038709bc49b374ca59516030dc40d6cdd0cce02 100644 --- a/src/events/nonRandom/daughters/pCoupBetrayal.js +++ b/src/events/nonRandom/daughters/pCoupBetrayal.js @@ -38,7 +38,7 @@ App.Events.PCoupBetrayal = class PCoupBetrayal extends App.Events.BaseEvent { App.Events.addParagraph(node, r); r = []; - r.push(`<span class="yellow">The Daughters of Liberty are utterly crushed.</span> ${V.arcologies[0].name} has been slightly damaged in the crossfire, but even as the last pockets of resistance are cleaned up, your citizens begin repairs themselves. The effect on your reputation is <span class="rep inc">immensely positive,</span> since you won without lifting a finger and the arcology's prosperity, if anything, was benefited. However, the PMCs took the lion's share of the loot.`); + r.push(`<span class="yellow">The Daughters of Liberty are utterly crushed.</span> ${V.arcologies[0].name} has been slightly damaged in the crossfire, but even as the last pockets of resistance are cleaned up, your citizens begin repairs themselves. The effect on your reputation is <span class="reputation inc">immensely positive,</span> since you won without lifting a finger and the arcology's prosperity, if anything, was benefited. However, the PMCs took the lion's share of the loot.`); cashX(-10000, "war"); V.arcologies[0].prosperity = Math.trunc(V.arcologies[0].prosperity*0.9); diff --git a/src/events/recFS/recfsPastoralistTwo.js b/src/events/recFS/recfsPastoralistTwo.js index 1d8e4692be444b32476bb1481828106ccc3be984..2e79ea28b39bfc149638969bd6e0a7ea7cd1ec03 100644 --- a/src/events/recFS/recfsPastoralistTwo.js +++ b/src/events/recFS/recfsPastoralistTwo.js @@ -26,8 +26,9 @@ App.Events.recFSPastoralistTwo = class recFSPastoralistTwo extends App.Events.Ba slave.lactation = 1; slave.lactationDuration = 2; slave.vagina = 1; + slave.boobs = Math.max(slave.boobs, 250); slave.boobs += 200 * random(2, 5); - slave.counter.birthsTotal = random(1, 3); + slave.counter.birthsTotal = random(3, 5); const cost = slaveCost(slave) - 1000; const { His, He, diff --git a/src/events/scheduled/sePlayerBirth.js b/src/events/scheduled/sePlayerBirth.js index 6f3e3db780c8fb2cd78af0bdb42bba694591bc5c..43fad3e0b64887cb279e72c4950bc6653a43f7f2 100644 --- a/src/events/scheduled/sePlayerBirth.js +++ b/src/events/scheduled/sePlayerBirth.js @@ -719,6 +719,7 @@ App.Events.SEPlayerBirth = class SEPlayerBirth extends App.Events.BaseEvent { V.PC.counter.birthClient += clients; V.PC.counter.birthElite += elite; V.PC.counter.birthLab += lab; + V.PC.counter.birthFutaSis += futaS; V.PC.counter.birthDegenerate += slavesLength; if (curBabies === 1) { diff --git a/src/events/scheduled/seWedding.js b/src/events/scheduled/seWedding.js index 3b0399cb8f538597f73c53ee1cadd893d4ccd904..e797b4a9a80dd7aef074d7ec7ae177a31c326272 100644 --- a/src/events/scheduled/seWedding.js +++ b/src/events/scheduled/seWedding.js @@ -890,7 +890,7 @@ App.Events.SEWedding = class SEWedding extends App.Events.BaseEvent { if (brides.every((b) => b.fetish === "pregnancy" && b.fetishStrength > 60)) { if (brides.every((b) => b.devotion + b.trust >= 175)) { - r.push(`As pregnancy fetishists, <span class="hotpink">they confidently believes this wedding will be the high point of ${hisC} life.</span>`); + r.push(`As pregnancy fetishists, <span class="hotpink">they confidently believe this wedding will be the high point of their lives.</span>`); brides.forEach(slave => slave.devotion += 20); } else if (brides.every((b) => b.devotion < -20 && b.trust > 20)) { r.push(`As hateful pregnancy fetishists, <span class="hotpink">getting pregnant was the best part of the ceremony.</span>`); diff --git a/src/events/schools/resFailure.js b/src/events/schools/resFailure.js index 7db163ac6802a05e8bedbfb2d3e1ee8ab56f567c..5d21eaa379ddd134b60d002fcefec12d267b9ce9 100644 --- a/src/events/schools/resFailure.js +++ b/src/events/schools/resFailure.js @@ -145,7 +145,7 @@ App.Events.RESFailure = class RESFailure extends App.Events.BaseEvent { slave.preg = -3; if (V.TFS.farmUpgrade > 0) { slave.ovaries = 1; - slave.preg = -1 + slave.preg = -1; if (V.TFS.farmUpgrade >= 2) { slave.preg = random(1, 41); if (V.TFS.farmUpgrade === 3) { diff --git a/src/facilities/incubator/inspectTankSettings.js b/src/facilities/incubator/inspectTankSettings.js index 8dc3c1492679e5502daacc308b3cf4a694b20879..bd6ab537b93e876a18ed56ab3b86a8f96af9259d 100644 --- a/src/facilities/incubator/inspectTankSettings.js +++ b/src/facilities/incubator/inspectTankSettings.js @@ -181,7 +181,7 @@ App.UI.inspectTankSettings = function(isFetus, isPCMother = false) { } if (tankSetting.growthStims === 1) { - section.append(`${He} will be injected with the recommended dosage of stimulants; ${he} will grow to their full expected height. `); + section.append(`${He} will be injected with the recommended dosage of stimulants; ${he} will grow to ${his} full expected height. `); } else { linkArray.push(makeLink(`Limit`, () => { tankSetting.growthStims = 1; })); } diff --git a/src/facilities/penthouse/HGSelect.js b/src/facilities/penthouse/HGSelect.js index 1001e4333b008e1567aa15f81cd952956cd8bf98..bc70420ccf8d05ace725204ee8ba4a5ca233a0b9 100644 --- a/src/facilities/penthouse/HGSelect.js +++ b/src/facilities/penthouse/HGSelect.js @@ -147,6 +147,7 @@ App.Facilities.HGSelect = function() { } App.Events.addNode(f, r, "div", "indent"); + r = []; if (V.seePreg !== 0) { if (V.universalRulesImpregnation === "HG") { App.UI.DOM.appendNewElement("div", f, `${HGName} is responsible for impregnating fertile slaves.`); @@ -183,19 +184,19 @@ App.Facilities.HGSelect = function() { } else { App.UI.DOM.appendNewElement("div", f, `However, ${S.HeadGirl.slaveName} cannot perform this duty.`); } - r.push(App.UI.DOM.link(` Rescind ${his} impregnation responsibility`, () => { + r.push(App.UI.DOM.link(`Rescind ${his} impregnation responsibility`, () => { V.universalRulesImpregnation = "none"; App.UI.reload(); })); - r.push(App.UI.DOM.link(` See to it yourself`, () => { + r.push(App.UI.DOM.link(`See to it yourself`, () => { V.universalRulesImpregnation = "PC"; App.UI.reload(); })); - f.append(App.UI.DOM.generateLinksStrip(r)); + f.append(' ', App.UI.DOM.generateLinksStrip(r)); } else { if (canPenetrate(S.HeadGirl) && S.HeadGirl.pubertyXY === 1) { - App.UI.DOM.appendNewElement("div", f, `${HGName} is capable of impregnating slaves, but it's not part of ${his} responsibilities.`).append( - App.UI.DOM.link(` Assign ${him} to impregnate`, () => { + App.UI.DOM.appendNewElement("div", f, `${HGName} is capable of impregnating slaves, but it's not part of ${his} responsibilities. `).append( + App.UI.DOM.link(`Assign ${him} to impregnate`, () => { V.universalRulesImpregnation = "HG"; App.UI.reload(); })); diff --git a/src/facilities/studio/studio.js b/src/facilities/studio/studio.js index ec90569d1b8ffb6bbae1d80e9204beab68bd4648..021795f0e95376f318df0ea67d5adda1b152956a 100644 --- a/src/facilities/studio/studio.js +++ b/src/facilities/studio/studio.js @@ -224,7 +224,7 @@ App.UI.mediaStudio = function() { } } else { r.push(App.UI.DOM.link("Start releasing porn", () => { - slave.porn.feed = 0; + slave.porn.feed = 1; App.UI.reload(); })); } diff --git a/src/gui/options/options.js b/src/gui/options/options.js index 0b20cab2c4b8557c2384e6fb5ad4b99948691666..0dbac803f6079c1d3523d07d9b74c8d21e13c6b8 100644 --- a/src/gui/options/options.js +++ b/src/gui/options/options.js @@ -492,7 +492,7 @@ App.UI.optionsPassage = function() { option.addComment("Enable cheat mode to edit genetics."); } - options.addCustomOption("Rules Assistant").addButton("Reset Rules", () => { initRules(); }, "Rules Assistant"); + options.addCustomOption("Rules Assistant").addButton("Reset Rules", () => {initRules();}, "Rules Assistant"); options.addOption("Passage Profiler is", "profiler") .addValue("Enabled", 1).on().addValue("Disabled", 0).off() @@ -1148,6 +1148,7 @@ App.UI.artOptions = function() { ["Revamped embedded vector art", 3], ["Non-embedded vector art", 2], ["Shokushu's rendered image pack", 5], + ["Anon's AI image generation", 6], ]); if (V.imageChoice === 1) { options.addComment("The only 2D art in somewhat recent development. Contains many outfits."); @@ -1231,6 +1232,45 @@ App.UI.artOptions = function() { .addValue("6", 6).off().addValue("12", 12).on().addValue("24", 24).off().addValue("32", 32).off(); } else if (V.imageChoice === 2) { option.addComment("This art development is dead since vanilla. Since it is not embedded, requires a separate art pack to be downloaded."); + } else if (V.imageChoice === 6) { + options.addComment("This is highly experimental. Please follow the setup instructions below."); + options.addCustom(App.UI.DOM.stableDiffusionInstallationGuide("Stable Diffusion Installation Guide")); + options.addOption("API URL", "aiApiUrl").showTextBox().addComment("The URL of the Automatic 1111 Stable Diffusion API."); + options.addOption("AI style prompting", "aiStyle") + .addValueList([ + ["Photorealistic", 1], + ["Anime/Hentai", 2], + ["Custom", 0] + ]); + if (V.aiStyle === 0) { + options.addOption("AI custom style positive prompt", "aiCustomStylePos").showTextBox({large: true, forceString: true}) + .addComment("Include desired LoRA triggers (<code><lora:LowRA:0.5></code>) and general style prompts relevant to your chosen model ('<code>hand drawn, dark theme, black background</code>'), but no slave-specific prompts"); + options.addOption("AI custom style negative prompt", "aiCustomStyleNeg").showTextBox({large: true, forceString: true}) + .addComment("Include undesired general style prompts relevant to your chosen model ('<code>greyscale, photography, forest, low camera angle</code>'), but no slave-specific prompts"); + } else if (V.aiStyle === 1) { + options.addComment("For best results, use an appropriately-trained photorealistic base model, such as MajicMIX or Life Like Diffusion."); + } else if (V.aiStyle === 2) { + options.addComment("For best results, use an appropriately-trained hentai base model, such as Hassaku."); + } + options.addOption("Sampling Method", "aiSamplingMethod").showTextBox() + .addComment(`The sampling method used by AI. You can query ${V.aiApiUrl}/sdapi/v1/samplers to see the list of available samplers.`); + options.addOption("CFG Scale", "aiCfgScale").showTextBox() + .addComment("The higher this number, the more the prompt influences the image. Generally between 5 to 12."); + options.addOption("Sampling Steps", "aiSamplingSteps").showTextBox() + .addComment("The number of steps used when generating the image. More steps might reduce artifacts but increases generation time. Generally between 20 to 50."); + options.addOption("Height", "aiHeight").showTextBox() + .addComment("The height of the image."); + options.addOption("Width", "aiWidth").showTextBox() + .addComment("The width of the image."); + options.addOption("Upscaling/highres fix", "aiUpscale") + .addValue("Enabled", true).on().addValue("Disabled", false).off() + .addComment("Use AI upscaling to produce higher-resolution images. Significantly increases both time to generate and image quality."); + if (V.aiUpscale) { + options.addOption("Upscaling size", "aiUpscaleScale").showTextBox() + .addComment("Scales the dimensions of the image by this factor. Defaults to 1.7."); + options.addOption("Upscaling method", "aiUpscaler").showTextBox() + .addComment(`The method used for upscaling the image. You can query ${V.aiApiUrl}/sdapi/v1/upscalers to see the list of available upscalers.`); + } } options.addOption("PA avatar art is", "seeAvatar") @@ -1244,7 +1284,6 @@ App.UI.artOptions = function() { } } - el.append(options.render()); return el; }; diff --git a/src/gui/options/stableDiffusionInstallationGuide.js b/src/gui/options/stableDiffusionInstallationGuide.js new file mode 100644 index 0000000000000000000000000000000000000000..f53db979ca21cf538fc19fc8ef7148b034361c09 --- /dev/null +++ b/src/gui/options/stableDiffusionInstallationGuide.js @@ -0,0 +1,65 @@ +const html = ` +<h1>What is Stable Diffusion and Automatic1111's Stable Diffusion WebUI?</h1> +Stable Diffusion is an AI model for generating images given a text prompt. Automatic1111's Stable Diffusion WebUI is a web interface for running Stable Diffusion. It is the easiest way to run Stable Diffusion on your computer, and provides an API that we can use to integrate Stable Diffusion into other applications. + +<h1>Steps</h1> +<ol> + <li>Install Automatic1111's Stable Diffusion WebUI</li> + <li>Download the relevant models</li> + <li>Place the models in their corresponding directories</li> + <li>Configure Automatic1111's Stable Diffusion WebUI</li> + <li>Running the WebUI</li> + <li>Confirm successful setup</li> +</ol> + +<h2>2. Install Automatic1111's Stable Diffusion WebUI</h2> +<p>Before we start, you need to install the Stable Diffusion WebUI. To do this, follow the detailed instructions for installation on Windows, Linux, and Mac on the <a href="https://github.com/AUTOMATIC1111/stable-diffusion-webui#installation-and-running">Stable Diffusion WebUI GitHub page</a>.</p> + +<h2>3. Download the relevant models</h2> +<p>You will now need to download the following models:</p> +<ul> + <li><a href="https://civitai.com/models/43331?modelVersionId=94640">MajicMix v6</a></li> + <li><a href="https://civitai.com/models/48139?modelVersionId=63006">LowRA v2</a></li> +</ul> + +<p>Note that MajicMix is a photorealistic model heavily biased towards Asian girls; if you have a more diverse arcology, you may prefer a different base model like <a href="https://civitai.com/models/16804">Life Like Diffusion</a>, or you might want to try a different style entirely, like <a href="https://civitai.com/models/2583?modelVersionId=106922">Hassaku</a> for an anime style. Your results may vary with other models, since generated prompts are tuned primarily for these models, but don't be afraid to experiment.</p> + +<h2>4. Place the models in their corresponding directories</h2> +<p>Next, you need to place the models in the appropriate directories in the Stable Diffusion WebUI:</p> +<ul> + <li>Place MajicMix v6 in <code>stable-diffusion-webui/models/Stable-diffusion</code></li> + <li>Place LowRA v2 in <code>stable-diffusion-webui/models/Lora</code></li> +</ul> + +<h2>5. Configure Automatic1111's Stable Diffusion WebUI</h2> +<p>To prepare the WebUI for running, you need to modify the <code>COMMANDLINE_ARGS</code> line in either <code>webui-user.sh</code> (if you're using Linux/Mac) or <code>webui-user.bat</code> (if you're using Windows) to include the following:</p> +<p>Linux/Mac:</p> +<pre><code>export COMMANDLINE_ARGS="--medvram --no-half-vae --listen --port=7860 --api --cors-allow-origins *"</code></pre> +<p>Windows:</p> +<pre><code>set COMMANDLINE_ARGS=--medvram --no-half-vae --listen --port=7860 --api --cors-allow-origins *</code></pre> + +<p>You may need to use <code>--cors-allow-origins null</code> instead of <code>--cors-allow-origins *</code> if you are using a Chromium-based host (Chrome, Edge, FCHost, or similar), <i>and</i> are running Free Cities from a local HTML file rather than a webserver.</p> + +<h2>6. Running the WebUI</h2> +<p>Now you can run the WebUI by executing either <code>webui.sh</code> (Linux/Mac) or <code>webui-user.bat</code> (Windows). Note that the WebUI server needs to be running the whole time you're using it. + +Once it's running, open your browser and go to <code>localhost:7860</code>. The WebUI should open. In the top left is a dropdown for model selection, pick MajicMix (or a different model of your choice) in that dropdown.</p> + +<h2>7. Check it works</h2> +<p>At this point, if you go to a slave's detail page their image should load after a short (<30 seconds) delay. If it doesn't seem to be working, have a look at the terminal window running Automatic1111's Stable Diffusion WebUI to see if there are any errors.</p> +<p>The request will time out if the image can't be generated fast enough; if this is the case for you, try to find a guide to optimizing Stable Diffusion for your particular hardware setup, or disable the "Upscaling/highres fix" option.</p> +`; + +/** + * Generates a link which shows a Stable Diffusion installation guide. + * @param {string} [text] link text to use + * @returns {HTMLElement} link + */ +App.UI.DOM.stableDiffusionInstallationGuide = function(text) { + return App.UI.DOM.link(text ? text : "Stable Diffusion Installation Guide", () => { + Dialog.setup("Stable Diffusion Installation Guide"); + const content = document.createElement("div").innerHTML = html; + Dialog.append(content); + Dialog.open(); + }); +}; diff --git a/src/interaction/main/walkPast.js b/src/interaction/main/walkPast.js index 72a9c946a79420063b3d7c796fa14a1da81fc098..88701507d46114d4acd8a46931b40fc1b079d13f 100644 --- a/src/interaction/main/walkPast.js +++ b/src/interaction/main/walkPast.js @@ -1920,7 +1920,7 @@ globalThis.walkPast = (function() { } else if (!canWalk(slave)) { if (canMove(slave)) { if (slave.rules.mobility === "permissive") { - t += `wheels past your desk on ${his} way to`; + t += `wheels past your desk on ${his} way to `; } else { t += `crawls past your desk on `; if (hasBothArms(slave) && hasBothLegs(slave)) { diff --git a/src/interaction/siCustom.js b/src/interaction/siCustom.js index 793b3720a035bde6689ad854f767d44b7f04a0ea..1dae4e72c53d249c514b8cb04b78c47225d40820 100644 --- a/src/interaction/siCustom.js +++ b/src/interaction/siCustom.js @@ -17,7 +17,7 @@ App.UI.SlaveInteract.custom = function(slave, refresh) { el.append( customSlaveImage(), customHairImage(), - artSeed() + artSeed(), ); App.UI.DOM.appendNewElement("h3", el, `Names`); @@ -688,8 +688,8 @@ App.UI.SlaveInteract.custom = function(slave, refresh) { function artSeed() { const frag = new DocumentFragment(); - if (V.imageChoice === 4) { // webGL only right now - App.UI.DOM.appendNewElement("p", frag, `WebGL rendering uses a "seed value" to make small changes to the appearance of your slaves. If you're dissatisfied with this slave's appearance and correcting ${his} physical parameters doesn't seem to help, you can try replacing the seed value. Slaves with identical seeds will look identical; the game carefully preserves this value for clones and identical twins, but if you change it here it becomes your responsibility.`); + if (V.imageChoice === 4 || V.imageChoice === 6) { // webGL and AI art only right now + App.UI.DOM.appendNewElement("p", frag, `Some rendering methods use a "seed value" to make small changes to the appearance of your slaves. If you're dissatisfied with this slave's appearance and correcting ${his} physical parameters doesn't seem to help, you can try replacing the seed value. Slaves with identical seeds will look identical; the game carefully preserves this value for clones and identical twins, but if you change it here it becomes your responsibility.`); const setArtSeed = (/** @type {number} */ num) => { slave.natural.artSeed = num; @@ -707,6 +707,11 @@ App.UI.SlaveInteract.custom = function(slave, refresh) { button )); } + + // Debug information for AI art + if (V.imageChoice === 6 && V.debugMode === 1) { + frag.append(genAIPrompt()); + } return frag; } @@ -823,4 +828,18 @@ App.UI.SlaveInteract.custom = function(slave, refresh) { return el; } + + function genAIPrompt() { + let el = document.createElement('p'); + + el.appendChild(document.createElement('h4')).textContent = `Image generation AI (eg. Stable Diffusion):`; + + let prompt = buildPrompt(slave); + el.appendChild(document.createElement('h5')).textContent = `Positive prompt`; + el.appendChild(document.createElement('kbd')).textContent = prompt.positive(); + el.appendChild(document.createElement('h5')).textContent = `Negative prompt`; + el.appendChild(document.createElement('kbd')).textContent = prompt.negative(); + + return el; + } }; diff --git a/src/js/SlaveState.js b/src/js/SlaveState.js index 29de7c69fa552f95f79412a77fd7dc2d4a2967d9..ec2d2a43d117d2f92982965b56e2c317ca076ec5 100644 --- a/src/js/SlaveState.js +++ b/src/js/SlaveState.js @@ -382,6 +382,13 @@ App.Entity.SlaveCustomAddonsState = class SlaveCustomAddonsState { * @type {FC.Zeroable<string>} */ this.hairVector = 0; + /** + * holds the ai image ID + * + * used if ai images are enabled + * @type {FC.Zeroable<number>} + */ + this.aiImageId = null; } }; diff --git a/src/js/rulesAssistantOptions.js b/src/js/rulesAssistantOptions.js index c58966882e843d8440749b1b6d3eb7af527f683b..3969178ed0a6f0be1be276d001be8ac9ea146ea9 100644 --- a/src/js/rulesAssistantOptions.js +++ b/src/js/rulesAssistantOptions.js @@ -16,6 +16,11 @@ App.RA.options = (function() { let current_rule; let root; + /** + * + * @param div + * @returns {undefined} + */ function rulesAssistantOptions(div) { V.nextButton = "Back to Main"; V.nextLink = "Main"; @@ -36,6 +41,11 @@ App.RA.options = (function() { }); } + /** + * + * @param e + * @returns {boolean} if enter key was pressed + */ function returnP(e) { return e.keyCode === 13; } function newRule() { @@ -76,6 +86,11 @@ App.RA.options = (function() { reload(); } + /** + * + * @param container + * @returns {Function} + */ function rename(container) { let rename = false; return () => { @@ -139,7 +154,7 @@ App.RA.options = (function() { const parse = { integer(string) { - let n = parseInt(string, 10); + const n = parseInt(string, 10); return isNaN(n) ? 0 : n; }, boobs(string) { @@ -181,6 +196,7 @@ App.RA.options = (function() { /** * returns the first argument to simplify creation of basic container items + * @param {...any} args * @returns {*} */ render(...args) { @@ -299,7 +315,7 @@ App.RA.options = (function() { } } - let _blockCallback = Symbol("Block Callback"); + const _blockCallback = Symbol("Block Callback"); // list of clickable elements // has a short explanation (the prefix) and a value display // value display can optionally be an editable text input field @@ -342,7 +358,6 @@ App.RA.options = (function() { return elem; } - inputEdited() { if (this.selectedItem) { this.selectedItem.deselect(); } this.setValue(this.getTextData()); @@ -354,6 +369,7 @@ App.RA.options = (function() { this.setValue(item.data); this.propagateChange(); } + trySetValue(what) { if (what == null && this._allowNullValue) { this.setValue(what); @@ -366,6 +382,7 @@ App.RA.options = (function() { this.setValue(null); } } + setValue(what) { if (what == null && !this._allowNullValue) { what = ""; } this.realValue = what; @@ -378,9 +395,10 @@ App.RA.options = (function() { this[_blockCallback] = false; } } + setTextValue(what) { const str = what === null ? "no default setting" : `${what}`; - if (this.value) { + if (this.value != null) { if (this.value.tagName === "INPUT") { this.value.value = str; } else { @@ -392,6 +410,7 @@ App.RA.options = (function() { getData() { return this.realValue; } + getTextData() { return (this.value.tagName === "INPUT" ? this.parse(this.value.value) : this.selectedItem.data); } @@ -404,9 +423,11 @@ App.RA.options = (function() { this.onchange(this.getData()); } } + dataEqual(left, right) { return _.isEqual(left, right); } + updateSelected() { const dataValue = this.getData(); let selected; @@ -419,8 +440,10 @@ App.RA.options = (function() { if (selected.length === 1) { const listItem = selected[0]; listItem.select(false); - if (this.selectedItem != null && - !_.isEqual(this.selectedItem, listItem)) { + if ( + this.selectedItem != null + && !_.isEqual(this.selectedItem, listItem) + ) { this.selectedItem.deselect(); } this.selectedItem = listItem; @@ -492,7 +515,7 @@ App.RA.options = (function() { this.values_ = new Map(); // now add options if (allowNullValue) { - let nullOpt = document.createElement("option"); + const nullOpt = document.createElement("option"); nullOpt.value = noDefaultSetting.value; nullOpt.text = capFirstChar(noDefaultSetting.text); this.value.appendChild(nullOpt); @@ -500,7 +523,7 @@ App.RA.options = (function() { } for (const dr of data) { const dv = Array.isArray(dr) ? (dr.length > 1 ? [dr[1], dr[0]] : [dr[0], dr[0]]) : [dr, dr]; - let opt = document.createElement("option"); + const opt = document.createElement("option"); opt.value = dv[0]; opt.text = capFirstChar(dv[1]); this.value.appendChild(opt); @@ -582,7 +605,7 @@ App.RA.options = (function() { this.values_ = new Map(); this.radios_ = new Map(); - let values = []; + const values = []; if (allowNullValue) { values.push([noDefaultSetting.value, noDefaultSetting.text]); this.values_.set(noDefaultSetting.value, null); @@ -594,13 +617,13 @@ App.RA.options = (function() { } for (const v of values) { - let inp = document.createElement("input"); + const inp = document.createElement("input"); inp.type = "radio"; inp.name = this.name_; inp.id = `${prefix}_${v[0]}`; inp.value = v[0]; - let lbl = document.createElement("label"); + const lbl = document.createElement("label"); lbl.htmlFor = inp.id; lbl.className = "ra-radio-label"; lbl.innerHTML = capFirstChar(v[1]); @@ -653,7 +676,7 @@ App.RA.options = (function() { } createEditor() { - let res = document.createElement("input"); + const res = document.createElement("input"); res.setAttribute("type", "text"); res.classList.add("rajs-value"); // // call the variable binding when the input field is no longer being edited, and when the enter key is pressed @@ -686,7 +709,7 @@ App.RA.options = (function() { } createEditor() { - let res = document.createElement("input"); + const res = document.createElement("input"); res.setAttribute("type", "text"); res.classList.add("rajs-value"); // call the variable binding when the input field is no longer being edited, and when the enter key is pressed @@ -732,18 +755,18 @@ App.RA.options = (function() { render(prefix) { const elem = document.createElement("div"); - let switchContainer = document.createElement("div"); + const switchContainer = document.createElement("div"); switchContainer.className = "ra-onoffswitch"; this.checkBox_ = document.createElement("input"); this.checkBox_.type = "checkbox"; this.checkBox_.className = "ra-onoffswitch-checkbox"; this.checkBox_.id = `ra-option-${prefix}`; - let switchLabel = document.createElement("label"); + const switchLabel = document.createElement("label"); switchLabel.className = "ra-onoffswitch-label"; switchLabel.htmlFor = this.checkBox_.id; - let innerSpan = document.createElement("span"); + const innerSpan = document.createElement("span"); innerSpan.className = "ra-onoffswitch-inner"; - let switchSpan = document.createElement("span"); + const switchSpan = document.createElement("span"); switchSpan.className = "ra-onoffswitch-switch"; switchLabel.appendChild(innerSpan); switchLabel.appendChild(switchSpan); @@ -789,12 +812,17 @@ App.RA.options = (function() { } createEditor(min, max) { + /** + * + * @param op + * @param ui + */ function makeOp(op, ui) { - return {op: op, ui: ui}; + return { op, ui }; } this.opSelector = document.createElement("select"); for (const o of [makeOp('==', '='), makeOp('>=', "⩾"), makeOp('<=', '⩽'), makeOp('>', '>'), makeOp('<', '<')]) { - let opt = document.createElement("option"); + const opt = document.createElement("option"); opt.textContent = o.ui; opt.value = o.op; this.opSelector.appendChild(opt); @@ -876,8 +904,14 @@ App.RA.options = (function() { createEditor(min, max) { this._min = min; this._max = max; - let res = document.createElement("span"); + const res = document.createElement("span"); + /** + * + * @param lbl + * @param container + * @param editor + */ function makeElem(lbl, container, editor) { const spinBox = document.createElement("input"); spinBox.type = "number"; @@ -924,13 +958,18 @@ App.RA.options = (function() { } getTextData() { + /** + * + * @param what + */ function parse(what) { return what === "" ? null : parseInt(what); } const vMin = parse(this._minEditor.value); const vMax = parse(this._maxEditor.value); - return (vMin === null && vMax === null) ? null + return (vMin === null && vMax === null) + ? null : App.Utils.makeRange(vMin !== null ? vMin : this._min, vMax !== null ? vMax : this._max); } @@ -960,12 +999,17 @@ App.RA.options = (function() { } createEditor(min, max) { + /** + * + * @param op + * @param ui + */ function makeOp(op, ui) { - return {op: op, ui: ui}; + return { op, ui }; } this.opSelector = document.createElement("select"); for (const o of [makeOp('==', '='), makeOp('>=', "⩾"), makeOp('<=', '⩽'), makeOp('>', '>'), makeOp('<', '<')]) { - let opt = document.createElement("option"); + const opt = document.createElement("option"); opt.textContent = o.ui; opt.value = o.op; this.opSelector.appendChild(opt); @@ -1089,6 +1133,7 @@ App.RA.options = (function() { this.label = label; this.onclick = onclick; } + render(label, onclick) { const elem = document.createElement("span"); elem.classList.add("rajs-list-item"); @@ -1125,7 +1170,7 @@ App.RA.options = (function() { constructor(label, setvalue, selected = false) { super(label, selected); this.selected = selected; - this.setvalue = setvalue ? setvalue : label; + this.setvalue = setvalue || label; } render(label, selected) { @@ -1160,7 +1205,7 @@ App.RA.options = (function() { } render() { - let element = document.getElementById("importfield"); + const element = document.getElementById("importfield"); if (element !== null) { return element; } @@ -1226,7 +1271,7 @@ App.RA.options = (function() { App.UI.DOM.appendNewElement("div", greeting, `Please note that surgeries will only be applied to slaves that are in the penthouse, and will not be applied until the end of the week. Surgery outcomes are included in a slave's individual report, instead of with the other effects of the RA.`); const summary = App.UI.DOM.appendNewElement("div", greeting, `You can always see an overview of all of your rules in the `); summary.append(App.UI.DOM.passageLink("summary view", "Rules Assistant Summary")); - summary.append(`.`); + summary.append("."); div.append(greeting); element.append(div); return element; @@ -1248,8 +1293,8 @@ App.RA.options = (function() { class RuleSelector extends List { constructor() { const f = new DocumentFragment(); - f.append("Current rule: ") - App.UI.DOM.appendNewElement("strong", f, current_rule.name) + f.append("Current rule: "); + App.UI.DOM.appendNewElement("strong", f, current_rule.name); super(f, V.defaultRules.map(i => [i.name, i]), false); this.onchange = function (rule) { V.currentRule = rule.ID; @@ -2697,6 +2742,9 @@ App.RA.options = (function() { class SmartFetishList extends ListSelector { constructor() { const pairs = [ + ["off"], + ["All sex", "all"], + ["No sex", "none"], ["vanilla"], ["random"], ["oral"], @@ -2707,11 +2755,11 @@ App.RA.options = (function() { ["humiliation"], ["Preg", "pregnancy"], ["Pain", "masochist"], - ["Sadism", "sadist"] + ["Sadism", "sadist"], ]; super("Smart piercing fetish target", pairs); this.setValue(current_rule.set.clitSetting); - this.onchange = (value) => current_rule.set.clitSetting = value; + this.onchange = (value) => (current_rule.set.clitSetting = value); } } @@ -3859,7 +3907,7 @@ App.RA.options = (function() { * @param {string} label * @param {string} target rule.set.surgery member name */ - constructor(label, target ) { + constructor(label, target) { const items = [ ["Narrowing", -1], ["Broadening", 1] @@ -3994,7 +4042,7 @@ App.RA.options = (function() { } const penthouse = App.Entity.facilities.penthouse; - /** @type {Object.<string, App.Entity.Facilities.Facility>} */ + /** @type {[string, App.Entity.Facilities.Facility]} */ const facilities = App.Entity.facilities; const facilitiesToSkip = [penthouse, facilities.pit, facilities.armory, facilities.arcologyAgent]; for (const fn in facilities) { @@ -4043,7 +4091,7 @@ App.RA.options = (function() { class LabelList extends StringEditor { constructor() { - super("Custom label(s) (separate by '|')", [], true, true); + super("Set label(s) to slave (separate by '|')", [], true, true); this.setValue(current_rule.set.label); this.onchange = (value) => current_rule.set.label = value; } @@ -4051,7 +4099,7 @@ App.RA.options = (function() { class LabelRemoveList extends StringEditor { constructor() { - super("Default label(s) (separate by '|')", [], true, true); + super("Remove label(s) from slave (separate by '|')", [], true, true); this.setValue(current_rule.set.removeLabel); this.onchange = (value) => current_rule.set.removeLabel = value; } diff --git a/src/js/slaveListing.js b/src/js/slaveListing.js index 1ccae21bcc8a74707c6631036603f8636e1d8c56..c72083e8a51b6d1360406f07ba5dd7441666de26 100644 --- a/src/js/slaveListing.js +++ b/src/js/slaveListing.js @@ -194,7 +194,7 @@ App.UI.SlaveList.render = function(IDs, rejectedSlaves, interactionLink, postNot if (slave.fetish === Fetish.MINDBROKEN) { assignment.innerText += `, mindbroken`; } else { - if ((slave.sexualFlaw !== "none") || (slave.behavioralFlaw !== "none")) { + if (V.spaFix !== 2 && ((slave.sexualFlaw !== "none") || (slave.behavioralFlaw !== "none"))) { list.push(`overcoming flaws`); } if ((slave.trust < 60) || (slave.devotion < 60)) { diff --git a/src/markets/specificMarkets/eliteSlave.js b/src/markets/specificMarkets/eliteSlave.js index 8e5a2e99f015c4e5cbd8431ad69071cf9c676012..09f2c1745ccef9d143f73c0a48036fa72d1f78a0 100644 --- a/src/markets/specificMarkets/eliteSlave.js +++ b/src/markets/specificMarkets/eliteSlave.js @@ -251,7 +251,7 @@ App.Markets["Elite Slave"] = function() { slave.pubertyXX = 1; slave.breedingMark = 1; - const complianceText = App.Desc.lawCompliance(slave, "Elite Slave") + const complianceText = App.Desc.lawCompliance(slave, "Elite Slave"); const cost = slaveCost(slave, false, true); App.UI.DOM.appendNewElement("p", el, `It will take ${cashFormat(cost)} to win the auction.`); diff --git a/src/markets/specificMarkets/schools.js b/src/markets/specificMarkets/schools.js index 34cf7b1197c1a20af825e13178880849ca9ad696..7069cb20780c20b1f261ccbacda19cc5e14cdea3 100644 --- a/src/markets/specificMarkets/schools.js +++ b/src/markets/specificMarkets/schools.js @@ -9,7 +9,7 @@ App.Markets.makeSchoolmodifiers = function(school) { modifiers.push({factor: -0.2, reason: "slave school endowment discount"}); } return modifiers; -} +}; App.Markets.GRI = function() { const el = new DocumentFragment(); diff --git a/src/npc/generate/generateGenetics.js b/src/npc/generate/generateGenetics.js index 5f0b207a5392a21ca379643f2c3713447d53fd2e..a4d45e5fa1e0a6b1fb2c95afd387c618f1f856ff 100644 --- a/src/npc/generate/generateGenetics.js +++ b/src/npc/generate/generateGenetics.js @@ -1293,7 +1293,7 @@ globalThis.generateChild = function(mother, ovum, incubator = false) { } child.premature = 1; } - if ((child.geneticQuirks.dwarfism === 2 || (child.geneticQuirks.neoteny === 3 && child.actualAge > 12)) && child.geneticQuirks.gigantism !== 2) { + if ((child.geneticQuirks.dwarfism === 2 || (child.geneticQuirks.neoteny === 2 && child.actualAge > 12)) && child.geneticQuirks.gigantism !== 2) { child.height = Height.random(child, {limitMult: [-4, -1], spread: 0.15}); } else if (child.geneticQuirks.gigantism === 2 && child.geneticQuirks.dwarfism !== 2) { child.height = Height.random(child, {limitMult: [3, 10], spread: 0.15}); diff --git a/src/npc/generate/newSlaveIntro.js b/src/npc/generate/newSlaveIntro.js index 9b8354445cb52400c4f7c7e2dfb0879ebd28a9c8..b3b5114fb967d8cc480aa3e5b188018c2e821210 100644 --- a/src/npc/generate/newSlaveIntro.js +++ b/src/npc/generate/newSlaveIntro.js @@ -1819,7 +1819,7 @@ App.UI.newSlaveIntro = function(slave, slave2, {tankBorn = false, momInterest = if (PC.dick === 0) { r.push(`fake`); } - r.push(`cock down ${his} throat as far as it will go. Over the course of the next several hours, you ensure that ${he} understands the fine points of nonconsensual oral${slave.vagina > -1 ? `, vaginal,` : ``} and anal intercourse as intimately as possible. When you're finally too tired to continue,you unshackle ${his} <span class="health dec">bruised and bloody body</span> and ask ${him} what ${he} learned. ${His} voice hoarse from the same brutal fucking that has gaped ${his} <span class="lime">asshole</span> ${(slave.vagina > -1) ? `and <span class="lime">pussy</span>` : ``}, ${he} hesitantly replies that ${he} has <span class="hotpink">learned a great deal about true dominance,</span> before fainting on the spot from a mixture of total exhaustion and pure terror. You've taught your student well.`); + r.push(`cock down ${his} throat as far as it will go. Over the course of the next several hours, you ensure that ${he} understands the fine points of nonconsensual oral${slave.vagina > -1 ? `, vaginal,` : ``} and anal intercourse as intimately as possible. When you're finally too tired to continue, you unshackle ${his} <span class="health dec">bruised and bloody body</span> and ask ${him} what ${he} learned. ${His} voice hoarse from the same brutal fucking that has gaped ${his} <span class="lime">asshole</span> ${(slave.vagina > -1) ? `and <span class="lime">pussy</span>` : ``}, ${he} hesitantly replies that ${he} has <span class="hotpink">learned a great deal about true dominance,</span> before fainting on the spot from a mixture of total exhaustion and pure terror. You've taught your student well.`); actX(slave, "oral", 15); slave.anus = 2; actX(slave, "anal", 15); diff --git a/src/npc/interaction/fDick.js b/src/npc/interaction/fDick.js index fe3c1d73bd3b6bbf775279691f67bca385c910df..ee6e89e307b221bf88c1519a014785384fffcdd1 100644 --- a/src/npc/interaction/fDick.js +++ b/src/npc/interaction/fDick.js @@ -316,9 +316,13 @@ App.Interact.fDick = function(slave) { text.push(`Grinding against`); } - text.push(`your firm belly, ${he} decides ${his} job is not yet done and begins reaming you once more, dead set on taking this opportunity to <span class="orangered">show you your place by knocking you up with ${his} child.</span> ${He} manages to empty ${his} balls in your womb several more times before exhaustion kicks in, forcing ${him} to leave you twitching and drooling cum.`); - text.push(knockMeUp(V.PC, 100, 0, slave.ID)); - seX(V.PC, "vaginal", slave, "penetrative", 5); + text.push(`your firm belly, ${he} decides ${his} job is not yet done and begins reaming you once more, dead set on taking this opportunity to <span class="orangered">show you your place by knocking you up with ${his} child.</span> ${He} manages to empty ${his} balls in your ${V.PC.mpreg ? "anal " : ""}womb several more times before exhaustion kicks in, forcing ${him} to leave you twitching and drooling cum.`); + text.push(knockMeUp(V.PC, 100, 2, slave.ID)); + if (V.PC.mpreg) { + seX(V.PC, "anal", slave, "penetrative", 5); + } else { + seX(V.PC, "vaginal", slave, "penetrative", 5); + } } else { text.push(`Contently sighing, ${he} pulls ${his} still very hard cock from your overwhelmed body and forces it into your mouth, ready to blow a second load and give you a <span class="orangered">taste of your place,</span> before leaving you twitching and drooling cum.`); seX(V.PC, "oral", slave, "penetrative"); diff --git a/src/npc/interaction/fSuckDick.js b/src/npc/interaction/fSuckDick.js index ce2f6f4642a802425db22841a202a616a6af8bdb..1a60071eff3a97725aa294d6d81ac6e113028d12 100644 --- a/src/npc/interaction/fSuckDick.js +++ b/src/npc/interaction/fSuckDick.js @@ -273,7 +273,7 @@ App.Interact.fSuckDick = function(slave) { r.push(rumors()) App.Events.addParagraph(node, r); - V.PC.degeneracy += 3; + V.PC.degeneracy += 1; } App.Events.addParagraph(node, r); @@ -293,7 +293,7 @@ App.Interact.fSuckDick = function(slave) { r.push(rumors()) App.Events.addParagraph(node, r); - V.PC.degeneracy += 2; + V.PC.degeneracy += 1; } App.Events.addParagraph(node, r); @@ -320,7 +320,7 @@ App.Interact.fSuckDick = function(slave) { r.push(rumors()) App.Events.addParagraph(node, r); - V.PC.degeneracy += 2; + V.PC.degeneracy += 1; } App.Events.addParagraph(node, r); @@ -342,7 +342,7 @@ App.Interact.fSuckDick = function(slave) { r.push(rumors()); App.Events.addParagraph(node, r); - V.PC.degeneracy += 2; + V.PC.degeneracy += 1; } slave.trust -= 2; @@ -388,7 +388,7 @@ App.Interact.fSuckDick = function(slave) { } else if (V.PC.dick < 0) { r.push(`while your anus contracts rhythmically.`); } - if (amount + (V.PC.bellyPreg > 1500 ? 100 : 0) > 150) { + if (amount + (V.PC.belly > 5000 ? 100 : 0) > 150) { r.push(`${His} cumshot is so voluminous that your stomach can't take it anymore and you raise your head, releasing what you have in your mouth.`); r.push(cumFinal(150)); } @@ -411,13 +411,13 @@ App.Interact.fSuckDick = function(slave) { r.push(rumors()) App.Events.addParagraph(node, r); - V.PC.degeneracy += 2; + V.PC.degeneracy += 1; } App.Events.addParagraph(node, r); return node; - } else if ((disobedience(slave) > 0 || (slave.fetish === Fetish.DOM && slave.devotion < 40 && slave.trust > -30)) && hasAnyArms(slave)) { // Disobedient slaves try to force the player to swallow, dominant slaves have a plus + } else if ((disobedience(slave) > 0 || (slave.fetish === Fetish.DOM && slave.devotion < 20 && slave.trust > 20)) && hasAnyArms(slave)) { // Disobedient slaves try to force the player to swallow, dominant slaves have a plus r.push(`You feel a throbbing at the base of the dick, and ${amount <= 100 ? "liquid splatters in the back of your mouth" : "a thick spurt fills your mouth"}. You feel ${hasBothArms(slave) ? "the slave's hands" : "a hand"} on your head pushing you down.`); if (canTalk(slave)) { r.push(`<span class="orangered">${Spoken(slave, "Yeah, now, I cum! All for you!")}</span>`); @@ -473,7 +473,7 @@ App.Interact.fSuckDick = function(slave) { r.push(rumors()) App.Events.addParagraph(node, r); - V.PC.degeneracy += 2; + V.PC.degeneracy += 1; } slave.trust += 10; @@ -496,7 +496,7 @@ App.Interact.fSuckDick = function(slave) { r.push(rumors()) App.Events.addParagraph(node, r); - V.PC.degeneracy += 2; + V.PC.degeneracy += 1; } slave.trust += 2; @@ -525,7 +525,7 @@ App.Interact.fSuckDick = function(slave) { r.push(rumors()); App.Events.addParagraph(node, r); - V.PC.degeneracy += 2; + V.PC.degeneracy += 1; } slave.trust += 2; @@ -536,7 +536,7 @@ App.Interact.fSuckDick = function(slave) { } else if (isHorny(slave) && slave.devotion < 50 && slave.trust > 50 && jsRandom(1, 100) > 50) { // Slaves unable to warn with time enough r.push(`${He} warns you that ${his} orgasm is imminent at the time you feel a throbbing at the base of the dick, and ${amount <= 100 ? "liquid splatters in the back of your mouth" : "a thick spurt fills your mouth"}.`); - r.push(`You pull away immediately, letting go what's in your mouth. ${He} let ${himself} go, the expression on ${his} face is worried, but ${his} orgasm and ${his} moans increase in intensity, having cummed in your mouth arouses ${him}${isHorny(V.PC) ? ", and you too." : "."} You look at ${him} threateningly, you're really angry.`); + r.push(`You pull away immediately, taking out what's in your mouth. ${He} let ${himself} go, the expression on ${his} face is worried, but ${his} orgasm and ${his} moans increase in intensity, having cummed in your mouth arouses ${him}${isHorny(V.PC) ? ", and you too." : "."} You look at ${him} threateningly, you're really angry.`); if (isHorny(V.PC)) { r.push(`You can't control yourself anymore and an intense, extremely pleasurable and endless orgasm takes control of your mind,`); @@ -575,13 +575,13 @@ App.Interact.fSuckDick = function(slave) { r.push(rumors()); App.Events.addParagraph(node, r); - V.PC.degeneracy += 2; + V.PC.degeneracy += 1; } slave.devotion += 2; slave.trust += 5; - } else if (V.week - slave.weekAcquired > 30 && slave.devotion > 90 && isHorny(V.PC)) { // Senior devoted slaves can be rewarded by a horny player + } else if (V.week - slave.weekAcquired > 30 && slave.devotion > 90 && isPlayerLusting()) { // Senior devoted slaves can be rewarded by a horny player r.push(`This devoted slave has been serving you for quite some time. Horny as you are, you decide to reward ${him} by letting ${him} go all the way. When ${he} tells you that ${he} is about to cum, instead of taking your mouth away, you take ${his} hand with yours, guiding it to the top of your head, letting ${him} know that you want ${him} to take the initiative and set the right pace. The slave understands it and, breathing heavily, ${he} prepares to fulfill your wish with great pleasure.`); r.push(swallow()); @@ -598,7 +598,7 @@ App.Interact.fSuckDick = function(slave) { r.push(rumors()); App.Events.addParagraph(node, r); - V.PC.degeneracy += 2; + V.PC.degeneracy += 1; } slave.devotion += 5; @@ -636,7 +636,7 @@ App.Interact.fSuckDick = function(slave) { r.push(rumors()); App.Events.addParagraph(node, r); - V.PC.degeneracy += 2; + V.PC.degeneracy += 1; } slave.trust += 2; @@ -665,7 +665,7 @@ App.Interact.fSuckDick = function(slave) { } else { text.push(`as ${he} pushes your head down and ${his} hips up, making sure all of ${his} ${dickAdj} cock is inside your mouth. Your ${lipsLong} encircle the base of ${his} phallus and your nose flattens against ${his} pubis as you feel ${his} cock throb, releasing ${his} semen onto your tongue at the back of your mouth.`); } - if (amount + (V.PC.bellyPreg > 1500 ? 100 : 0) > 150) { + if (amount + (V.PC.belly > 5000 ? 100 : 0) > 150) { text.push(`${His} cumshot is so voluminous that your stomach can't take it anymore and you slap your hand on ${his} hips to let ${him} know. ${He} understands and lets go of your head, so you can release ${his} member from your ${slave.dick > 2 ? "throat" : "mouth"}.`); text.push(cumFinal(150)); } diff --git a/src/npc/interaction/slaveOnSlaveFeeding/fSlaveFeed.js b/src/npc/interaction/slaveOnSlaveFeeding/fSlaveFeed.js index 24c88a5261cf1102b3eeb8d5db47d0fabe8b5476..63e30476231ff1ccd81e9524ca63c82947a7d641 100644 --- a/src/npc/interaction/slaveOnSlaveFeeding/fSlaveFeed.js +++ b/src/npc/interaction/slaveOnSlaveFeeding/fSlaveFeed.js @@ -551,7 +551,7 @@ globalThis.FSlaveFeed = function(slave, milkTap) { r.push(`and teasing ${his} wobbling, gurgling stomach`); } } else if (slave.inflation === 1) { - r.push(`${his} belly is distended and sloshing with milk. ${He} pops off ${his} ${relative}'s nipple and settles into ${his2} breasts for a short rest while hiccupping}`); + r.push(`${his} belly is distended and sloshing with milk. ${He} pops off ${his} ${relative}'s nipple and settles into ${his2} breasts for a short rest while hiccupping`); if (hasAnyArms(slave)) { r.push(`and teasing ${his} gurgling stomach`); } @@ -1859,7 +1859,7 @@ globalThis.FSlaveFeed = function(slave, milkTap) { } else { r.push(`You cum as you feel ${his} belly slowly round with cum under your molesting fingers.`); } - r.push(`When you release ${him} from under your weight, ${he} drops to the ground panting. Neither slave seems to have truly enjoyed it, instead opting to just get it over with, though ${milkTap.slaveName} makes sure to thank ${slave.slaveName} for dealing with ${his} pent up loads.`); + r.push(`When you release ${him} from under your weight, ${he} drops to the ground panting. Neither slave seems to have truly enjoyed it, instead opting to just get it over with, though ${milkTap.slaveName} makes sure to thank ${slave.slaveName} for dealing with ${his2} pent up loads.`); if (canDoVaginal(slave) && (slave.vagina === 0)) { slave.vagina = 1; } else if (canDoAnal(slave) && (slave.anus === 0)) { diff --git a/src/npc/startingGirls/startingGirls.js b/src/npc/startingGirls/startingGirls.js index a7dc89943513cc21fdc524db54d077938b553cc6..930b0f8f3fc1167703ac59db7095952ef6cc7469 100644 --- a/src/npc/startingGirls/startingGirls.js +++ b/src/npc/startingGirls/startingGirls.js @@ -882,6 +882,10 @@ App.StartingGirls.physical = function(slave, cheat = false) { ["Succubus", "succubus"], ["Dragon", "dragon"], ]); + options.addOption("Tail color", "tailColor", slave) + .addValue (`Matches main hair color (${slave.hColor})`, slave.hColor) + .addValue("White", "white").off() + .addComment(`More extensive coloring options are available in the Salon tab`); } } diff --git a/src/npc/surgery/bodySwap/bodySwap.js b/src/npc/surgery/bodySwap/bodySwap.js index a14253a4510200b1abd031c42a917888560f8df1..f295d17368feddb5acf010d4cc2635e92a05b359 100644 --- a/src/npc/surgery/bodySwap/bodySwap.js +++ b/src/npc/surgery/bodySwap/bodySwap.js @@ -36,6 +36,12 @@ globalThis.bodySwap = function(soul, body, fromGenepool) { soul.tail = body.tail; soul.tailShape = body.tailShape; soul.tailColor = body.tailColor; + soul.PBack = body.PBack; + soul.wingsShape = body.wingsShape; + soul.appendages = soul.appendages; + soul.appendagesColor = soul.appendagesColor; + soul.appendagesEffectColor = body.appendagesEffectColor; + soul.appendagesEffect = body.appendagesEffect; soul.origHColor = body.origHColor; soul.hColor = body.hColor; soul.hLength = body.hLength; diff --git a/src/npc/surgery/bodySwap/bodySwapReaction.js b/src/npc/surgery/bodySwap/bodySwapReaction.js index a95555ac5a3700df722b0ede0a4756916b121cac..96dc0c3daaf37b947cf264dcf2b1a32f1d0e94df 100644 --- a/src/npc/surgery/bodySwap/bodySwapReaction.js +++ b/src/npc/surgery/bodySwap/bodySwapReaction.js @@ -1126,10 +1126,10 @@ globalThis.bodySwapReaction = function(body, soul) { let puberty = 0; if ( ( - (body.physicalAge < V.fertilityAge && + (body.physicalAge < body.pubertyAgeXX && (body.ovaries === 1 || body.mpreg === 1) ) || - (body.physicalAge < V.potencyAge && body.balls > 0) + (body.physicalAge < body.pubertyAgeXY && body.balls > 0) ) && (soul.pubertyXX === 1 || soul.pubertyXY === 1) ) { @@ -2887,10 +2887,10 @@ globalThis.bodySwapReaction = function(body, soul) { let puberty = 0; if ( ( - (body.physicalAge < V.fertilityAge && + (body.physicalAge < body.pubertyAgeXX && (body.ovaries === 1 || body.mpreg === 1) ) || - (body.physicalAge < V.potencyAge && body.balls > 0) + (body.physicalAge < body.pubertyAgeXY && body.balls > 0) ) && (soul.pubertyXX === 1 || soul.pubertyXY === 1) ) { diff --git a/src/player/pcSurgeryDegradation.js b/src/player/pcSurgeryDegradation.js index 1e3a1d07bb97d763790a4ddccdbad1c46ffc84c1..9803107789755c7e0d214bc265c82c9f1ed4e910 100644 --- a/src/player/pcSurgeryDegradation.js +++ b/src/player/pcSurgeryDegradation.js @@ -496,7 +496,7 @@ App.UI.PCSurgeryDegradation = function(surgeryType) { } r.push(`${HeU} is quite good at ${hisU} job and quickly brings you to climax; your new${V.PC.vagina === 0 ? " virgin" : ""} pussy squirting girlcum across ${hisU} face. ${HeU} rises from your crotch and licks ${hisU} lips. "I always did like the taste of you. Feel free to rest as long as you need before departing.`); if (V.PC.degeneracy > 0 && V.PC.vagina === 0) { - r.push(`Ah, I forgot, ${V.doctor.state > 0 ? "your" : "a renowned "} doctor came while you were sedated, did an examination, issued a virginity certificate and <span class="rep inc">made a public statement that you are a virgin.</span>`); + r.push(`Ah, I forgot, ${V.doctor.state > 0 ? "your" : "a renowned "} doctor came while you were sedated, did an examination, issued a virginity certificate and <span class="reputation inc">made a public statement that you are a virgin.</span>`); V.PC.degeneracy = Math.max(V.PC.degeneracy - 10, 0); /** -10 points */ if (V.PC.degeneracy > 0) { V.PC.degeneracy = V.PC.degeneracy - Math.max(Math.floor(V.PC.degeneracy / 2), 50); /** reduces half of the points from 11 to 60 */ @@ -534,7 +534,7 @@ App.UI.PCSurgeryDegradation = function(surgeryType) { } r.push(`You feel ${hisU} face brush your inner legs as ${heU} brings ${hisU} mouth to your cunt and begins to enthusiastically eat you out. ${HisU} long tongue enters your vagina and you feel ${heU} rhythmically pressing into your new hymen with just enough pressure for you to feel it. ${HeU} is quite good at ${hisU} job and quickly brings you to climax; your virgin pussy squirting girlcum across ${hisU} face. ${HeU} rises from your crotch and licks ${hisU} lips. "I always did like the taste of you. Feel free to rest as long as you need before departing.`); if (V.PC.degeneracy > 0 && V.PC.vagina === 0) { - r.push(`Ah, I forgot, ${V.doctor.state > 0 ? "your" : "a renowned "} doctor came while you were sedated, did an examination, issued a virginity certificate and <span class="rep inc">made a public statement that you are a virgin.</span>`); + r.push(`Ah, I forgot, ${V.doctor.state > 0 ? "your" : "a renowned "} doctor came while you were sedated, did an examination, issued a virginity certificate and <span class="reputation inc">made a public statement that you are a virgin.</span>`); V.PC.degeneracy = Math.max(V.PC.degeneracy - 10, 0); /** -10 points */ if (V.PC.degeneracy > 0) { V.PC.degeneracy = V.PC.degeneracy - Math.max(Math.floor(V.PC.degeneracy / 2), 50); /** reduces half of the points from 11 to 60 */ diff --git a/src/pregmod/surrogacy.js b/src/pregmod/surrogacy.js index e85d84f3e9bf336e8efe05c750e7f606e9ea76bd..4affba58d49674adebe1618973744670efcd6cfd 100644 --- a/src/pregmod/surrogacy.js +++ b/src/pregmod/surrogacy.js @@ -95,9 +95,9 @@ App.UI.surrogacy = function() { r.push(`Since the surgery required only a local anesthetic, you are very aware that you are now carrying ${slave.slaveName}'s ${child}. You slowly`); if (canWalk(V.PC)) { r.push(`rise to your feet, a hand to`); - } else if (canMove(PC)) { + } else if (canMove(V.PC)) { r.push(`rise to a sitting position, a hand to`); - } else if (isMovable(PC)) { + } else if (isMovable(V.PC)) { r.push(`run a hand across`); } r.push(`your lower belly, appreciating the new ${bulk ? "lives" : "life"} growing within you.`);