diff --git a/devTools/types/FC/arcology.d.ts b/devTools/types/FC/arcology.d.ts index 0157430870bc8786f3515fd9c79ca6d3d3e7304c..ca632f6f171d1c35719aefae5db47bac73680c5c 100644 --- a/devTools/types/FC/arcology.d.ts +++ b/devTools/types/FC/arcology.d.ts @@ -248,4 +248,50 @@ declare namespace FC { NEO_IMPERIAL: RevivalSocietyFreezeValue.NeoImperial, ROMAN: RevivalSocietyFreezeValue.Roman, } + + const enum FSHumanDevelopmentVector { + /** Positive for mature preference, negative for young */ + Age, + /** Positive for statuesque preference, negative for petite */ + Height, + /** Positive for decadence preference negative for physical idealist */ + Weight, + /** Positive for transformation preference, negative for body purism */ + Modifications, + /** Positive for expansionist preference, negative for slim */ + Assets, + /** Positive for professionalism, negative for intellectual dependency */ + Intelligence, + /** Positive for radicalism, negative for fundamentalism */ + Gender, + /** Positive for repopulation, negative for eugenics */ + Breeding, + /** Positive for paternalism, negative for degradationism */ + LifeQuality, + } + + interface FSHumanDevelopmentVectorFreeze extends Record<string, FSHumanDevelopmentVector> { + AGE: FSHumanDevelopmentVector.Age, + HEIGHT: FSHumanDevelopmentVector.Height, + WEIGHT: FSHumanDevelopmentVector.Weight, + MODIFICATIONS: FSHumanDevelopmentVector.Modifications, + ASSETS: FSHumanDevelopmentVector.Assets, + INTELLIGENCE: FSHumanDevelopmentVector.Intelligence, + GENDER: FSHumanDevelopmentVector.Gender, + BREEDING: FSHumanDevelopmentVector.Breeding, + LIFE_QUALITY: FSHumanDevelopmentVector.LifeQuality, + } + + interface SlaveSocialEffect { + /** FS */ + FS: FutureSocietyDeco | ""; + /** positive or negative value (a small integer, or a fraction between -1 and 1) */ + magnitude: number; + /** for compact/list mode (plain text string) */ + shortDesc: string; + /** for expanded mode (HTML string) */ + longDesc: string; + /** true if the effect is only due to the facility she's assigned to */ + creditFacility?: boolean; + } } diff --git a/js/003-data/constants.js b/js/003-data/constants.js index 0db2cd4b10e03e4508be81bf3bb4a7ba52841795..0e0e29c0b54564406b7505b007bf520875d8fb8c 100644 --- a/js/003-data/constants.js +++ b/js/003-data/constants.js @@ -148,3 +148,19 @@ globalThis.RevivalSociety = Object.freeze({ NEO_IMPERIAL: 6, ROMAN: 7 }); + +/** + * @type {FC.FSHumanDevelopmentVectorFreeze} + * @enum {number} + */ +globalThis.FSHumanVector = Object.freeze({ + AGE: 0, + HEIGHT: 1, + WEIGHT: 2, + MODIFICATIONS: 3, + ASSETS: 4, + INTELLIGENCE: 5, + GENDER: 6, + BREEDING: 7, + LIFE_QUALITY: 8, +}); diff --git a/src/005-passages/managePassages.js b/src/005-passages/managePassages.js index 9531718e2596d0ed4281c50667a184334487db85..17d80261a60166cf27268cfd8a73484e8e16b73a 100644 --- a/src/005-passages/managePassages.js +++ b/src/005-passages/managePassages.js @@ -8,7 +8,9 @@ new App.DomPassage("Main", }, ["jump-to-safe", "jump-from-safe"] ); -new App.DomPassage("Future Society", () => { return App.UI.fsPassage(); }, ["jump-to-safe", "jump-from-safe"]); +new App.DomPassage("Future Society", () => {return App.UI.fsPassage();}, ["jump-to-safe", "jump-from-safe"]); + +new App.DomPassage("Slave FS Conformance", () => App.FSConformance.slaveConformancePassage(), ["jump-to-safe", "jump-from-safe"]); new App.DomPassage("Manage Penthouse", () => { diff --git a/src/endWeek/saSocialEffects.js b/src/endWeek/saSocialEffects.js index 517a0f97a26a9a8aea434512f23e6ca3d68c4283..e63336419e8854dcb4b375c4f65cc8dd4072e708 100644 --- a/src/endWeek/saSocialEffects.js +++ b/src/endWeek/saSocialEffects.js @@ -20,7 +20,7 @@ App.SlaveAssignment.saSocialEffects = function(slave) { this.creditFacility = creditFacility; } - /** @returns {SocialEffect[]} */ + /** @returns {FC.SlaveSocialEffect[]} */ function makeSocialEffects() { let t = []; @@ -1126,10 +1126,15 @@ App.SlaveAssignment.saSocialEffects = function(slave) { return _.differenceBy(newSocialEffects, socialEffects, s => s.shortDesc); } + function effects() { + return socialEffects; + } + const socialEffects = [...makeSocialEffects(), ...makeShelterGirlEffects()]; return { report, - newForFS + newForFS, + effects }; }; diff --git a/src/futureSocieties/fsPassage.js b/src/futureSocieties/fsPassage.js index bf7e69ebfb35b1eed6e49878554e7317e96a116c..f496a63cd5321f17a7f3826feeaee5ae8b7f52c5 100644 --- a/src/futureSocieties/fsPassage.js +++ b/src/futureSocieties/fsPassage.js @@ -171,6 +171,9 @@ App.UI.fsPassage = function() { if (V.FSSpending > 10000) { App.UI.DOM.appendNewElement("div", el, `Spending more than ${cashFormat(10000)} weekly is a waste`, "note"); } + if (App.FSConformance.anyVectorsDefined()) { + App.Events.addNode(el, [App.UI.DOM.passageLink("Check own slaves conformance", "Slave FS Conformance")]); + } return el; } diff --git a/src/futureSocieties/futureSociety.js b/src/futureSocieties/futureSociety.js index cd65e3555fb743e2f11d69cdf68e643ddaaa0e82..5179e55c847722da14a4ec327df1c7cc558ae0c2 100644 --- a/src/futureSocieties/futureSociety.js +++ b/src/futureSocieties/futureSociety.js @@ -31,6 +31,7 @@ globalThis.FutureSocieties = (function() { isActive, policyActive, advance, + humanVector, }; /** get the list of FSes active for a particular arcology @@ -775,4 +776,32 @@ globalThis.FutureSocieties = (function() { if (!FutureSocieties.isActive(fs, arc)) { return null; } return arc[fs] += amount; } + + /** + * Returns numeric value for one of the active FS policies pair, or null when both are inactive + * @param {FC.FSHumanDevelopmentVector} vector the vector to test + * @param {FC.ArcologyState} [arcology] Arcology to test, defaults to the PC's arcology + * @returns {number|null} The number is positive or negative according to comments for FSHumanDevelopmentVector + */ + function humanVector(vector, arcology) { + const arc = arcology ?? V.arcologies[0]; + /** + * @param {FC.FSPolicyValue} positiveFS + * @param {FC.FSPolicyValue} negativeFS + * @returns {FC.FSPolicyValue} + */ + const select = (positiveFS, negativeFS) => positiveFS !== null ? positiveFS : (negativeFS !== null ? -negativeFS : null); + + switch (vector) { + case FSHumanVector.AGE: return select(arc.FSMaturityPreferentialist, arc.FSYouthPreferentialist); + case FSHumanVector.HEIGHT: return select(arc.FSStatuesqueGlorification, arc.FSPetiteAdmiration); + case FSHumanVector.WEIGHT: return select(arc.FSHedonisticDecadence, arc.FSPhysicalIdealist); + case FSHumanVector.MODIFICATIONS: return select(arc.FSTransformationFetishist, arc.FSBodyPurist); + case FSHumanVector.ASSETS: return select(arc.FSAssetExpansionist, arc.FSSlimnessEnthusiast); + case FSHumanVector.INTELLIGENCE: return select(arc.FSSlaveProfessionalism, arc.FSIntellectualDependency); + case FSHumanVector.GENDER: return select(arc.FSGenderRadicalist, arc.FSGenderFundamentalist); + case FSHumanVector.BREEDING: return select(arc.FSRepopulationFocus, arc.FSRestart); + case FSHumanVector.LIFE_QUALITY: return select(arc.FSPaternalist, arc.FSDegradationist); + } + } })(); diff --git a/src/js/fsConformance.js b/src/js/fsConformance.js new file mode 100644 index 0000000000000000000000000000000000000000..70f22b25692cd5e9024672d03776d0fb217c7e61 --- /dev/null +++ b/src/js/fsConformance.js @@ -0,0 +1,104 @@ +/** Utilities to list slave basing on their conformance to FS policies */ +App.FSConformance = function() { + /** @type {Record<FC.FSHumanDevelopmentVector, [FC.FutureSocietyDeco, FC.FutureSocietyDeco]>} */ + const decosForVector = { + [FSHumanVector.AGE]: ['Maturity Preferentialist', 'Youth Preferentialist'], + [FSHumanVector.HEIGHT]: ['Statuesque Glorification', 'Petite Admiration'], + [FSHumanVector.WEIGHT]: ['Hedonistic', 'Physical Idealist'], + [FSHumanVector.MODIFICATIONS]: ['Transformation Fetishist', 'Body Purist'], + [FSHumanVector.ASSETS]: ['Asset Expansionist', 'Slimness Enthusiast'], + [FSHumanVector.INTELLIGENCE]: ['Slave Professionalism', 'Intellectual Dependency'], + [FSHumanVector.GENDER]: ['Gender Radicalist', 'Gender Fundamentalist'], + [FSHumanVector.BREEDING]: ['Repopulationist', 'Eugenics'], + [FSHumanVector.LIFE_QUALITY]: ['Paternalist', 'Degradationist'], + }; + + function anyVectorsDefined() { + return Object.values(FSHumanVector).some(v => FutureSocieties.humanVector(v, V.arcologies[0]) !== null); + } + + /** + * @param {{slave: FC.ReportSlave, effects: FC.SlaveSocialEffect[]}[]} slavesWithEffects + * @param {FC.FutureSocietyDeco} fsDeco + * @returns {{slave: FC.ReportSlave, magnitude: number}[]} + */ + function affectedSlaves(slavesWithEffects, fsDeco) { + return slavesWithEffects + .filter(item => item.effects.some(ef => fsDeco === ef.FS)) + .map(item => { + const totalMagnitude = item.effects.filter(ef => fsDeco === ef.FS).reduce((total, ef) => total + ef.magnitude, 0); + return {slave: item.slave, magnitude: totalMagnitude}; + }); + } + + /** + * @param {{slave: FC.ReportSlave, magnitude: number}[]} slavesWithEffects + * @param {FC.FutureSocietyDeco} deco + * @param {boolean} top + */ + function listSlaves(slavesWithEffects, deco, top) { + if (top) { + slavesWithEffects.sort((left, right) => right.magnitude - left.magnitude); + } else { + slavesWithEffects.sort((left, right) => left.magnitude - right.magnitude); + } + + return App.UI.SlaveList.render( + slavesWithEffects.slice(0, V.underperformersCount).map(item => item.slave.ID), + [], + App.UI.SlaveList.SlaveInteract.stdInteract, + ); + } + + function slaveConformancePassage() { + const node = new DocumentFragment(); + const r = []; + r.push(App.UI.DOM.makeElement("div", `${properMaster()}, while you spend numerous hours and put a lot of efforts to shape the society in your arcology for the envisioned future, you might be interested to know how well your own slaves conform to that vision.`)); + + App.Events.addParagraph(node, r); + + /** @type {{slave: FC.ReportSlave, effects: FC.SlaveSocialEffect[]}[]} */ + const slaveSocialEffects = []; + for (const rpSlave of App.SlaveAssignment.reportSlaves(V.slaves)) { + slaveSocialEffects.push({slave: rpSlave, effects: App.SlaveAssignment.saSocialEffects(rpSlave).effects()}); + } + + const conformandDiv = App.UI.DOM.makeElement("div"); + const conformantTabBar = new App.UI.Tabs.TabBar("fs-conformance-list"); + + const nonConformantDiv = App.UI.DOM.makeElement("div"); + const nonConformantTabBar = new App.UI.Tabs.TabBar("fs-non-conformance-list"); + + for (const vector of Object.values(FSHumanVector)) { + const vectorValue = FutureSocieties.humanVector(vector, V.arcologies[0]); + if (vectorValue === null) {continue;} + const deco = vectorValue > 0 ? decosForVector[vector][0] : decosForVector[vector][1]; + + const slavesForDeco = affectedSlaves(slaveSocialEffects, deco); + + conformantTabBar.addTab(deco, `${deco}-conform`, listSlaves(slavesForDeco.filter(item => item.magnitude > 0), deco, true)); + nonConformantTabBar.addTab(deco, `${deco}-non-conform`, listSlaves(slavesForDeco.filter(item => item.magnitude <= 0), deco, false)); + } + + conformandDiv.append(conformantTabBar.render()); + nonConformantDiv.append(nonConformantTabBar.render()); + + node.append(App.UI.DOM.accordion("Conformant", conformandDiv)); + node.append(App.UI.DOM.accordion("Non-conformant", nonConformantDiv)); + + const g = new App.UI.OptionsGroup(); + g.addOption("Maximum number of slaves", "underperformersCount") + .addValue("Default", 7).showTextBox(); + node.append(g.render()); + + V.nextButton = "Back"; + V.nextLink = "Future Society"; + + return node; + } + + return { + anyVectorsDefined, + slaveConformancePassage, + }; +}();