import { bagSpecParser } from "./string_utils.js";
import { createBag } from "./create_bag.js";
import { createCharacter } from "./create_character.js";
import { createContainer } from "./create_container.js";
import { createEventLayer } from "./create_event_layer.js";
import { createFamily } from "./create_family.js";
import { createHistoricalLayer } from "./create_historical_layer.js";
import { createLocationName } from "./create_location_name.js";
import { createRelationship } from "./create_relationship.js";
import { Location } from "./models/location.js";
import { logger } from "./utils.js";
import { toList } from "./string_utils.js";
import { random, randomElement, roll, selectElements, test } from "./random_utils.js";
import { LOCATION_DATABASE as DATABASE, BARRIERS } from "./location_loader.js";
const LIST = [];
const LOCATION_TYPES = DATABASE.models
.filter((m) => m.not("room") && m.not("abstract") && m.not("area"))
.map((m) => m.type)
.sort();
const STORE_TYPES = DATABASE.models
.filter((m) => m.owner)
.map((m) => m.type)
.sort();
export function getLocationTypes() {
return LOCATION_TYPES;
}
export function getStoreTypes() {
return STORE_TYPES;
}
/**
* Create a location. You must supply the type of a location template and an instance tree
* will be created from that point in the template hierarchy, working downward throw all
* child nodes templates, returning the resulting instance tree. Tags are not currently used
* in selection of templates, but may be in the future.
*/
export function createLocation({
type = random(LOCATION_TYPES),
history = true,
events = false,
} = {}) {
logger.start("createLocation", { type });
LIST.length = 0;
let template = DATABASE.findOne({ type });
if (!template) {
throw new Error("No location template found for type: " + type);
}
let root = createInstance(new Location({ sequences: [] }), template);
walkTemplate(root, template);
if (history) {
createHistoricalLayer({ list: LIST });
}
if (events) {
//createEventLayer({ list: LIST });
}
return logger.end(root);
}
// Intended as a replacement for create_store.js, but the metadata needs to be moved from that file
// to the locations.data file.
export function createStore({ type = random(STORE_TYPES) } = {}) {
logger.start("createStore", { type });
LIST.length = 0;
let template = DATABASE.findOne({ type });
let store = createInstance(new Location({ sequences: [] }), template);
walkTemplate(store, template);
return logger.end(store);
}
function walkTemplate(parent, template) {
let elements = selectElements(template.ch, true);
elements.forEach((type) => {
let childTemplate = DATABASE.findOne({ type });
if (childTemplate.is("abstract")) {
// Abstract templates are not instantiated, but their children are processed. We can add
// other fields besides image, if appropriate.
parent.image = childTemplate.image;
walkTemplate(parent, childTemplate);
} else {
let childLocation = createInstance(parent, childTemplate);
walkTemplate(childLocation, childTemplate);
}
});
}
function createInstance(parent, childTemplate) {
let child = new Location(childTemplate, parent);
if (childTemplate.owner) {
const traits = Object.fromEntries(childTemplate.traits.map((trait) => [trait, 1]));
if (test(30)) {
child.owner = createRelationship();
} else if (test(30)) {
child.owner = createFamily({ generations: 2 }).getOldestRelationship();
} else {
child.owner = createCharacter({ postProfession: childTemplate.owner, traits });
}
if (child.owner.older) {
child.owner.older = retrainToProfession(childTemplate.owner, traits, child.owner.older);
child.owner.younger = retrainToProfession(childTemplate.owner, traits, child.owner.younger);
}
}
const looted = test(childTemplate.looted);
if (childTemplate.barriers.length) {
const barriers = createBarriers(looted, childTemplate.barriers);
child.description.unshift(barriers);
}
// contents is what exists in a room or place, including containers that contain things
child.contents = determineContents(childTemplate, looted);
if (parent) {
parent.addChild(child);
}
child.name = createLocationName({ child });
LIST.push(child);
return child;
}
// create a description of all barriers into/out of a location
function createBarriers(looted, barriers) {
const index = looted ? roll(barriers.length) - 1 : -2;
return (
barriers
.map((barr, i) => {
const locks = randomElement(barr.locks);
return index === i
? `<b>${barr.entrance}</b> was secured by ${toList(locks.map(brokenBarrier))}`
: `<b>${barr.entrance}</b> secured by ${toList(locks.map(barrier))}`;
})
.join(". ") + "."
);
}
// describe a compromised barrier
function brokenBarrier(lock) {
const config = BARRIERS.get(lock);
return `${random(config.descr)}, ${random(config.br)}`;
}
// describe a functional barrier
function barrier(lock) {
const config = BARRIERS.get(lock);
return `${random(config.descr)} (${random(config.challenge)})`;
}
function retrainToProfession(profName, traits, character) {
return createCharacter({
...character,
postProfession: profName,
traits,
});
}
// Use the contents array to generate stuff at this location
function determineContents(template, looted) {
return selectElements(template.contents, false)
.map(createContents)
.filter((el) => el !== null)
.reduce(combineBags, [])
.map(lootBag(looted));
}
// Create a bag or a container depending on the syntax
function createContents(child) {
if (child.includes("(")) {
const spec = bagSpecParser(child);
return createBag(spec);
}
return createContainer({ type: child });
}
// combine anonymous bags next to each other in the array, this is easier to read
function combineBags(arr, bag) {
if (bag.name || bag.description || arr.length === 0) {
arr.push(bag);
} else {
let lastBag = arr[arr.length - 1];
lastBag.addBag(bag);
}
return arr;
}
// if location has been looted, remove valuable items
// TODO: This isn't recursive, eventually that will matter
function lootBag(looted) {
return (bag) => {
if (looted) {
bag.entries = bag.entries.filter(
(entry) => entry.item.not("cash") && entry.item.not("food") && entry.item.value <= 3,
);
}
return bag;
};
}