import {TIME} from "/src/config/logging"; import {getFriendlyHeight} from "./ui/general"; import {last} from "/src/utils/arrayUtils"; window.Markers = (function () { let config = []; let occupied = []; function getDefaultConfig() { const culturesSet = document.getElementById("culturesSet").value; const isFantasy = culturesSet.includes("Fantasy"); /* Default markers config: type - short description (snake-case) icon - unicode character, make sure it's supported by most of the browsers. Source: emojipedia.org dx: icon offset in x direction, in pixels dy: icon offset in y direction, in pixels min: minimum number of candidates to add at least 1 marker each: how many of the candidates should be added as markers multiplier: multiply markers quantity to add list: function to select candidates add: function to add marker legend */ // prettier-ignore return [ {type: "volcanoes", icon: "🌋", dx: 52, px: 13, min: 10, each: 500, multiplier: 1, list: listVolcanoes, add: addVolcano}, {type: "hot-springs", icon: "♨️", dy: 52, min: 30, each: 1200, multiplier: 1, list: listHotSprings, add: addHotSpring}, {type: "mines", icon: "⛏️", dx: 48, px: 13, min: 1, each: 15, multiplier: 1, list: listMines, add: addMine}, {type: "bridges", icon: "🌉", px: 14, min: 1, each: 5, multiplier: 1, list: listBridges, add: addBridge}, {type: "inns", icon: "🍻", px: 14, min: 1, each: 100, multiplier: 1, list: listInns, add: addInn}, {type: "lighthouses", icon: "🚨", px: 14, min: 1, each: 2, multiplier: 1, list: listLighthouses, add: addLighthouse}, {type: "waterfalls", icon: "⟱", dy: 54, px: 16, min: 1, each: 5, multiplier: 1, list: listWaterfalls, add: addWaterfall}, {type: "battlefields", icon: "⚔️", dy: 52, min: 50, each: 700, multiplier: 1, list: listBattlefields, add: addBattlefield}, {type: "dungeons", icon: "🗝️", dy: 51, px: 13, min: 30, each: 200, multiplier: 1, list: listDungeons, add: addDungeon}, {type: "lake-monsters", icon: "🐉", dy: 48, min: 2, each: 10, multiplier: 1, list: listLakeMonsters, add: addLakeMonster}, {type: "sea-monsters", icon: "🦑", min: 50, each: 700, multiplier: 1, list: listSeaMonsters, add: addSeaMonster}, {type: "hill-monsters", icon: "👹", dy: 54, px: 13, min: 30, each: 600, multiplier: 1, list: listHillMonsters, add: addHillMonster}, {type: "sacred-mountains", icon: "🗻", dy: 48, min: 1, each: 5, multiplier: 1, list: listSacredMountains, add: addSacredMountain}, {type: "sacred-forests", icon: "🌳", min: 30, each: 1000, multiplier: 1, list: listSacredForests, add: addSacredForest}, {type: "sacred-pineries", icon: "🌲", px: 13, min: 30, each: 800, multiplier: 1, list: listSacredPineries, add: addSacredPinery}, {type: "sacred-palm-groves", icon: "🌴", px: 13, min: 1, each: 100, multiplier: 1, list: listSacredPalmGroves, add: addSacredPalmGrove}, {type: "brigands", icon: "💰", px: 13, min: 50, each: 100, multiplier: 1, list: listBrigands, add: addBrigands}, {type: "pirates", icon: "🏴☠️", dx: 51, min: 40, each: 300, multiplier: 1, list: listPirates, add: addPirates}, {type: "statues", icon: "🗿", min: 80, each: 1200, multiplier: 1, list: listStatues, add: addStatue}, {type: "ruins", icon: "🏺", min: 80, each: 1200, multiplier: 1, list: listRuins, add: addRuins}, {type: "circuses", icon: "🎪", min: 80, each: 1000, multiplier: 1, list: listCircuses, add: addCircuses}, {type: "jousts", icon: "🤺", dx: 48, min: 5, each: 500, multiplier: 1, list: listJousts, add: addJousts}, {type: "canoes", icon: "🛶", min: 1000, each: 2000, multiplier: 1, list: listCanoes, add: addCanoes}, {type: "migration", icon: "🐗", min: 20, each: 1000, multiplier: 1, list: listMigrations, add: addMigrations}, {type: "dances", icon: "💃🏽", min: 5, each: 60, multiplier: 1, list: listDances, add: addDances}, {type: "mirage", icon: "💦", min: 10, each: 400, multiplier: 1, list: listMirage, add: addMirage}, {type: "portals", icon: "🌀", px: 14, min: 16, each: 8, multiplier: +isFantasy, list: listPortals, add: addPortal}, {type: "rifts", icon: "🎆", min: 1, each: 3000, multiplier: +isFantasy, list: listRifts, add: addRifts} ]; } const getConfig = () => config; const setConfig = newConfig => { config = newConfig; }; const generate = function () { setConfig(getDefaultConfig()); pack.markers = []; generateTypes(); }; const regenerate = () => { pack.markers = pack.markers.filter(({i, lock, cell}) => { if (lock) { occupied[cell] = true; return true; } const id = `marker${i}`; document.getElementById(id)?.remove(); const index = notes.findIndex(note => note.id === id); if (index != -1) notes.splice(index, 1); return false; }); generateTypes(); }; const add = marker => { const base = config.find(c => c.type === marker.type); if (base) { const {icon, type, dx, dy, px} = base; marker = addMarker({icon, type, dx, dy, px}, marker); base.add("marker" + marker.i, marker.cell); return marker; } const i = last(pack.markers)?.i + 1 || 0; pack.markers.push({...marker, i}); occupied[marker.cell] = true; return {...marker, i}; }; function generateTypes() { TIME && console.time("addMarkers"); config.forEach(({type, icon, dx, dy, px, min, each, multiplier, list, add}) => { if (multiplier === 0) return; let candidates = Array.from(list(pack)); let quantity = getQuantity(candidates, min, each, multiplier); // uncomment for debugging: // console.log(`${icon} ${type}: each ${each} of ${candidates.length}, min ${min} candidates. Got ${quantity}`); while (quantity && candidates.length) { const [cell] = extractAnyElement(candidates); const marker = addMarker({icon, type, dx, dy, px}, {cell}); add("marker" + marker.i, cell); quantity--; } }); occupied = []; TIME && console.timeEnd("addMarkers"); } function getQuantity(array, min, each, multiplier) { if (!array.length || array.length < min / multiplier) return 0; const requestQty = Math.ceil((array.length / each) * multiplier); return array.length < requestQty ? array.length : requestQty; } function extractAnyElement(array) { const index = Math.floor(Math.random() * array.length); return array.splice(index, 1); } function getMarkerCoordinates(cell) { const {cells, burgs} = pack; const burgId = cells.burg[cell]; if (burgId) { const {x, y} = burgs[burgId]; return [x, y]; } return cells.p[cell]; } function addMarker(base, marker) { const i = last(pack.markers)?.i + 1 || 0; const [x, y] = getMarkerCoordinates(marker.cell); marker = {...base, x, y, ...marker, i}; pack.markers.push(marker); occupied[marker.cell] = true; return marker; } function deleteMarker(markerId) { const noteId = "marker" + markerId; notes = notes.filter(note => note.id !== noteId); pack.markers = pack.markers.filter(m => m.i !== markerId); } function listVolcanoes({cells}) { return cells.i.filter(i => !occupied[i] && cells.h[i] >= 70); } function addVolcano(id, cell) { const {cells} = pack; const proper = Names.getCulture(cells.culture[cell]); const name = P(0.3) ? "Mount " + proper : Math.random() > 0.3 ? proper + " Volcano" : proper; notes.push({id, name, legend: `Active volcano. Height: ${getFriendlyHeight(cells.p[cell])}`}); } function listHotSprings({cells}) { return cells.i.filter(i => !occupied[i] && cells.h[i] > 50); } function addHotSpring(id, cell) { const {cells} = pack; const proper = Names.getCulture(cells.culture[cell]); const temp = convertTemperature(gauss(35, 15, 20, 100)); notes.push({id, name: proper + " Hot Springs", legend: `A hot springs area. Average temperature: ${temp}`}); } function listMines({cells}) { return cells.i.filter(i => !occupied[i] && cells.h[i] > 47 && cells.burg[i]); } function addMine(id, cell) { const {cells} = pack; const resources = {salt: 5, gold: 2, silver: 4, copper: 2, iron: 3, lead: 1, tin: 1}; const resource = rw(resources); const burg = pack.burgs[cells.burg[cell]]; const name = `${burg.name} — ${resource} mining town`; const population = rn(burg.population * populationRate * urbanization); const legend = `${burg.name} is a mining town of ${population} people just nearby the ${resource} mine`; notes.push({id, name, legend}); } function listBridges({cells, burgs}) { const meanFlux = d3.mean(cells.fl.filter(fl => fl)); return cells.i.filter( i => !occupied[i] && cells.burg[i] && cells.t[i] !== 1 && burgs[cells.burg[i]].population > 20 && cells.r[i] && cells.fl[i] > meanFlux ); } function addBridge(id, cell) { const {cells} = pack; const burg = pack.burgs[cells.burg[cell]]; const river = pack.rivers.find(r => r.i === pack.cells.r[cell]); const riverName = river ? `${river.name} ${river.type}` : "river"; const name = river && P(0.2) ? `${river.name} Bridge` : `${burg.name} Bridge`; const weightedAdjectives = { stone: 10, wooden: 1, lengthy: 2, formidable: 2, rickety: 1, beaten: 1, weathered: 1 }; const barriers = [ "collapse during the flood", "being rumoured to attract trolls", "the drying up of local trade", "banditry infested the area", "the old waypoints crumbled" ]; const legend = P(0.7) ? `A ${rw(weightedAdjectives)} bridge spans over the ${riverName} near ${burg.name}` : `An old crossing of the ${riverName}, rarely used since ${ra(barriers)}`; notes.push({id, name, legend}); } function listInns({cells}) { return cells.i.filter(i => !occupied[i] && cells.h[i] >= 20 && cells.road[i] > 4 && cells.pop[i] > 10); } function addInn(id, cell) { const colors = [ "Dark", "Light", "Bright", "Golden", "White", "Black", "Red", "Pink", "Purple", "Blue", "Green", "Yellow", "Amber", "Orange", "Brown", "Grey" ]; const animals = [ "Antelope", "Ape", "Badger", "Bear", "Beaver", "Bison", "Boar", "Buffalo", "Cat", "Crane", "Crocodile", "Crow", "Deer", "Dog", "Eagle", "Elk", "Fox", "Goat", "Goose", "Hare", "Hawk", "Heron", "Horse", "Hyena", "Ibis", "Jackal", "Jaguar", "Lark", "Leopard", "Lion", "Mantis", "Marten", "Moose", "Mule", "Narwhal", "Owl", "Panther", "Rat", "Raven", "Rook", "Scorpion", "Shark", "Sheep", "Snake", "Spider", "Swan", "Tiger", "Turtle", "Wolf", "Wolverine", "Camel", "Falcon", "Hound", "Ox" ]; const adjectives = [ "New", "Good", "High", "Old", "Great", "Big", "Major", "Happy", "Main", "Huge", "Far", "Beautiful", "Fair", "Prime", "Ancient", "Golden", "Proud", "Lucky", "Fat", "Honest", "Giant", "Distant", "Friendly", "Loud", "Hungry", "Magical", "Superior", "Peaceful", "Frozen", "Divine", "Favorable", "Brave", "Sunny", "Flying" ]; const methods = [ "Boiled", "Grilled", "Roasted", "Spit-roasted", "Stewed", "Stuffed", "Jugged", "Mashed", "Baked", "Braised", "Poached", "Marinated", "Pickled", "Smoked", "Dried", "Dry-aged", "Corned", "Fried", "Pan-fried", "Deep-fried", "Dressed", "Steamed", "Cured", "Syrupped", "Flame-Broiled" ]; const courses = [ "beef", "pork", "bacon", "chicken", "lamb", "chevon", "hare", "rabbit", "hart", "deer", "antlers", "bear", "buffalo", "badger", "beaver", "turkey", "pheasant", "duck", "goose", "teal", "quail", "pigeon", "seal", "carp", "bass", "pike", "catfish", "sturgeon", "escallop", "pie", "cake", "pottage", "pudding", "onions", "carrot", "potato", "beet", "garlic", "cabbage", "eggplant", "eggs", "broccoli", "zucchini", "pepper", "olives", "pumpkin", "spinach", "peas", "chickpea", "beans", "rice", "pasta", "bread", "apples", "peaches", "pears", "melon", "oranges", "mango", "tomatoes", "cheese", "corn", "rat tails", "pig ears" ]; const types = [ "hot", "cold", "fire", "ice", "smoky", "misty", "shiny", "sweet", "bitter", "salty", "sour", "sparkling", "smelly" ]; const drinks = [ "wine", "brandy", "gin", "whisky", "rom", "beer", "cider", "mead", "liquor", "spirits", "vodka", "tequila", "absinthe", "nectar", "milk", "kvass", "kumis", "tea", "water", "juice", "sap" ]; const typeName = P(0.3) ? "inn" : "tavern"; const isAnimalThemed = P(0.7); const animal = ra(animals); const name = isAnimalThemed ? P(0.6) ? ra(colors) + " " + animal : ra(adjectives) + " " + animal : ra(adjectives) + " " + capitalize(typeName); const meal = isAnimalThemed && P(0.3) ? animal : ra(courses); const course = `${ra(methods)} ${meal}`.toLowerCase(); const drink = `${P(0.5) ? ra(types) : ra(colors)} ${ra(drinks)}`.toLowerCase(); const legend = `A big and famous roadside ${typeName}. Delicious ${course} with ${drink} is served here`; notes.push({id, name: "The " + name, legend}); } function listLighthouses({cells}) { return cells.i.filter( i => !occupied[i] && cells.harbor[i] > 6 && cells.c[i].some(c => cells.h[c] < 20 && cells.road[c]) ); } function addLighthouse(id, cell) { const {cells} = pack; const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]); notes.push({ id, name: getAdjective(proper) + " Lighthouse" + name, legend: `A lighthouse to serve as a beacon for ships in the open sea` }); } function listWaterfalls({cells}) { return cells.i.filter( i => cells.r[i] && !occupied[i] && cells.h[i] >= 50 && cells.c[i].some(c => cells.h[c] < 40 && cells.r[c]) ); } function addWaterfall(id, cell) { const {cells} = pack; const descriptions = [ "A gorgeous waterfall flows here", "The rapids of an exceptionally beautiful waterfall", "An impressive waterfall has cut through the land", "The cascades of a stunning waterfall", "A river drops down from a great height forming a wonderous waterfall", "A breathtaking waterfall cuts through the landscape" ]; const proper = cells.burg[cell] ? pack.burgs[cells.burg[cell]].name : Names.getCulture(cells.culture[cell]); notes.push({id, name: getAdjective(proper) + " Waterfall" + name, legend: `${ra(descriptions)}`}); } function listBattlefields({cells}) { return cells.i.filter( i => !occupied[i] && cells.state[i] && cells.pop[i] > 2 && cells.h[i] < 50 && cells.h[i] > 25 ); } function addBattlefield(id, cell) { const {cells, states} = pack; const state = states[cells.state[cell]]; if (!state.campaigns) state.campaigns = BurgsAndStates.generateCampaign(state); const campaign = ra(state.campaigns); const date = generateDate(campaign.start, campaign.end); const name = Names.getCulture(cells.culture[cell]) + " Battlefield"; const legend = `A historical battle of the ${campaign.name}. \r\nDate: ${date} ${options.era}`; notes.push({id, name, legend}); } function listDungeons({cells}) { return cells.i.filter(i => !occupied[i] && cells.pop[i] && cells.pop[i] < 3); } function addDungeon(id, cell) { const dungeonSeed = `${seed}${cell}`; const name = "Dungeon"; const legend = `