create_location.js

import { bagSpecParser } from "./string_utils.js";
import { createContainer, createStockpile, newCreateBag } from "./create_bag.js";
import { createLocationName } from "./create_location_name.js";
import { Location, Store } from "./models/location.js";
import { logger } from "./utils.js";
import { random, selectElements } from "./random_utils.js";
import LocationDatabase from "./db/location_database.js";

/*
Loot:
    Agricultural
    Automotive
    Civic
    Criminal
    Garage
    Hospital
    House
    Industrial
    Institution
    Lodging
    Military
    Office
    Public
    Research
    Restaurant
    School
    Tourism
    Travel

central: church, synagogue, academic campus, restaurant, police station,
    fire station, fraternal organization (masons, IOOF, Elk, Moose, Eagle,
    only the cool ones), bus station, train station, city park (playground, pool,
    sports fields, picnic areas), hotel, hospital, city hall, courthouse, post office,
    jailhouse/county jail

    ordnance depot
*/

const HOUSE_LAYOUTS = {
  allOf: {
    "Entryway": 80,
    "Kitchen": 100,
    "Living Room": 100,
    "1d2:Bathroom": 100,
    "1d3:Bedroom": 100,
    "Laundry": 60,
    "Playroom": 20,
  },
  oneOf: {
    "Recreation Room": 20,
    "Den": 20,
    "Study": 5,
  },
};

export var database = new LocationDatabase();
database.add({
  type: "Root",
  abstract: true,
  ch: {
    oneOf: [
      "Natural",
      "Rural",
      "Road",
      "Rail",
      "Rural Industrial",
      "Suburb",
      "Urban",
      "Settlement Types",
    ],
  },
});

// Natural stuff
database.add({
  type: "Natural",
  abstract: true,
  ch: {
    oneOf: [
      "Canyon",
      "Cave",
      "Caverns",
      "Crater",
      "Forest",
      "Hill",
      "Prairie",
      "Lake",
      "Radioactive Area",
      "Rest Area",
      "Scenic Overlook",
      "River",
      "Riverbed",
    ],
  },
});
database.add({ type: "Canyon" });
database.add({ type: "Cave" });
database.add({ type: "Caverns", descr: "Cave Complex" });
database.add({
  type: "Crater",
  abstract: true,
  ch: {
    oneOf: ["Asteroid Crater", "Crash Site"],
  },
});
database.add({ type: "Asteroid Crater" });
database.add({
  type: "Crash Site",
  descr: "Plane or rocket",
});
database.add({
  type: "Forest",
  name: "Woods",
  ch: {
    oneOf: {
      "Wilderness Settlements": 10,
      "Logging Camp": 10,
      "Ranger Station": 10,
    },
  },
});
database.add({
  type: "Logging Camp",
});
database.add({
  type: "Hill",
  name: "Elevation",
  ch: {
    allOf: {
      "Fire Watch": 10,
      "Wilderness Settlements": 10,
    },
  },
});
database.add({
  type: "Prairie",
  name: "Flat",
  ch: {
    allOf: {
      "Wilderness Settlements": 10,
      "Chuck Wagon": 5,
    },
  },
});
database.add({
  type: "Lake",
});
database.add({
  type: "Radioactive Area",
  descr: "The underlying space could be anything though",
});
database.add({ type: "Rest Area", ch: ["Restroom"] });
database.add({ type: "Scenic Overlook" });
database.add({
  type: "River",
  ch: ["Wilderness Settlements"],
});
database.add({ type: "Riverbed", name: "{Dry riverbed|Gulch}" });

database.add({
  type: "Fire Watch",
});

database.add({
  type: "Rural",
  abstract: true,
  ch: {
    oneOf: [
      "Farm",
      "Ranch",
      "Dairy Farm",
      "Windmill Pump",
      "Shack",
      "Fairground",
      "Grange Building",
      "Stables",
      "Cemetery",
      "Landfill",
      "Race Track",
      "Luxury Home",
      "Mobile Home Park",
      "Prison",
      "Reformatory School",
      "Ranger Station",
      "Power Station",
      "Power Line",
      "Dam",
      "Reservoir",
      "Water Tower",
      "Cistern",
      "Indian Teepee",
    ],
  },
});

database.add({
  type: "Farm",
  ch: {
    "allOf": ["Farm House", "Barn"],
    "someOf:1d5+2": [
      "Garage",
      "Stables",
      "Storm Cellar",
      "Root Cellar",
      "Shed",
      "Chicken Coop",
      "Silo",
      "Grainery",
      "Greenhouse",
      "Fallout Shelter",
      "Well House",
      "Water Mill",
      "Windmill",
      "Horsemill",
      "Pigpen",
    ],
  },
});
database.add({ type: "Farm House", ch: HOUSE_LAYOUTS });
database.add({ type: "Barn" });
database.add({ room: "Garage" });
database.add({ room: "Storm Cellar" });
database.add({ room: "Root Cellar" });
database.add({ room: "Shed" });
database.add({ room: "Chicken Coop" });
database.add({ room: "Silo" });
database.add({ room: "Grainery" });
database.add({ room: "Greenhouse" });
database.add({ room: "Fallout Shelter" });
database.add({ room: "Well House" });
database.add({ room: "Water Mill" });
database.add({ room: "Windmill" });
database.add({ room: "Horsemill" });
database.add({ room: "Pigpen", name: "{Pigpen|Sty}" });

database.add({
  type: "Ranch",
  ch: {
    "allOf": ["Ranch House"],
    "someOf:1d4-1": ["Bunkhouse", "Barn", "Stables", "Chuck Wagon", "Holding Pen"],
  },
});
database.add({ type: "Ranch House", ch: HOUSE_LAYOUTS });
database.add({ room: "Bunkhouse" });
database.add({ room: "Chuck Wagon" });
database.add({ room: "Holding Pen" });

database.add({ type: "Dairy Farm" });
database.add({
  type: "Windmill Pump",
  desc: "Water trough with windmill pump",
});
database.add({ type: "Shack" });
database.add({ type: "Fairground" });
database.add({ type: "Grange Building" });
database.add({
  type: "Stables",
  ch: { allOf: { "2d3:Stall": 100, "Restroom": 30, "Feed Bins": 90 } },
});
database.add({ type: "Feed Bins", name: "{Feed Bins|Feed Storage}" });
database.add({ type: "Stall" });
database.add({ type: "Cemetery" });
database.add({ type: "Landfill", name: () => "{Landfill|Dump}" });
database.add({ type: "Race Track" });
database.add({ type: "Luxury Home" });
database.add({ type: "Mobile Home Park" });
database.add({ type: "Prison" });
database.add({ type: "Reformatory School" });
database.add({ type: "Ranger Station" });
database.add({ type: "Power Station" });
database.add({ type: "Power Line" });
database.add({ type: "Dam" });
database.add({ type: "Reservoir" });
database.add({ type: "Water Tower" });
database.add({ type: "Cistern" });
database.add({ type: "Indian Teepee" });

database.add({
  type: "Church",
  ch: {
    "allOf": ["Chapel", "Restroom"],
    "someOf:1d3-1": ["Industrial Kitchen", "Meeting Hall"],
  },
});
database.add({ type: "Chapel" });
database.add({ type: "Industrial Kitchen" });
database.add({ type: "Meeting Hall" });

database.add({
  room: "Restroom",
  abstract: true,
  ch: {
    oneOf: {
      "Men’s Restroom & Women’s Restroom": 80,
      "Public Restroom": 20,
    },
  },
});
database.add({
  room: "Men’s Restroom",
  contents: {
    oneOf: {
      "Bag($1 restroom -female)": 30,
    },
  },
});
database.add({
  room: "Women’s Restroom",
  contents: {
    oneOf: {
      "Bag($2 restroom -male)": 30,
    },
  },
});
database.add({
  room: "Public Restroom",
  contents: {
    oneOf: {
      "Bag($1 restroom -female)": 15,
      "Bag($1 restroom -male)": 15,
    },
  },
});

database.add({
  type: "Road",
  abstract: true,
  ch: {
    oneOf: [
      "Highway Patrol Station",
      "Battle Wreckage",
      "Car Accident",
      "Roadside Memorial",
      "Roadside Service Building",
      "Mini Golf",
      "Slaughterhouse",
      "Grain Silo",
      "Grain Storage Depot",
      "Feed Store",
      "Radio Station",
    ],
  },
  descr: "What is around roads between destinations",
});
database.add({ type: "Roadside Stand" });
database.add({ type: "Highway Patrol Station" });
database.add({ type: "Battle Wreckage" });
database.add({ type: "Car Accident" });
database.add({ type: "Roadside Memorial" });
database.add({
  type: "Roadside Service Building",
  abstract: true,
  ch: {
    oneOf: [
      "Gas Station",
      "Motel",
      "Diner",
      "Drive-In Diner",
      "Truck Stop",
      "Rest Stop",
      "Gun Shop",
      "Car Dealership",
      "Roadside Chapel",
      "Dude Ranch",
      "Gift Shop",
      "Campground",
      "Boat Shop",
      "Bait Shop",
      "Casino",
    ],
  },
});

database.add({
  room: "Showroom Floor Guns",
  tags: ["firearm", "ammo", "cash"],
});
database.add({ room: "Back Room", ch: ["Restroom"], tags: ["office"] });

database.add({
  type: "Gas Station",
  ch: { allOf: ["Gas Station Office", "1d3:Gas Pump", "1d3-1:Garage Bay"] },
  contents: {
    allOf: {
      "Newspaper Box": 70,
    },
  },
});
database.add({
  room: "Gas Station Office",
  contents: {
    allOf: {
      "Magazine Rack": 30,
      "Newspaper Box": 30,
    },
    oneOf: {
      "Cash Register": 80,
      "Cash On Hand": 20,
    },
  },
});
database.add({ room: "Gas Pump" });
database.add({
  room: "Garage Bay",
  contents: { allOf: { "Tool Rack": 90 } },
});

database.add({ type: "Motel" });
database.add({
  type: "Diner",
  contents: {
    allOf: {
      "Newspaper Box": 70,
    },
  },
});
database.add({ type: "Drive-In Diner" });
database.add({ type: "Truck Stop" });
database.add({ type: "Rest Stop", ch: ["Restroom"] });
database.add({ type: "Gun Shop", ch: ["Showroom Floor Guns", "Back Room"] });
database.add({ type: "Car Dealership" });
database.add({ type: "Roadside Chapel" });
database.add({ type: "Dude Ranch" });
database.add({ type: "Gift Shop" });
database.add({ type: "Campground" });
database.add({ type: "Boat Shop" });
database.add({ type: "Bait Shop" });

database.add({ type: "Casino", ch: ["Cashier Pen", "Game Floor", "Bar", "Restroom"] });
database.add({ room: "Cashier Pen" });
database.add({ room: "Game Floor" });

database.add({ type: "Mini Golf" });
database.add({ type: "Slaughterhouse" });
database.add({ type: "Grain Silo" });
database.add({ type: "Grain Storage Depot", ch: ["Grain Silo"] });
database.add({ type: "Feed Store" });

// TODO: name of station, call sign
database.add({
  type: "Radio Station",
  ch: { allOf: ["Broadcast Building", "1d2:Radio Tower"] },
});
database.add({
  type: "Broadcast Building",
  ch: {
    "allOf": ["Lobby", "Restroom"],
    "someOf:1d2+1": ["Break Room", "2d2:Office", "1d2:Storage Room"],
  },
});
database.add({
  room: "Break Room",
  contents: {
    allOf: {
      "Bag($8 kitchen -fresh -luxury -useful)": 100,
    },
  },
});
database.add({
  room: "Lobby",
  contents: {
    allOf: {
      "Reception Desk": 90,
      "Magazine Rack": 10,
    },
  },
});
database.add({
  room: "Office",
  series: "${name} #10$1",
  contents: {
    allOf: {
      "Office Desk": 90,
    },
  },
});
database.add({ room: "Storage Room" });
database.add({ type: "Radio Tower" });

database.add({
  room: "Rail",
  abstract: true,
  ch: {
    oneOf: ["Train Station", "Grain Transfer Depot", "Trainyard", "Train Wreckage"],
  },
});
database.add({ type: "Train Station" });
database.add({ type: "Grain Transfer Depot", ch: ["Silo"] });
database.add({
  type: "Trainyard",
  ch: {
    allOf: {
      "Administrative Offices": 90,
      "Rail Yard": 80,
      "Roundhouse": 50,
      "Repair Building": 50,
    },
  },
});
database.add({ type: "Roundhouse", name: "{Roundhouse|Engine House}" });
database.add({ type: "Rail Yard" });
database.add({ type: "Repair Building" });
database.add({ type: "Administrative Offices" });
database.add({
  type: "Train Wreckage",
  abstract: true,
  ch: {
    oneOf: ["Abandoned Train Cars", "Derailed Train Cars"],
  },
});
database.add({ type: "Abandoned Train Cars" });
database.add({ type: "Derailed Train Cars" });

database.add({
  type: "Rural Industrial",
  abstract: true,
  ch: {
    oneOf: ["Military Installation", "Research Facility", "Mine", "Quarry", "Oil Field", "Airport"],
  },
});
database.add({
  type: "Military Installation",
  abstract: true,
  ch: {
    oneOf: {
      "ICBM Missile Silo": 10,
      "Underground Bunker Complex": 10,
      "Ammunition Factory": 20,
      "Army Base": 30,
      "Air Force Base": 30,
    },
  },
});
database.add({ type: "ICBM Missile Silo" });
database.add({ type: "Underground Bunker Complex" });
database.add({ type: "Ammunition Factory" });
database.add({ type: "Army Base" });
database.add({ type: "Air Force Base" });

database.add({
  type: "Research Facility",
  abstract: true,
  ch: {
    oneOf: ["Astronomical Observatory", "University Campus", "Industrial Research Facility"],
  },
});
database.add({ type: "Astronomical Observatory" });
database.add({ type: "University Campus", ch: ["Research Library"] });

database.add({ type: "Research Library", contents: ["Bag($200 book)", "Bag($20 research)"] });

database.add({ type: "Industrial Research Facility" });

database.add({ type: "Mine" });
database.add({ type: "Quarry" });
database.add({ type: "Oil Field" });
database.add({ type: "Airport" });

database.add({
  type: "Suburb",
  abstract: true,
  ch: {
    "someOf:3d3": [
      "Business District",
      "Church",
      "Synagogue",
      "Cemetery",
      "Drive-In Movie Theater",
      "Mobile Home Park",
      "Elementary School",
      "High School",
      "College Campus",
      "Produce Stands",
      "Police Station",
      "Fire Station",
      "Apartment Building",
      "Retirement Home",
      "Sanitarium",
      "Nurseries",
      "Hospital",
      "City Park",
    ],
    "allOf": ["1d3:Housing Tract"],
  },
});

database.add({
  type: "City Park",
  ch: { allOf: ["Playground", "Pool", "Sports Field", "Picnic Area", "Swimming Pool"] },
});
database.add({ room: "Playground" });
database.add({ room: "Pool" });
database.add({ room: "Sports Field" });
database.add({ room: "Picnic Area" });
database.add({ room: "Swimming Pool" });

database.add({ type: "Synagogue" });
database.add({ type: "Drive-In Movie Theater" });
database.add({ type: "Elementary School" });
database.add({ type: "High School" });
database.add({ type: "College Campus" });
database.add({ type: "Produce Stands" });
database.add({ type: "Police Station" });
database.add({ type: "Fire Station" });
database.add({ type: "Apartment Building" });
database.add({ type: "Retirement Home" });
database.add({ type: "Sanitarium" });
database.add({ type: "Nurseries" });
database.add({ type: "Hospital" });

// garage, playroom, trash
database.add({
  type: "Housing Tract",
  ch: ["2d5:Street"],
});

database.add({
  type: "Street",
  ch: ["1d4+4:House"],
});

database.add({
  type: "House",
  ch: HOUSE_LAYOUTS,
});

database.add({
  room: "Entryway",
  descr: "with a coat closet",
  contents: ["Bag($3 feet | head | coat)"],
});
database.add({
  room: "Living Room",
  contents: ["Bag($8 livingroom)"],
});
database.add({
  room: "Playroom",
  contents: ["Bag($8 playroom | toy)"],
});
database.add({
  room: "Bathroom",
  contents: ["Bag($5 bathroom)"],
});
database.add({
  room: "Kitchen",
  contents: ["Bag($10 kitchen -fresh)"],
});
database.add({
  room: "Bedroom",
  contents: {
    allOf: {
      "Bag($10 bedroom | clothing -coat -feet -head)": 90,
      "Bag($10 coat feet head)": 10,
    },
  },
});
database.add({
  room: "Laundry",
  contents: ["Bag($5 laundry | clothing -military -coat -feet -head)"],
});
database.add({ room: "Den", contents: ["Bag($5 den)"] });
database.add({ room: "Recreation Room", contents: ["Bag($5 rec-room)"] });
database.add({ room: "Study", contents: ["Bag($8 study)"] });

database.add({
  type: "Business District",
  ch: {
    "someOf:3d3": [
      "Grocery Store",
      "Restaurant",
      "Salon",
      "Drug Store",
      "Bookstore",
      "Liquor Store",
      "Pet Shop",
      "Record Shop",
      "Instrumental Music Store",
      "Bank",
      "Bakery",
      "Library",
      "History Museum",
      "Movie Theater",
      "Theater",
      "Florist",
      "Bar",
      "Diner",
      "Hotel",
    ],
  },
});
database.add({ type: "Grocery Store" });
database.add({ type: "Restaurant" });
database.add({ type: "Salon", name: "{Barber Shop|Beauty Salon}" });
database.add({ type: "Drug Store" });
database.add({ type: "Bookstore" });
database.add({ type: "Liquor Store" });
database.add({ type: "Pet Shop" });
database.add({ type: "Record Shop" });
database.add({ type: "Instrumental Music Store" });
database.add({ type: "Bank" });
database.add({ type: "Bakery" });
database.add({ type: "Library" });
database.add({ type: "History Museum" });
database.add({ type: "Movie Theater" });
database.add({ type: "Theater" });
database.add({ type: "Florist" });
database.add({ type: "Bar" });
database.add({ type: "Hotel" });

database.add({
  type: "Urban",
  name: "{Town|City}",
  ch: { allOf: ["Factory"] },
});

database.add({
  type: "Factory",
  abstract: true,
  ch: {
    oneOf: [
      "Junk Yard",
      "Salvage Yard",
      "Scrap Yard",
      "Warehouse",
      "Manufacturing Plant",
      "Soda Bottling Plant",
      "Business Headquarters",
      "Industrial Bakery",
      "Trucking Facilities",
      "Foundry",
      "Power Transfer Station",
      "Bus Station",
      "Train Station",
      "Business District",
    ],
  },
});
database.add({ type: "Junk Yard" });
database.add({ type: "Salvage Yard" });
database.add({ type: "Scrap Yard" });
database.add({ type: "Warehouse" });
database.add({ type: "Manufacturing Plant" });
database.add({ type: "Soda Bottling Plant" });
database.add({ type: "Business Headquarters" });
database.add({ type: "Industrial Bakery" });
database.add({ type: "Trucking Facilities" });
database.add({ type: "Foundry" });
database.add({ type: "Power Transfer Station" });
database.add({ type: "Bus Station" });

database.add({
  type: "Settlement Types",
  abstract: true,
  ch: {
    oneOf: [
      "Road Settlements",
      "Rail Settlements",
      "Wilderness Settlements",
      "Post-Collapse Settlements",
    ],
  },
});
database.add({
  type: "Road Settlements",
  abstract: true,
  ch: { oneOf: ["RV Encampment", "Tent City", "Semi-Permanent Camp"] },
});
database.add({
  type: "Rail Settlements",
  abstract: true,
  ch: { oneOf: ["Hobo Camp", "Camp Site"] },
});
database.add({
  type: "Wilderness Settlements",
  abstract: true,
  ch: { oneOf: ["Camp Site", "Cabin", "Bunker", "Makeshift Fortification"] },
});
database.add({
  type: "Post-Collapse Settlements",
  abstract: true,
  ch: {
    oneOf: [
      "Cabin",
      "Tent City",
      "Makeshift Fortification",
      "Semi-Permanent Camp",
      "Post Settlement",
    ],
  },
});
database.add({ type: "Semi-Permanent Camp" });
database.add({ type: "Hobo Camp" });
database.add({ type: "Bunker" });
database.add({ type: "Camp Site" });
database.add({ type: "Makeshift Fortification" });
database.add({ type: "Cabin" });
database.add({ type: "Tent City" });
database.add({ type: "RV Encampment" });

// POST-COLLAPSE, many of these should be stores.

// TODO: Stockpile logic kind of sucks. We may want the newBag algorithm
database.add({
  type: "Post Bakery",
  name: "Bakery",
  owner: "Trader",
  inventory: ["Stockpile($20 fresh baked food)"],
});
database.add({
  type: "Post Bar",
  name: "Bar",
  policy: "Will have bottles to purchase.",
  owner: "Trader",
  inventory: ["Stockpile($40 alcohol -bottleofwine -collectible)"],
});
database.add({
  type: "Post Market",
  name: "Market",
  owner: "Trader",
  inventory: ["Stockpile($10 fresh food)", "Stockpile($10 preserved food)"],
});
database.add({
  type: "Post Settlement",
  name: "Settlement",
  ch: {
    allOf: {
      "Post Bakery": 50,
      "1d2:Post Bar": 50,
      "Post Market": 80,
    },
  },
});

database.verify();

// ------------------------------------------

const LOCATION_TYPES = database.models
  .filter((m) => !m.room)
  .map((m) => m.type)
  .sort();

export function getLocationTypes() {
  return LOCATION_TYPES;
}

/**
 * Create a location. You must supply the starting point to create a location and all the
 * “contained” sites in that location will be produced in an instance of that location.
 * Note that name here is the name of the location template, not the name that is applied
 * to the location that is created (that can sometimes be quite creative). It’s usually
 * very similar to the ID.
 */
export function createLocation({ type = random(LOCATION_TYPES), tags } = {}) {
  logger.start("createLocation", { type, tags });
  let template = database.findOne({ type, tags });
  if (template) {
    let root = createInstance(null, -1, template);
    walkLocation(root, template);
    return logger.end(root);
  }
  return logger.end([]);
}

function walkLocation(parent, template) {
  let elements = selectElements(template.ch);
  elements.forEach((type, i) => {
    let childTemplate = database.findOne({ type });
    if (!childTemplate) {
      console.error("Could not find template for " + type);
    }
    if (childTemplate.abstract) {
      walkLocation(parent, childTemplate);
    } else {
      let pos = getPosition(elements, type, i);
      let childLocation = createInstance(parent, pos, childTemplate);
      walkLocation(childLocation, childTemplate);
    }
  });
}

function createInstance(parent, pos, template) {
  let childLocation = template.owner ? new Store(template) : new Location(template);
  childLocation.contents = determineContents(template, "contents");
  childLocation.inventory = determineContents(template, "inventory");
  childLocation.name = labelInSeries(parent, pos, template);
  if (parent) {
    parent.addChild(childLocation);
  }
  return childLocation;
}

function getPosition(elements, type, i) {
  if (elements) {
    let index = elements.indexOf(type);
    let lastIndex = elements.lastIndexOf(type);
    return index !== lastIndex ? i - index + 1 : -1;
  }
  return -1;
}

function determineContents(template, propName) {
  return selectElements(template[propName])
    .map((child) => {
      if (child.startsWith("Bag")) {
        return newCreateBag(bagSpecParser(child));
      } else if (child.startsWith("Stockpile")) {
        return newCreateBag(bagSpecParser(child));
        // return createStockpile(bagSpecParser(child));
      }
      return createContainer({ type: child });
    })
    .filter((el) => el !== null);
}

function labelInSeries(parent, pos, template) {
  if (pos > -1) {
    return createLocationName({ parent, type: template.type, series: template.series, pos });
  } else {
    return createLocationName({
      type: template.type,
      series: template.series,
      pos: -1,
    });
  }
}