import { Builder, resolve } from "./string_utils.js";
import { createCharacterName as ccn } from "./create_character_name.js";
import { logger } from "./utils.js";
import { mapLoader, percentTable, rarityTable } from "./data_loaders.js";
import { random, roll, test } from "./random_utils.js";
import RarityTable from "./tables/rarity_table.js";
import Table from "./tables/table.js";
const REF_STRING = "ref-";
const SEQ_STRING = "seq-";
// refer to a sequence, but do not increment it
const REF_REGEX = /\{ref-[^\}]+\}/g;
// refer to a sequence, and then increment it
const SEQ_REGEX = /\{seq-[^\}]+\}/g;
const map = await mapLoader("location.names.data");
const GEO_FEATURE_TYPES = {
Depression: map.get("features.depression"),
Prairie: map.get("features.prairie"),
Hill: map.get("features.hill"),
Water: map.get("features.water"),
Lake: map.get("features.lake"),
River: map.get("features.river"),
Junction: map.get("features.junction"),
Forest: map.get("features.forest"),
};
const RIVER_FORKS = map.get("features.river.forks");
const NATURAL_PLACE_ADJECTIVES = map.get("natural.place.adjectives");
const CTY_NAMES = map.get("city.names");
const CTY_DIRS = map.get("city.dirs");
const CTY_SUFFIXES = map.get("city.suffixes");
const COMMON_NOUNS = map.get("common.nouns");
const COMMUNITY_DESCRIPTORS = map.get("community.descriptors");
const COMMUNITY_SUFFIXES = map.get("community.suffixes");
const COMMUNITY_PLACE_NAME = map.get("community.names");
const STREET_TYPES = map.get("street.types");
const DIRS = map.get("street.dirs");
const LODGING_NAMES = map.get("lodging.names");
const CORP_NAMES_1 = map.get("corp.names.1");
const CORP_NAMES_2 = map.get("corp.names.2");
const CORP_NAMES_3 = map.get("corp.names.3");
const CORP_NAMES_4 = map.get("corp.names.4");
const CORP_NAMES_5 = map.get("corp.names.5");
const BAR_NAMES = Object.freeze({
"clientele:lobrow": map.get("bars.lobrow.names"),
"clientele:nobrow": map.get("bars.nobrow.names"),
"clientele:hibrow": map.get("bars.hibrow.names"),
});
const BAR_TYPES = Object.freeze({
"clientele:lobrow": map.get("bars.lobrow.types"),
"clientele:nobrow": map.get("bars.nobrow.types"),
"clientele:hibrow": map.get("bars.hibrow.types"),
});
const RESTAURANT_NAMES = map.get("restaurant.names");
const RESTAURANT_TYPES = rarityTable(map.get("restaurant.types"));
const RESTAURANTS = new RarityTable()
.add("common", () => `${random(RESTAURANT_NAMES)}`)
.add("common", () => `${random(RESTAURANT_NAMES)} ${RESTAURANT_TYPES.get()}`)
.add("rare", ({ owner }) => `${fullOrPartialName(owner, true)} ${RESTAURANT_TYPES.get()}`);
const HOTEL_TYPES = rarityTable(map.get("hotel.types"));
const MOTEL_TYPES = rarityTable(map.get("motel.types"));
const CLIENTELE = Object.freeze(Object.keys(BAR_NAMES));
// RANDOMIZATION TABLES
const NATURAL_AREAS = new RarityTable()
// East Pond
.add("common", () => random(NATURAL_PLACE_ADJECTIVES))
// Red Hollow
.add("common", () => random(COMMON_NOUNS))
// Williams Crossing
.add("uncommon", () => getName())
// West Bison Lake
.add("uncommon", (loc) => regionByLocation(loc) + " " + random(COMMON_NOUNS))
// Ford Trail Pond
.add("rare", () => (test(50) ? random(COMMON_NOUNS) : getName()) + " Trail")
// Alfalfa
.add("rare", () => random(CTY_NAMES))
// West Anderson Junction
.add("rare", (loc) => regionByLocation(loc) + " " + getName());
const MOTELS = new Table();
MOTELS.add(70, () => `${random(LODGING_NAMES)} ${MOTEL_TYPES.get()}`);
MOTELS.add(15, () => `${MOTEL_TYPES.get()} ${random(LODGING_NAMES)}`);
MOTELS.add(10, ({ owner }) => `${fullOrPartialName(owner, false)} ${MOTEL_TYPES.get()}`);
MOTELS.add(5, ({ owner }) => `${fullOrPartialName(owner, true)} ${MOTEL_TYPES.get()}`);
const HOTELS = new Table();
HOTELS.add(70, () => `${random(LODGING_NAMES)} ${HOTEL_TYPES.get()}`);
HOTELS.add(15, () => `${HOTEL_TYPES.get()} ${random(LODGING_NAMES)}`);
HOTELS.add(10, ({ owner }) => `${fullOrPartialName(owner, false)} ${HOTEL_TYPES.get()}`);
HOTELS.add(5, ({ owner }) => `${fullOrPartialName(owner, true)} ${HOTEL_TYPES.get()}`);
const MOBILE_PARK_TYPES = percentTable(map.get("mobile.park.types"));
// add suburban names too
const CORP_1 = new Table();
CORP_1.add(10, () => random(CORP_NAMES_1));
CORP_1.add(10, () => random(CORP_NAMES_2));
CORP_1.add(50, () => `${random(CORP_NAMES_1)} ${random(CORP_NAMES_2)}`);
CORP_1.add(15, (o) => `${o.family} ${random(CORP_NAMES_2)}`);
CORP_1.add(15, (o) => o.family);
const CORP_2 = new Table();
CORP_2.add(25, () => ` ${random(CORP_NAMES_3)}`);
CORP_2.add(25, () => ` ${random(CORP_NAMES_4)}`);
CORP_2.add(50, () => ` ${random(CORP_NAMES_3)} ${random(CORP_NAMES_4)}`);
const CORP_3 = new Table();
CORP_3.add(90, (o) => `${CORP_1.get()(o)}${CORP_2.get()()}`);
CORP_3.add(10, () => `${random(CORP_NAMES_5)} ${random(CORP_NAMES_4)}`);
const RESEARCH_FACILITY_TYPE = "{Research|Lab|Labs}{| Facility| Facilities| Division}";
const BARS = new Table({ outFunction: (f) => f() });
BARS.add(5, () => `${placeName()} ${random(BAR_TYPES["clientele:nobrow"])}`);
BARS.add(5, () => `The ${fullOrPartialName(ccn(), true)} ${random(BAR_TYPES["clientele:nobrow"])}`);
BARS.add(
10,
() => `The ${fullOrPartialName(ccn(), false)} ${random(BAR_TYPES["clientele:nobrow"])}`,
);
BARS.add(
80,
() => `${random(BAR_NAMES[random(CLIENTELE)])} ${random(BAR_TYPES["clientele:nobrow"])}`,
);
const STREET_NAMES = new Table({ outFunction: (f) => f() });
STREET_NAMES.add(80, () => `${random(COMMUNITY_DESCRIPTORS)} ${random(STREET_TYPES)}`);
STREET_NAMES.add(
20,
() => `${random(COMMUNITY_DESCRIPTORS)} ${random(STREET_TYPES)} ${random(DIRS)}`,
);
// UTILITIES
function regionByLocation(location) {
return random(location === "River" && test(70) ? RIVER_FORKS : NATURAL_PLACE_ADJECTIVES);
}
function getName() {
const name = ccn();
return test(10) ? name.given : name.family;
}
function fullOrPartialName(owner, possessive) {
const p = /[sz]$/.test(owner.toString()) ? "’" : "’s";
const name = test(70) ? owner.family : owner.toString();
return possessive ? name + p : name;
}
function nameHierarchy(start, defaultFunc) {
let array = [];
for (let n = start; n != null; n = n.parent) {
if (n.name) {
array.push(n.name);
}
}
if (array.length === 0) {
array.push(defaultFunc());
}
return array;
}
function geoFeature({ type = random(Object.keys(GEO_FEATURE_TYPES)) } = {}) {
return random(GEO_FEATURE_TYPES[type]);
}
// EXPORT FUNCTIONS
export function getLocationNameTypes() {
return Object.keys(CREATORS);
}
// GENERATOR FUNCTIONS
function bar({ parent }) {
if (parent.type === "Casino") {
return `${random(BAR_NAMES[random("clientele:hibrow")])} ${random(
BAR_TYPES["clientele:hibrow"],
)}`;
}
return BARS.get();
}
function suburb() {
const b = new Builder();
b.if(test(80), random(COMMUNITY_DESCRIPTORS), geoFeature());
b.if(test(30), random(COMMUNITY_SUFFIXES));
b.if(test(20), " " + geoFeature());
b.append(" " + random(["Community", "District", "Neighborhood"]));
return b.toString().replace("*", "") + " (Suburb)";
}
function housingTract() {
const b = new Builder();
b.if(test(80), random(COMMUNITY_DESCRIPTORS), geoFeature());
b.if(test(30), random(COMMUNITY_SUFFIXES));
b.if(test(20), " " + geoFeature());
b.if(test(70), " " + random(COMMUNITY_PLACE_NAME));
b.preIf(test(30) && b.toString().includes("*"), "The ");
b.if(b.length === 1, random(COMMUNITY_SUFFIXES));
return b.toString().replace("*", "");
}
function street() {
return STREET_NAMES.get();
}
function house({ parent } = {}) {
const names = nameHierarchy(parent, street);
if (!parent.streetNumber) {
parent.streetNumber = roll("1d20") * random([100, 100]) + roll("1d9*10");
}
parent.streetNumber += random([2, 3, 4, 5]);
return `${parent.streetNumber} ${names[0]}`;
}
function placeName({ type = random(Object.keys(GEO_FEATURE_TYPES)) } = {}) {
let name = random(NATURAL_AREAS.get());
if (name.startsWith("*")) {
return resolve(name.substring(1));
}
return resolve(name + " " + random(GEO_FEATURE_TYPES[type]));
}
function corporateName({ type }) {
const bizHq = type !== "Industrial Research Facility";
const person = ccn();
let oneName = selectUntil(
() => CORP_3.get()(person),
(name) => bizHq || name.split(" ").length < 3,
);
oneName = oneName.replace(/_/g, " ");
return bizHq ? oneName : `${oneName} ${random(RESEARCH_FACILITY_TYPE)}`;
}
function highway() {
// Interstate 50, Interstate 60, Interstate 23, Interstate 27
// "U.S. Route {50|60|23|27}"
return resolve("{Highway|U.S. Route} {2|3|4|5|6|7|8|9}{0|1|2|3|4|5|6|7|8|9}");
}
function selectUntil(selector, testor) {
do {
var result = selector();
} while (!testor(result));
return result;
}
function nameForCity(field) {
return selectUntil(() => ccn()[field], takesSuffix);
}
function nounForCity() {
return selectUntil(() => random(COMMON_NOUNS), takesSuffix);
}
function cityForSuffix() {
return selectUntil(() => random(CTY_NAMES), takesSuffix);
}
function takesSuffix(word) {
return word.length < 7 && /[nselra]$/.test(word) && !word.includes(" ") && !word.endsWith("ton");
}
function suffix() {
return random(CTY_SUFFIXES).replace("_", " ");
}
function urban() {
const option = roll(10);
if (1 === option) {
return `${cityForSuffix()}${suffix()}`; // Canonville
} else if (2 === option || 3 === option) {
return `${nounForCity()}${suffix()}`; // Cinderville
} else if (4 === option || 5 === option) {
return `${nameForCity("family")}${suffix()}`; // Aliceville
} else if (6 === option) {
return `${nameForCity("given")}${suffix()}`; // Davisville
} else if (7 === option) {
return `${resolve(CTY_DIRS)} ${random(CTY_NAMES)}`; // Northern Deadditch
}
return random(CTY_NAMES); // Deadditch
}
function ranch() {
const test = roll(10);
if (test <= 2) {
const familyName = ccn().family;
return `${familyName} Ranch`;
} else if (test <= 4) {
const familyName = ccn().family;
return `The ${familyName} Family Ranch`;
}
return `${placeName()} Ranch`;
}
function mobileHomePark() {
return random(LODGING_NAMES) + " " + MOBILE_PARK_TYPES.get();
}
function hotel() {
return HOTELS.get()({ owner: ccn() });
}
function motel() {
return MOTELS.get()({ owner: ccn() });
}
function restaurant() {
return RESTAURANTS.get()({ owner: ccn() });
}
// TODO: Names?
function officeBuilding() {
return resolve("The {Stock|West}bridge");
}
function corporateNameFloor({ name, type }) {
return test(80) ? `${name}: General Offices` : `${name}: ${corporateName({ type })}`;
}
const CREATORS = {
"Bar": bar,
"Business Headquarters": corporateName,
"OBS Mid Floor": corporateNameFloor,
"OBS Top Floor": corporateNameFloor,
"OBM Mid Floor": corporateNameFloor,
"OBM Top Floor": corporateNameFloor,
"OBL Mid Floor": corporateNameFloor,
"OBL Top Floor": corporateNameFloor,
"Forest": placeName,
"Highway": highway,
"Hill": placeName,
"Hotel": hotel,
"House": house,
"Housing Tract": housingTract,
"Industrial Research Facility": corporateName,
"Lake": placeName,
"Mobile Home Park": mobileHomePark,
"Motel": motel,
"Office Building": officeBuilding,
"Post Bar": bar,
"Prairie": placeName,
"Ranch": ranch,
"Restaurant": restaurant,
"River": placeName,
"Street": street,
"Suburb": suburb,
"Urban": urban,
};
function createLocationNameByType({
name,
type = random(getLocationNameTypes()),
parent = {},
} = {}) {
if (CREATORS[type]) {
return CREATORS[type]({ name, type, parent });
}
return name;
}
function nameInSequence(child) {
let selfName = child.name;
// e.g. "{pigsty|hogswallow}" takes precedence over a sequence information demarcated with braces
if (selfName.includes("|")) {
return resolve(selfName);
}
selfName = replaceSeq(child, selfName, REF_STRING);
selfName = replaceSeq(child, selfName, SEQ_STRING);
// don't do the inline sequence if we've managed to replace a sequence in the string
if (selfName === child.name) {
selfName = replaceInlineSeq(child, selfName);
}
return selfName;
}
function replaceSeq(child, selfName, indicator) {
// work through all indicators for references or sequences
while (selfName.includes(`{${indicator}`)) {
const regexp = indicator === REF_STRING ? REF_REGEX : SEQ_REGEX;
// get all syntax for the types
const allI = selfName.match(regexp);
allI.forEach((i) => {
// extract the id from the reference, e.g r8 == id of 8
const id = i.split("-")[1].replace("}", "");
const seqData = getSequence(child, id);
// If there's no sequence data, we will substitute in the type, if it's not already part of
// the name (if it's part of the name, we remove the sequence information by just replacing
// the whole name string with the type.
if (!seqData) {
if (selfName.indexOf(child.type) === -1) {
selfName = selfName.replace(i, child.type);
} else {
selfName = child.type;
}
return;
}
const seq =
map.get("seq." + seqData.seqType) || seqData.seqType.split(",").map((s) => s.trim());
// nonsense around matching the right index and/or advancing for the s case. If the index is -1,
// the sequence hasn't been referenced in that path of location creation, so we can use the value
// "" for it. This is a little too magical but currently works where we use it.
const index = indicator === REF_STRING ? seqData.index - 1 : seqData.index;
const seqValue = index === -1 ? "" : seq[index % seq.length];
selfName = selfName.replace(i, seqValue);
if (indicator === SEQ_STRING) {
seqData.index++;
}
});
}
return selfName;
}
// The sequence was defined alongside the definition of child nodes, and is in the sequence itself.
function replaceInlineSeq(child, selfName) {
const seqData = child.parent?.sequences?.find((seq) => seq.id === child.type);
if (!seqData || !seqData.options) {
return selfName;
}
selfName = seqData.options[seqData.index % seqData.options.length];
seqData.index++;
return selfName;
}
/**
* Generates a name for a location node based on its type and its position in the location hierarchy.
* Supports sequence-based naming (e.g. "Room 1", "Room 2") and type-specific name generation
* (e.g. street names, building names, geographic features).
*
* @param {Object} [options={}] - Location name options
* @param {Object} options.child - The location node to name, with `type` (string) and `parent` (Object)
* properties representing its place in the location hierarchy.
* @returns {string} A generated name for the location node
*/
export function createLocationName({ child } = {}) {
logger.start("createLocationName", { child });
child.name = createLocationNameByType({
name: child.name,
type: child.type,
parent: child.parent,
});
let name = nameInSequence(child);
/*
let name =
createLocationNameByType({ name: child.name, type: child.type, parent: child.parent }) ||
nameInSequence(child);
*/
return logger.end(name);
}
function getSequence(location, id) {
for (let n = location; n != null; n = n.parent) {
if (n.sequences?.length && n.sequences.some((seq) => seq.id === id)) {
return n.sequences.find((seq) => seq.id === id);
}
}
return null;
}