import { createCharacter } from "./create_character.js";
import { createContainer } from "./create_container.js";
import { createFamily } from "./create_family.js";
import { createItem } from "./create_item.js";
import { matcher } from "./tag_utils.js";
import { random, roll, shuffle, test } from "./random_utils.js";
import { resolve, parseDelimiters, sentenceCase } from "./string_utils.js";
import { ymlParser } from "./data_loaders.js";
import Bag from "./models/bag.js";
const parser = ymlParser({
lists: ["params", "except", "items"],
bools: ["child", "always"],
ints: ["per"],
keyProp: "name",
});
const ENTRIES = await parser("historical.data");
function parseEntriesNum(totalNodes, per) {
const numEntries = Math.round(totalNodes / per);
return numEntries === 0 && test(10) ? 1 : numEntries;
}
// children except A,B to ["children", ["A", "B"]]
function parseClear(clear) {
const [target, excStr] = clear.split(" except ");
const exceptions = excStr ? excStr.split(",").map((s) => s.trim()) : [];
return [target, exceptions];
}
// Not sure why locks are in the location generator since breaking them open would be something
// that involves the historical layer. It's also clear we need to know about being inside of
// secured areas—how does a scavenger die inside a locked building, for example.
/**
* Enriches a location map with pre-collapse historical narrative content. Selects locations
* and modifies them according to history rules: adding descriptions, renaming locations,
* placing items or containers, and optionally clearing their existing contents.
*
* @param {Object} options - Historical layer options
* @param {Location[]} options.list - A list of locations (which are also in a parent/child relationship,
* as is produced by the createLocation function)
* @returns {void}
*/
export function createHistoricalLayer({ list } = {}) {
if (!list) {
throw new Error("List is required to create a historical layer.");
}
shuffle(ENTRIES);
for (const entry of ENTRIES) {
const per = entry.per;
const eligibleNodes = list.filter(
(loc) => entry.always || (matcher(entry.select, loc.tags) && hasChildType(entry.has, loc)),
);
const totalNodes = eligibleNodes.length;
let numEntries = parseEntriesNum(totalNodes, per);
if (entry.always && numEntries === 0) {
numEntries = 1;
}
if (numEntries === 0 || totalNodes === 0) {
continue;
}
for (let i = 0; i < numEntries; i++) {
let location = random(eligibleNodes);
// if nothing matches in this production, or the node has already been altered, skip it
let parent = getParent(location);
if (!location || parent.h) {
continue;
}
parent.h = true;
// not accurate since if we selected a previously selected node, we skip this iteration
console.log(
`Changing ${numEntries} of ${totalNodes} locations for historical event ${entry.name}`,
);
// finally we begin with this
// generate template parameters
let params = getLocationParams({}, location);
if (entry.params?.includes("character")) {
params = getCharacterParams(params);
}
if (entry.params?.includes("family")) {
params = getFamilyParams(params);
}
// use template parameters
if (entry.text) {
location.description.push(resolve(entry.text, params));
}
if (entry.rename) {
location.name = resolve(entry.rename, params);
}
if (entry.container) {
location.contents.push(createContainer({ type: entry.container }));
}
if (entry.items) {
location.contents.push(addItems(entry.items));
}
if (entry.clear) {
let [target, exceptions] = parseClear(entry.clear);
if (target === "room") {
clearLocation(location);
} else if (target === "children") {
clearRecursively(location, exceptions);
// now do it again to trim any children that are now empty
clearRecursively(location, exceptions);
clearLocation(location);
}
}
}
}
}
function getParent(location) {
for (let n = location; ; n = n.parent) {
if (n != null && !n.tags.has("room") && !n.tags.has("area")) {
return n;
}
}
}
function hasChildType(sel, location) {
if (!sel || matcher(sel, location.tags)) {
return true;
}
return location.children.some((child) => hasChildType(sel, child));
}
function addItems(items) {
let bag = new Bag();
items.forEach((tag) => {
const [hasParens, tagStr, rollStr] = parseDelimiters(tag);
const item = createItem({ tags: tagStr });
if (item) {
const count = hasParens ? roll(rollStr) : 1;
bag.add(item, count);
}
});
return bag;
}
function clearRecursively(location, exceptions) {
console.log(`Clearing location ${location.type} children except for: ${exceptions}`);
location.children = location.children.filter((child) => {
return exceptions.includes(child.type) || child.children.length > 0;
});
location.children.forEach((child) => clearRecursively(child, exceptions));
}
function clearLocation(location) {
delete location.image;
location.contents = [];
}
function getFamilyParams(params) {
const family = createFamily({ generations: 1 });
params.familyName = family.parent.partner.name.family;
params.fatherName = family.parent.partner.name.toString();
params.motherName = family.parent.name.toString();
return params;
}
function getCharacterParams(params) {
// "Traveling" will set motorcycles and horses, and that's weird (Some isn’t an option)
const character = createCharacter({ possessions: "Some" });
params.personal = character.personal;
params.personalUpper = sentenceCase(character.personal);
params.clothing = character.clothing.toString().toLowerCase();
params.clothingUpper = sentenceCase(character.clothing.toString());
params.possessions = character.possessions.toString().toLowerCase();
params.possessionsUpper = sentenceCase(character.possessions.toString());
params.professionName = character.profession.toLowerCase();
params.professionNameUpper = character.profession;
return params;
}
function getLocationParams(params = {}, location) {
params.locationName = location.name.toLowerCase();
params.locationNameUpper = location.name;
params.locationType = location.type.toLowerCase();
params.locationTypeUpper = location.type;
return params;
}