@@ -6087,6 +6192,15 @@
browser
+
+ Default map
+
+
+
Maps are saved in .map format, that can be loaded back via the Load in menu. There is no way to
restore the progress if file is lost. Please keep old save files on your machine or cloud storage as backups.
@@ -8118,7 +8232,7 @@
-
+
@@ -8140,7 +8254,7 @@
-
+
@@ -8155,7 +8269,7 @@
-
+
@@ -8163,10 +8277,13 @@
-
-
+
+
+
+
+
diff --git a/main.js b/main.js
index 848ff3c5..1f91ed4a 100644
--- a/main.js
+++ b/main.js
@@ -291,6 +291,28 @@ async function checkLoadParameters() {
return;
}
+ // restore onloadBehavior from localStorage if saved
+ const storedBehavior = localStorage.getItem("onloadBehavior");
+ if (storedBehavior) {
+ byId("onloadBehavior").value = storedBehavior;
+ }
+
+ // check if there is a default map saved to indexedDB
+ if (byId("onloadBehavior").value === "default") {
+ try {
+ const blob = await ldb.get("defaultMap");
+ if (blob) {
+ WARN && console.warn("Loading default map");
+ uploadMap(blob);
+ return;
+ } else {
+ WARN && console.warn("No default map set, generating random map");
+ }
+ } catch (error) {
+ ERROR && console.error(error);
+ }
+ }
+
// check if there is a map saved to indexedDB
if (byId("onloadBehavior").value === "lastSaved") {
try {
diff --git a/modules/burgs-and-states.js b/modules/burgs-and-states.js
index 8e3879c1..f5ff63fc 100644
--- a/modules/burgs-and-states.js
+++ b/modules/burgs-and-states.js
@@ -198,17 +198,22 @@ window.BurgsAndStates = (() => {
else b.y = rn(b.y - shift, 2);
}
- // define emblem
- const state = pack.states[b.state];
- const stateCOA = state.coa;
- let kinship = 0.25;
- if (b.capital) kinship += 0.1;
- else if (b.port) kinship -= 0.1;
- if (b.culture !== state.culture) kinship -= 0.25;
- b.type = getType(i, b.port);
- const type = b.capital && P(0.2) ? "Capital" : b.type === "Generic" ? "City" : b.type;
- b.coa = COA.generate(stateCOA, kinship, null, type);
- b.coa.shield = COA.getShield(b.culture, b.state);
+ // Attach biome info
+ b.biome = pack.cells.biome[i];
+ // Attach province info if available
+ b.province = pack.cells.province ? pack.cells.province[i] : undefined;
+
+ // define emblem
+ const state = pack.states[b.state];
+ const stateCOA = state.coa;
+ let kinship = 0.25;
+ if (b.capital) kinship += 0.1;
+ else if (b.port) kinship -= 0.1;
+ if (b.culture !== state.culture) kinship -= 0.25;
+ b.type = getType(i, b.port);
+ const type = b.capital && P(0.2) ? "Capital" : b.type === "Generic" ? "City" : b.type;
+ b.coa = COA.generate(stateCOA, kinship, null, type);
+ b.coa.shield = COA.getShield(b.culture, b.state);
}
// de-assign port status if it's the only one on feature
diff --git a/modules/io/load.js b/modules/io/load.js
index ccfccecb..9e235e57 100644
--- a/modules/io/load.js
+++ b/modules/io/load.js
@@ -280,7 +280,7 @@ async function parseLoadedData(data, mapVersion) {
family === usedFamily && unicodeRange === usedRange && variant === usedVariant
);
if (!defaultFont) fonts.push(usedFont);
- declareFont(usedFont);
+ if (typeof declareFont !== "undefined") declareFont(usedFont);
});
}
}
@@ -460,7 +460,7 @@ async function parseLoadedData(data, mapVersion) {
if (isVisible(scaleBar)) turnOn("toggleScaleBar");
if (isVisibleNode(byId("vignette"))) turnOn("toggleVignette");
- getCurrentPreset();
+ if (typeof getCurrentPreset !== "undefined") getCurrentPreset();
}
{
@@ -477,7 +477,7 @@ async function parseLoadedData(data, mapVersion) {
}
// add custom heightmap color scheme if any
- if (heightmapColorSchemes) {
+ if (typeof heightmapColorSchemes !== "undefined" && heightmapColorSchemes && typeof addCustomColorScheme !== "undefined") {
const oceanScheme = byId("oceanHeights")?.getAttribute("scheme");
if (oceanScheme && !(oceanScheme in heightmapColorSchemes)) addCustomColorScheme(oceanScheme);
const landScheme = byId("#landHeights")?.getAttribute("scheme");
@@ -487,7 +487,7 @@ async function parseLoadedData(data, mapVersion) {
{
// add custom texture if any
const textureHref = texture.attr("data-href");
- if (textureHref) updateTextureSelectValue(textureHref);
+ if (textureHref && typeof updateTextureSelectValue !== "undefined") updateTextureSelectValue(textureHref);
}
// data integrity checks
@@ -625,7 +625,7 @@ async function parseLoadedData(data, mapVersion) {
capitalBurgs.forEach(burg => {
burg.capital = 0;
- moveBurgToGroup(burg.i, "towns");
+ if (typeof moveBurgToGroup !== "undefined") moveBurgToGroup(burg.i, "towns");
});
return;
@@ -640,7 +640,7 @@ async function parseLoadedData(data, mapVersion) {
capitalBurgs.forEach((burg, i) => {
if (!i) return;
burg.capital = 0;
- moveBurgToGroup(burg.i, "towns");
+ if (typeof moveBurgToGroup !== "undefined") moveBurgToGroup(burg.i, "towns");
});
return;
@@ -650,7 +650,7 @@ async function parseLoadedData(data, mapVersion) {
ERROR &&
console.error(`[Data integrity] State ${state.i} has no capital. Assigning the first burg as capital`);
stateBurgs[0].capital = 1;
- moveBurgToGroup(stateBurgs[0].i, "cities");
+ if (typeof moveBurgToGroup !== "undefined") moveBurgToGroup(stateBurgs[0].i, "cities");
}
});
@@ -731,9 +731,9 @@ async function parseLoadedData(data, mapVersion) {
{
if (window.restoreDefaultEvents) restoreDefaultEvents();
- focusOn(); // based on searchParams focus on point, cell or burg
- invokeActiveZooming();
- fitMapToScreen();
+ if (typeof focusOn !== "undefined") focusOn(); // based on searchParams focus on point, cell or burg
+ if (typeof invokeActiveZooming !== "undefined") invokeActiveZooming();
+ if (typeof fitMapToScreen !== "undefined") fitMapToScreen();
}
WARN && console.warn(`TOTAL: ${rn((performance.now() - uploadMap.timeStart) / 1000, 2)}s`);
diff --git a/modules/io/obsidian-bridge.js b/modules/io/obsidian-bridge.js
new file mode 100644
index 00000000..f7f5ae45
--- /dev/null
+++ b/modules/io/obsidian-bridge.js
@@ -0,0 +1,710 @@
+"use strict";
+
+// Obsidian Vault Integration for Fantasy Map Generator
+// Uses Obsidian Local REST API plugin
+// https://github.com/coddingtonbear/obsidian-local-rest-api
+
+const ObsidianBridge = (() => {
+ // Configuration
+ const config = {
+ apiUrl: "http://127.0.0.1:27123",
+ apiKey: "", // Set via UI
+ enabled: false,
+ vaultName: ""
+ };
+
+ // Cache for vault file list
+ let vaultFilesCache = {
+ files: null,
+ timestamp: null,
+ ttl: 5 * 60 * 1000 // 5 minutes cache
+ };
+
+ // Index: fmg-id → file path for fast lookups
+ let fmgIdIndex = {};
+
+ // Initialize from localStorage
+ function init() {
+ const stored = localStorage.getItem("obsidianConfig");
+ if (stored) {
+ try {
+ const parsed = JSON.parse(stored);
+ Object.assign(config, parsed);
+ } catch (error) {
+ ERROR && console.error("Failed to load Obsidian config:", error);
+ }
+ }
+
+ // Load FMG ID index from localStorage
+ const storedIndex = localStorage.getItem("obsidianFmgIdIndex");
+ if (storedIndex) {
+ try {
+ fmgIdIndex = JSON.parse(storedIndex);
+ INFO && console.log(`Loaded FMG ID index with ${Object.keys(fmgIdIndex).length} entries`);
+ } catch (error) {
+ ERROR && console.error("Failed to load FMG ID index:", error);
+ fmgIdIndex = {};
+ }
+ }
+
+ // Pre-warm cache if Obsidian is already enabled
+ if (config.enabled) {
+ INFO && console.log("Obsidian enabled, pre-warming cache...");
+ prewarmCache();
+ }
+ }
+
+ // Save configuration
+ function saveConfig() {
+ localStorage.setItem("obsidianConfig", JSON.stringify(config));
+ }
+
+ // Test connection to Obsidian
+ async function testConnection() {
+ if (!config.apiKey) {
+ throw new Error("API key not set. Please configure in Options.");
+ }
+
+ try {
+ const response = await fetch(`${config.apiUrl}/`, {
+ headers: {
+ Authorization: `Bearer ${config.apiKey}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`Connection failed: ${response.status}`);
+ }
+
+ const data = await response.json();
+ INFO && console.log("Obsidian connection successful:", data);
+ config.enabled = true;
+ saveConfig();
+
+ // Pre-warm the cache in the background (don't await)
+ prewarmCache();
+
+ return true;
+ } catch (error) {
+ ERROR && console.error("Obsidian connection failed:", error);
+ config.enabled = false;
+ saveConfig();
+ throw error;
+ }
+ }
+
+ // Pre-warm the vault files cache in the background
+ async function prewarmCache() {
+ try {
+ INFO && console.log("Pre-warming vault file cache...");
+ await getVaultFiles();
+ INFO && console.log("Vault file cache pre-warmed successfully!");
+
+ // Also build the complete FMG ID index
+ await buildCompleteIndex();
+ } catch (error) {
+ WARN && console.warn("Failed to pre-warm cache:", error);
+ // Don't throw - this is just optimization
+ }
+ }
+
+ // Build complete index of all fmg-ids in the vault
+ async function buildCompleteIndex() {
+ try {
+ INFO && console.log("Building complete FMG ID index...");
+ TIME && console.time("buildCompleteIndex");
+
+ const files = vaultFilesCache.files || await getVaultFiles();
+ let indexed = 0;
+ let skipped = 0;
+
+ for (const filePath of files) {
+ try {
+ const content = await getNote(filePath);
+ const {frontmatter} = parseFrontmatter(content);
+
+ const fmgId = frontmatter["fmg-id"] || frontmatter.fmgId;
+ if (fmgId) {
+ fmgIdIndex[fmgId] = filePath;
+ indexed++;
+ } else {
+ skipped++;
+ }
+ } catch (error) {
+ DEBUG && console.debug(`Skipping file ${filePath}:`, error);
+ skipped++;
+ }
+ }
+
+ // Save the complete index
+ saveFmgIdIndex();
+
+ TIME && console.timeEnd("buildCompleteIndex");
+ INFO && console.log(`Complete FMG ID index built: ${indexed} notes indexed, ${skipped} skipped`);
+ } catch (error) {
+ ERROR && console.error("Failed to build complete index:", error);
+ }
+ }
+
+ // Recursively scan a directory and all subdirectories for .md files
+ async function scanDirectory(path = "") {
+ const response = await fetch(`${config.apiUrl}/vault/${encodeURIComponent(path)}`, {
+ headers: {
+ Authorization: `Bearer ${config.apiKey}`
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch directory ${path}: ${response.status}`);
+ }
+
+ const data = await response.json();
+ const entries = data.files || [];
+ const mdFiles = [];
+
+ for (const entry of entries) {
+ const fullPath = path ? `${path}${entry}` : entry;
+
+ if (entry.endsWith("/")) {
+ // It's a directory - recurse into it
+ DEBUG && console.log(`Scanning directory: ${fullPath}`);
+ const subFiles = await scanDirectory(fullPath);
+ mdFiles.push(...subFiles);
+ } else if (entry.endsWith(".md")) {
+ // It's a markdown file - add it
+ mdFiles.push(fullPath);
+ }
+ }
+
+ return mdFiles;
+ }
+
+ // Clear the vault files cache
+ function clearVaultCache() {
+ vaultFilesCache.files = null;
+ vaultFilesCache.timestamp = null;
+ INFO && console.log("Vault file cache cleared");
+ }
+
+ // Save FMG ID index to localStorage
+ function saveFmgIdIndex() {
+ try {
+ localStorage.setItem("obsidianFmgIdIndex", JSON.stringify(fmgIdIndex));
+ DEBUG && console.log(`Saved FMG ID index with ${Object.keys(fmgIdIndex).length} entries`);
+ } catch (error) {
+ ERROR && console.error("Failed to save FMG ID index:", error);
+ }
+ }
+
+ // Add entry to FMG ID index
+ function addToFmgIdIndex(fmgId, filePath) {
+ if (!fmgId) return;
+ fmgIdIndex[fmgId] = filePath;
+ saveFmgIdIndex();
+ DEBUG && console.log(`Added to index: ${fmgId} → ${filePath}`);
+ }
+
+ // Get file path from FMG ID index
+ function getFromFmgIdIndex(fmgId) {
+ return fmgIdIndex[fmgId] || null;
+ }
+
+ // Get all markdown files from vault (recursively, with caching)
+ async function getVaultFiles(forceRefresh = false) {
+ if (!config.enabled) {
+ throw new Error("Obsidian not connected");
+ }
+
+ // Check cache
+ const now = Date.now();
+ const cacheValid = vaultFilesCache.files !== null &&
+ vaultFilesCache.timestamp !== null &&
+ (now - vaultFilesCache.timestamp) < vaultFilesCache.ttl;
+
+ if (cacheValid && !forceRefresh) {
+ INFO && console.log(`getVaultFiles: Using cached list (${vaultFilesCache.files.length} files)`);
+ return vaultFilesCache.files;
+ }
+
+ try {
+ TIME && console.time("getVaultFiles");
+ INFO && console.log("getVaultFiles: Scanning vault (cache miss or expired)...");
+
+ // Recursively scan all directories
+ const mdFiles = await scanDirectory("");
+
+ // Update cache
+ vaultFilesCache.files = mdFiles;
+ vaultFilesCache.timestamp = now;
+
+ INFO && console.log(`getVaultFiles: Found ${mdFiles.length} markdown files (recursive scan, cached)`);
+ DEBUG && console.log("Sample files:", mdFiles.slice(0, 10));
+
+ TIME && console.timeEnd("getVaultFiles");
+
+ return mdFiles;
+ } catch (error) {
+ ERROR && console.error("Failed to get vault files:", error);
+ TIME && console.timeEnd("getVaultFiles");
+ throw error;
+ }
+ }
+
+ // Get note content by path
+ async function getNote(notePath) {
+ if (!config.enabled) {
+ throw new Error("Obsidian not connected");
+ }
+
+ try {
+ const response = await fetch(`${config.apiUrl}/vault/${encodeURIComponent(notePath)}`, {
+ headers: {
+ Authorization: `Bearer ${config.apiKey}`,
+ Accept: "text/markdown"
+ }
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch note: ${response.status}`);
+ }
+
+ return await response.text();
+ } catch (error) {
+ ERROR && console.error("Failed to get note:", error);
+ throw error;
+ }
+ }
+
+ // Update note content
+ async function updateNote(notePath, content) {
+ if (!config.enabled) {
+ throw new Error("Obsidian not connected");
+ }
+
+ try {
+ const response = await fetch(`${config.apiUrl}/vault/${encodeURIComponent(notePath)}`, {
+ method: "PUT",
+ headers: {
+ Authorization: `Bearer ${config.apiKey}`,
+ "Content-Type": "text/markdown"
+ },
+ body: content
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to update note: ${response.status}`);
+ }
+
+ INFO && console.log("Note updated successfully:", notePath);
+ return true;
+ } catch (error) {
+ ERROR && console.error("Failed to update note:", error);
+ throw error;
+ }
+ }
+
+ // Create new note
+ async function createNote(notePath, content) {
+ if (!config.enabled) {
+ throw new Error("Obsidian not connected");
+ }
+
+ try {
+ const response = await fetch(`${config.apiUrl}/vault/${encodeURIComponent(notePath)}`, {
+ method: "PUT",
+ headers: {
+ Authorization: `Bearer ${config.apiKey}`,
+ "Content-Type": "text/markdown"
+ },
+ body: content
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to create note: ${response.status}`);
+ }
+
+ INFO && console.log("Note created successfully:", notePath);
+ return true;
+ } catch (error) {
+ ERROR && console.error("Failed to create note:", error);
+ throw error;
+ }
+ }
+
+ // Parse YAML frontmatter from markdown content
+ function parseFrontmatter(content) {
+ const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---/;
+ const match = content.match(frontmatterRegex);
+
+ if (!match) {
+ return {frontmatter: {}, content};
+ }
+
+ const frontmatterText = match[1];
+ const bodyContent = content.slice(match[0].length).trim();
+
+ // Simple YAML parser (handles basic key-value pairs and nested objects)
+ const frontmatter = {};
+ const lines = frontmatterText.split("\n");
+ let currentKey = null;
+ let currentObj = frontmatter;
+ let indentLevel = 0;
+
+ for (const line of lines) {
+ const trimmed = line.trim();
+ if (!trimmed || trimmed.startsWith("#")) continue;
+
+ const indent = line.search(/\S/);
+ const colonIndex = trimmed.indexOf(":");
+
+ if (colonIndex === -1) continue;
+
+ const key = trimmed.slice(0, colonIndex).trim();
+ const value = trimmed.slice(colonIndex + 1).trim();
+
+ if (indent === 0) {
+ currentObj = frontmatter;
+ currentKey = key;
+
+ if (value) {
+ // Simple value
+ frontmatter[key] = parseValue(value);
+ } else {
+ // Nested object or array
+ frontmatter[key] = {};
+ currentObj = frontmatter[key];
+ }
+ } else if (currentObj && key) {
+ // Nested property
+ currentObj[key] = parseValue(value);
+ }
+ }
+
+ return {frontmatter, content: bodyContent};
+ }
+
+ // Parse YAML value (handle strings, numbers, arrays)
+ function parseValue(value) {
+ if (!value) return null;
+
+ // Array
+ if (value.startsWith("[") && value.endsWith("]")) {
+ return value
+ .slice(1, -1)
+ .split(",")
+ .map(v => v.trim())
+ .filter(v => v);
+ }
+
+ // Number
+ if (!isNaN(value) && value !== "") {
+ return parseFloat(value);
+ }
+
+ // Boolean
+ if (value === "true") return true;
+ if (value === "false") return false;
+
+ // String (remove quotes if present)
+ return value.replace(/^["']|["']$/g, "");
+ }
+
+ // Calculate distance between two coordinates
+ function calculateDistance(x1, y1, x2, y2) {
+ const dx = x2 - x1;
+ const dy = y2 - y1;
+ return Math.sqrt(dx * dx + dy * dy);
+ }
+
+ // Find notes by coordinates
+ async function findNotesByCoordinates(x, y, limit = 8) {
+ TIME && console.time("findNotesByCoordinates");
+
+ try {
+ const files = await getVaultFiles();
+ const mdFiles = files.filter(f => f.endsWith(".md"));
+
+ const matches = [];
+
+ for (const filePath of mdFiles) {
+ try {
+ const content = await getNote(filePath);
+ const {frontmatter} = parseFrontmatter(content);
+
+ // Check for coordinates in frontmatter
+ let noteX, noteY;
+
+ // Support various coordinate formats
+ if (frontmatter.coordinates) {
+ noteX = frontmatter.coordinates.x || frontmatter.coordinates.X;
+ noteY = frontmatter.coordinates.y || frontmatter.coordinates.Y;
+ } else {
+ noteX = frontmatter.x || frontmatter.X;
+ noteY = frontmatter.y || frontmatter.Y;
+ }
+
+ if (noteX !== undefined && noteY !== undefined) {
+ const distance = calculateDistance(x, y, noteX, noteY);
+
+ matches.push({
+ path: filePath,
+ name: filePath.replace(/\.md$/, "").split("/").pop(),
+ frontmatter,
+ distance,
+ coordinates: {x: noteX, y: noteY}
+ });
+ }
+ } catch (error) {
+ // Skip files that can't be read
+ DEBUG && console.debug("Skipping file:", filePath, error);
+ }
+ }
+
+ // Sort by distance and return top matches
+ matches.sort((a, b) => a.distance - b.distance);
+ const results = matches.slice(0, limit);
+
+ TIME && console.timeEnd("findNotesByCoordinates");
+ INFO && console.log(`Found ${results.length} nearby notes for (${x}, ${y})`);
+
+ return results;
+ } catch (error) {
+ ERROR && console.error("Failed to find notes by coordinates:", error);
+ TIME && console.timeEnd("findNotesByCoordinates");
+ throw error;
+ }
+ }
+
+ // Find note by FMG ID in frontmatter (with index for fast lookup)
+ async function findNoteByFmgId(fmgId) {
+ if (!fmgId) return null;
+
+ try {
+ // First, check the index for instant lookup
+ const indexedPath = getFromFmgIdIndex(fmgId);
+ if (indexedPath) {
+ INFO && console.log(`Found note in index: ${fmgId} → ${indexedPath}`);
+ try {
+ const content = await getNote(indexedPath);
+ const {frontmatter} = parseFrontmatter(content);
+
+ // Verify the fmg-id still matches (file might have been modified)
+ if (frontmatter["fmg-id"] === fmgId || frontmatter.fmgId === fmgId) {
+ return {
+ path: indexedPath,
+ name: indexedPath.replace(/\.md$/, "").split("/").pop(),
+ content,
+ frontmatter
+ };
+ } else {
+ // Index is stale, remove the entry
+ WARN && console.warn(`Index entry stale for ${fmgId}, removing`);
+ delete fmgIdIndex[fmgId];
+ saveFmgIdIndex();
+ }
+ } catch (error) {
+ // File no longer exists, remove from index
+ WARN && console.warn(`Indexed file not found: ${indexedPath}, removing from index`);
+ delete fmgIdIndex[fmgId];
+ saveFmgIdIndex();
+ }
+ }
+
+ // Not in index or index was stale, search all files
+ INFO && console.log(`Searching vault for fmg-id: ${fmgId}`);
+ const files = await getVaultFiles();
+ const mdFiles = files.filter(f => f.endsWith(".md"));
+
+ for (const filePath of mdFiles) {
+ try {
+ const content = await getNote(filePath);
+ const {frontmatter} = parseFrontmatter(content);
+
+ if (frontmatter["fmg-id"] === fmgId || frontmatter.fmgId === fmgId) {
+ // Found it! Add to index for next time
+ addToFmgIdIndex(fmgId, filePath);
+ INFO && console.log(`Found note and added to index: ${fmgId} → ${filePath}`);
+
+ return {
+ path: filePath,
+ name: filePath.replace(/\.md$/, "").split("/").pop(),
+ content,
+ frontmatter
+ };
+ }
+ } catch (error) {
+ DEBUG && console.debug("Skipping file:", filePath, error);
+ }
+ }
+
+ return null;
+ } catch (error) {
+ ERROR && console.error("Failed to find note by FMG ID:", error);
+ throw error;
+ }
+ }
+
+ // Generate note template for FMG element
+ function generateNoteTemplate(element, type, elementId) {
+ const {x, y} = element;
+ const lat = pack.cells.lat?.[element.cell] || 0;
+ const lon = pack.cells.lon?.[element.cell] || 0;
+
+ const frontmatter = {
+ "fmg-id": elementId || element.id || `${type}${element.i}`,
+ "fmg-type": type,
+ coordinates: {x, y, lat, lon},
+ tags: [type],
+ created: new Date().toISOString()
+ };
+
+ if (element.name) {
+ frontmatter.aliases = [element.name];
+ }
+
+ const yaml = Object.entries(frontmatter)
+ .map(([key, value]) => {
+ if (typeof value === "object" && !Array.isArray(value)) {
+ const nested = Object.entries(value)
+ .map(([k, v]) => ` ${k}: ${v}`)
+ .join("\n");
+ return `${key}:\n${nested}`;
+ } else if (Array.isArray(value)) {
+ return `${key}:\n - ${value.join("\n - ")}`;
+ }
+ return `${key}: ${value}`;
+ })
+ .join("\n");
+
+ const title = element.name || `${type} ${element.i}`;
+
+ return `---
+${yaml}
+---
+
+# ${title}
+
+*This note was created from Fantasy Map Generator*
+
+## Description
+
+Add your lore here...
+
+## History
+
+## Notable Features
+
+## Related
+`;
+ }
+
+ // Search notes by text query (searches in filename and frontmatter)
+ async function searchNotes(query) {
+ if (!query || query.trim() === "") {
+ return [];
+ }
+
+ const allFiles = await getVaultFiles();
+ const searchTerm = query.toLowerCase();
+ const results = [];
+
+ for (const filePath of allFiles) {
+ const fileName = filePath.split("/").pop().replace(".md", "").toLowerCase();
+
+ // Check if filename matches
+ if (fileName.includes(searchTerm)) {
+ try {
+ const content = await getNote(filePath);
+ const {frontmatter} = parseFrontmatter(content);
+
+ results.push({
+ path: filePath,
+ name: filePath.split("/").pop().replace(".md", ""),
+ frontmatter,
+ matchType: "filename"
+ });
+ } catch (error) {
+ WARN && console.warn(`Could not read file ${filePath}:`, error);
+ }
+ }
+ }
+
+ return results;
+ }
+
+ // List all notes with basic info (reads frontmatter - slow for large vaults)
+ async function listAllNotes() {
+ const allFiles = await getVaultFiles();
+ const notes = [];
+
+ INFO && console.log(`listAllNotes: Processing ${allFiles.length} files`);
+
+ for (const filePath of allFiles) {
+ try {
+ const content = await getNote(filePath);
+ const {frontmatter} = parseFrontmatter(content);
+
+ notes.push({
+ path: filePath,
+ name: filePath.split("/").pop().replace(".md", ""),
+ frontmatter,
+ folder: filePath.includes("/") ? filePath.substring(0, filePath.lastIndexOf("/")) : ""
+ });
+ } catch (error) {
+ WARN && console.warn(`Could not read file ${filePath}:`, error);
+ }
+ }
+
+ // Sort by path
+ notes.sort((a, b) => a.path.localeCompare(b.path));
+
+ INFO && console.log(`listAllNotes: Returning ${notes.length} notes`);
+ DEBUG && console.log("Sample note paths:", notes.slice(0, 5).map(n => n.path));
+
+ return notes;
+ }
+
+ // List just file paths (fast - no content reading)
+ async function listAllNotePaths() {
+ const allFiles = await getVaultFiles();
+
+ INFO && console.log(`listAllNotePaths: Found ${allFiles.length} files`);
+
+ // Convert to note objects with just path and name
+ const notes = allFiles.map(filePath => ({
+ path: filePath,
+ name: filePath.split("/").pop().replace(".md", ""),
+ folder: filePath.includes("/") ? filePath.substring(0, filePath.lastIndexOf("/")) : ""
+ }));
+
+ // Sort by path
+ notes.sort((a, b) => a.path.localeCompare(b.path));
+
+ return notes;
+ }
+
+ return {
+ init,
+ config,
+ saveConfig,
+ testConnection,
+ getVaultFiles,
+ clearVaultCache,
+ getNote,
+ updateNote,
+ createNote,
+ parseFrontmatter,
+ findNotesByCoordinates,
+ findNoteByFmgId,
+ generateNoteTemplate,
+ searchNotes,
+ listAllNotes,
+ listAllNotePaths,
+ addToFmgIdIndex,
+ getFromFmgIdIndex,
+ buildCompleteIndex
+ };
+})();
+
+// Initialize on load
+ObsidianBridge.init();
diff --git a/modules/io/save.js b/modules/io/save.js
index 304fef59..c4db90a2 100644
--- a/modules/io/save.js
+++ b/modules/io/save.js
@@ -167,6 +167,36 @@ async function saveToStorage(mapData, showTip = false) {
showTip && tip("Map is saved to the browser storage", false, "success");
}
+// save current map as the default map
+async function saveAsDefaultMap() {
+ if (customization) return tip("Map cannot be saved in EDIT mode, please complete the edit and retry", false, "error");
+
+ try {
+ const mapData = prepareMapData();
+ const blob = new Blob([mapData], {type: "text/plain"});
+ await ldb.set("defaultMap", blob);
+ localStorage.setItem("onloadBehavior", "default");
+ byId("onloadBehavior").value = "default";
+ tip("Map is set as default and will open on load", true, "success", 5000);
+ } catch (error) {
+ ERROR && console.error(error);
+ tip("Failed to set default map", true, "error", 3000);
+ }
+}
+
+// clear the default map setting
+async function clearDefaultMap() {
+ try {
+ await ldb.set("defaultMap", null);
+ localStorage.removeItem("onloadBehavior");
+ byId("onloadBehavior").value = "random";
+ tip("Default map cleared", false, "success", 2000);
+ } catch (error) {
+ ERROR && console.error(error);
+ tip("Failed to clear default map", false, "error", 2000);
+ }
+}
+
// download map file
function saveToMachine(mapData, filename) {
const blob = new Blob([mapData], {type: "text/plain"});
diff --git a/modules/markers-generator.js b/modules/markers-generator.js
index 367cdd5f..ebf544e2 100644
--- a/modules/markers-generator.js
+++ b/modules/markers-generator.js
@@ -1,3 +1,26 @@
+// Assign biome and province info to existing markers, burgs, and provinces after loading a map
+window.assignBiomeAndProvinceInfo = function() {
+ // Markers
+ if (pack.markers && pack.cells && pack.cells.biome) {
+ pack.markers.forEach(marker => {
+ if (marker.cell !== undefined) {
+ marker.biome = pack.cells.biome[marker.cell];
+ marker.province = pack.cells.province ? pack.cells.province[marker.cell] : undefined;
+ }
+ });
+ }
+ // Burgs
+ if (pack.burgs && pack.cells && pack.cells.biome) {
+ pack.burgs.forEach(burg => {
+ if (burg.cell !== undefined) {
+ burg.biome = pack.cells.biome[burg.cell];
+ burg.province = pack.cells.province ? pack.cells.province[burg.cell] : undefined;
+ }
+ });
+ }
+ // Provinces (if you want to attach biome info, though provinces are usually collections of cells)
+ // You could aggregate biomes for each province if needed
+};
"use strict";
window.Markers = (function () {
@@ -154,10 +177,20 @@ window.Markers = (function () {
if (marker.cell === undefined) return;
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;
+ // Attach biome and province info
+ const biome = pack.cells.biome[marker.cell];
+ const province = pack.cells.province ? pack.cells.province[marker.cell] : undefined;
+ // Add Obsidian note path (customize as needed)
+ const obsidianNotePath = `Neblub/Orbis/Markers/${marker.type}-${marker.cell}`;
+ marker = {...base, x, y, ...marker, i, biome, province, obsidianNotePath};
+// Utility to open Obsidian note for a marker
+window.openObsidianNote = function(notePath) {
+ const uri = `obsidian://open?vault=Neblub&file=${encodeURIComponent(notePath)}`;
+ window.open(uri, '_blank');
+};
+ pack.markers.push(marker);
+ occupied[marker.cell] = true;
+ return marker;
}
function deleteMarker(markerId) {
diff --git a/modules/ui/burg-editor.js b/modules/ui/burg-editor.js
index 13e32850..25576f33 100644
--- a/modules/ui/burg-editor.js
+++ b/modules/ui/burg-editor.js
@@ -486,9 +486,17 @@ function editBurg(id) {
}
function editBurgLegend() {
- const id = elSelected.attr("data-id");
- const name = elSelected.text();
- editNotes("burg" + id, name);
+ const id = +elSelected.attr("data-id");
+ const burg = pack.burgs[id];
+
+ // Use Obsidian integration if available, otherwise fall back to old notes system
+ if (typeof editObsidianNote !== "undefined") {
+ const coordinates = {x: burg.x, y: burg.y};
+ editObsidianNote("burg" + id, "burg", coordinates);
+ } else {
+ const name = elSelected.text();
+ editNotes("burg" + id, name);
+ }
}
function showTemperatureGraph() {
diff --git a/modules/ui/burgs-overview.js b/modules/ui/burgs-overview.js
index 2a487b67..1e2ee2bf 100644
--- a/modules/ui/burgs-overview.js
+++ b/modules/ui/burgs-overview.js
@@ -481,7 +481,7 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
}
function downloadBurgsData() {
- let data = `Id,Burg,Province,Province Full Name,State,State Full Name,Culture,Religion,Population,X,Y,Latitude,Longitude,Elevation (${heightUnit.value}),Temperature,Temperature likeness,Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town,Emblem,City Generator Link\n`; // headers
+ let data = `Id,Burg,Province,Province Full Name,State,State Full Name,Culture,Religion,Population,X,Y,Latitude,Longitude,Elevation (${heightUnit.value}),Temperature,Temperature likeness,Biome,Province Id,Capital,Port,Citadel,Walls,Plaza,Temple,Shanty Town,Emblem,City Generator Link\n`; // headers
const valid = pack.burgs.filter(b => b.i && !b.removed); // all valid burgs
valid.forEach(b => {
@@ -506,6 +506,11 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) {
data += convertTemperature(temperature) + ",";
data += getTemperatureLikeness(temperature) + ",";
+ // add biome and province id
+ const biomeName = b.biome !== undefined ? window.Biomes.getDefault().name[b.biome] : "";
+ data += biomeName + ",";
+ data += b.province !== undefined ? b.province + "," : ",";
+
// add status data
data += b.capital ? "capital," : ",";
data += b.port ? "port," : ",";
diff --git a/modules/ui/markers-editor.js b/modules/ui/markers-editor.js
index 9f56db9a..fbcf6407 100644
--- a/modules/ui/markers-editor.js
+++ b/modules/ui/markers-editor.js
@@ -221,7 +221,14 @@ function editMarker(markerI) {
function editMarkerLegend() {
const id = element.id;
- editNotes(id, id);
+
+ // Use Obsidian integration if available, otherwise fall back to old notes system
+ if (typeof editObsidianNote !== "undefined" && marker) {
+ const coordinates = {x: marker.x, y: marker.y};
+ editObsidianNote(id, "marker", coordinates);
+ } else {
+ editNotes(id, id);
+ }
}
function toggleMarkerLock() {
diff --git a/modules/ui/markers-overview.js b/modules/ui/markers-overview.js
index 02999eb0..b673a9b7 100644
--- a/modules/ui/markers-overview.js
+++ b/modules/ui/markers-overview.js
@@ -68,7 +68,9 @@ function overviewMarkers() {
function addLines() {
const lines = pack.markers
- .map(({i, type, icon, pinned, lock}) => {
+ .map(({i, type, icon, pinned, lock, biome, province}) => {
+ const biomeName = biome !== undefined ? window.Biomes.getDefault().name[biome] : "";
+ const provinceName = province !== undefined && pack.provinces[province] ? pack.provinces[province].name : "";
return /* html */ `
${
@@ -77,6 +79,8 @@ function overviewMarkers() {
: `
${icon}`
}
${type}
+
${biomeName}
+
${provinceName}