/** * @file Functions for rendering lists of slave summaries for various purposes. This includes * lists for the penthouse/facilities, selecting a slaves, facility leaders. * * For documentation see devNotes/slaveListing.md */ App.UI.SlaveList = {}; /** * @callback slaveTextGenerator * @param {App.Entity.SlaveState} slave * @returns {string} */ /** * @callback slaveToElement * @param {App.Entity.SlaveState} slave * @returns {HTMLElement|DocumentFragment} */ App.UI.SlaveList.render = function() { const facilityPassages = new Set( ["Main", "Head Girl Suite", "Spa", "Brothel", "Club", "Arcade", "Clinic", "Schoolroom", "Dairy", "Farmyard", "Servants' Quarters", "Master Suite", "Cellblock"]); /** @type {string} */ let passageName; // potentially can be a problem if played long enough to reach Number.MAX_SAFE_INTEGER let listID = Number.MIN_SAFE_INTEGER; /** @type {App.Art.SlaveArtBatch?} */ let batchRenderer = null; return listDOM; /** * @param {number[]} IDs * @param {Array.<{id: number, rejects: string[]}>} rejectedSlaves * @param {slaveToElement} interactionLink * @param {slaveToElement} [postNote] * @returns {DocumentFragment} */ function listDOM(IDs, rejectedSlaves, interactionLink, postNote) { passageName = passage(); let res = document.createDocumentFragment(); if ((V.seeImages === 1) && (V.seeSummaryImages === 1)) { batchRenderer = new App.Art.SlaveArtBatch(IDs, 1); res.appendChild(batchRenderer.writePreamble()); } if (V.useSlaveListInPageJSNavigation === 1) { res.appendChild(createQuickList(IDs)); } const fcs = App.Entity.facilities; const penthouse = fcs.penthouse; let anyFacilityExists = false; for (const f of Object.values(fcs)) { if (f !== penthouse && f.established) { anyFacilityExists = true; break; } } let showTransfers = false; if (anyFacilityExists) { if (facilityPassages.has(passageName)) { V.returnTo = passageName; showTransfers = true; } } for (const _sid of IDs) { let ss = renderSlave(_sid, interactionLink, showTransfers, postNote); let slaveDiv = document.createElement("div"); slaveDiv.id = `slave-${_sid}`; slaveDiv.classList.add("slaveSummary"); if (V.slavePanelStyle === 2) { slaveDiv.classList.add("card"); } slaveDiv.appendChild(ss); res.appendChild(slaveDiv); } for (const rs of rejectedSlaves) { const slave = slaveStateById(rs.id); const rejects = rs.rejects; const slaveName = SlaveFullName(slave); let slaveDiv = document.createElement("div"); slaveDiv.id = `slave-${slave.ID}`; slaveDiv.style.setProperty("clear", "both"); if (rejects.length === 1) { slaveDiv.innerHTML = rejects[0]; } else { slaveDiv.appendChild(document.createTextNode(`${slaveName}: `)); let ul = document.createElement("ul"); for (const rr of rejects) { const li = document.createElement("li"); li.innerHTML = rr; ul.appendChild(li); } slaveDiv.appendChild(ul); } res.appendChild(slaveDiv); } batchRenderer = null; // release reference return res; } /** * @param {number} id * @param {slaveToElement} interactionLink * @param {boolean} showTransfers * @param {slaveToElement} [postNote] * @returns {DocumentFragment} */ function renderSlave(id, interactionLink, showTransfers, postNote) { let res = document.createDocumentFragment(); if (V.slavePanelStyle === 0) { res.appendChild(document.createElement("br")); } else if (V.slavePanelStyle === 1) { const hr = document.createElement("hr"); hr.style.margin = "0"; res.appendChild(hr); } const slave = slaveStateById(id); if (batchRenderer) { let imgDiv = document.createElement("div"); imgDiv.classList.add("imageRef"); imgDiv.classList.add("smlImg"); imgDiv.appendChild(batchRenderer.render(slave)); res.appendChild(imgDiv); } // res.push(dividerAndImage(slave)); res.appendChild(interactionLink(slave)); if ((slave.choosesOwnClothes === 1) && (slave.clothes === "choosing her own clothes")) { const _oldDevotion = slave.devotion; App.SlaveAssignment.choosesOwnClothes(slave); slave.devotion = _oldDevotion; /* restore devotion value so repeatedly changing clothes isn't an exploit */ } SlaveStatClamp(slave); slave.trust = Math.trunc(slave.trust); slave.devotion = Math.trunc(slave.devotion); slave.health.condition = Math.trunc(slave.health.condition); slave.health.shortDamage = Math.trunc(slave.health.shortDamage); slave.health.longDamage = Math.trunc(slave.health.longDamage); slave.health.illness = Math.trunc(slave.health.illness); slave.health.tired = Math.trunc(slave.health.tired); slave.health.health = Math.trunc(slave.health.health); res.appendChild(document.createTextNode(' will ')); const assignment = document.createElement("span"); if ((slave.assignment === Job.REST) && (slave.health.condition >= -20)) { assignment.className = "freeAssignment"; assignment.innerText = slave.assignment; } else if ((slave.assignment === Job.CONFINEMENT) && ((slave.devotion > 20) || ((slave.trust < -20) && (slave.devotion >= -20)) || ((slave.trust < -50) && (slave.devotion >= -50)))) { assignment.innerText = slave.assignment; if (slave.sentence > 0) { assignment.innerText += ` (${slave.sentence} weeks)`; } } else if (slave.choosesOwnAssignment === 1) { assignment.innerText = `choose ${getPronouns(slave).possessive} own job`; } else { assignment.innerText = slave.assignment; if (slave.assignment === Job.CLASSES) { const role = tutorForSlave(slave); if (role) { assignment.innerText += ` on being a ${role}`; } } if (slave.sentence > 0) { assignment.innerText += ` ${slave.sentence} weeks`; } } if (slave.assignment === Job.CLINIC) { let list = []; if (slave.health.condition <= 40) { list.push(`poor health`); } if (slave.health.shortDamage >= 10) { list.push(`injuries`); } if (S.Nurse) { if ((slave.chem > 15) && (V.clinicUpgradeFilters === 1)) { list.push(`unhealthy chemicals`); } if ((V.clinicInflateBelly > 0) && (slave.bellyImplant >= 0) && (slave.bellyImplant <= (V.arcologies[0].FSTransformationFetishistResearch ? 800000 : 130000))) { list.push(`implant filling`); } if ((slave.pregKnown === 1) && (V.clinicSpeedGestation > 0 || slave.pregControl === "speed up") && ((slave.pregAdaptation*1000 < slave.bellyPreg || slave.preg > slave.pregData.normalBirth/1.33))) { list.push(`observation of accelerated pregnancy`); } else if ((slave.pregKnown === 1) && (V.clinicSpeedGestation > 0 || slave.pregControl === "speed up")) { list.push(`safely hurrying pregnancy along`); } else if ((slave.pregAdaptation*1000 < slave.bellyPreg || slave.preg > slave.pregData.normalBirth/1.33)) { list.push(`observation during pregnancy`); } } if (list.length > 0) { assignment.innerText += ` for ${arrayToSentence(list)}`; } else { assignment.innerText += ", preparing to check out"; } } else if (slave.assignment === Job.SPA) { let list = []; let i; if (slave.fetish === "mindbroken") { assignment.innerText += `, mindbroken`; } else { if ((slave.sexualFlaw !== "none") || (slave.behavioralFlaw !== "none")) { list.push(`overcoming flaws`); } if ((slave.trust < 60) || (slave.devotion < 60)) { list.push(`learning to accept life as a slave`); } if (slave.health.condition < 20) { list.push(`improving in health`); } if (list.length > 0) { assignment.innerText += `, ${arrayToSentence(list)}`; } else { assignment.innerText += ", preparing to move out"; } } } else if (slave.assignment === Job.SCHOOL) { let lessons = []; if (V.schoolroomRemodelBimbo === 1 && slave.intelligenceImplant > -15) { lessons.push("being a dumb bimbo"); } else if (V.schoolroomRemodelBimbo === 0 && slave.intelligenceImplant < 30) { lessons.push("general education"); } if (!((slave.voice === 0) || (slave.accent <= 1) || ((V.schoolroomUpgradeLanguage === 0) && (slave.accent <= 2)))) { lessons.push("speech"); } if (!((slave.skill.oral > 30) || ((V.schoolroomUpgradeSkills === 0) && (slave.skill.oral > 10)))) { lessons.push("oral"); } if (!((slave.skill.whoring > 30) || ((V.schoolroomUpgradeSkills === 0) && (slave.skill.whoring > 10)))) { lessons.push("whoring"); } if (!((slave.skill.entertainment > 30) || ((V.schoolroomUpgradeSkills === 0) && (slave.skill.entertainment > 10)))) { lessons.push("entertainment"); } if (!((slave.skill.anal > 30) || ((V.schoolroomUpgradeSkills === 0) && (slave.skill.anal > 10)))) { lessons.push("anal"); } if (!((slave.skill.vaginal > 30) || ((V.schoolroomUpgradeSkills === 0) && (slave.skill.vaginal > 10)) || (slave.vagina < 0))) { lessons.push("vaginal"); } const role = tutorForSlave(slave); if (role && needsTutoring(slave)) { lessons.push(`being a good ${role}`); } if (lessons.length > 0) { assignment.innerText += `, practicing ${arrayToSentence(lessons)}`; } else { assignment.innerText += ", studying for finals"; } } else if (slave.assignment === Job.SUBORDINATE) { if (slave.subTarget === -1) { assignment.innerText += ", serving as your Stud"; } else if (slave.subTarget !== 0) { const domSlave = getSlave(slave.subTarget); if (domSlave) { assignment.innerText += ", serving " + SlaveFullName(domSlave) + " exclusively"; } else { slave.subTarget = 0; } } } else if (slave.assignment === Job.AGENT) { const arc = V.arcologies.find((a) => a.leaderID === slave.ID); if (arc) { assignment.innerText += `, leading `; if (passageName === "Neighbor Interact") { assignment.appendChild(App.UI.DOM.makeElement("span", arc.name, "name")); } else { assignment.appendChild(App.UI.DOM.passageLink(arc.name, "Neighbor Interact")); } } } else if (slave.assignment === Job.AGENTPARTNER) { const arc = V.arcologies.find((a) => a.leaderID === slave.relationshipTarget); if (arc) { assignment.innerText += ` in `; if (passageName === "Neighbor Interact") { assignment.appendChild(App.UI.DOM.makeElement("span", arc.name, "name")); } else { assignment.appendChild(App.UI.DOM.passageLink(arc.name, "Neighbor Interact")); } } } assignment.appendChild(document.createTextNode('.')); res.appendChild(assignment); if (V.assignmentRecords[slave.ID]) { res.appendChild(document.createTextNode(` Last assigned to ${V.assignmentRecords[slave.ID]}.`)); } res.appendChild(document.createTextNode(' ')); if ((V.displayAssignments === 1) && (passageName === "Main") && (slave.ID !== V.HeadGirlID) && (slave.ID !== V.RecruiterID) && (slave.ID !== V.BodyguardID)) { res.appendChild(App.UI.jobLinks.assignmentsFragment(slave.ID, "Main", (slave, assignment) => { App.UI.SlaveList.ScrollPosition.record(); assignJob(slave, assignment); })); } if (showTransfers) { res.appendChild(document.createElement("br")); res.appendChild(document.createTextNode('Transfer to: ')); res.appendChild(App.UI.jobLinks.transfersFragment(slave.ID, (slave, assignment) => { App.UI.SlaveList.ScrollPosition.record(); assignJob(slave, assignment); })); } res.appendChild(App.UI.SlaveSummary.render(slave)); if (postNote) { const pn = postNote(slave); if (pn) { let r = document.createElement("p"); r.classList.add("si"); r.appendChild(pn); res.appendChild(r); } } return res; } /** * @param {number[]} IDs * @returns {DocumentFragment} */ function createQuickList(IDs) { /** * * @param {Node} container * @param {string} tagName * @param {string} [content] * @param {string|string[]} [classNames] * @param {Object.<string, any>} [attributes] * @returns {HTMLElement} */ function makeElement(container, tagName, content, classNames, attributes) { let res = document.createElement(tagName); container.appendChild(res); if (content) { res.textContent = content; } if (Array.isArray(classNames)) { for (const c of classNames) { res.classList.add(c); } } else if (classNames !== undefined) { res.classList.add(classNames); } if (attributes) { for (const [k, v] of Object.entries(attributes)) { res.setAttribute(k, v); } } return res; } const res = document.createDocumentFragment(); /* Useful for finding weird combinations — usages of this passage that don't yet generate the quick index. * <<print 'pass/count/indexed/flag::[' + passageName + '/' + _Count + '/' + _indexed + '/' + V.SlaveSummaryFiler + ']'>> */ if (IDs.length > 1 && passageName === "Main") { const _buttons = []; let _offset = -50; if (/Select/i.test(passageName)) { _offset = -25; } res.appendChild(document.createElement("br")); /* * we want <button data-quick-index="<<= listID>>">... */ const quickIndexBtn = document.createElement("button"); res.appendChild(quickIndexBtn); quickIndexBtn.id = `quick-list-toggle${listID}`; quickIndexBtn.setAttribute('data-quick-index', listID.toString()); quickIndexBtn.onclick = function(ev) { let which = /** @type {HTMLElement} */ (ev.target).attributes["data-quick-index"].value; let quick = $("div#list_index" + which); quick.toggleClass("ql-hidden"); }; /* * we want <div id="list_index3" class=" hidden">... */ const listIndex = makeElement(res, "div", undefined, "ql-hidden"); listIndex.id = `list_index${listID}`; for (const sID of IDs) { const _IndexSlave = slaveStateById(sID); const _indexSlaveName = SlaveFullName(_IndexSlave); const _devotionClass = getSlaveDevotionClass(_IndexSlave); const _trustClass = getSlaveTrustClass(_IndexSlave); _buttons.push({ "data-name": _indexSlaveName, "data-scroll-to": `#slave-${_IndexSlave.ID}`, "data-scroll-offset": _offset, "data-devotion": _IndexSlave.devotion, "data-trust": _IndexSlave.trust, "class": `${_devotionClass} ${_trustClass}` }); } if (_buttons.length > 0) { V.sortQuickList = V.sortQuickList || 'Devotion'; makeElement(listIndex, "em", "Sorting: "); const qlSort = makeElement(listIndex, "span", V.sortQuickList, "strong"); qlSort.id = "qlSort"; listIndex.appendChild(document.createTextNode(". ")); const linkSortByDevotion = makeElement(listIndex, "a", "Sort by Devotion"); linkSortByDevotion.onclick = (ev) => { ev.preventDefault(); V.sortQuickList = "Devotion"; $("#qlSort").text(V.sortQuickList); $("#qlWrapper").removeClass("trust").addClass("devotion"); sortButtonsByDevotion(); }; const linkSortByTrust = makeElement(listIndex, "a", "Sort by Trust"); linkSortByTrust.onclick = (ev) => { ev.preventDefault(); V.sortQuickList = "Trust"; $("#qlSort").text(V.sortQuickList); $("#qlWrapper").removeClass("devotion").addClass("trust"); sortButtonsByTrust(); }; makeElement(listIndex, "br"); const qlWrapper = makeElement(listIndex, "div", undefined, ["quick-list", "devotion"]); qlWrapper.id = "qlWrapper"; for (const _button of _buttons) { const btn = makeElement(listIndex, 'button', _button['data-name'], undefined, _button); btn.onclick = App.UI.quickBtnScrollToHandler; } } } return res; } }(); App.UI.SlaveList.Decoration = {}; /** * returns "HG", "BG", "PA", and "RC" prefixes * @param {App.Entity.SlaveState} slave * @returns {HTMLElement} */ App.UI.SlaveList.Decoration.penthousePositions = function(slave) { const fcs = App.Entity.facilities; if (fcs.headGirlSuite.manager.isEmployed(slave)) { return App.UI.DOM.makeElement("span", 'HG', ['lightcoral', 'strong']); } if (fcs.penthouse.manager.isEmployed(slave)) { return App.UI.DOM.makeElement("span", 'RC', ['lightcoral', 'strong']); } if (fcs.armory.manager.isEmployed(slave)) { return App.UI.DOM.makeElement("span", 'BG', ['lightcoral', 'strong']); } if (Array.isArray(V.personalAttention) && V.personalAttention.some(s => s.ID === slave.ID)) { return App.UI.DOM.makeElement("span", 'PA', ['lightcoral', 'strong']); } return null; }; App.UI.SlaveList.ScrollPosition = (function() { let lastPassage = null; let position = 0; return { reset: function() { lastPassage = null; position = 0; }, record: function() { lastPassage = passage(); position = window.pageYOffset; }, restore: function() { $(document).one(':passageend', () => { if (lastPassage === passage()) { window.scrollTo(0, position); } this.reset(); }); } }; })(); App.UI.SlaveList.SlaveInteract = {}; /** * @param {App.Entity.SlaveState} slave * @param {string} [text] print this text instead of slave name * @returns {DocumentFragment|HTMLElement} */ App.UI.SlaveList.SlaveInteract.stdInteract = function(slave, text) { const link = App.UI.DOM.passageLink(text ? text : SlaveFullName(slave), "Slave Interact", () => { App.UI.SlaveList.ScrollPosition.record(); V.AS = slave.ID; }); if (V.favorites.includes(slave.ID)) { return App.UI.DOM.combineNodes( App.UI.DOM.makeElement("span", String.fromCharCode(0xe800), ["icons", "favorite"]), " ", link); } return link; }; /** * @param {App.Entity.SlaveState} slave * @returns {DocumentFragment|HTMLElement} */ App.UI.SlaveList.SlaveInteract.penthouseInteract = function(slave) { let decoration = App.UI.SlaveList.Decoration.penthousePositions(slave); let stdLink = App.UI.SlaveList.SlaveInteract.stdInteract(slave); if (decoration) { let fr = document.createDocumentFragment(); fr.appendChild(decoration); fr.appendChild(document.createTextNode(' ')); fr.appendChild(stdLink); return fr; } return stdLink; }; /** * Adds/removes a slave with the given id to/from the personal attention array * @param {number} id slave id */ App.UI.selectSlaveForPersonalAttention = function(id) { if (!Array.isArray(V.personalAttention)) { // first PA target V.personalAttention = [{ ID: id, trainingRegimen: "undecided" }]; } else { const _pai = V.personalAttention.findIndex(s => s.ID === id); if (_pai === -1) { // not already a PA target; add V.personalAttention.push({ ID: id, trainingRegimen: "undecided" }); } else { // already a PA target; remove V.personalAttention.deleteAt(_pai); if (V.personalAttention.length === 0) { V.personalAttention = "sex"; } } } SugarCube.Engine.play("Personal Attention Select"); }; /** * @param {string} passage * @returns {HTMLElement} */ App.UI.SlaveList.sortingLinks = function(passage) { const outerDiv = document.createElement("div"); const textify = string => capFirstChar(string.replace(/([A-Z])/g, " $1")); let innerDiv = App.UI.DOM.makeElement("div", "Sort by: ", "indent"); let order = ["devotion", "name", "assignment", "seniority", "actualAge", "visualAge", "physicalAge", "weeklyIncome", "health", "weight", "muscles", "sexDrive", "pregnancy"] .map(so => V.sortSlavesBy !== so ? App.UI.DOM.passageLink(textify(so), passage, () => { V.sortSlavesBy = so; }) : textify(so)); innerDiv.append(App.UI.DOM.arrayToList(order, " | ", " | ")); outerDiv.append(innerDiv); innerDiv = App.UI.DOM.makeElement("div", "Sort direction: ", "indent"); order = ["descending", "ascending"].map(so => V.sortSlavesOrder !== so ? App.UI.DOM.passageLink(textify(so), passage, () => { V.sortSlavesOrder = so; }) : textify(so)); innerDiv.append(App.UI.DOM.arrayToList(order, " | ", " | ")); outerDiv.append(innerDiv); return outerDiv; }; /** * Standard tabs for a facility with a single job (SJ) * @param {App.Entity.Facilities.Facility} facility * @param {string} [facilityPassage] * @param {boolean} [showTransfersTab=false] * @param {{assign: string, remove: string, transfer: (string| undefined)}} [tabCaptions] * @returns {DocumentFragment} */ App.UI.SlaveList.listSJFacilitySlaves = function(facility, facilityPassage, showTransfersTab = false, tabCaptions = undefined) { const job = facility.job(); facilityPassage = facilityPassage || passage(); tabCaptions = tabCaptions || { assign: 'Assign a slave', remove: 'Remove a slave', transfer: 'Transfer from Facility' }; const frag = document.createDocumentFragment(); if (V.sortSlavesMain) { frag.append(this.sortingLinks(facilityPassage)); } const tabBar = App.UI.DOM.appendNewElement("div", frag, '', "tab-bar"); tabBar.append( App.UI.tabBar.tabButton('assign', tabCaptions.assign), App.UI.tabBar.tabButton('remove', tabCaptions.remove), (showTransfersTab ? App.UI.tabBar.tabButton('transfer', tabCaptions.transfer) : '') ); const facilitySlaves = [...job.employeesIDs()]; if (facilitySlaves.length > 0) { SlaveSort.IDs(facilitySlaves); frag.append(App.UI.tabBar.makeTab("remove", App.UI.SlaveList.render(facilitySlaves, [], App.UI.SlaveList.SlaveInteract.stdInteract, (slave) => App.UI.DOM.link(`Retrieve ${getPronouns(slave).object} from ${facility.name}`, () => removeJob(slave, job.desc.assignment), [], facilityPassage) ))); } else { frag.append(App.UI.tabBar.makeTab("remove", App.UI.DOM.makeElement("em", `${capFirstChar(facility.name)} is empty for the moment`))); } /** * @param {number[]} slaveIDs * @returns {DocumentFragment} */ function assignableTabContent(slaveIDs) { SlaveSort.IDs(slaveIDs); let rejectedSlaves = []; let passedSlaves = []; slaveIDs.forEach((id) => { const rejects = facility.canHostSlave(slaveStateById(id)); if (rejects.length > 0) { rejectedSlaves.push({id: id, rejects: rejects}); } else { passedSlaves.push(id); } }, []); return App.UI.SlaveList.render(passedSlaves, rejectedSlaves, App.UI.SlaveList.SlaveInteract.stdInteract, (slave) => App.UI.DOM.link(`Send ${getPronouns(slave).object} to ${facility.name}`, () => { assignmentTransition(slave, job.desc.assignment, facilityPassage); })); } if (facility.hasFreeSpace) { const assignableSlaveIDs = job.desc.partTime ? V.slaves.map(slave => slave.ID) : // all slaves can work here [...App.Entity.facilities.penthouse.employeesIDs()]; // only slaves from the penthouse can be transferred here frag.append(App.UI.tabBar.makeTab("assign", assignableTabContent(assignableSlaveIDs))); } else { frag.append(App.UI.tabBar.makeTab("assign", App.UI.DOM.makeElement("strong", `${capFirstChar(facility.name)} is full and cannot hold any more slaves`))); } if (showTransfersTab) { if (facility.hasFreeSpace) { // slaves from other facilities can be transferred here const transferableIDs = V.slaves.reduce((acc, slave) => { if (!assignmentVisible(slave) && !facility.isHosted(slave)) { acc.push(slave.ID); } return acc; }, []); frag.append(App.UI.tabBar.makeTab("transfer", assignableTabContent(transferableIDs))); } else { frag.append(App.UI.tabBar.makeTab("transfer", App.UI.DOM.makeElement("strong", `${capFirstChar(facility.name)} is full and cannot hold any more slaves`))); } } App.UI.tabBar.handlePreSelectedTab(); return frag; }; /** * @param {string[]} classNames */ App.UI.SlaveList.makeNameDecorator = function(classNames) { return (slave) => { const r = document.createElement("span"); for (const c of classNames) { r.classList.add(c); } r.textContent = SlaveFullName(slave); return r; }; }; /** * @returns {DocumentFragment} */ App.UI.SlaveList.listNGPSlaves = function() { const thisPassage = 'New Game Plus'; const frag = document.createDocumentFragment(); frag.append(this.sortingLinks(thisPassage)); const tabBar = App.UI.DOM.appendNewElement("div", frag, '', "tab-bar"); tabBar.append( App.UI.tabBar.tabButton('assign', "Import a slave"), App.UI.tabBar.tabButton('remove', "Remove from import") ); let imported = []; let nonImported = []; for (const slave of V.slaves) { // @ts-ignore: handle the legacy assignment string if (slave.assignment === "be imported") { slave.assignment = Job.IMPORTED; } if (slave.assignment === Job.IMPORTED) { imported.push(slave.ID); } else { nonImported.push(slave.ID); } } if (imported.length > 0) { SlaveSort.IDs(imported); frag.append(App.UI.tabBar.makeTab("remove", App.UI.SlaveList.render(imported, [], App.UI.SlaveList.makeNameDecorator(["emphasizedSlave", "pink"]), (s) => App.UI.DOM.passageLink('Remove from import list', thisPassage, () => removeJob(s, Job.IMPORTED)) ))); } else { frag.append(App.UI.tabBar.makeTab("remove", App.UI.DOM.makeElement('em', "No slaves will go with you to the new game"))); } if (imported.length < V.slavesToImportMax) { SlaveSort.IDs(nonImported); frag.append(App.UI.tabBar.makeTab("assign", App.UI.SlaveList.render(nonImported, [], App.UI.SlaveList.makeNameDecorator(["emphasizedSlave", "pink"]), (s) => App.UI.DOM.passageLink('Add to import list', thisPassage, () => assignJob(s, Job.IMPORTED)) ))); } else { frag.append(App.UI.tabBar.makeTab("assign", App.UI.DOM.makeElement('strong', `Slave import limit reached`))); } App.UI.tabBar.handlePreSelectedTab(); return frag; }; /** * Renders facility manager summary or a note with a link to select one * @param {App.Entity.Facilities.Facility} facility * @param {string} [selectionPassage] passage name for manager selection. "${Manager} Select" if omitted * @returns {DocumentFragment} */ App.UI.SlaveList.displayManager = function(facility, selectionPassage) { const managerCapName = capFirstChar(facility.desc.manager.position); selectionPassage = selectionPassage || `${managerCapName} Select`; const manager = facility.manager.currentEmployee; if (manager) { return this.render([manager.ID], [], App.UI.SlaveList.SlaveInteract.stdInteract, () => App.UI.DOM.passageLink(`Change or remove ${managerCapName}`, selectionPassage)); } else { const frag = document.createDocumentFragment(); frag.append(`You do not have a slave serving as a ${managerCapName}. `, App.UI.DOM.passageLink(`Appoint one`, selectionPassage)); return frag; } }; /** * Displays standard facility page with manager and list of workers * @param {App.Entity.Facilities.Facility} facility * @param {boolean} [showTransfersPage] * @returns {DocumentFragment} */ App.UI.SlaveList.stdFacilityPage = function(facility, showTransfersPage) { const frag = this.displayManager(facility); frag.append(document.createElement('br')); // TODO: replace with margin on one of the divs? frag.append(this.listSJFacilitySlaves(facility, passage(), showTransfersPage)); return frag; }; App.UI.SlaveList.penthousePage = function() { const ph = App.Entity.facilities.penthouse; function overviewTabContent() { const fragment = document.createDocumentFragment(); const A = V.arcologies[0]; let slaveWrapper = document.createElement("div"); // first is a div so we have no space between slave and buttons if (V.HeadGirlID) { const HG = S.HeadGirl; slaveWrapper.append(App.UI.DOM.makeElement("span", SlaveFullName(HG), "slave-name"), " is serving as your Head Girl"); if (A.FSEgyptianRevivalistLaw === 1) { slaveWrapper.append(" and Consort"); } slaveWrapper.append(". "); const link = App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Manage Head Girl", "HG Select"), "major-link"); link.id = "manageHG"; slaveWrapper.append(link, " ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("HG Select"), "hotkey")); slaveWrapper.append(App.UI.SlaveList.render([HG.ID], [], App.UI.SlaveList.SlaveInteract.penthouseInteract)); } else { if (V.slaves.length > 1) { slaveWrapper.append("You have ", App.UI.DOM.makeElement("span", "not", "warning"), " selected a Head Girl"); if (A.FSEgyptianRevivalistLaw === 1) { slaveWrapper.append(" and Consort"); } slaveWrapper.append(". ", App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Select One", "HG Select"), "major-link"), " ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("HG Select"), "hotkey")); slaveWrapper.id = "manageHG"; if (V.slavePanelStyle === 2) { slaveWrapper.classList.add("slaveSummary", "card"); } } else { slaveWrapper.append("You do not have enough slaves to keep a Head Girl"); slaveWrapper.classList.add("note"); } } fragment.append(slaveWrapper); slaveWrapper = document.createElement("p"); if (V.RecruiterID) { /** @type {App.Entity.SlaveState} */ const RC = S.Recruiter; const {he} = getPronouns(RC); slaveWrapper.append(App.UI.DOM.makeElement("span", SlaveFullName(RC), "slave-name"), " is working"); if (V.recruiterTarget !== "other arcologies") { slaveWrapper.append(" to recruit girls"); } else { slaveWrapper.append(" as a Sexual Ambassador"); if (A.influenceTarget === -1) { slaveWrapper.append(", but ", App.UI.DOM.makeElement("span", `${he} has no target to influence.`, "warning")); } else { const targetName = V.arcologies.find(a => a.direction === A.influenceTarget).name; slaveWrapper.append(` to ${targetName}.`); } } slaveWrapper.append(". "); const link = App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Manage Recruiter", "Recruiter Select"), "major-link"); link.id = "manageRecruiter"; slaveWrapper.append(link, " ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("Recruiter Select"), "hotkey")); slaveWrapper.append(App.UI.SlaveList.render([RC.ID], [], App.UI.SlaveList.SlaveInteract.penthouseInteract)); } else { slaveWrapper.append("You have ", App.UI.DOM.makeElement("span", "not", "warning"), " selected a Recruiter. ", App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Select one", "Recruiter Select"), "major-link"), " ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("Recruiter Select"), "hotkey")); slaveWrapper.id = "manageRecruiter"; if (V.slavePanelStyle === 2) { slaveWrapper.classList.add("slaveSummary", "card"); } } fragment.append(slaveWrapper); if (V.dojo) { slaveWrapper = document.createElement("p"); const BG = S.Bodyguard; if (BG) { slaveWrapper.append(App.UI.DOM.makeElement("span", SlaveFullName(BG), "slave-name"), " is serving as your bodyguard. "); const link = App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Manage Bodyguard", "BG Select"), "major-link"); link.id = "manageBG"; slaveWrapper.append(link, " ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("BG Select"), "hotkey")); slaveWrapper.append(App.UI.SlaveList.render([BG.ID], [], App.UI.SlaveList.SlaveInteract.penthouseInteract)); slaveWrapper.append(App.MainView.useGuard()); } else { slaveWrapper.append("You have ", App.UI.DOM.makeElement("span", "not", "warning"), " selected a Bodyguard. ", App.UI.DOM.makeElement("span", App.UI.DOM.passageLink("Select one", "BG Select"), "major-link"), " ", App.UI.DOM.makeElement("span", App.UI.Hotkeys.hotkeys("BG Select"), "hotkey")); slaveWrapper.id = "manageBG"; if (V.slavePanelStyle === 2) { slaveWrapper.classList.add("slaveSummary", "card"); } } fragment.append(slaveWrapper); } return fragment; } /** * @param {string} job * @returns {{n: number, dom: DocumentFragment}} */ function _slavesForJob(job) { const employeesIDs = job === 'all' ? [...ph.employeesIDs()] : [...ph.job(job).employeesIDs()]; if (employeesIDs.length === 0) { return {n: 0, dom: document.createDocumentFragment()}; } SlaveSort.IDs(employeesIDs); return { n: employeesIDs.length, dom: App.UI.SlaveList.render(employeesIDs, [], App.UI.SlaveList.SlaveInteract.penthouseInteract, V.fucktoyInteractionsPosition === 1 && job === "fucktoy" ? App.MainView.useFucktoy : null) }; } /** * @typedef tabDesc * @property {string} tabName * @property {string} caption * @property {Node} content */ /** * @param {string} tabName * @param {string} caption * @param {Node} content * @returns {tabDesc} */ function makeTabDesc(tabName, caption, content) { return { tabName: tabName, caption: caption, content: content }; } /** * Displays encyclopedia entries for occupations at the top of the tab, if enabled * @returns {HTMLSpanElement} */ function encycTips(jn) { const span = document.createElement("span"); span.classList.add("note"); if (V.showTipsFromEncy) { switch (jn) { case "rest": span.append(App.Encyclopedia.Entries.rest()); break; case "chooseOwn": break; /* no entry written for choose own */ case "fucktoy": span.append(App.Encyclopedia.Entries.fucktoy()); break; case "classes": span.append(App.Encyclopedia.Entries.attendingClasses()); break; case "houseServant": span.append(App.Encyclopedia.Entries.servitude()); break; case Job.WHORE: span.append(App.Encyclopedia.Entries.whoring()); break; case "publicServant": span.append(App.Encyclopedia.Entries.publicService()); break; case "subordinateSlave": span.append(App.Encyclopedia.Entries.sexualServitude()); break; case "cow": span.append(App.Encyclopedia.Entries.milking()); break; case "gloryhole": span.append(App.Encyclopedia.Entries.gloryHole()); break; case "confinement": span.append(App.Encyclopedia.Entries.confinement()); break; default: span.append(App.UI.DOM.makeElement("span", "missing tip for this tab", "error")); break; } } return span; } function allTab() { const penthouseSlavesIDs = [...ph.employeesIDs()]; if (S.HeadGirl) { penthouseSlavesIDs.push(S.HeadGirl.ID); } if (S.Recruiter) { penthouseSlavesIDs.push(S.Recruiter.ID); } if (S.Bodyguard) { penthouseSlavesIDs.push(S.Bodyguard.ID); } SlaveSort.IDs(penthouseSlavesIDs); return makeTabDesc("all", `All${V.useSlaveSummaryTabs > 0 ? ` (${penthouseSlavesIDs.length})` : ""}`, App.UI.SlaveList.render(penthouseSlavesIDs, [], App.UI.SlaveList.SlaveInteract.penthouseInteract)); } function favorites() { SlaveSort.IDs(V.favorites); return makeTabDesc("favorites", `Favorites${V.useSlaveSummaryTabs > 0 ? ` (${V.favorites.length})` : ""}`, App.UI.SlaveList.render(V.favorites, [], App.UI.SlaveList.SlaveInteract.penthouseInteract)); } let fragment = document.createDocumentFragment(); if (V.positionMainLinks >= 0) { fragment.append(App.UI.DOM.makeElement("div", App.UI.View.mainLinks(), "center")); } if (V.sortSlavesMain) { fragment.append(App.UI.SlaveList.sortingLinks("Main")); } /** @type {tabDesc[]} */ let tabs = []; // Overview tab if (V.useSlaveSummaryOverviewTab) { tabs.push(makeTabDesc("overview", "Special Roles", overviewTabContent())); } if (V.useSlaveSummaryTabs === 0) { tabs.push(allTab()); } if (V.favorites.length > 0 || V.useSlaveSummaryTabs === 0) { tabs.push(favorites()); } // tabs for each assignment for (const jn of ph.jobsNames) { const slaves = _slavesForJob(jn); if (slaves.n > 0) { tabs.push(makeTabDesc(jn, `${ph.desc.jobs[jn].position}${V.useSlaveSummaryTabs > 0 ? ` (${slaves.n})` : ""}`, App.UI.DOM.combineNodes(encycTips(jn), slaves.dom))); } else if (V.useSlaveSummaryTabs === 0) { tabs.push(makeTabDesc(jn, ph.desc.jobs[jn].position, encycTips(jn))); } } if (V.useSlaveSummaryTabs > 0) { tabs.push(allTab()); } const div = document.createElement("div"); div.classList.add("tab-bar"); if (V.useSlaveSummaryTabs === 0) { const links = []; for (const tab of tabs) { links.push(App.UI.tabBar.tabButton(tab.tabName, tab.caption, true)); } div.append(App.UI.DOM.arrayToList(links, " | ", " | ")); } else { for (const tab of tabs) { const button = App.UI.tabBar.tabButton(tab.tabName, tab.caption); if (V.useSlaveSummaryTabs === 2) { button.classList.add("card"); } div.append(button); } } fragment.append(div); for (const tab of tabs) { const div = App.UI.tabBar.makeTab(tab.tabName, tab.content); if (V.useSlaveSummaryTabs === 0) { div.classList.add("noFade"); } else if (V.useSlaveSummaryTabs === 2) { div.classList.add("card"); } fragment.append(div); } if (V.positionMainLinks <= 0) { fragment.append(App.UI.DOM.makeElement("div", App.UI.View.mainLinks(), "center")); } App.UI.tabBar.handlePreSelectedTab(); return fragment; }; /** * @callback slaveFilterCallbackReasoned * @param {App.Entity.SlaveState} slave * @returns {string[]} */ /** * @callback slaveFilterCallbackSimple * @param {App.Entity.SlaveState} slave * @returns {boolean} */ App.UI.SlaveList.slaveSelectionList = function() { const selectionElementId = "slaveSelectionList"; /** * @property {slaveFilterCallbackReasoned|slaveFilterCallbackSimple} filter * @property {slaveToElement} interactionLink * @property {slaveTestCallback} [expCheck] * @property {slaveToElement} [postNote] */ let options = null; return selection; /** * @param {slaveFilterCallbackReasoned|slaveFilterCallbackSimple} filter * @param {slaveToElement} interactionLink * @param {slaveTestCallback} [experienceChecker] * @param {slaveToElement} [postNote] * @returns {HTMLElement} */ function selection(filter, interactionLink, experienceChecker, postNote) { if (experienceChecker === null) { experienceChecker = undefined; } options = { filter: filter, interactionLink: interactionLink, expCheck: experienceChecker, postNote: postNote }; $(document).one(':passagedisplay', () => { _updateList('all'); }); const div = document.createElement("div"); div.append(_assignmentFilter(experienceChecker !== undefined)); const selectionElement = App.UI.DOM.appendNewElement("div", div); selectionElement.id = selectionElementId; return div; } function _updateList(assignment) { const e = document.getElementById(selectionElementId); e.innerHTML = ''; e.appendChild(_listSlaves(assignment)); } /** * Displays assignment filter links * @param {boolean} includeExperienced * @returns {HTMLElement} */ function _assignmentFilter(includeExperienced) { let filters = { all: "All" }; let fNames = Object.keys(App.Entity.facilities); fNames.sort(); for (const fn of fNames) { /** @type {App.Entity.Facilities.Facility} */ const f = App.Entity.facilities[fn]; if (f.established && f.hasEmployees) { filters[fn] = f.name; } } let links = []; for (const f in filters) { links.push(App.UI.DOM.link(filters[f], () => _updateList(f))); } if (includeExperienced) { links.push(App.UI.DOM.makeElement("span", App.UI.DOM.link('Experienced', () => _updateList('experienced')), "lime")); } return App.UI.DOM.generateLinksStrip(links); } /** * @param {string} assignmentStr * @returns {DocumentFragment} */ function _listSlaves(assignmentStr) { const slaves = V.slaves; let unfilteredIDs = []; switch (assignmentStr) { case 'all': unfilteredIDs = slaves.map(s => s.ID); break; case 'experienced': unfilteredIDs = slaves.reduce((acc, s) => { if (options.expCheck(s)) { acc.push(s.ID); } return acc; }, []); break; default: unfilteredIDs = [...App.Entity.facilities[assignmentStr].employeesIDs()]; // set to array break; } SlaveSort.IDs(unfilteredIDs); let passingIDs = []; let rejects = []; unfilteredIDs.forEach(id => { const fr = options.filter(slaveStateById(id)); if (fr === true || (Array.isArray(fr) && fr.length === 0)) { passingIDs.push(id); } else { if (Array.isArray(fr)) { rejects.push({id: id, rejects: fr}); } } }); // clamsi fragment to create a function which combines results of two optional tests // done this way to test for tests presence only once const listPostNote = options.expCheck ? (options.postNote ? s => options.expCheck(s) ? App.UI.DOM.combineNodes( App.UI.DOM.makeElement("span", "Has applicable career experience.", "lime"), document.createElement("br"), options.postNote(s) ) : options.postNote(s) : s => options.expCheck(s) ? App.UI.DOM.makeElement("span", "Has applicable career experience.", "lime") : null) : options.postNote ? s => options.postNote(s) : () => null; return App.UI.SlaveList.render(passingIDs, rejects, options.interactionLink, listPostNote); } }(); /** * @param {App.Entity.Facilities.Facility} facility * @param {string} passage go here after the new facility manager is selected * @returns {HTMLElement} */ App.UI.SlaveList.facilityManagerSelection = function(facility, passage) { return this.slaveSelectionList(slave => facility.manager.canEmploy(slave), (slave) => App.UI.DOM.passageLink(SlaveFullName(slave), passage, () => { assignJob(slave, facility.manager.desc.assignment); }), slave => facility.manager.slaveHasExperience(slave)); };