[Migration] NPM (#1266)

* chore: add npm + vite for progressive enhancement

* fix: update Dockerfile to copy only the dist folder contents

* fix: update Dockerfile to use multi-stage build for optimized production image

* fix: correct nginx config file copy command in Dockerfile

* chore: add netlify configuration for build and redirects

* fix: add NODE_VERSION to environment in Netlify configuration

* remove wrong dist folder

* Update package.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* chore: split public and src

* migrating all util files from js to ts

* feat: Implement HeightmapGenerator and Voronoi module

- Added HeightmapGenerator class for generating heightmaps with various tools (Hill, Pit, Range, Trough, Strait, etc.).
- Introduced Voronoi class for creating Voronoi diagrams using Delaunator.
- Updated index.html to include new modules.
- Created index.ts to manage module imports.
- Enhanced arrayUtils and graphUtils with type definitions and improved functionality.
- Added utility functions for generating grids and calculating Voronoi cells.

* chore: add GitHub Actions workflow for deploying to GitHub Pages

* fix: update branch name in GitHub Actions workflow from 'main' to 'master'

* chore: update package.json to specify Node.js engine version and remove unused launch.json

* Initial plan

* Update copilot guidelines to reflect NPM/Vite/TypeScript migration

Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com>

* Update src/modules/heightmap-generator.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/utils/graphUtils.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/modules/heightmap-generator.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* feat: Add TIME and ERROR variables to global scope in HeightmapGenerator

* fix: Update base path in vite.config.ts for Netlify deployment

* fix: Update Node.js version in Dockerfile to 24-alpine

---------

Co-authored-by: Marc Emmanuel <marc.emmanuel@tado.com>
Co-authored-by: Marc Emmanuel <marcwissler@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Azgaar <26469650+Azgaar@users.noreply.github.com>
This commit is contained in:
Azgaar 2026-01-22 12:20:12 +01:00 committed by GitHub
parent 0c26f0831f
commit 9e0eb03618
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
713 changed files with 5182 additions and 2161 deletions

261
public/modules/io/save.js Normal file
View file

@ -0,0 +1,261 @@
"use strict";
// functions to save the project to a file
async function saveMap(method) {
if (customization) return tip("Map cannot be saved in EDIT mode, please complete the edit and retry", false, "error");
closeDialogs("#alert");
try {
const mapData = prepareMapData();
const filename = getFileName() + ".map";
saveToStorage(mapData, method === "storage"); // any method saves to indexedDB
if (method === "machine") saveToMachine(mapData, filename);
if (method === "dropbox") saveToDropbox(mapData, filename);
} catch (error) {
ERROR && console.error(error);
alertMessage.innerHTML = /* html */ `An error is occured on map saving. If the issue persists, please copy the message below and report it on ${link(
"https://github.com/Azgaar/Fantasy-Map-Generator/issues",
"GitHub"
)}. <p id="errorBox">${parseError(error)}</p>`;
$("#alert").dialog({
resizable: false,
title: "Saving error",
width: "28em",
buttons: {
Retry: function () {
$(this).dialog("close");
saveMap(method);
},
Close: function () {
$(this).dialog("close");
}
},
position: {my: "center", at: "center", of: "svg"}
});
}
}
function prepareMapData() {
const date = new Date();
const dateString = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate();
const license = "File can be loaded in azgaar.github.io/Fantasy-Map-Generator";
const params = [VERSION, license, dateString, seed, graphWidth, graphHeight, mapId].join("|");
const settings = [
distanceUnitInput.value,
distanceScale,
areaUnit.value,
heightUnit.value,
heightExponentInput.value,
temperatureScale.value,
"", // previously used for barSize.value
"", // previously used for barLabel.value
"", // previously used for barBackColor.value
"", // previously used for barBackColor.value
"", // previously used for barPosX.value
"", // previously used for barPosY.value
populationRate,
urbanization,
mapSizeOutput.value,
latitudeOutput.value,
"", // previously used for temperatureEquatorOutput.value
"", // previously used for tempNorthOutput.value
precOutput.value,
JSON.stringify(options),
mapName.value,
+hideLabels.checked,
stylePreset.value,
+rescaleLabels.checked,
urbanDensity,
longitudeOutput.value,
growthRate.value
].join("|");
const coords = JSON.stringify(mapCoordinates);
const biomes = [biomesData.color, biomesData.habitability, biomesData.name].join("|");
const notesData = JSON.stringify(notes);
const rulersString = rulers.toString();
const fonts = JSON.stringify(getUsedFonts(svg.node()));
// save svg
const cloneEl = document.getElementById("map").cloneNode(true);
// reset transform values to default
cloneEl.setAttribute("width", graphWidth);
cloneEl.setAttribute("height", graphHeight);
cloneEl.querySelector("#viewbox").removeAttribute("transform");
cloneEl.querySelector("#ruler").innerHTML = ""; // always remove rulers
const serializedSVG = new XMLSerializer().serializeToString(cloneEl);
const {spacing, cellsX, cellsY, boundary, points, features, cellsDesired} = grid;
const gridGeneral = JSON.stringify({spacing, cellsX, cellsY, boundary, points, features, cellsDesired});
const packFeatures = JSON.stringify(pack.features);
const cultures = JSON.stringify(pack.cultures);
const states = JSON.stringify(pack.states);
const burgs = JSON.stringify(pack.burgs);
const religions = JSON.stringify(pack.religions);
const provinces = JSON.stringify(pack.provinces);
const rivers = JSON.stringify(pack.rivers);
const markers = JSON.stringify(pack.markers);
const cellRoutes = JSON.stringify(pack.cells.routes);
const routes = JSON.stringify(pack.routes);
const zones = JSON.stringify(pack.zones);
// store name array only if not the same as default
const defaultNB = Names.getNameBases();
const namesData = nameBases
.map((b, i) => {
const names = defaultNB[i] && defaultNB[i].b === b.b ? "" : b.b;
return `${b.name}|${b.min}|${b.max}|${b.d}|${b.m}|${names}`;
})
.join("/");
// round population to save space
const pop = Array.from(pack.cells.pop).map(p => rn(p, 4));
// data format as below
const mapData = [
params,
settings,
coords,
biomes,
notesData,
serializedSVG,
gridGeneral,
grid.cells.h,
grid.cells.prec,
grid.cells.f,
grid.cells.t,
grid.cells.temp,
packFeatures,
cultures,
states,
burgs,
pack.cells.biome,
pack.cells.burg,
pack.cells.conf,
pack.cells.culture,
pack.cells.fl,
pop,
pack.cells.r,
[], // deprecated pack.cells.road
pack.cells.s,
pack.cells.state,
pack.cells.religion,
pack.cells.province,
[], // deprecated pack.cells.crossroad
religions,
provinces,
namesData,
rivers,
rulersString,
fonts,
markers,
cellRoutes,
routes,
zones
].join("\r\n");
return mapData;
}
// save map file to indexedDB
async function saveToStorage(mapData, showTip = false) {
const blob = new Blob([mapData], {type: "text/plain"});
await ldb.set("lastMap", blob);
showTip && tip("Map is saved to the browser storage", false, "success");
}
// download map file
function saveToMachine(mapData, filename) {
const blob = new Blob([mapData], {type: "text/plain"});
const URL = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.download = filename;
link.href = URL;
link.click();
tip('Map is saved to the "Downloads" folder (CTRL + J to open)', true, "success", 8000);
window.URL.revokeObjectURL(URL);
}
async function saveToDropbox(mapData, filename) {
await Cloud.providers.dropbox.save(filename, mapData);
tip("Map is saved to your Dropbox", true, "success", 8000);
}
async function initiateAutosave() {
const MINUTE = 60000; // munite in milliseconds
let lastSavedAt = Date.now();
async function autosave() {
const timeoutMinutes = byId("autosaveIntervalOutput").valueAsNumber;
if (!timeoutMinutes) return;
const diffInMinutes = (Date.now() - lastSavedAt) / MINUTE;
if (diffInMinutes < timeoutMinutes) return;
if (customization) return tip("Autosave: map cannot be saved in edit mode", false, "warning", 2000);
try {
tip("Autosave: saving map...", false, "warning", 3000);
const mapData = prepareMapData();
await saveToStorage(mapData);
tip("Autosave: map is saved", false, "success", 2000);
lastSavedAt = Date.now();
} catch (error) {
ERROR && console.error(error);
}
}
setInterval(autosave, MINUTE / 2);
}
// TODO: unused code
async function compressData(uncompressedData) {
const compressedStream = new Blob([uncompressedData]).stream().pipeThrough(new CompressionStream("gzip"));
let compressedData = [];
for await (const chunk of compressedStream) {
compressedData = compressedData.concat(Array.from(chunk));
}
return new Uint8Array(compressedData);
}
const saveReminder = function () {
if (localStorage.getItem("noReminder")) return;
const message = [
"Please don't forget to save the project to desktop from time to time",
"Please remember to save the map to your desktop",
"Saving will ensure your data won't be lost in case of issues",
"Safety is number one priority. Please save the map",
"Don't forget to save your map on a regular basis!",
"Just a gentle reminder for you to save the map",
"Please don't forget to save your progress (saving to desktop is the best option)",
"Don't want to get reminded about need to save? Press CTRL+Q"
];
const interval = 15 * 60 * 1000; // remind every 15 minutes
saveReminder.reminder = setInterval(() => {
if (customization) return;
tip(ra(message), true, "warn", 2500);
}, interval);
saveReminder.status = 1;
};
saveReminder();
function toggleSaveReminder() {
if (saveReminder.status) {
tip("Save reminder is turned off. Press CTRL+Q again to re-initiate", true, "warn", 2000);
clearInterval(saveReminder.reminder);
localStorage.setItem("noReminder", true);
saveReminder.status = 0;
} else {
tip("Save reminder is turned on. Press CTRL+Q to turn off", true, "warn", 2000);
localStorage.removeItem("noReminder");
saveReminder();
}
}