Forked from
pregmodfan / fc-pregmod
13402 commits behind the upstream repository.
slaveListing.js 39.75 KiB
/**
* @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));
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 ${toSentence(list)}`;
} else {
assignment.innerText += ", preparing to check out";
}
} else if (slave.assignment === Job.SPA) {
let list = [];
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 += `, ${toSentence(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 ${toSentence(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;
/** @type {Array<number>} */
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));
};