diff --git a/src/gui/userButton.js b/src/gui/sideBar.js similarity index 66% rename from src/gui/userButton.js rename to src/gui/sideBar.js index 0c8cf11d416de2f83b950d4cc7650cd3bb5bcfad..9c1e8e3508a06265f8f80745c8a64a4805fae3cf 100644 --- a/src/gui/userButton.js +++ b/src/gui/sideBar.js @@ -1,3 +1,26 @@ +/** Notify the game that the sidebar needs to be refreshed as soon as possible, but do not do it immediately. + * This allows us to call this function repeatedly without impacting performance (for example, from repX() and cashX()). + * The game will redraw the sidebar exactly once, as soon as all the scripts have finished executing. + */ +App.Utils.scheduleSidebarRefresh = (function() { + let refresh = false; + + function updateSidebar() { + refresh = false; + UIBar.update(); + App.UI.updateSidebarTooltips(); + } + + function schedule() { + if (!refresh) { + refresh = true; + setTimeout(updateSidebar, 0); + } + } + + return schedule; +})(); + App.Utils.userButton = function(nextButton = V.nextButton, nextLink = V.nextLink) { const el = document.createElement("span"); el.id = "next-button-wrapper"; // We must always have a named element so we have something to refresh even if the button is hidden for a scene diff --git a/src/interaction/prostheticLab.js b/src/interaction/prostheticLab.js new file mode 100644 index 0000000000000000000000000000000000000000..5a34abceb4f036a6730c7dd06fbdf59bcf0ace65 --- /dev/null +++ b/src/interaction/prostheticLab.js @@ -0,0 +1,26 @@ +/** + * @returns {string} + */ + +globalThis.getProstheticsStockpile = function() { + return `<div>Prosthetics interfaces: ${num(V.prosthetics.interfaceP1.amount + V.prosthetics.interfaceP2.amount)}</div>` + + `<div class="choices">Basic: ${V.prosthetics.interfaceP1.amount}</div>` + + `<div class="choices">Advanced: ${V.prosthetics.interfaceP2.amount}</div>` + + `<div>Limbs: ${num(V.prosthetics.basicL.amount + V.prosthetics.sexL.amount + V.prosthetics.beautyL.amount + + V.prosthetics.combatL.amount + V.prosthetics.cyberneticL.amount)}</div>` + + `<div class="choices">Basic: ${V.prosthetics.basicL.amount}</div>` + + `<div class="choices">Sex: ${V.prosthetics.sexL.amount}</div>` + + `<div class="choices">Beauty: ${V.prosthetics.beautyL.amount}</div>` + + `<div class="choices">Combat: ${V.prosthetics.combatL.amount}</div>` + + `<div class="choices">Cybernetic: ${V.prosthetics.cyberneticL.amount}</div>` + + `<div>Implants: ${num(V.prosthetics.ocular.amount + V.prosthetics.cochlear.amount + V.prosthetics.electrolarynx.amount)}</div>` + + `<div class="choices">Ocular: ${V.prosthetics.ocular.amount}</div>` + + `<div class="choices">Cochlear: ${V.prosthetics.cochlear.amount}</div>` + + /* `<div class="choices">Erectile: ${V.prosthetics.erectile.amount}</div>` + */ + `<div class="choices">Electrolarynx: ${V.prosthetics.electrolarynx.amount}</div>` + + `<div>Tail interface: ${V.prosthetics.interfaceTail.amount}</div>` + + `<div>Tails: ${num(V.prosthetics.modT.amount + V.prosthetics.sexT.amount + V.prosthetics.combatT.amount)}</div>` + + `<div class="choices">Modular: ${V.prosthetics.modT.amount}</div>` + + `<div class="choices">Pleasure: ${V.prosthetics.sexT.amount}</div>` + + `<div class="choices">Combat: ${V.prosthetics.combatT.amount}</div>`; +}; diff --git a/src/js/rulesAssistant.js b/src/js/rulesAssistant.js index b25965885ae9744963c8a840e280933f6bd85ffa..52199fd9c050ef135adb64295328edc9dd7e1c47 100644 --- a/src/js/rulesAssistant.js +++ b/src/js/rulesAssistant.js @@ -602,3 +602,15 @@ App.RA.ruleDeepAssign = function deepAssign(target, source) { } return target; }; + +globalThis.initRules = function() { + const rule = emptyDefaultRule(); + rule.name = "Obedient Slaves"; + rule.condition.function = "between"; + rule.condition.data.attribute = "devotion"; + rule.condition.data.value = [20, null]; + rule.set.removalAssignment = "rest"; + + V.defaultRules = [rule]; + V.rulesToApplyOnce = {}; +}; diff --git a/src/js/utilsArcology.js b/src/js/utilsArcology.js new file mode 100644 index 0000000000000000000000000000000000000000..4dd1a73849d74394f265fc3c2fe54401ba66624a --- /dev/null +++ b/src/js/utilsArcology.js @@ -0,0 +1,104 @@ +/** Returns the revivalist nationality associated with the player's arcology, or 0 if none + * @returns {string|0} + */ +globalThis.getRevivalistNationality = function() { + if (V.arcologies[0].FSRomanRevivalist > 90) { + return "Roman Revivalist"; + } else if (V.arcologies[0].FSAztecRevivalist > 90) { + return "Aztec Revivalist"; + } else if (V.arcologies[0].FSEgyptianRevivalist > 90) { + return "Ancient Egyptian Revivalist"; + } else if (V.arcologies[0].FSEdoRevivalist > 90) { + return "Edo Revivalist"; + } else if (V.arcologies[0].FSArabianRevivalist > 90) { + return "Arabian Revivalist"; + } else if (V.arcologies[0].FSChineseRevivalist > 90) { + return "Ancient Chinese Revivalist"; + } + return 0; +}; + +/** Calculate and return economic uncertainty multiplier for a given arcology + * @param {number} arcologyID + * @returns {number} + */ +App.Utils.economicUncertainty = function(arcologyID) { + let uncertainty = arcologyID === 0 ? 5 : 10; + if (assistant.power === 1) { + uncertainty -= Math.max(Math.trunc(uncertainty/2), 0); + } else if (assistant.power > 1) { + uncertainty = 0; + } + return jsRandom(100 - uncertainty, 100 + uncertainty) / 100; +}; + +/** + * @returns {number} + */ +App.Utils.schoolCounter = function() { + return Array.from(App.Data.misc.schools.keys()).filter(s => V[s].schoolPresent).length; +}; + +/** + * @returns {string} + */ +App.Utils.schoolFailure = function() { + return Array.from(App.Data.misc.schools.keys()).find(s => V[s].schoolPresent && V[s].schoolProsperity <= -10); +}; + +/** + * @typedef {Object} menialObject + * @property {string} text + * @property {number} value + */ + +/** + * @returns {menialObject} + */ +globalThis.menialPopCap = function() { + let r = ""; + + let popCap = 500 * (1 + V.building.findCells(cell => cell instanceof App.Arcology.Cell.Manufacturing && cell.type === "Pens").length); + + let overMenialCap = V.menials + V.fuckdolls + V.menialBioreactors - popCap; + if (overMenialCap > 0) { + const price = menialSlaveCost(-overMenialCap); + if (V.menials > 0) { + if (V.menials > overMenialCap) { + cashX((overMenialCap * price), "menialTrades"); + V.menialDemandFactor -= overMenialCap; + V.menials -= overMenialCap; + overMenialCap = 0; + r += "You don't have enough room for all your menials and are obliged to sell some."; + } else { + cashX((V.menials * price), "menialTrades"); + V.menialDemandFactor -= V.menials; + overMenialCap -= V.menials; + V.menials = 0; + r += "You don't have enough room for your menials and are obliged to sell them."; + } + } + if (overMenialCap > 0 && V.fuckdolls > 0) { + if (V.fuckdolls > overMenialCap) { + cashX(overMenialCap * (price * 2), "menialTrades"); + V.menialDemandFactor -= overMenialCap; + V.fuckdolls -= overMenialCap; + overMenialCap = 0; + r += "You don't have enough room for all your Fuckdolls and are obliged to sell some."; + } else { + cashX(V.fuckdolls * (price * 2), "menialTrades"); + V.menialDemandFactor -= V.fuckdolls; + overMenialCap -= V.fuckdolls; + V.fuckdolls = 0; + r += "You don't have enough room for your Fuckdolls and are obliged to sell them."; + } + } + if (overMenialCap > 0 && V.menialBioreactors > 0) { + cashX(overMenialCap * (price - 100), "menialTrades"); + V.menialDemandFactor -= overMenialCap; + V.menialBioreactors -= overMenialCap; + r += "You don't have enough room for all your menial bioreactors and are obliged to sell some."; + } + } + return {text: r, value: popCap}; +}; diff --git a/src/js/utilsAssessSlave.js b/src/js/utilsAssessSlave.js new file mode 100644 index 0000000000000000000000000000000000000000..f42e0ad0677b34ed9d951769daf66905feea0e47 --- /dev/null +++ b/src/js/utilsAssessSlave.js @@ -0,0 +1,243 @@ +/* +* +* This file focuses on slave related functions that assess qualities about slaves. Are they/can they X? +* +*/ + +/** + * @param {App.Entity.SlaveState} slave + * @returns {string} + */ +globalThis.getSlaveDevotionClass = function(slave) { + if ((!slave) || (!State)) { + return undefined; + } + if (slave.fetish === "mindbroken") { + return "mindbroken"; + } + if (slave.devotion < -95) { + return "very-hateful"; + } else if (slave.devotion < -50) { + return "hateful"; + } else if (slave.devotion < -20) { + return "resistant"; + } else if (slave.devotion <= 20) { + return "ambivalent"; + } else if (slave.devotion <= 50) { + return "accepting"; + } else if (slave.devotion <= 95) { + return "devoted"; + } else { + return "worshipful"; + } +}; + +/** + * @param {App.Entity.SlaveState} slave + * @returns {string} + */ +globalThis.getSlaveTrustClass = function(slave) { + if ((!slave) || (!State)) { + return undefined; + } + + if (slave.fetish === "mindbroken") { + return ""; + } + + if (slave.trust < -95) { + return "extremely-terrified"; + } else if (slave.trust < -50) { + return "terrified"; + } else if (slave.trust < -20) { + return "frightened"; + } else if (slave.trust <= 20) { + return "fearful"; + } else if (slave.trust <= 50) { + if (slave.devotion < -20) { + return "hate-careful"; + } else { + return "careful"; + } + } else if (slave.trust <= 95) { + if (slave.devotion < -20) { + return "bold"; + } else { + return "trusting"; + } + } else if (slave.devotion < -20) { + return "defiant"; + } else { + return "profoundly-trusting"; + } +}; + +/** + * Returns a "disobedience factor" between 0 (perfectly obedient) and 100 (completely defiant) + * @param {App.Entity.SlaveState} slave + * @returns {number} + */ +globalThis.disobedience = function(slave) { + const devotionBaseline = 20; // with devotion above this number slaves will obey completely + const trustBaseline = -20; // with trust below this number slaves will obey completely + + if (slave.devotion > devotionBaseline || slave.trust < trustBaseline) { + return 0; // no chance of disobedience + } + + // factors are between 0 (right on the boundary of perfectly obedient) and 10 (completely disobedient) + let devotionFactor = 10 - ((10 * (slave.devotion + 100)) / (devotionBaseline + 100)); + let trustFactor = (10 * (slave.trust - trustBaseline)) / (100 - trustBaseline); + return Math.round(devotionFactor * trustFactor); +}; + +/** + * Returns how exposing a slave's outfit is, after taking into consideration a topless outfit is more revealing for beboobed slaves or female ones. + * @param {App.Entity.SlaveState} slave + * @returns {0|1|2|3|4} + */ +globalThis.getExposure = function(slave) { + const clothes = App.Data.clothes.get(slave.clothes); + return (clothes.topless && clothes.exposure < 3 && (slave.boobs > 299 || (slave.genes === 'XX' && slave.vagina >= 0))) ? 3 : clothes.exposure; +}; + +/** + * @param {App.Entity.SlaveState} slave + * @returns {boolean} + */ +globalThis.canImproveIntelligence = function(slave) { + let origIntel = V.genePool.find(function(s) { return s.ID === slave.ID; }).intelligence; + return (slave.intelligence < origIntel + 15) && (slave.intelligence < 100); +}; + +/** + * @param {App.Entity.SlaveState} slave + * @returns {number} + */ +globalThis.maxHeight = function(slave) { + let max = Math.trunc(Math.clamp((Height.mean(slave) * 1.25), 0, 274)); /* max achievable height is expected height plus 25% */ + + if (slave.geneticQuirks.dwarfism === 2 && slave.geneticQuirks.gigantism !== 2) { + max = Math.min(max, 160); + } + + return max; +}; + +/** + * @param {App.Entity.SlaveState} slave + * @returns {boolean} + */ +globalThis.canImproveHeight = function(slave) { + return slave.height < maxHeight(slave); +}; + +/** + * @param {App.Entity.SlaveState} slave + * @param {FC.HumanState} target + * @returns {boolean} + */ +globalThis.haveRelationshipP = function(slave, target) { + return slave.relationshipTarget === target.ID; +}; + +/** + * @param {App.Entity.SlaveState} slave + * @param {App.Entity.SlaveState} target + * @returns {boolean} + */ +globalThis.isRivalP = function(slave, target) { + return slave.rivalryTarget === target.ID; +}; + +/** + * @param {FC.HumanState} slave + * @returns {boolean} + */ +globalThis.supremeRaceP = function(slave) { + return V.arcologies[0].FSSupremacistRace === slave.race; +}; + +/** + * @param {FC.HumanState} slave + * @returns {boolean} + */ +globalThis.inferiorRaceP = function(slave) { + return V.arcologies[0].FSSubjugationistRace === slave.race; +}; + +/** + * @param {App.Entity.SlaveState} slave + * @returns {boolean} + */ +globalThis.isLeaderP = function(slave) { + const leaders = [S.HeadGirl, S.Bodyguard, S.Recruiter, S.Concubine, S.Nurse, S.Attendant, S.Matron, S.Madam, S.DJ, S.Milkmaid, S.Farmer, S.Stewardess, S.Schoolteacher, S.Wardeness]; + + return leaders.some(leader => leader && leader.ID === slave.ID); +}; + +/** Get the written title for a given slave, without messing with global state. + * @param {App.Entity.SlaveState} [slave] + * @returns {string} + */ +globalThis.getWrittenTitle = function(slave) { + if (slave && slave.custom.title !== undefined && slave.custom.title !== "" && slave.rudeTitle === 0) { + return slave.custom.title; + } + if (V.PC.customTitle !== undefined) { + return V.PC.customTitle; + } else if (V.PC.title !== 0) { + return "Master"; + } else { + return "Mistress"; + } +}; + +/** + * @param {App.Entity.SlaveState} slave + * @returns {string} + */ +globalThis.SlaveFullName = function(slave) { + const pair = slave.slaveSurname ? [slave.slaveName, slave.slaveSurname] : [slave.slaveName]; + if ((V.surnameOrder !== 1 && ["Cambodian", "Chinese", "Hungarian", "Japanese", "Korean", "Mongolian", "Taiwanese", "Vietnamese"].includes(slave.nationality)) || (V.surnameOrder === 2)) { + pair.reverse(); + } + return pair.join(" "); +}; + +/** Is the slave a shelter slave? + * @param {App.Entity.SlaveState} slave + * @returns {boolean} + */ +globalThis.isShelterSlave = function(slave) { + return (typeof slave.origin === "string" && slave.origin.includes("Slave Shelter")); +}; + +/** + * Returns if a slave appears male, female, or androgynous. + * + * @param {App.Entity.SlaveState} slave + * @returns {number} + */ +globalThis.perceivedGender = function(slave) { + return -1; +}; + +/** + * @param {App.Entity.SlaveState} A + * @param {App.Entity.SlaveState} B + * @returns {boolean} + */ +globalThis.sameAssignmentP = function(A, B) { + return A.assignment === B.assignment; +}; + +/** Determine whether a given penthouse slave can move into a private room or not. + * @param {App.Entity.SlaveState} slave + * @returns {boolean} + */ +globalThis.canMoveToRoom = function(slave) { + const partner = slave.relationship >= 4 ? getSlave(slave.relationshipTarget) : null; + const partnerHasRoom = partner && assignmentVisible(partner) && partner.rules.living === "luxurious"; + return partnerHasRoom || V.rooms - V.roomsPopulation >= 1; +}; diff --git a/src/js/utilsFC.js b/src/js/utilsFC.js deleted file mode 100644 index 59ed3ac912bb4d544253d6a78bf53cbed248abdb..0000000000000000000000000000000000000000 --- a/src/js/utilsFC.js +++ /dev/null @@ -1,514 +0,0 @@ -globalThis.Categorizer = class { - /** - * @param {...[]} pairs - */ - constructor(...pairs) { - this.cats = Array.prototype.slice.call(pairs) - .filter(function(e, i, a) { - return Array.isArray(e) && e.length === 2 && typeof e[0] === "number" && !isNaN(e[0]) && - a.findIndex(function(val) { - return e[0] === val[0]; - }) === i; /* uniqueness test */ - }) - .sort(function(a, b) { - return b[0] - a[0]; /* reverse sort */ - }); - } - - cat(val, def) { - let result = def; - if (typeof val === "number" && !isNaN(val)) { - let foundCat = this.cats.find(function(e) { - return val >= e[0]; - }); - if (foundCat) { - result = foundCat[1]; - } - } - // Record the value for the result's getter, if it is an object - // and doesn't have the property yet - if (typeof result === "object" && !isNaN(result)) { - result.value = val; - } - return result; - } -}; - -/** - * @param {string} category - * @param {string} title - * @returns {string} - */ -globalThis.budgetLine = function(category, title) { - let income; - let expenses; - - if (passage() === "Rep Budget") { - income = "lastWeeksRepIncome"; - expenses = "lastWeeksRepExpenses"; - - if (V[income][category] || V[expenses][category] || V.showAllEntries.repBudget) { - return `<tr>\ - <td>${title}</td>\ - <td>${repFormat(V[income][category])}</td>\ - <td>${repFormat(V[expenses][category])}</td>\ - <td>${repFormat(V[income][category] + V[expenses][category])}</td>\ - </tr>`; - } - } else if (passage() === "Costs Budget") { - income = "lastWeeksCashIncome"; - expenses = "lastWeeksCashExpenses"; - - if (V[income][category] || V[expenses][category] || V.showAllEntries.costsBudget) { - return `<tr>\ - <td>${title}</td>\ - <td>${cashFormatColor(V[income][category])}</td>\ - <td>${cashFormatColor(-Math.abs(V[expenses][category]))}</td>\ - <td>${cashFormatColor(V[income][category] + V[expenses][category])}</td>\ - </tr>`; - } - } - return ``; -}; - -/* -Make everything waiting for this execute. Usage: - -let doSomething = function() { - ... your initialization code goes here ... -}; -if(typeof Categorizer === 'function') { - doSomething(); -} else { - jQuery(document).one('categorizer.ready', doSomething); -} -*/ -jQuery(document).trigger("categorizer.ready"); - -globalThis.getProstheticsStockpile = function() { - return `<div>Prosthetics interfaces: ${num(V.prosthetics.interfaceP1.amount + V.prosthetics.interfaceP2.amount)}</div>` + - `<div class="choices">Basic: ${V.prosthetics.interfaceP1.amount}</div>` + - `<div class="choices">Advanced: ${V.prosthetics.interfaceP2.amount}</div>` + - `<div>Limbs: ${num(V.prosthetics.basicL.amount + V.prosthetics.sexL.amount + V.prosthetics.beautyL.amount + - V.prosthetics.combatL.amount + V.prosthetics.cyberneticL.amount)}</div>` + - `<div class="choices">Basic: ${V.prosthetics.basicL.amount}</div>` + - `<div class="choices">Sex: ${V.prosthetics.sexL.amount}</div>` + - `<div class="choices">Beauty: ${V.prosthetics.beautyL.amount}</div>` + - `<div class="choices">Combat: ${V.prosthetics.combatL.amount}</div>` + - `<div class="choices">Cybernetic: ${V.prosthetics.cyberneticL.amount}</div>` + - `<div>Implants: ${num(V.prosthetics.ocular.amount + V.prosthetics.cochlear.amount + V.prosthetics.electrolarynx.amount)}</div>` + - `<div class="choices">Ocular: ${V.prosthetics.ocular.amount}</div>` + - `<div class="choices">Cochlear: ${V.prosthetics.cochlear.amount}</div>` + - /* `<div class="choices">Erectile: ${V.prosthetics.erectile.amount}</div>` + */ - `<div class="choices">Electrolarynx: ${V.prosthetics.electrolarynx.amount}</div>` + - `<div>Tail interface: ${V.prosthetics.interfaceTail.amount}</div>` + - `<div>Tails: ${num(V.prosthetics.modT.amount + V.prosthetics.sexT.amount + V.prosthetics.combatT.amount)}</div>` + - `<div class="choices">Modular: ${V.prosthetics.modT.amount}</div>` + - `<div class="choices">Pleasure: ${V.prosthetics.sexT.amount}</div>` + - `<div class="choices">Combat: ${V.prosthetics.combatT.amount}</div>`; -}; - -/** - * Converts an array of strings into a sentence parted by commas. - * @param {Array} array ["apple", "bannana", "carrot"] - * @returns {string} "apple, bannana and carrot" - */ -globalThis.arrayToSentence = function(array) { - return array.reduce((res, ch, i, arr) => res + (i === arr.length - 1 ? ' and ' : ', ') + ch); -}; - -/** Returns the revivalist nationality associated with the player's arcology, or 0 if none - * @returns {string|0} - */ -globalThis.getRevivalistNationality = function() { - if (V.arcologies[0].FSRomanRevivalist > 90) { - return "Roman Revivalist"; - } else if (V.arcologies[0].FSAztecRevivalist > 90) { - return "Aztec Revivalist"; - } else if (V.arcologies[0].FSEgyptianRevivalist > 90) { - return "Ancient Egyptian Revivalist"; - } else if (V.arcologies[0].FSEdoRevivalist > 90) { - return "Edo Revivalist"; - } else if (V.arcologies[0].FSArabianRevivalist > 90) { - return "Arabian Revivalist"; - } else if (V.arcologies[0].FSChineseRevivalist > 90) { - return "Ancient Chinese Revivalist"; - } - return 0; -}; - -globalThis.penthouseCensus = function() { - function occupiesRoom(slave) { - if (slave.rules.living !== "luxurious") { - return false; // assigned to dormitory - } else if (slave.assignment === Job.HEADGIRL && V.HGSuite > 0) { - return false; // lives in HG suite - } else if (slave.assignment === Job.BODYGUARD && V.dojo > 0) { - return false; // lives in dojo - } else if (slave.relationship >= 4) { - const partner = getSlave(slave.relationshipTarget); - if (assignmentVisible(partner) && partner.ID < slave.ID && partner.rules.living === "luxurious") { - return false; // living with partner, who is already assigned a room (always allocate a room to the partner with the lower ID) - } - } - return true; // takes her own room - } - - const penthouseSlaves = V.slaves.filter(s => assignmentVisible(s)); - V.roomsPopulation = penthouseSlaves.filter(occupiesRoom).length; - V.dormitoryPopulation = penthouseSlaves.filter(s => s.rules.living !== "luxurious").length; -}; - -/** Determine whether a given penthouse slave can move into a private room or not. - * @param {App.Entity.SlaveState} slave - * @returns {boolean} - */ -globalThis.canMoveToRoom = function(slave) { - const partner = slave.relationship >= 4 ? getSlave(slave.relationshipTarget) : null; - const partnerHasRoom = partner && assignmentVisible(partner) && partner.rules.living === "luxurious"; - return partnerHasRoom || V.rooms - V.roomsPopulation >= 1; -}; - -/** - * @param {App.Entity.Facilities.Job|App.Entity.Facilities.Facility} jobOrFacility job or facility object - * @returns {App.Entity.SlaveState[]} array of slaves employed at the job or facility, sorted in accordance to user choice - */ -App.Utils.sortedEmployees = function(jobOrFacility) { - const employees = jobOrFacility.employees(); - SlaveSort.slaves(employees); - return employees; -}; - -/** - * @param {Array<string|App.Entity.Facilities.Facility>} [facilities] - * @param {Object.<string, string>} [mapping] Optional mapping for the property names in the result object. Keys - * are the standard facility names, values are the desired names. - * @returns {Object.<string, number>} - */ -App.Utils.countFacilityWorkers = function(facilities = null, mapping = {}) { - facilities = facilities || Object.values(App.Entity.facilities); - /** @type {App.Entity.Facilities.Facility[]} */ - const fObjects = facilities.map(f => typeof f === "string" ? App.Entity.facilities[f] : f); - return fObjects.reduce((acc, cur) => { - acc[mapping[cur.desc.baseName] || cur.desc.baseName] = cur.employeesIDs().size; return acc; - }, {}); -}; - -/** - * @param {string[]} [assignments] array of assignment strings. The default is to count for all assignments - * @returns {Object.<string, number>} - */ -App.Utils.countAssignmentWorkers = function(assignments) { - assignments = assignments || Object.values(Job); - return assignments.reduce((acc, cur) => { acc[cur] = V.JobIDMap[cur].size; return acc; }, {}); -}; - -/** Calculate and return economic uncertainty multiplier for a given arcology - * @param {number} arcologyID - * @returns {number} - */ -App.Utils.economicUncertainty = function(arcologyID) { - let uncertainty = arcologyID === 0 ? 5 : 10; - if (assistant.power === 1) { - uncertainty -= Math.max(Math.trunc(uncertainty/2), 0); - } else if (assistant.power > 1) { - uncertainty = 0; - } - return jsRandom(100 - uncertainty, 100 + uncertainty) / 100; -}; - -/** Notify the game that the sidebar needs to be refreshed as soon as possible, but do not do it immediately. - * This allows us to call this function repeatedly without impacting performance (for example, from repX() and cashX()). - * The game will redraw the sidebar exactly once, as soon as all the scripts have finished executing. - */ -App.Utils.scheduleSidebarRefresh = (function() { - let refresh = false; - - function updateSidebar() { - refresh = false; - UIBar.update(); - App.UI.updateSidebarTooltips(); - } - - function schedule() { - if (!refresh) { - refresh = true; - setTimeout(updateSidebar, 0); - } - } - - return schedule; -})(); - -/** Calculate various averages for the master suite slaves - * @returns {{energy: number, milk: number, cum: number, dom: number, sadism: number, dick: number, preg: number}} - */ -App.Utils.masterSuiteAverages = (function() { - const domMap = {dom: 1, submissive: -1}; - const sadismMap = {sadism: 1, masochism: -1}; - - /** Returns either zero or the results of mapping the slave's fetish through an object containing fetish names and result values - * @param {App.Entity.SlaveState} s - * @param {Object.<string, number>} map - * @returns {number} - */ - const fetishMapOrZero = (s, map) => map.hasOwnProperty(s.fetish) ? map[s.fetish] : 0; - - return () => { - const msSlaves = App.Entity.facilities.masterSuite.employees(); - return { - energy: _.mean(msSlaves.map(s => s.energy)), - milk: _.mean(msSlaves.map(s => s.lactation*(s.boobs-s.boobsImplant))), - cum: _.mean(msSlaves.map(s => canAchieveErection(s) ? s.balls : 0)), - dick: _.mean(msSlaves.map(s => canAchieveErection(s) ? s.dick : 0)), - preg: _.mean(msSlaves.map(s => s.preg)), - sadism: _.mean(msSlaves.map(s => (s.fetishStrength * fetishMapOrZero(s, sadismMap)))), - dom: _.mean(msSlaves.map(s => (s.fetishStrength * fetishMapOrZero(s, domMap)))) - }; - }; -})(); - -App.Utils.schoolCounter = function() { - return Array.from(App.Data.misc.schools.keys()).filter(s => V[s].schoolPresent).length; -}; - -App.Utils.schoolFailure = function() { - return Array.from(App.Data.misc.schools.keys()).find(s => V[s].schoolPresent && V[s].schoolProsperity <= -10); -}; - -App.Utils.alphabetizeIterable = function(iterable) { - const compare = function(a, b) { - let aTitle = a.toLowerCase(); - let bTitle = b.toLowerCase(); - - aTitle = removeArticles(aTitle); - bTitle = removeArticles(bTitle); - - if (aTitle > bTitle) { - return 1; - } - if (aTitle < bTitle) { - return -1; - } - return 0; - }; - - function removeArticles(str) { - const words = str.split(" "); - if (words.length <= 1) { - return str; - } - if ( words[0] === 'a' || words[0] === 'the' || words[0] === 'an' ) { - return words.splice(1).join(" "); - } - return str; - } - const clonedArray = (Array.from(iterable)); - return clonedArray.sort(compare); -}; - -/** - * @param {App.Entity.SlaveState[]} [slaves] - * @returns {Object.<number, number>} - */ -globalThis.slaves2indices = function(slaves = V.slaves) { - return slaves.reduce((acc, slave, i) => { acc[slave.ID] = i; return acc; }, {}); -}; - -/** - * @param {number} ID - * @returns {App.Entity.SlaveState} - */ -globalThis.getSlave = function(ID) { - const index = V.slaveIndices[ID]; - return index === undefined ? undefined : V.slaves[index]; -}; - -/** - * @param {number} ID - * @returns {App.Entity.SlaveState} - */ -globalThis.slaveStateById = function(ID) { - const index = V.slaveIndices[ID]; - return index === undefined ? null : V.slaves[index]; -}; - -globalThis.getChild = function(ID) { - return V.cribs.find(s => s.ID === ID); -}; - -globalThis.SlaveSort = function() { - const effectivePreg = (slave) => { - // slave.preg is only *mostly* usable for sorting - if (slave.preg > 0 && !slave.pregKnown) { - // don't reveal unknown pregnancies - return 0; - } - if (slave.pubertyXX === 0 && (slave.ovaries === 1 || slave.mpreg === 1)) { - // not ovulating yet - sort between barren slaves and slaves on contraceptives - return -1.2; - } else if (slave.ovaryAge >= 47 && (slave.ovaries === 1 || slave.mpreg === 1)) { - // menopausal - sort between barren slaves and slaves on contraceptives - return -1.1; - } else if (slave.pregWeek < 0) { - // postpartum - sort between slaves on contraceptives and fertile slaves - return -0.1; - } - return slave.preg; - }; - - const effectiveEnergy = (slave) => { - return slave.attrKnown === 1 ? slave.energy : -101; - }; - - const comparators = { - Aassignment: (a, b) => a.assignment < b.assignment ? -1 : 1, - Dassignment: (a, b) => a.assignment > b.assignment ? -1 : 1, - Aname: (a, b) => a.slaveName < b.slaveName ? -1 : 1, - Dname: (a, b) => a.slaveName > b.slaveName ? -1 : 1, - Aseniority: (a, b) => b.weekAcquired - a.weekAcquired, - Dseniority: (a, b) => a.weekAcquired - b.weekAcquired, - AactualAge: (a, b) => a.actualAge - b.actualAge, - DactualAge: (a, b) => b.actualAge - a.actualAge, - AvisualAge: (a, b) => a.visualAge - b.visualAge, - DvisualAge: (a, b) => b.visualAge - a.visualAge, - AphysicalAge: (a, b) => a.physicalAge - b.physicalAge, - DphysicalAge: (a, b) => b.physicalAge - a.physicalAge, - Adevotion: (a, b) => a.devotion - b.devotion, - Ddevotion: (a, b) => b.devotion - a.devotion, - AID: (a, b) => a.ID - b.ID, - DID: (a, b) => b.ID - a.ID, - AweeklyIncome: (a, b) => a.lastWeeksCashIncome - b.lastWeeksCashIncome, - DweeklyIncome: (a, b) => b.lastWeeksCashIncome - a.lastWeeksCashIncome, - Ahealth: (a, b) => a.health.health - b.health.health, - Dhealth: (a, b) => b.health.health - a.health.health, - Aweight: (a, b) => a.weight - b.weight, - Dweight: (a, b) => b.weight - a.weight, - Amuscles: (a, b) => a.muscles - b.muscles, - Dmuscles: (a, b) => b.muscles - a.muscles, - AsexDrive: (a, b) => effectiveEnergy(a) - effectiveEnergy(b), - DsexDrive: (a, b) => effectiveEnergy(b) - effectiveEnergy(a), - Apregnancy: (a, b) => effectivePreg(a) - effectivePreg(b), - Dpregnancy: (a, b) => effectivePreg(b) - effectivePreg(a), - }; - - return { - slaves: sortSlaves, - IDs: sortIDs, - indices: sortIndices - }; - - /** @param {App.Entity.SlaveState[]} [slaves] */ - function sortSlaves(slaves) { - slaves = slaves || V.slaves; - slaves.sort(_comparator()); - if (slaves === V.slaves) { - V.slaveIndices = slaves2indices(); - } - } - - /** @param {number[]} [slaveIDs] */ - function sortIDs(slaveIDs) { - const slaves = V.slaves; - const slaveIndices = V.slaveIndices; - const cmp = _comparator(); - slaveIDs = slaveIDs || slaves.map(s => s.ID); - slaveIDs.sort((IDa, IDb) => cmp(slaves[slaveIndices[IDa]], slaves[slaveIndices[IDb]])); - } - - /** @param {number[]} [slaveIdxs] */ - function sortIndices(slaveIdxs) { - const slaves = V.slaves; - const cmp = _comparator(); - slaveIdxs = slaveIdxs || [...slaves.keys()]; - slaveIdxs.sort((ia, ib) => cmp(slaves[ia], slaves[ib])); - } - - /** - * @callback slaveComparator - * @param {App.Entity.SlaveState} a - * @param {App.Entity.SlaveState} b - * @returns {number} - */ - /** @returns {slaveComparator} */ - function _comparator() { - return _makeStableComparator(comparators[(V.sortSlavesOrder === "ascending" ? 'A' : 'D') + V.sortSlavesBy]); - } - - /** secondary-sort by ascending ID if the primary comparator would return 0 (equal), so we have a guaranteed stable order regardless of input - * @param {slaveComparator} comparator - * @returns {slaveComparator} - */ - function _makeStableComparator(comparator) { - return function(a, b) { - return comparator(a, b) || comparators.AID(a, b); - }; - } -}(); - -/** - * @param {App.Entity.SlaveState[]} slaves - */ -globalThis.slaveSortMinor = function(slaves) { - slaves.sort((a, b) => a.slaveName < b.slaveName ? -1 : 1); -}; - -globalThis.menialPopCap = function() { - let r = ""; - - let popCap = 500 * (1 + V.building.findCells(cell => cell instanceof App.Arcology.Cell.Manufacturing && cell.type === "Pens").length); - - let overMenialCap = V.menials + V.fuckdolls + V.menialBioreactors - popCap; - if (overMenialCap > 0) { - const price = menialSlaveCost(-overMenialCap); - if (V.menials > 0) { - if (V.menials > overMenialCap) { - cashX((overMenialCap * price), "menialTrades"); - V.menialDemandFactor -= overMenialCap; - V.menials -= overMenialCap; - overMenialCap = 0; - r += "You don't have enough room for all your menials and are obliged to sell some."; - } else { - cashX((V.menials * price), "menialTrades"); - V.menialDemandFactor -= V.menials; - overMenialCap -= V.menials; - V.menials = 0; - r += "You don't have enough room for your menials and are obliged to sell them."; - } - } - if (overMenialCap > 0 && V.fuckdolls > 0) { - if (V.fuckdolls > overMenialCap) { - cashX(overMenialCap * (price * 2), "menialTrades"); - V.menialDemandFactor -= overMenialCap; - V.fuckdolls -= overMenialCap; - overMenialCap = 0; - r += "You don't have enough room for all your Fuckdolls and are obliged to sell some."; - } else { - cashX(V.fuckdolls * (price * 2), "menialTrades"); - V.menialDemandFactor -= V.fuckdolls; - overMenialCap -= V.fuckdolls; - V.fuckdolls = 0; - r += "You don't have enough room for your Fuckdolls and are obliged to sell them."; - } - } - if (overMenialCap > 0 && V.menialBioreactors > 0) { - cashX(overMenialCap * (price - 100), "menialTrades"); - V.menialDemandFactor -= overMenialCap; - V.menialBioreactors -= overMenialCap; - r += "You don't have enough room for all your menial bioreactors and are obliged to sell some."; - } - } - return {text: r, value: popCap}; -}; - -globalThis.initRules = function() { - const rule = emptyDefaultRule(); - rule.name = "Obedient Slaves"; - rule.condition.function = "between"; - rule.condition.data.attribute = "devotion"; - rule.condition.data.value = [20, null]; - rule.set.removalAssignment = "rest"; - - V.defaultRules = [rule]; - V.rulesToApplyOnce = {}; -}; diff --git a/src/js/utilsMisc.js b/src/js/utilsMisc.js new file mode 100644 index 0000000000000000000000000000000000000000..ab9e8f6928c3a475339641231554e4dd3d127654 --- /dev/null +++ b/src/js/utilsMisc.js @@ -0,0 +1,75 @@ +globalThis.Categorizer = class { + /** + * @param {...[]} pairs + */ + constructor(...pairs) { + this.cats = Array.prototype.slice.call(pairs) + .filter(function(e, i, a) { + return Array.isArray(e) && e.length === 2 && typeof e[0] === "number" && !isNaN(e[0]) && + a.findIndex(function(val) { + return e[0] === val[0]; + }) === i; /* uniqueness test */ + }) + .sort(function(a, b) { + return b[0] - a[0]; /* reverse sort */ + }); + } + + cat(val, def) { + let result = def; + if (typeof val === "number" && !isNaN(val)) { + let foundCat = this.cats.find(function(e) { + return val >= e[0]; + }); + if (foundCat) { + result = foundCat[1]; + } + } + // Record the value for the result's getter, if it is an object + // and doesn't have the property yet + if (typeof result === "object" && !isNaN(result)) { + result.value = val; + } + return result; + } +}; + +/** + * Converts an array of strings into a sentence parted by commas. + * @param {Array} array ["apple", "banana", "carrot"] + * @returns {string} "apple, banana and carrot" + */ +globalThis.arrayToSentence = function(array) { + return array.reduce((res, ch, i, arr) => res + (i === arr.length - 1 ? ' and ' : ', ') + ch); +}; + +App.Utils.alphabetizeIterable = function(iterable) { + const compare = function(a, b) { + let aTitle = a.toLowerCase(); + let bTitle = b.toLowerCase(); + + aTitle = removeArticles(aTitle); + bTitle = removeArticles(bTitle); + + if (aTitle > bTitle) { + return 1; + } + if (aTitle < bTitle) { + return -1; + } + return 0; + }; + + function removeArticles(str) { + const words = str.split(" "); + if (words.length <= 1) { + return str; + } + if ( words[0] === 'a' || words[0] === 'the' || words[0] === 'an' ) { + return words.splice(1).join(" "); + } + return str; + } + const clonedArray = (Array.from(iterable)); + return clonedArray.sort(compare); +}; diff --git a/src/js/utilsSlave.js b/src/js/utilsSlave.js index 34fbfd2d6b5a576ee2ed668724c51dc9305fb653..9d055f0f8b2cd0565aced7afa8dc2983195db75f 100644 --- a/src/js/utilsSlave.js +++ b/src/js/utilsSlave.js @@ -909,119 +909,6 @@ As a categorizer <</if>> <<print $cats.muscleCat.cat(_Slave.muscles)>> */ -/** - * @param {App.Entity.SlaveState} slave - * @returns {string} - */ -globalThis.getSlaveDevotionClass = function(slave) { - if ((!slave) || (!State)) { - return undefined; - } - if (slave.fetish === "mindbroken") { - return "mindbroken"; - } - if (slave.devotion < -95) { - return "very-hateful"; - } else if (slave.devotion < -50) { - return "hateful"; - } else if (slave.devotion < -20) { - return "resistant"; - } else if (slave.devotion <= 20) { - return "ambivalent"; - } else if (slave.devotion <= 50) { - return "accepting"; - } else if (slave.devotion <= 95) { - return "devoted"; - } else { - return "worshipful"; - } -}; - -/** - * @param {App.Entity.SlaveState} slave - * @returns {string} - */ -globalThis.getSlaveTrustClass = function(slave) { - if ((!slave) || (!State)) { - return undefined; - } - - if (slave.fetish === "mindbroken") { - return ""; - } - - if (slave.trust < -95) { - return "extremely-terrified"; - } else if (slave.trust < -50) { - return "terrified"; - } else if (slave.trust < -20) { - return "frightened"; - } else if (slave.trust <= 20) { - return "fearful"; - } else if (slave.trust <= 50) { - if (slave.devotion < -20) { - return "hate-careful"; - } else { - return "careful"; - } - } else if (slave.trust <= 95) { - if (slave.devotion < -20) { - return "bold"; - } else { - return "trusting"; - } - } else if (slave.devotion < -20) { - return "defiant"; - } else { - return "profoundly-trusting"; - } -}; - -/** - * @param {App.Entity.SlaveState} slave - * @param {number} [induce] - * @returns {string} - */ -globalThis.induceLactation = function(slave, induce = 0) { - const {His} = getPronouns(slave); - let r = ""; - let lactationStartChance = jsRandom(10, 100); - slave.induceLactation += induce; - if (slave.boobs < 300) { - lactationStartChance *= 1.5; - } else if (slave.boobs < 400 || slave.boobs >= 5000) { - lactationStartChance *= 1.2; - } - if (slave.pubertyXX === 0) { - lactationStartChance *= 1.5; - } - if (slave.preg > (slave.pregData.normalBirth / 1.33)) { - lactationStartChance *= .5; - } - if (slave.health.condition < -20) { - lactationStartChance *= 2; - } - if (slave.weight <= -30) { - lactationStartChance *= 1.5; - } - if (slave.boobsImplant > 0) { - lactationStartChance *= (1 + (slave.boobsImplant / slave.boobs)); - } - if (slave.lactationAdaptation > 0) { - lactationStartChance = (lactationStartChance / (slave.lactationAdaptation / 10)); - } - if (slave.geneticQuirks.galactorrhea === 2) { - lactationStartChance *= .5; - } - lactationStartChance = Math.floor(lactationStartChance); - if (slave.induceLactation >= lactationStartChance) { - r += `${His} breasts have been stimulated often enough to <span class="lime">induce lactation.</span>`; - slave.induceLactation = 0; - slave.lactationDuration = 2; - slave.lactation = 1; - } - return r; -}; globalThis.pronounReplacer = function(slavetext) { switch (slavetext) { @@ -1484,6 +1371,7 @@ globalThis.pronounReplacer = function(slavetext) { } return slavetext; }; + globalThis.convertCareer = function(slave) { let job = slave.career; if ((V.diversePronouns === 1) && (slave.pronoun === App.Data.Pronouns.Kind.male)) { @@ -1587,151 +1475,10 @@ globalThis.convertCareer = function(slave) { }; /** - * @param {string} targetSkill - Skill to be checked. - * @param {Object} slave - Slave to be checked. - * @param {number} [skillIncrease=1] - * @returns {string} + * + * @param {App.Entity.SlaveState} slave + * @returns {string|null} */ -globalThis.slaveSkillIncrease = function(targetSkill, slave, skillIncrease = 1) { - let r = "", skillDec; - const {He, his, him} = getPronouns(slave); - const isleadershipRole = function() { - if (['headGirl', 'recruiter', 'bodyguard', 'madam', 'DJ', 'nurse', 'teacher', 'attendant', 'matron', 'stewardess', 'milkmaid', 'farmer', 'wardeness'].includes(targetSkill)) { - return true; - } - return false; - }; - - if (slave.skill[targetSkill] <= 10) { - switch(targetSkill) { - case 'oral': - case 'vaginal': - case 'anal': - skillDec = `knowledge about ${targetSkill} sex,`; break; - case 'whoring': - skillDec = `knowledge about how to whore,`; break; - case 'entertainment': - skillDec = `knowledge about how to be entertaining,`; break; - } - if (isleadershipRole()) { - skillDec = `${capFirstChar(targetSkill)} skills.`; - } - - if (slave.skill[targetSkill] + skillIncrease > 10) { - r = `<span class="green">${He} now has basic ${skillDec}</span>`; - switch(targetSkill) { - case 'oral': - r += ` and at least suck a dick without constant gagging.`; break; - case 'vaginal': - r += ` and can avoid some of the common pitfalls and turnoffs.`; break; - case 'anal': - r += ` and can accept penetration of ${his} anus without danger.`; break; - case 'whoring': - r += ` and can avoid some potentially dangerous situations.`; break; - case 'entertainment': - r += ` and can usually avoid serious faux pas.`; break; - } - } - } else if (slave.skill[targetSkill] <= 30) { - switch(targetSkill) { - case 'oral': - case 'vaginal': - case 'anal': - skillDec = `${targetSkill} skills,`; break; - case 'whoring': - skillDec = `skill as a whore,`; break; - case 'entertainment': - skillDec = `skill as an entertainer,`; break; - } - if (isleadershipRole()) { - skillDec = `skill as a ${capFirstChar(targetSkill)}.`; - } - - if (slave.skill.oral + skillIncrease > 30) { - r = `<span class="green">${He} now has some ${skillDec}</span>`; - switch(targetSkill) { - case 'oral': - r += ` and can reliably bring dicks and pussies to climax with ${his} mouth.`; break; - case 'vaginal': - r += ` and can do more than just lie there and take it.`; break; - case 'anal': - r += ` and needs less preparation before taking rough penetration.`; break; - case 'whoring': - r += ` and knows how to sell ${his} body at a good price.`; break; - case 'entertainment': - r += ` and can flirt, dance, and strip acceptably.`; break; - } - } - } else if (slave.skill[targetSkill] <= 60) { - switch(targetSkill) { - case 'oral': - case 'vaginal': - case 'anal': - skillDec = `${targetSkill} sex expert,`; break; - case 'whoring': - skillDec = `expert whore,`; break; - case 'entertainment': - skillDec = `expert entertainer,`; break; - } - if (isleadershipRole()) { - skillDec = `expert ${capFirstChar(targetSkill)}.`; - } - - if (slave.skill[targetSkill] + skillIncrease > 60) { - r = `<span class="green">${He} is now an ${skillDec}</span>`; - switch(targetSkill) { - case 'oral': - r += ` and has a delightfully experienced tongue.`; break; - case 'vaginal': - r += ` and has the muscular control to massage anything that's inside ${him}.`; break; - case 'anal': - r += ` and knows how to use ${his} sphincter to please.`; break; - case 'whoring': - r += ` and can often make clients forget that $he's a prostitute they're paying for.`; break; - case 'entertainment': - r += ` and can flirt engagingly, dance alluringly, and strip arousingly.`; break; - } - } - } else if (slave.skill[targetSkill] < 100) { - switch(targetSkill) { - case 'oral': - case 'vaginal': - case 'anal': - skillDec = `has mastered ${targetSkill} sex,`; break; - case 'whoring': - skillDec = `is now a masterful whore,`; break; - case 'entertainment': - skillDec = `is now a masterful entertainer,`; break; - } - if (isleadershipRole()) { - skillDec = `is now a masterful ${capFirstChar(targetSkill)}.`; - } - - if (slave.skill[targetSkill] + skillIncrease >= 100) { - r = `<span class="green">${He} ${skillDec}</span>`; - switch(targetSkill) { - case 'oral': - r += ` and can learn nothing more about sucking dick or eating pussy.`; break; - case 'vaginal': - r += ` and can learn nothing more about tribbing or taking dick.`; break; - case 'anal': - r += ` and can learn nothing more about taking it up the ass.`; break; - case 'whoring': - r += ` and can learn nothing more about prostitution.`; break; - case 'entertainment': - r += ` and can learn nothing more about flirting, dancing, or stripping.`; break; - } - } - } - - if (isleadershipRole() && slave.skill[targetSkill] + skillIncrease >= 100) { - V.tutorGraduate.push(slave.ID); - V.slaveTutor[capFirstChar(targetSkill)].delete(slave.ID); - } - slave.skill[targetSkill] += skillIncrease; - return r; -}; - globalThis.tutorForSlave = function(slave) { for (const tutor of Object.keys(V.slaveTutor)) { const pupils = V.slaveTutor[tutor]; @@ -1742,6 +1489,11 @@ globalThis.tutorForSlave = function(slave) { return null; }; +/** + * + * @param {string} skill + * @returns {number} + */ globalThis.upgradeMultiplier = function(skill) { if (skill === 'medicine' && V.PC.career === "medicine" || skill === 'engineering' && V.PC.career === "engineer" || ((skill === 'medicine' || skill === 'engineering') && V.arcologies[0].FSRestartDecoration >= 100 && V.eugenicsFullControl === 0)) { @@ -2136,125 +1888,6 @@ globalThis.moreNational = function(nation) { return country; }; -/** - * Returns a "disobedience factor" between 0 (perfectly obedient) and 100 (completely defiant) - * @param {App.Entity.SlaveState} slave - * @returns {number} - */ -globalThis.disobedience = function(slave) { - const devotionBaseline = 20; // with devotion above this number slaves will obey completely - const trustBaseline = -20; // with trust below this number slaves will obey completely - - if (slave.devotion > devotionBaseline || slave.trust < trustBaseline) { - return 0; // no chance of disobedience - } - - // factors are between 0 (right on the boundary of perfectly obedient) and 10 (completely disobedient) - let devotionFactor = 10 - ((10 * (slave.devotion + 100)) / (devotionBaseline + 100)); - let trustFactor = (10 * (slave.trust - trustBaseline)) / (100 - trustBaseline); - return Math.round(devotionFactor * trustFactor); -}; - -/** - * Returns a valid rape target for a slave who is going to rape one of his peers into rivalry with him. - * @param {App.Entity.SlaveState} slave - * @param {function(App.Entity.SlaveState): boolean} predicate - * @returns {App.Entity.SlaveState | undefined} - */ -globalThis.randomRapeRivalryTarget = function(slave, predicate) { - const willIgnoreRules = disobedience(slave) > jsRandom(0, 100); - - function canBeARapeRival(s) { - return (s.devotion <= 95 && s.energy <= 95 && !s.rivalry && !s.fuckdoll && s.fetish !== "mindbroken"); - } - - function canRape(rapist, rapee) { - const opportunity = (assignmentVisible(rapist) && assignmentVisible(rapee)) || rapist.assignment === rapee.assignment; - const taboo = V.seeIncest === 0 && areRelated(rapist, rapee); - const desire = !(rapist.relationship >= 3 && rapist.relationshipTarget === rapee.id) && !taboo; - const permission = willIgnoreRules || App.Utils.sexAllowed(rapist, rapee); - return opportunity && desire && permission; - } - - if (typeof predicate !== 'function') { - predicate = (() => true); - } - - const arr = V.slaves.filter((s) => { return canBeARapeRival(s) && canRape(slave, s); }).shuffle(); - return arr.find(predicate); -}; - - -/** @typedef {object} getBestSlavesParams - * @property {string|function(App.Entity.SlaveState): number} part slave object property or custom function - * @property {number} [count] number of slaves to return - * @property {boolean} [largest] should it search for the biggest or smallest value - * @property {function(App.Entity.SlaveState): boolean} [filter] filter out undesired slaves - */ - -/** - * @param {getBestSlavesParams} params - * @returns {App.Entity.SlaveState[]} sorted from best to worst - */ -globalThis.getBestSlaves = function({part, count = 3, largest = true, filter = (() => true)}) { - const partCB = _.isFunction(part) ? part : (slave) => slave[part]; - - const sortMethod = largest ? (left, right) => right.value - left.value : (left, right) => left.value - right.value; - return V.slaves.filter(slave => filter(slave)) - .map(slave => ({slave, value: partCB(slave)})) - .sort(sortMethod) - .slice(0, count) - .map(slaveInfo => slaveInfo.slave); -}; -/** - * @param {getBestSlavesParams} info - * @returns {number[]} - */ -globalThis.getBestSlavesIDs = function(info) { - return getBestSlaves(info).map(slave => slave.ID); -}; - -/* -//Example -getBestSlaves({part:"butt", count: 5}); -getBestSlaves({part:"boobs"});//defaults to top 3 -getBestSlaves({part:"dick", smallest:true, filter:(slave)=>slave.dick > 0});//defaults to top 3 -getBestSlaves({part:slave=>slave.intelligence+slave.intelligenceImplant}); -*/ - -/** - * Generates a new slave ID that is guaranteed to be unused - * @returns {number} slave ID - */ -globalThis.generateSlaveID = function() { - // household liquidators and recETS generate slaves at an offset of 1000 (and many such slaves already exist) - // if you go through enough slaves you WILL generate collisions, so make sure we haven't just done that. - let allSlaveIDs = [...V.slaves.map((s) => s.ID), ...V.tanks.map((s) => s.ID), ...V.cribs.map((s) => s.ID)]; - while (allSlaveIDs.includes(V.IDNumber)) { - V.IDNumber++; - } - return V.IDNumber++; -}; - -globalThis.ASDump = function() { - if ((typeof V.activeSlave === undefined) || (V.activeSlave === 0)) { - return `<span class="red">ERROR:</span> AS Dump, activeSlave invalid, returnTo is 'V.returnTo', previous passage was '${previous()}'. Please report this. `; - } else { - let SL = V.slaves.length; - let ID = V.activeSlave.ID; - if (V.i >= 0 && V.i < SL && V.slaves[V.i].ID === ID) { /* shortcut if V.i is already pointing to this slave */ - V.slaves[V.i] = V.activeSlave; - } else { - V.i = V.slaveIndices[ID]; // find V.i if exists - if (typeof V.i === undefined) { /* not found, so new slave */ - newSlave(V.activeSlave); - } else { - V.slaves[V.i] = V.activeSlave; - } - } - } -}; - /** Deflate a slave (reset inflation to none) * @param {App.Entity.SlaveState} slave */ @@ -2267,100 +1900,6 @@ globalThis.deflate = function(slave) { SetBellySize(slave); }; -/** - * Returns how exposing a slave's outfit is, after taking into consideration a topless outfit is more revealing for beboobed slaves or female ones. - * @param {App.Entity.SlaveState} slave - * @returns {0|1|2|3|4} - */ -globalThis.getExposure = function(slave) { - const clothes = App.Data.clothes.get(slave.clothes); - return (clothes.topless && clothes.exposure < 3 && (slave.boobs > 299 || (slave.genes === 'XX' && slave.vagina >= 0))) ? 3 : clothes.exposure; -}; - -/** - * @param {App.Entity.SlaveState} A - * @param {App.Entity.SlaveState} B - * @returns {boolean} - */ -globalThis.sameAssignmentP = function(A, B) { - return A.assignment === B.assignment; -}; - -/** - * @param {App.Entity.SlaveState} slave - * @returns {boolean} - */ -globalThis.canImproveIntelligence = function(slave) { - let origIntel = V.genePool.find(function(s) { return s.ID === slave.ID; }).intelligence; - return (slave.intelligence < origIntel + 15) && (slave.intelligence < 100); -}; - -/** - * @param {App.Entity.SlaveState} slave - * @returns {number} - */ -globalThis.maxHeight = function(slave) { - let max = Math.trunc(Math.clamp((Height.mean(slave) * 1.25), 0, 274)); /* max achievable height is expected height plus 25% */ - - if (slave.geneticQuirks.dwarfism === 2 && slave.geneticQuirks.gigantism !== 2) { - max = Math.min(max, 160); - } - - return max; -}; - -/** - * @param {App.Entity.SlaveState} slave - * @returns {boolean} - */ -globalThis.canImproveHeight = function(slave) { - return slave.height < maxHeight(slave); -}; - -/** - * @param {App.Entity.SlaveState} slave - * @param {FC.HumanState} target - * @returns {boolean} - */ -globalThis.haveRelationshipP = function(slave, target) { - return slave.relationshipTarget === target.ID; -}; - -/** - * @param {App.Entity.SlaveState} slave - * @param {App.Entity.SlaveState} target - * @returns {boolean} - */ -globalThis.isRivalP = function(slave, target) { - return slave.rivalryTarget === target.ID; -}; - -/** - * @param {FC.HumanState} slave - * @returns {boolean} - */ -globalThis.supremeRaceP = function(slave) { - return V.arcologies[0].FSSupremacistRace === slave.race; -}; - -/** - * @param {FC.HumanState} slave - * @returns {boolean} - */ -globalThis.inferiorRaceP = function(slave) { - return V.arcologies[0].FSSubjugationistRace === slave.race; -}; - -/** - * @param {App.Entity.SlaveState} slave - * @returns {boolean} - */ -globalThis.isLeaderP = function(slave) { - const leaders = [S.HeadGirl, S.Bodyguard, S.Recruiter, S.Concubine, S.Nurse, S.Attendant, S.Matron, S.Madam, S.DJ, S.Milkmaid, S.Farmer, S.Stewardess, S.Schoolteacher, S.Wardeness]; - - return leaders.some(leader => leader && leader.ID === slave.ID); -}; - /** * colors skin, eyes and hair based on genetic Color. * Takes .override_*_Color into account. @@ -2546,23 +2085,6 @@ globalThis.newSlave = function(slave) { } }; -/** Get the written title for a given slave, without messing with global state. - * @param {App.Entity.SlaveState} [slave] - * @returns {string} - */ -globalThis.getWrittenTitle = function(slave) { - if (slave && slave.custom.title !== undefined && slave.custom.title !== "" && slave.rudeTitle === 0) { - return slave.custom.title; - } - if (V.PC.customTitle !== undefined) { - return V.PC.customTitle; - } else if (V.PC.title !== 0) { - return "Master"; - } else { - return "Mistress"; - } -}; - /** * @param {App.Entity.SlaveState} slave * @returns {number} @@ -2589,22 +2111,10 @@ globalThis.fetishChangeChance = function(slave) { sex = ((slave.actualAge - V.fertilityAge) / 4); } } - chance = Math.trunc(Math.clamp((slave.devotion / 4) - (fetish) - (sex), 0, 100)); - } - - return chance; -}; - -/** - * @param {App.Entity.SlaveState} slave - * @returns {string} - */ -globalThis.SlaveFullName = function(slave) { - const pair = slave.slaveSurname ? [slave.slaveName, slave.slaveSurname] : [slave.slaveName]; - if ((V.surnameOrder !== 1 && ["Cambodian", "Chinese", "Hungarian", "Japanese", "Korean", "Mongolian", "Taiwanese", "Vietnamese"].includes(slave.nationality)) || (V.surnameOrder === 2)) { - pair.reverse(); + chance = Math.trunc(Math.clamp((slave.devotion / 4) - (fetish) - (sex), 0, 100)); } - return pair.join(" "); + + return chance; }; /** @@ -3428,6 +2938,9 @@ globalThis.DegradingName = function(slave) { slave.slaveSurname = surname; }; +/** + * @param {App.Entity.SlaveState} slave + */ globalThis.PaternalistName = function(slave) { if (slave.slaveName.search("Miss") === -1) { if (slave.slaveName.search("Ms.") === -1) { @@ -3444,6 +2957,11 @@ globalThis.PaternalistName = function(slave) { } }; +/** + * + * @param {App.Entity.SlaveState} parent + * @param {App.Entity.SlaveState} child + */ globalThis.parentNames = function(parent, child) { const slaves = V.slaves; @@ -3737,20 +3255,267 @@ globalThis.ageSlave = function(slave, forceDevelopment = false) { } }; -/** Is the slave a shelter slave? +/** * @param {App.Entity.SlaveState} slave - * @returns {boolean} + * @param {number} [induce] + * @returns {string} */ -globalThis.isShelterSlave = function(slave) { - return (typeof slave.origin === "string" && slave.origin.includes("Slave Shelter")); +globalThis.induceLactation = function(slave, induce = 0) { + const {His} = getPronouns(slave); + let r = ""; + let lactationStartChance = jsRandom(10, 100); + slave.induceLactation += induce; + if (slave.boobs < 300) { + lactationStartChance *= 1.5; + } else if (slave.boobs < 400 || slave.boobs >= 5000) { + lactationStartChance *= 1.2; + } + if (slave.pubertyXX === 0) { + lactationStartChance *= 1.5; + } + if (slave.preg > (slave.pregData.normalBirth / 1.33)) { + lactationStartChance *= .5; + } + if (slave.health.condition < -20) { + lactationStartChance *= 2; + } + if (slave.weight <= -30) { + lactationStartChance *= 1.5; + } + if (slave.boobsImplant > 0) { + lactationStartChance *= (1 + (slave.boobsImplant / slave.boobs)); + } + if (slave.lactationAdaptation > 0) { + lactationStartChance = (lactationStartChance / (slave.lactationAdaptation / 10)); + } + if (slave.geneticQuirks.galactorrhea === 2) { + lactationStartChance *= .5; + } + lactationStartChance = Math.floor(lactationStartChance); + if (slave.induceLactation >= lactationStartChance) { + r += `${His} breasts have been stimulated often enough to <span class="lime">induce lactation.</span>`; + slave.induceLactation = 0; + slave.lactationDuration = 2; + slave.lactation = 1; + } + return r; }; /** - * Returns if a slave appears male, female, or androgynous. - * + * @param {string} targetSkill - Skill to be checked. + * @param {Object} slave - Slave to be checked. + * @param {number} [skillIncrease=1] + * @returns {string} + */ +globalThis.slaveSkillIncrease = function(targetSkill, slave, skillIncrease = 1) { + let r = "", skillDec; + const {He, his, him} = getPronouns(slave); + const isleadershipRole = function() { + if (['headGirl', 'recruiter', 'bodyguard', 'madam', 'DJ', 'nurse', 'teacher', 'attendant', 'matron', 'stewardess', 'milkmaid', 'farmer', 'wardeness'].includes(targetSkill)) { + return true; + } + return false; + }; + + if (slave.skill[targetSkill] <= 10) { + switch(targetSkill) { + case 'oral': + case 'vaginal': + case 'anal': + skillDec = `knowledge about ${targetSkill} sex,`; break; + case 'whoring': + skillDec = `knowledge about how to whore,`; break; + case 'entertainment': + skillDec = `knowledge about how to be entertaining,`; break; + } + if (isleadershipRole()) { + skillDec = `${capFirstChar(targetSkill)} skills.`; + } + + if (slave.skill[targetSkill] + skillIncrease > 10) { + r = `<span class="green">${He} now has basic ${skillDec}</span>`; + switch(targetSkill) { + case 'oral': + r += ` and at least suck a dick without constant gagging.`; break; + case 'vaginal': + r += ` and can avoid some of the common pitfalls and turnoffs.`; break; + case 'anal': + r += ` and can accept penetration of ${his} anus without danger.`; break; + case 'whoring': + r += ` and can avoid some potentially dangerous situations.`; break; + case 'entertainment': + r += ` and can usually avoid serious faux pas.`; break; + } + } + } else if (slave.skill[targetSkill] <= 30) { + switch(targetSkill) { + case 'oral': + case 'vaginal': + case 'anal': + skillDec = `${targetSkill} skills,`; break; + case 'whoring': + skillDec = `skill as a whore,`; break; + case 'entertainment': + skillDec = `skill as an entertainer,`; break; + } + if (isleadershipRole()) { + skillDec = `skill as a ${capFirstChar(targetSkill)}.`; + } + + if (slave.skill.oral + skillIncrease > 30) { + r = `<span class="green">${He} now has some ${skillDec}</span>`; + switch(targetSkill) { + case 'oral': + r += ` and can reliably bring dicks and pussies to climax with ${his} mouth.`; break; + case 'vaginal': + r += ` and can do more than just lie there and take it.`; break; + case 'anal': + r += ` and needs less preparation before taking rough penetration.`; break; + case 'whoring': + r += ` and knows how to sell ${his} body at a good price.`; break; + case 'entertainment': + r += ` and can flirt, dance, and strip acceptably.`; break; + } + } + } else if (slave.skill[targetSkill] <= 60) { + switch(targetSkill) { + case 'oral': + case 'vaginal': + case 'anal': + skillDec = `${targetSkill} sex expert,`; break; + case 'whoring': + skillDec = `expert whore,`; break; + case 'entertainment': + skillDec = `expert entertainer,`; break; + } + if (isleadershipRole()) { + skillDec = `expert ${capFirstChar(targetSkill)}.`; + } + + if (slave.skill[targetSkill] + skillIncrease > 60) { + r = `<span class="green">${He} is now an ${skillDec}</span>`; + switch(targetSkill) { + case 'oral': + r += ` and has a delightfully experienced tongue.`; break; + case 'vaginal': + r += ` and has the muscular control to massage anything that's inside ${him}.`; break; + case 'anal': + r += ` and knows how to use ${his} sphincter to please.`; break; + case 'whoring': + r += ` and can often make clients forget that $he's a prostitute they're paying for.`; break; + case 'entertainment': + r += ` and can flirt engagingly, dance alluringly, and strip arousingly.`; break; + } + } + } else if (slave.skill[targetSkill] < 100) { + switch(targetSkill) { + case 'oral': + case 'vaginal': + case 'anal': + skillDec = `has mastered ${targetSkill} sex,`; break; + case 'whoring': + skillDec = `is now a masterful whore,`; break; + case 'entertainment': + skillDec = `is now a masterful entertainer,`; break; + } + if (isleadershipRole()) { + skillDec = `is now a masterful ${capFirstChar(targetSkill)}.`; + } + + if (slave.skill[targetSkill] + skillIncrease >= 100) { + r = `<span class="green">${He} ${skillDec}</span>`; + switch(targetSkill) { + case 'oral': + r += ` and can learn nothing more about sucking dick or eating pussy.`; break; + case 'vaginal': + r += ` and can learn nothing more about tribbing or taking dick.`; break; + case 'anal': + r += ` and can learn nothing more about taking it up the ass.`; break; + case 'whoring': + r += ` and can learn nothing more about prostitution.`; break; + case 'entertainment': + r += ` and can learn nothing more about flirting, dancing, or stripping.`; break; + } + } + } + + if (isleadershipRole() && slave.skill[targetSkill] + skillIncrease >= 100) { + V.tutorGraduate.push(slave.ID); + V.slaveTutor[capFirstChar(targetSkill)].delete(slave.ID); + } + slave.skill[targetSkill] += skillIncrease; + return r; +}; + +/* +//Example +getBestSlaves({part:"butt", count: 5}); +getBestSlaves({part:"boobs"});//defaults to top 3 +getBestSlaves({part:"dick", smallest:true, filter:(slave)=>slave.dick > 0});//defaults to top 3 +getBestSlaves({part:slave=>slave.intelligence+slave.intelligenceImplant}); +*/ + +/** + * Generates a new slave ID that is guaranteed to be unused + * @returns {number} slave ID + */ +globalThis.generateSlaveID = function() { + // household liquidators and recETS generate slaves at an offset of 1000 (and many such slaves already exist) + // if you go through enough slaves you WILL generate collisions, so make sure we haven't just done that. + let allSlaveIDs = [...V.slaves.map((s) => s.ID), ...V.tanks.map((s) => s.ID), ...V.cribs.map((s) => s.ID)]; + while (allSlaveIDs.includes(V.IDNumber)) { + V.IDNumber++; + } + return V.IDNumber++; +}; + +/** + * @param {number} ID + * @returns {App.Entity.SlaveState} + */ +globalThis.slaveStateById = function(ID) { + const index = V.slaveIndices[ID]; + return index === undefined ? null : V.slaves[index]; +}; + +/** + * @param {number} ID + * @returns {App.Entity.SlaveState} + */ +globalThis.getSlave = function(ID) { + const index = V.slaveIndices[ID]; + return index === undefined ? undefined : V.slaves[index]; +}; + +globalThis.getChild = function(ID) { + return V.cribs.find(s => s.ID === ID); +}; + +/** + * Returns a valid rape target for a slave who is going to rape one of his peers into rivalry with him. * @param {App.Entity.SlaveState} slave - * @returns {number} + * @param {function(App.Entity.SlaveState): boolean} predicate + * @returns {App.Entity.SlaveState | undefined} */ -globalThis.perceivedGender = function(slave) { - return -1; +globalThis.randomRapeRivalryTarget = function(slave, predicate) { + const willIgnoreRules = disobedience(slave) > jsRandom(0, 100); + + function canBeARapeRival(s) { + return (s.devotion <= 95 && s.energy <= 95 && !s.rivalry && !s.fuckdoll && s.fetish !== "mindbroken"); + } + + function canRape(rapist, rapee) { + const opportunity = (assignmentVisible(rapist) && assignmentVisible(rapee)) || rapist.assignment === rapee.assignment; + const taboo = V.seeIncest === 0 && areRelated(rapist, rapee); + const desire = !(rapist.relationship >= 3 && rapist.relationshipTarget === rapee.id) && !taboo; + const permission = willIgnoreRules || App.Utils.sexAllowed(rapist, rapee); + return opportunity && desire && permission; + } + + if (typeof predicate !== 'function') { + predicate = (() => true); + } + + const arr = V.slaves.filter((s) => { return canBeARapeRival(s) && canRape(slave, s); }).shuffle(); + return arr.find(predicate); }; diff --git a/src/js/utilsSlaves.js b/src/js/utilsSlaves.js new file mode 100644 index 0000000000000000000000000000000000000000..79222bba4de3558011b209d9be326bb6d86eba17 --- /dev/null +++ b/src/js/utilsSlaves.js @@ -0,0 +1,237 @@ +globalThis.SlaveSort = function() { + const effectivePreg = (slave) => { + // slave.preg is only *mostly* usable for sorting + if (slave.preg > 0 && !slave.pregKnown) { + // don't reveal unknown pregnancies + return 0; + } + if (slave.pubertyXX === 0 && (slave.ovaries === 1 || slave.mpreg === 1)) { + // not ovulating yet - sort between barren slaves and slaves on contraceptives + return -1.2; + } else if (slave.ovaryAge >= 47 && (slave.ovaries === 1 || slave.mpreg === 1)) { + // menopausal - sort between barren slaves and slaves on contraceptives + return -1.1; + } else if (slave.pregWeek < 0) { + // postpartum - sort between slaves on contraceptives and fertile slaves + return -0.1; + } + return slave.preg; + }; + + const effectiveEnergy = (slave) => { + return slave.attrKnown === 1 ? slave.energy : -101; + }; + + const comparators = { + Aassignment: (a, b) => a.assignment < b.assignment ? -1 : 1, + Dassignment: (a, b) => a.assignment > b.assignment ? -1 : 1, + Aname: (a, b) => a.slaveName < b.slaveName ? -1 : 1, + Dname: (a, b) => a.slaveName > b.slaveName ? -1 : 1, + Aseniority: (a, b) => b.weekAcquired - a.weekAcquired, + Dseniority: (a, b) => a.weekAcquired - b.weekAcquired, + AactualAge: (a, b) => a.actualAge - b.actualAge, + DactualAge: (a, b) => b.actualAge - a.actualAge, + AvisualAge: (a, b) => a.visualAge - b.visualAge, + DvisualAge: (a, b) => b.visualAge - a.visualAge, + AphysicalAge: (a, b) => a.physicalAge - b.physicalAge, + DphysicalAge: (a, b) => b.physicalAge - a.physicalAge, + Adevotion: (a, b) => a.devotion - b.devotion, + Ddevotion: (a, b) => b.devotion - a.devotion, + AID: (a, b) => a.ID - b.ID, + DID: (a, b) => b.ID - a.ID, + AweeklyIncome: (a, b) => a.lastWeeksCashIncome - b.lastWeeksCashIncome, + DweeklyIncome: (a, b) => b.lastWeeksCashIncome - a.lastWeeksCashIncome, + Ahealth: (a, b) => a.health.health - b.health.health, + Dhealth: (a, b) => b.health.health - a.health.health, + Aweight: (a, b) => a.weight - b.weight, + Dweight: (a, b) => b.weight - a.weight, + Amuscles: (a, b) => a.muscles - b.muscles, + Dmuscles: (a, b) => b.muscles - a.muscles, + AsexDrive: (a, b) => effectiveEnergy(a) - effectiveEnergy(b), + DsexDrive: (a, b) => effectiveEnergy(b) - effectiveEnergy(a), + Apregnancy: (a, b) => effectivePreg(a) - effectivePreg(b), + Dpregnancy: (a, b) => effectivePreg(b) - effectivePreg(a), + }; + + return { + slaves: sortSlaves, + IDs: sortIDs, + indices: sortIndices + }; + + /** @param {App.Entity.SlaveState[]} [slaves] */ + function sortSlaves(slaves) { + slaves = slaves || V.slaves; + slaves.sort(_comparator()); + if (slaves === V.slaves) { + V.slaveIndices = slaves2indices(); + } + } + + /** @param {number[]} [slaveIDs] */ + function sortIDs(slaveIDs) { + const slaves = V.slaves; + const slaveIndices = V.slaveIndices; + const cmp = _comparator(); + slaveIDs = slaveIDs || slaves.map(s => s.ID); + slaveIDs.sort((IDa, IDb) => cmp(slaves[slaveIndices[IDa]], slaves[slaveIndices[IDb]])); + } + + /** @param {number[]} [slaveIdxs] */ + function sortIndices(slaveIdxs) { + const slaves = V.slaves; + const cmp = _comparator(); + slaveIdxs = slaveIdxs || [...slaves.keys()]; + slaveIdxs.sort((ia, ib) => cmp(slaves[ia], slaves[ib])); + } + + /** + * @callback slaveComparator + * @param {App.Entity.SlaveState} a + * @param {App.Entity.SlaveState} b + * @returns {number} + */ + /** @returns {slaveComparator} */ + function _comparator() { + return _makeStableComparator(comparators[(V.sortSlavesOrder === "ascending" ? 'A' : 'D') + V.sortSlavesBy]); + } + + /** secondary-sort by ascending ID if the primary comparator would return 0 (equal), so we have a guaranteed stable order regardless of input + * @param {slaveComparator} comparator + * @returns {slaveComparator} + */ + function _makeStableComparator(comparator) { + return function(a, b) { + return comparator(a, b) || comparators.AID(a, b); + }; + } +}(); + +/** + * @param {App.Entity.SlaveState[]} slaves + */ +globalThis.slaveSortMinor = function(slaves) { + slaves.sort((a, b) => a.slaveName < b.slaveName ? -1 : 1); +}; + +/** @typedef {object} getBestSlavesParams + * @property {string|function(App.Entity.SlaveState): number} part slave object property or custom function + * @property {number} [count] number of slaves to return + * @property {boolean} [largest] should it search for the biggest or smallest value + * @property {function(App.Entity.SlaveState): boolean} [filter] filter out undesired slaves + */ + +/** + * @param {getBestSlavesParams} params + * @returns {App.Entity.SlaveState[]} sorted from best to worst + */ +globalThis.getBestSlaves = function({part, count = 3, largest = true, filter = (() => true)}) { + const partCB = _.isFunction(part) ? part : (slave) => slave[part]; + + const sortMethod = largest ? (left, right) => right.value - left.value : (left, right) => left.value - right.value; + return V.slaves.filter(slave => filter(slave)) + .map(slave => ({slave, value: partCB(slave)})) + .sort(sortMethod) + .slice(0, count) + .map(slaveInfo => slaveInfo.slave); +}; + +/** + * @param {getBestSlavesParams} info + * @returns {number[]} + */ +globalThis.getBestSlavesIDs = function(info) { + return getBestSlaves(info).map(slave => slave.ID); +}; + +/** + * @param {App.Entity.SlaveState[]} [slaves] + * @returns {Object.<number, number>} + */ +globalThis.slaves2indices = function(slaves = V.slaves) { + return slaves.reduce((acc, slave, i) => { acc[slave.ID] = i; return acc; }, {}); +}; + +/** Calculate various averages for the master suite slaves + * @returns {{energy: number, milk: number, cum: number, dom: number, sadism: number, dick: number, preg: number}} + */ +App.Utils.masterSuiteAverages = (function() { + const domMap = {dom: 1, submissive: -1}; + const sadismMap = {sadism: 1, masochism: -1}; + + /** Returns either zero or the results of mapping the slave's fetish through an object containing fetish names and result values + * @param {App.Entity.SlaveState} s + * @param {Object.<string, number>} map + * @returns {number} + */ + const fetishMapOrZero = (s, map) => map.hasOwnProperty(s.fetish) ? map[s.fetish] : 0; + + return () => { + const msSlaves = App.Entity.facilities.masterSuite.employees(); + return { + energy: _.mean(msSlaves.map(s => s.energy)), + milk: _.mean(msSlaves.map(s => s.lactation*(s.boobs-s.boobsImplant))), + cum: _.mean(msSlaves.map(s => canAchieveErection(s) ? s.balls : 0)), + dick: _.mean(msSlaves.map(s => canAchieveErection(s) ? s.dick : 0)), + preg: _.mean(msSlaves.map(s => s.preg)), + sadism: _.mean(msSlaves.map(s => (s.fetishStrength * fetishMapOrZero(s, sadismMap)))), + dom: _.mean(msSlaves.map(s => (s.fetishStrength * fetishMapOrZero(s, domMap)))) + }; + }; +})(); + +globalThis.penthouseCensus = function() { + function occupiesRoom(slave) { + if (slave.rules.living !== "luxurious") { + return false; // assigned to dormitory + } else if (slave.assignment === Job.HEADGIRL && V.HGSuite > 0) { + return false; // lives in HG suite + } else if (slave.assignment === Job.BODYGUARD && V.dojo > 0) { + return false; // lives in dojo + } else if (slave.relationship >= 4) { + const partner = getSlave(slave.relationshipTarget); + if (assignmentVisible(partner) && partner.ID < slave.ID && partner.rules.living === "luxurious") { + return false; // living with partner, who is already assigned a room (always allocate a room to the partner with the lower ID) + } + } + return true; // takes her own room + } + + const penthouseSlaves = V.slaves.filter(s => assignmentVisible(s)); + V.roomsPopulation = penthouseSlaves.filter(occupiesRoom).length; + V.dormitoryPopulation = penthouseSlaves.filter(s => s.rules.living !== "luxurious").length; +}; + +/** + * @param {App.Entity.Facilities.Job|App.Entity.Facilities.Facility} jobOrFacility job or facility object + * @returns {App.Entity.SlaveState[]} array of slaves employed at the job or facility, sorted in accordance to user choice + */ +App.Utils.sortedEmployees = function(jobOrFacility) { + const employees = jobOrFacility.employees(); + SlaveSort.slaves(employees); + return employees; +}; + +/** + * @param {Array<string|App.Entity.Facilities.Facility>} [facilities] + * @param {Object.<string, string>} [mapping] Optional mapping for the property names in the result object. Keys + * are the standard facility names, values are the desired names. + * @returns {Object.<string, number>} + */ +App.Utils.countFacilityWorkers = function(facilities = null, mapping = {}) { + facilities = facilities || Object.values(App.Entity.facilities); + /** @type {App.Entity.Facilities.Facility[]} */ + const fObjects = facilities.map(f => typeof f === "string" ? App.Entity.facilities[f] : f); + return fObjects.reduce((acc, cur) => { + acc[mapping[cur.desc.baseName] || cur.desc.baseName] = cur.employeesIDs().size; return acc; + }, {}); +}; + +/** + * @param {string[]} [assignments] array of assignment strings. The default is to count for all assignments + * @returns {Object.<string, number>} + */ +App.Utils.countAssignmentWorkers = function(assignments) { + assignments = assignments || Object.values(Job); + return assignments.reduce((acc, cur) => { acc[cur] = V.JobIDMap[cur].size; return acc; }, {}); +}; diff --git a/src/uncategorized/repBudget.js b/src/uncategorized/repBudget.js new file mode 100644 index 0000000000000000000000000000000000000000..dbdc715633d06773cb7ba87f766e9fd105f1c354 --- /dev/null +++ b/src/uncategorized/repBudget.js @@ -0,0 +1,36 @@ +/** + * @param {string} category + * @param {string} title + * @returns {string} + */ +globalThis.budgetLine = function(category, title) { + let income; + let expenses; + + if (passage() === "Rep Budget") { + income = "lastWeeksRepIncome"; + expenses = "lastWeeksRepExpenses"; + + if (V[income][category] || V[expenses][category] || V.showAllEntries.repBudget) { + return `<tr>\ + <td>${title}</td>\ + <td>${repFormat(V[income][category])}</td>\ + <td>${repFormat(V[expenses][category])}</td>\ + <td>${repFormat(V[income][category] + V[expenses][category])}</td>\ + </tr>`; + } + } else if (passage() === "Costs Budget") { + income = "lastWeeksCashIncome"; + expenses = "lastWeeksCashExpenses"; + + if (V[income][category] || V[expenses][category] || V.showAllEntries.costsBudget) { + return `<tr>\ + <td>${title}</td>\ + <td>${cashFormatColor(V[income][category])}</td>\ + <td>${cashFormatColor(-Math.abs(V[expenses][category]))}</td>\ + <td>${cashFormatColor(V[income][category] + V[expenses][category])}</td>\ + </tr>`; + } + } + return ``; +};