diff --git a/index.html b/index.html index 6898c955..b5bae9ca 100644 --- a/index.html +++ b/index.html @@ -297,7 +297,7 @@ id="regenerate" data-t="tipRegenerate" data-tip="Click to generate a new map. Shortcut: F2" - onclick="regeneratePrompt('drawer')" + onclick="regeneratePrompt()" class="options" style="display: none" > diff --git a/main.js b/main.js index afe54d88..e1d59b56 100644 --- a/main.js +++ b/main.js @@ -628,18 +628,24 @@ void (function addDragToUpload() { }); })(); -async function generate() { +async function generate(options) { try { const timeStart = performance.now(); + const {seed: precreatedSeed} = options || {}; + invokeActiveZooming(); - generateSeed(); + setSeed(precreatedSeed); INFO && console.group("Generated Map " + seed); + applyMapSize(); randomizeOptions(); - placePoints(); - calculateVoronoi(grid, grid.points); - drawScaleBar(scale); + + if (shouldRegenerateGrid()) { + placePoints(); + calculateVoronoi(grid, grid.points); + } await HeightmapGenerator.generate(); + markFeatures(); markupGridOcean(); addLakesInDeepDepressions(); @@ -677,6 +683,8 @@ async function generate() { Military.generate(); Markers.generate(); addZones(); + + drawScaleBar(scale); Names.getMapName(); WARN && console.warn(`TOTAL: ${rn((performance.now() - timeStart) / 1000, 2)}s`); @@ -711,27 +719,41 @@ async function generate() { } } -// generate map seed (string!) or get it from URL searchParams -function generateSeed() { - const first = !mapHistory[0]; - const url = new URL(window.location.href); - const params = url.searchParams; - const urlSeed = url.searchParams.get("seed"); - if (first && params.get("from") === "MFCG" && urlSeed.length === 13) seed = urlSeed.slice(0, -4); - else if (first && urlSeed) seed = urlSeed; - else if (optionsSeed.value && optionsSeed.value != seed) seed = optionsSeed.value; - else seed = Math.floor(Math.random() * 1e9).toString(); - optionsSeed.value = seed; +// set map seed (string!) +function setSeed(precreatedSeed) { + if (!precreatedSeed) { + const first = !mapHistory[0]; + const url = new URL(window.location.href); + const params = url.searchParams; + const urlSeed = url.searchParams.get("seed"); + if (first && params.get("from") === "MFCG" && urlSeed.length === 13) seed = urlSeed.slice(0, -4); + else if (first && urlSeed) seed = urlSeed; + else if (optionsSeed.value && optionsSeed.value != seed) seed = optionsSeed.value; + else seed = generateSeed(); + } else { + seed = precreatedSeed; + } + + byId("optionsSeed").value = seed; Math.random = aleaPRNG(seed); } +// check if new grid graph should be generated or we can use the existing one +function shouldRegenerateGrid() { + if (!grid.spacing) return true; + const cellsDesired = +byId("pointsInput").dataset.cells; + const newSpacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2); + return grid.spacing !== newSpacing; +} + // Place points to calculate Voronoi diagram function placePoints() { TIME && console.time("placePoints"); Math.random = aleaPRNG(seed); // reset PRNG - const cellsDesired = +pointsInput.dataset.cells; - const spacing = (grid.spacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2)); // spacing between points before jirrering + const cellsDesired = +byId("pointsInput").dataset.cells; + const spacing = rn(Math.sqrt((graphWidth * graphHeight) / cellsDesired), 2); // spacing between points before jirrering + grid.spacing = spacing; grid.boundary = getBoundaryPoints(graphWidth, graphHeight, spacing); grid.points = getJitteredGrid(graphWidth, graphHeight, spacing); // jittered square grid grid.cellsX = Math.floor((graphWidth + 0.5 * spacing - 1e-10) / spacing); @@ -1921,14 +1943,14 @@ function showStatistics() { INFO && console.log(stats); } -const regenerateMap = debounce(async function () { +const regenerateMap = debounce(async function (options) { WARN && console.warn("Generate new random map"); showLoading(); closeDialogs("#worldConfigurator, #options3d"); customization = 0; resetZoom(1000); undraw(); - await generate(); + await generate(options); restoreLayers(); if (ThreeD.options.isOn) ThreeD.redraw(); if ($("#worldConfigurator").is(":visible")) editWorld(); diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js index 104d91b2..10a71148 100644 --- a/modules/burgs-and-states.js +++ b/modules/burgs-and-states.js @@ -1077,7 +1077,7 @@ window.BurgsAndStates = (function () { const generateProvinces = function (regenerate) { TIME && console.time("generateProvinces"); - const localSeed = regenerate ? Math.floor(Math.random() * 1e9).toString() : seed; + const localSeed = regenerate ? generateSeed() : seed; Math.random = aleaPRNG(localSeed); const {cells, states, burgs} = pack; diff --git a/modules/dynamic/heightmap-selection.js b/modules/dynamic/heightmap-selection.js index c9e28fb8..4dc631d7 100644 --- a/modules/dynamic/heightmap-selection.js +++ b/modules/dynamic/heightmap-selection.js @@ -41,8 +41,6 @@ const heightmaps = [ {id: "world-from-pacific", name: "World from Pacific"} ]; -let seed = Math.floor(Math.random() * 1e9); - appendStyleSheet(); insertEditorHtml(); addListeners(); @@ -62,7 +60,20 @@ export function open() { $(this).dialog("close"); }, Select: function () { - $templateInput.value = getSelected(); + const id = getSelected(); + $templateInput.value = id; + lock("template"); + $(this).dialog("close"); + }, + "New Map": function () { + const id = getSelected(); + $templateInput.value = id; + lock("template"); + + const seed = getSeed(); + Math.random = aleaPRNG(seed); + + regeneratePrompt({seed}); $(this).dialog("close"); } } @@ -133,13 +144,12 @@ function appendStyleSheet() { } function insertEditorHtml() { + const seed = generateSeed(); + const templatesHtml = templates .map(({id, name}) => { Math.random = aleaPRNG(seed); - - HeightmapGenerator.resetHeights(); - const heights = HeightmapGenerator.fromTemplate(id); - HeightmapGenerator.cleanup(); + const heights = generateHeightmap(id); const dataUrl = drawHeights(heights); return /* html */ `
@@ -154,7 +164,7 @@ function insertEditorHtml() { const heightmapsHtml = heightmaps .map(({id, name}) => { - return /* html */ `
+ return /* html */ `
${name}
${name}
`; @@ -202,6 +212,10 @@ function setSelected(id) { $heightmapSelection.querySelector(`[data-id="${id}"]`)?.classList?.add("selected"); } +function getSeed() { + return byId("heightmapSelection").querySelector(".selected")?.dataset?.seed; +} + function drawHeights(heights) { const canvas = document.createElement("canvas"); canvas.width = grid.cellsX; @@ -222,14 +236,19 @@ function drawHeights(heights) { return canvas.toDataURL("image/png"); } -function regeneratePreview(article, id) { - seed = Math.floor(Math.random() * 1e9); - article.dataset.seed = seed; - Math.random = aleaPRNG(seed); - +function generateHeightmap(id) { HeightmapGenerator.resetHeights(); const heights = HeightmapGenerator.fromTemplate(id); HeightmapGenerator.cleanup(); + return heights; +} + +function regeneratePreview(article, id) { + const seed = generateSeed(); + article.dataset.seed = seed; + Math.random = aleaPRNG(seed); + + const heights = generateHeightmap(id); const dataUrl = drawHeights(heights); article.querySelector("img").src = dataUrl; } diff --git a/modules/heightmap-generator.js b/modules/heightmap-generator.js index 2213deb7..d8ec192d 100644 --- a/modules/heightmap-generator.js +++ b/modules/heightmap-generator.js @@ -7,61 +7,6 @@ window.HeightmapGenerator = (function () { const getHeights = () => heights; const cleanup = () => (heights = null); - const generate = async function () { - resetHeights(); - - const input = document.getElementById("templateInput"); - const selectedId = input.selectedIndex >= 0 ? input.selectedIndex : 0; - const type = input.options[selectedId]?.parentElement?.label; - - if (type === "Specific") { - // pre-defined heightmap - TIME && console.time("defineHeightmap"); - return new Promise(resolve => { - // create canvas where 1px correcponds to a cell - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - const {cellsX, cellsY} = grid; - canvas.width = cellsX; - canvas.height = cellsY; - - // load heightmap into image and render to canvas - const img = new Image(); - img.src = `./heightmaps/${input.value}.png`; - img.onload = () => { - ctx.drawImage(img, 0, 0, cellsX, cellsY); - const imageData = ctx.getImageData(0, 0, cellsX, cellsY); - assignColorsToHeight(imageData.data); - canvas.remove(); - img.remove(); - - grid.cells.h = heights; - cleanup(); - TIME && console.timeEnd("defineHeightmap"); - resolve(); - }; - }); - } - - // heightmap template - TIME && console.time("generateHeightmap"); - const template = input.value; - const templateString = HeightmapTemplates[template]; - const steps = templateString.split("\n"); - - if (!steps.length) throw new Error(`Heightmap template: no steps. Template: ${template}. Steps: ${steps}`); - - for (const step of steps) { - const elements = step.trim().split(" "); - if (elements.length < 2) throw new Error(`Heightmap template: steps < 2. Template: ${template}. Step: ${elements}`); - addStep(...elements); - } - - grid.cells.h = heights; - cleanup(); - TIME && console.timeEnd("generateHeightmap"); - }; - const fromTemplate = template => { const templateString = HeightmapTemplates[template]; const steps = templateString.split("\n"); @@ -77,6 +22,49 @@ window.HeightmapGenerator = (function () { return heights; }; + const fromPrecreated = id => { + return new Promise(resolve => { + TIME && console.time("defineHeightmap"); + // create canvas where 1px corresponts to a cell + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + const {cellsX, cellsY} = grid; + canvas.width = cellsX; + canvas.height = cellsY; + + // load heightmap into image and render to canvas + const img = new Image(); + img.src = `./heightmaps/${id}.png`; + img.onload = () => { + ctx.drawImage(img, 0, 0, cellsX, cellsY); + const imageData = ctx.getImageData(0, 0, cellsX, cellsY); + assignColorsToHeight(imageData.data); + canvas.remove(); + img.remove(); + + grid.cells.h = heights; + cleanup(); + TIME && console.timeEnd("defineHeightmap"); + resolve(); + }; + }); + }; + + const generate = async function () { + Math.random = aleaPRNG(seed); + resetHeights(); + const id = byId("templateInput").value; + + if (HeightmapTemplates[id]) { + TIME && console.time("generateHeightmap"); + grid.cells.h = fromTemplate(id); + cleanup(); + TIME && console.timeEnd("generateHeightmap"); + } else { + return fromPrecreated(id); + } + }; + function addStep(tool, a2, a3, a4, a5) { if (tool === "Hill") return addHill(a2, a3, a4, a5); if (tool === "Pit") return addPit(a2, a3, a4, a5); diff --git a/modules/ui/hotkeys.js b/modules/ui/hotkeys.js index d9f0e234..9c9a4a7f 100644 --- a/modules/ui/hotkeys.js +++ b/modules/ui/hotkeys.js @@ -25,7 +25,7 @@ function handleKeyup(event) { const alt = altKey || key === "Alt"; if (code === "F1") showInfo(); - else if (code === "F2") regeneratePrompt("hotkey"); + else if (code === "F2") regeneratePrompt(); else if (code === "F6") quickSave(); else if (code === "F9") quickLoad(); else if (code === "Tab") toggleOptions(event); diff --git a/modules/ui/options.js b/modules/ui/options.js index fa82ed06..260cdea5 100644 --- a/modules/ui/options.js +++ b/modules/ui/options.js @@ -249,9 +249,9 @@ function testSpeaker() { speechSynthesis.speak(speaker); } -function generateMapWithSeed(source) { +function generateMapWithSeed() { if (optionsSeed.value == seed) return tip("The current map already has this seed", false, "error"); - regeneratePrompt(source); + regeneratePrompt(); } function showSeedHistoryDialog() { @@ -280,7 +280,7 @@ function restoreSeed(id) { mapHeightInput.value = mapHistory[id].height; templateInput.value = mapHistory[id].template; if (locked("template")) unlock("template"); - regeneratePrompt("seed history"); + regeneratePrompt(); } function restoreDefaultZoomExtent() { @@ -513,7 +513,6 @@ function applyStoredOptions() { // randomize options if randomization is allowed (not locked or options='default') function randomizeOptions() { - Math.random = aleaPRNG(seed); // reset seed to initial one const randomize = new URL(window.location.href).searchParams.get("options") === "default"; // ignore stored options // 'Options' settings @@ -638,17 +637,17 @@ function restoreDefaultOptions() { // Sticked menu Options listeners document.getElementById("sticked").addEventListener("click", function (event) { const id = event.target.id; - if (id === "newMapButton") regeneratePrompt("sticky button"); + if (id === "newMapButton") regeneratePrompt(); else if (id === "saveButton") showSavePane(); else if (id === "exportButton") showExportPane(); else if (id === "loadButton") showLoadPane(); else if (id === "zoomReset") resetZoom(1000); }); -function regeneratePrompt(source) { +function regeneratePrompt(options) { if (customization) return tip("New map cannot be generated when edit mode is active, please exit the mode and retry", false, "error"); const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes - if (workingTime < 5) return regenerateMap(source); + if (workingTime < 5) return regenerateMap(options); alertMessage.innerHTML = /* html */ `Are you sure you want to generate a new map?
All unsaved changes made to the current map will be lost`; @@ -661,7 +660,7 @@ function regeneratePrompt(source) { }, Generate: function () { closeDialogs(); - regenerateMap(source); + regenerateMap(options); } } }); diff --git a/modules/ui/tools.js b/modules/ui/tools.js index 66eaa9ea..20041aaf 100644 --- a/modules/ui/tools.js +++ b/modules/ui/tools.js @@ -137,7 +137,7 @@ function recalculatePopulation() { } function regenerateStates() { - const localSeed = Math.floor(Math.random() * 1e9); // new random seed + const localSeed = generateSeed(); Math.random = aleaPRNG(localSeed); const statesCount = +regionsOutput.value; diff --git a/utils/probabilityUtils.js b/utils/probabilityUtils.js index 454b659c..7759e330 100644 --- a/utils/probabilityUtils.js +++ b/utils/probabilityUtils.js @@ -74,3 +74,7 @@ function getNumberInRange(r) { } return count; } + +function generateSeed() { + return String(Math.floor(Math.random() * 1e9)); +}