create_location.js

import { bagSpecParser } from "./string_utils.js";
import { createCharacter } from "./create_character.js";
import { createContainer } from "./create_container.js";
import { createHistoricalLayer } from "./create_historical_layer.js";
import { createLocationName } from "./create_location_name.js";
import { Location } from "./models/location.js";
import { locationsLoader } from "./data_loaders.js";
import { logger } from "./utils.js";
import { newCreateBag } from "./create_bag.js";
import { random, selectElements } from "./random_utils.js";
import LocationDatabase from "./db/location_database.js";

const locations = await locationsLoader("locations.data");
const MAP = new Map();

export const database = new LocationDatabase();
database.add.apply(database, locations);
database.verify();

function clearMap() {
  MAP.clear();
}

function addToMap(location) {
  let list = MAP.get(location.type) ?? [];
  list.push(location);
  MAP.set(location.type, list);
}

const LOCATION_TYPES = database.models
  .filter((m) => m.not("room") && m.not("abstract"))
  .map((m) => m.id)
  .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;
}

export function getAllExistingLocationTypes() {
  return Array.from(MAP.keys());
}

export function getExistingLocationByType({ type = random(getAllExistingLocationTypes()) } = {}) {
  let list = MAP.get(type) ?? [];
  if (list.length === 0) {
    return null;
  }
  return random(list);
}

/**
 * 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 } = {}) {
  logger.start("createLocation", { type });
  clearMap();
  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({ map: MAP });
  }
  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 });
  clearMap();
  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);
  elements.forEach((type) => {
    let childTemplate = database.findOne({ type });
    if (!childTemplate) {
      console.error("Could not find template for " + type);
      return;
    }
    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 = childTemplate.traits.reduce((acc, trait) => {
      acc[trait] = 1;
      return acc;
    }, {});
    child.owner = createCharacter({ postProfession: childTemplate.owner, traits });
    child.onhand = createContainer({ type: "Cash On Hand" });
  }
  // contents is what exists in a room or place, including containers that contain things
  child.contents = determineContents(childTemplate, "contents");
  // inventory is specifically for stores; it's possible to have inventory and contents.
  child.inventory = determineContents(childTemplate, "inventory");
  if (parent) {
    parent.addChild(child);
  }
  child.name = createLocationName({ child });
  addToMap(child);
  return child;
}

function determineContents(template, propName) {
  return selectElements(template[propName])
    .map((child) => {
      if (child.startsWith("Bag") || child.startsWith("Stock")) {
        const spec = bagSpecParser(child);
        const bag = newCreateBag(spec);
        return bag;
      }
      return createContainer({ type: child });
    })
    .filter((el) => el !== null);
}