import { createCharacter } from "./create_character.js";
import { createContainer } from "./create_container.js";
import { ymlParser } from "./data_loaders.js";
import { random, roll, test } from "./random_utils.js";
import { resolve, parseDelimiters, sentenceCase } from "./string_utils.js";
import { createItem } from "./create_item.js";
import { createFamily } from "./create_family.js";
import Bag from "./models/bag.js";
const parser = ymlParser({
lists: ["locations", "params", "except", "items"],
bools: ["child"],
ints: ["per"],
keyProp: "name",
});
const ENTRIES = await parser("historical.data");
function parseEntriesNum(totalNodes, per) {
const numEntries = Math.round(totalNodes / per);
return numEntries === 0 && test(40) ? 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];
}
/**
* 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 {Map} options.map - A location map keyed by location type. Modified in place.
* @returns {void}
*/
export function createHistoricalLayer({ map } = {}) {
if (!map) {
throw new Error("Map is required to create a historical layer.");
}
for (const entry of ENTRIES) {
const per = entry.per;
const totalNodes = countLocations(map, entry.locations);
const numEntries = parseEntriesNum(totalNodes, per);
console.log(`Altering ${numEntries} of ${totalNodes} locations for ${entry.name}`);
for (let i = 0; i < numEntries; i++) {
let location = findLocation(map, entry.locations, entry.sel, entry.has);
// if nothing matches in this production, or the node has already been altered, skip it
if (!location || location.h) {
continue;
}
location.h = true; // mark as altered
// 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 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 findLocation(map, locations, selType, childType) {
const selectedLocations = [];
for (let type of locations) {
let locList = map.get(type) || [];
if (selType === "child") {
locList = locList.map((loc) => findTerminalChild(loc));
}
if (selType === "selfOrChild") {
locList = locList.map((loc) => findTerminalChild(loc));
locList.push(map.get(type));
}
if (childType) {
locList = locList.filter((loc) => hasChildType(loc, childType));
}
selectedLocations.push(...locList);
}
return random(selectedLocations);
}
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 findTerminalChild(location) {
if (location.children.length === 0) {
return location;
}
const child = random(location.children);
if (child?.children?.length === 0) {
return child;
}
return findTerminalChild(child);
}
function hasChildType(location, type) {
if (!type) {
return true;
}
return (
location.children.filter((child) => {
return child.type === type || hasChildType(child, type);
}).length > 0
);
}
function countLocations(map, types = []) {
let count = 0;
for (const type of types) {
const list = map.get(type) || [];
count += list.length;
}
return count;
}
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;
}