mirror of
https://github.com/Azgaar/Fantasy-Map-Generator.git
synced 2025-12-19 02:21:24 +01:00
Merge branch 'master' into claude/sync-fork-verify-feature-011CUoWfkNGyyNtLigR5GVwf
This commit is contained in:
commit
05c53d276a
21 changed files with 10187 additions and 6 deletions
770
modules/external-api.js
Normal file
770
modules/external-api.js
Normal file
|
|
@ -0,0 +1,770 @@
|
|||
/**
|
||||
* External API Bridge for Fantasy Map Generator
|
||||
* Provides a clean interface for external tools (wikis, web UIs) to interact with FMG
|
||||
* @module external-api
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// ============================================================================
|
||||
// EVENT EMITTER SYSTEM
|
||||
// ============================================================================
|
||||
|
||||
class EventEmitter {
|
||||
constructor() {
|
||||
this.events = {};
|
||||
}
|
||||
|
||||
on(event, callback) {
|
||||
if (!this.events[event]) {
|
||||
this.events[event] = [];
|
||||
}
|
||||
this.events[event].push(callback);
|
||||
return () => this.off(event, callback);
|
||||
}
|
||||
|
||||
off(event, callback) {
|
||||
if (!this.events[event]) return;
|
||||
this.events[event] = this.events[event].filter(cb => cb !== callback);
|
||||
}
|
||||
|
||||
emit(event, data) {
|
||||
if (!this.events[event]) return;
|
||||
this.events[event].forEach(callback => {
|
||||
try {
|
||||
callback(data);
|
||||
} catch (error) {
|
||||
console.error(`Error in event handler for ${event}:`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
once(event, callback) {
|
||||
const wrapper = (data) => {
|
||||
callback(data);
|
||||
this.off(event, wrapper);
|
||||
};
|
||||
this.on(event, wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
const eventEmitter = new EventEmitter();
|
||||
|
||||
// ============================================================================
|
||||
// STATE MANAGEMENT & CHANGE DETECTION
|
||||
// ============================================================================
|
||||
|
||||
let isInitialized = false;
|
||||
let lastState = null;
|
||||
let changeDetectionEnabled = true;
|
||||
|
||||
function initializeChangeDetection() {
|
||||
if (isInitialized) return;
|
||||
|
||||
// Observe SVG changes for map updates
|
||||
const mapElement = document.getElementById('map');
|
||||
if (mapElement) {
|
||||
const observer = new MutationObserver(debounce(() => {
|
||||
if (changeDetectionEnabled) {
|
||||
eventEmitter.emit('map:changed', getMapState());
|
||||
}
|
||||
}, 500));
|
||||
|
||||
observer.observe(mapElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['d', 'transform', 'points']
|
||||
});
|
||||
}
|
||||
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
function debounce(func, wait) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CORE API METHODS
|
||||
// ============================================================================
|
||||
|
||||
const API = {
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// MAP LIFECYCLE
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a new map with optional parameters
|
||||
* @param {Object} options - Generation options
|
||||
* @returns {Promise<Object>} Map state
|
||||
*/
|
||||
async createMap(options = {}) {
|
||||
try {
|
||||
changeDetectionEnabled = false;
|
||||
|
||||
if (options.seed) {
|
||||
seed = options.seed;
|
||||
}
|
||||
|
||||
// Use existing generate function
|
||||
await generate(options);
|
||||
|
||||
changeDetectionEnabled = true;
|
||||
eventEmitter.emit('map:created', getMapState());
|
||||
|
||||
return {
|
||||
success: true,
|
||||
state: getMapState()
|
||||
};
|
||||
} catch (error) {
|
||||
changeDetectionEnabled = true;
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Load map from data
|
||||
* @param {String|Blob|File} mapData - Map data to load
|
||||
* @returns {Promise<Object>} Load result
|
||||
*/
|
||||
async loadMap(mapData) {
|
||||
try {
|
||||
changeDetectionEnabled = false;
|
||||
|
||||
let file;
|
||||
if (typeof mapData === 'string') {
|
||||
// Convert string data to Blob
|
||||
file = new Blob([mapData], {type: 'text/plain'});
|
||||
} else {
|
||||
file = mapData;
|
||||
}
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
uploadMap(file, (error) => {
|
||||
if (error) reject(error);
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
|
||||
changeDetectionEnabled = true;
|
||||
eventEmitter.emit('map:loaded', getMapState());
|
||||
|
||||
return {
|
||||
success: true,
|
||||
state: getMapState()
|
||||
};
|
||||
} catch (error) {
|
||||
changeDetectionEnabled = true;
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Save current map
|
||||
* @param {String} format - 'data' or 'blob'
|
||||
* @returns {Promise<Object>} Saved map data
|
||||
*/
|
||||
async saveMap(format = 'data') {
|
||||
try {
|
||||
const mapData = await prepareMapData();
|
||||
|
||||
if (format === 'blob') {
|
||||
return {
|
||||
success: true,
|
||||
data: new Blob([mapData], {type: 'text/plain'}),
|
||||
filename: getFileName() + '.map'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: mapData,
|
||||
filename: getFileName() + '.map'
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// DATA ACCESS
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Get complete map state
|
||||
* @returns {Object} Current map state
|
||||
*/
|
||||
getMapState() {
|
||||
return getMapState();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get specific data structure
|
||||
* @param {String} key - Data key (rivers, cultures, states, burgs, etc.)
|
||||
* @returns {*} Requested data
|
||||
*/
|
||||
getData(key) {
|
||||
if (!pack || !pack[key]) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(JSON.stringify(pack[key]));
|
||||
},
|
||||
|
||||
/**
|
||||
* Get rivers data
|
||||
* @returns {Array} Rivers array
|
||||
*/
|
||||
getRivers() {
|
||||
return pack.rivers ? JSON.parse(JSON.stringify(pack.rivers)) : [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get cultures data
|
||||
* @returns {Array} Cultures array
|
||||
*/
|
||||
getCultures() {
|
||||
return pack.cultures ? JSON.parse(JSON.stringify(pack.cultures)) : [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get states data
|
||||
* @returns {Array} States array
|
||||
*/
|
||||
getStates() {
|
||||
return pack.states ? JSON.parse(JSON.stringify(pack.states)) : [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get burgs (cities/towns) data
|
||||
* @returns {Array} Burgs array
|
||||
*/
|
||||
getBurgs() {
|
||||
return pack.burgs ? JSON.parse(JSON.stringify(pack.burgs)) : [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get religions data
|
||||
* @returns {Array} Religions array
|
||||
*/
|
||||
getReligions() {
|
||||
return pack.religions ? JSON.parse(JSON.stringify(pack.religions)) : [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get markers data
|
||||
* @returns {Array} Markers array
|
||||
*/
|
||||
getMarkers() {
|
||||
return pack.markers ? JSON.parse(JSON.stringify(pack.markers)) : [];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get grid data
|
||||
* @returns {Object} Grid object with cells data
|
||||
*/
|
||||
getGrid() {
|
||||
if (!grid) return null;
|
||||
|
||||
return {
|
||||
spacing: grid.spacing,
|
||||
cellsX: grid.cellsX,
|
||||
cellsY: grid.cellsY,
|
||||
features: grid.features,
|
||||
boundary: grid.boundary
|
||||
};
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// MUTATIONS
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Update rivers data
|
||||
* @param {Array} rivers - New rivers array
|
||||
* @returns {Object} Update result
|
||||
*/
|
||||
updateRivers(rivers) {
|
||||
try {
|
||||
changeDetectionEnabled = false;
|
||||
|
||||
pack.rivers = rivers;
|
||||
|
||||
// Redraw rivers
|
||||
if (window.Rivers && Rivers.specify) {
|
||||
Rivers.specify();
|
||||
}
|
||||
if (typeof drawRivers === 'function') {
|
||||
drawRivers();
|
||||
}
|
||||
|
||||
changeDetectionEnabled = true;
|
||||
eventEmitter.emit('rivers:updated', pack.rivers);
|
||||
|
||||
return {success: true};
|
||||
} catch (error) {
|
||||
changeDetectionEnabled = true;
|
||||
return {success: false, error: error.message};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update cultures data
|
||||
* @param {Array} cultures - New cultures array
|
||||
* @returns {Object} Update result
|
||||
*/
|
||||
updateCultures(cultures) {
|
||||
try {
|
||||
changeDetectionEnabled = false;
|
||||
|
||||
pack.cultures = cultures;
|
||||
|
||||
// Redraw cultures
|
||||
if (typeof drawCultures === 'function') {
|
||||
drawCultures();
|
||||
}
|
||||
|
||||
changeDetectionEnabled = true;
|
||||
eventEmitter.emit('cultures:updated', pack.cultures);
|
||||
|
||||
return {success: true};
|
||||
} catch (error) {
|
||||
changeDetectionEnabled = true;
|
||||
return {success: false, error: error.message};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update states data
|
||||
* @param {Array} states - New states array
|
||||
* @returns {Object} Update result
|
||||
*/
|
||||
updateStates(states) {
|
||||
try {
|
||||
changeDetectionEnabled = false;
|
||||
|
||||
pack.states = states;
|
||||
|
||||
// Redraw states
|
||||
if (typeof drawStates === 'function') {
|
||||
drawStates();
|
||||
}
|
||||
if (typeof drawBorders === 'function') {
|
||||
drawBorders();
|
||||
}
|
||||
|
||||
changeDetectionEnabled = true;
|
||||
eventEmitter.emit('states:updated', pack.states);
|
||||
|
||||
return {success: true};
|
||||
} catch (error) {
|
||||
changeDetectionEnabled = true;
|
||||
return {success: false, error: error.message};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update burgs (cities/towns) data
|
||||
* @param {Array} burgs - New burgs array
|
||||
* @returns {Object} Update result
|
||||
*/
|
||||
updateBurgs(burgs) {
|
||||
try {
|
||||
changeDetectionEnabled = false;
|
||||
|
||||
pack.burgs = burgs;
|
||||
|
||||
// Redraw burgs
|
||||
if (typeof drawBurgs === 'function') {
|
||||
drawBurgs();
|
||||
}
|
||||
|
||||
changeDetectionEnabled = true;
|
||||
eventEmitter.emit('burgs:updated', pack.burgs);
|
||||
|
||||
return {success: true};
|
||||
} catch (error) {
|
||||
changeDetectionEnabled = true;
|
||||
return {success: false, error: error.message};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a new burg (city/town)
|
||||
* @param {Object} burgData - Burg properties
|
||||
* @returns {Object} Result with new burg ID
|
||||
*/
|
||||
addBurg(burgData) {
|
||||
try {
|
||||
const newId = pack.burgs.length;
|
||||
const burg = {
|
||||
i: newId,
|
||||
cell: burgData.cell || 0,
|
||||
x: burgData.x || 0,
|
||||
y: burgData.y || 0,
|
||||
name: burgData.name || Names.getCulture(burgData.culture || 0),
|
||||
population: burgData.population || 1,
|
||||
type: burgData.type || 'town',
|
||||
...burgData
|
||||
};
|
||||
|
||||
pack.burgs.push(burg);
|
||||
|
||||
if (typeof drawBurgs === 'function') {
|
||||
drawBurgs();
|
||||
}
|
||||
|
||||
eventEmitter.emit('burg:added', burg);
|
||||
|
||||
return {success: true, id: newId, burg};
|
||||
} catch (error) {
|
||||
return {success: false, error: error.message};
|
||||
}
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// EXPORT
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Export map as SVG
|
||||
* @returns {String} SVG string
|
||||
*/
|
||||
exportSVG() {
|
||||
const svgElement = document.getElementById('map');
|
||||
if (!svgElement) return null;
|
||||
return svgElement.outerHTML;
|
||||
},
|
||||
|
||||
/**
|
||||
* Export map as PNG
|
||||
* @param {Number} width - Image width
|
||||
* @param {Number} height - Image height
|
||||
* @returns {Promise<Blob>} PNG blob
|
||||
*/
|
||||
async exportPNG(width = 2048, height = 2048) {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
const svgElement = document.getElementById('map');
|
||||
const svgString = new XMLSerializer().serializeToString(svgElement);
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const img = new Image();
|
||||
const blob = new Blob([svgString], {type: 'image/svg+xml;charset=utf-8'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
img.onload = function() {
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
URL.revokeObjectURL(url);
|
||||
canvas.toBlob((blob) => {
|
||||
resolve(blob);
|
||||
}, 'image/png');
|
||||
};
|
||||
|
||||
img.onerror = reject;
|
||||
img.src = url;
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Export specific data as JSON
|
||||
* @param {String} key - Data key
|
||||
* @returns {String} JSON string
|
||||
*/
|
||||
exportJSON(key) {
|
||||
const data = key ? API.getData(key) : getMapState();
|
||||
return JSON.stringify(data, null, 2);
|
||||
},
|
||||
|
||||
// ------------------------------------------------------------------------
|
||||
// EVENTS
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Subscribe to events
|
||||
* @param {String} event - Event name
|
||||
* @param {Function} callback - Callback function
|
||||
* @returns {Function} Unsubscribe function
|
||||
*/
|
||||
on(event, callback) {
|
||||
return eventEmitter.on(event, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Unsubscribe from events
|
||||
* @param {String} event - Event name
|
||||
* @param {Function} callback - Callback function
|
||||
*/
|
||||
off(event, callback) {
|
||||
eventEmitter.off(event, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Subscribe to event once
|
||||
* @param {String} event - Event name
|
||||
* @param {Function} callback - Callback function
|
||||
*/
|
||||
once(event, callback) {
|
||||
eventEmitter.once(event, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Emit custom event
|
||||
* @param {String} event - Event name
|
||||
* @param {*} data - Event data
|
||||
*/
|
||||
emit(event, data) {
|
||||
eventEmitter.emit(event, data);
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================================================
|
||||
|
||||
function getMapState() {
|
||||
return {
|
||||
seed: typeof seed !== 'undefined' ? seed : null,
|
||||
mapId: typeof mapId !== 'undefined' ? mapId : null,
|
||||
timestamp: Date.now(),
|
||||
pack: pack ? {
|
||||
cultures: pack.cultures || [],
|
||||
states: pack.states || [],
|
||||
burgs: pack.burgs || [],
|
||||
rivers: pack.rivers || [],
|
||||
religions: pack.religions || [],
|
||||
provinces: pack.provinces || [],
|
||||
markers: pack.markers || []
|
||||
} : null,
|
||||
grid: grid ? {
|
||||
spacing: grid.spacing,
|
||||
cellsX: grid.cellsX,
|
||||
cellsY: grid.cellsY,
|
||||
features: grid.features
|
||||
} : null,
|
||||
options: typeof options !== 'undefined' ? options : null
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// POSTMESSAGE BRIDGE
|
||||
// ============================================================================
|
||||
|
||||
const PostMessageBridge = {
|
||||
enabled: false,
|
||||
targetOrigin: '*',
|
||||
|
||||
/**
|
||||
* Enable PostMessage communication
|
||||
* @param {String} origin - Target origin for postMessage (default: '*')
|
||||
*/
|
||||
enable(origin = '*') {
|
||||
if (this.enabled) return;
|
||||
|
||||
this.targetOrigin = origin;
|
||||
this.enabled = true;
|
||||
|
||||
// Listen for messages from parent window
|
||||
window.addEventListener('message', this.handleMessage.bind(this));
|
||||
|
||||
// Forward all events to parent
|
||||
this.setupEventForwarding();
|
||||
|
||||
console.log('[FMG API] PostMessage bridge enabled');
|
||||
},
|
||||
|
||||
/**
|
||||
* Disable PostMessage communication
|
||||
*/
|
||||
disable() {
|
||||
this.enabled = false;
|
||||
window.removeEventListener('message', this.handleMessage.bind(this));
|
||||
console.log('[FMG API] PostMessage bridge disabled');
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle incoming messages
|
||||
*/
|
||||
async handleMessage(event) {
|
||||
if (!this.enabled) return;
|
||||
|
||||
const {type, payload, requestId} = event.data;
|
||||
if (!type) return;
|
||||
|
||||
console.log('[FMG API] Received message:', type, payload);
|
||||
|
||||
try {
|
||||
let result;
|
||||
|
||||
switch(type) {
|
||||
// Map lifecycle
|
||||
case 'CREATE_MAP':
|
||||
result = await API.createMap(payload);
|
||||
break;
|
||||
case 'LOAD_MAP':
|
||||
result = await API.loadMap(payload);
|
||||
break;
|
||||
case 'SAVE_MAP':
|
||||
result = await API.saveMap(payload?.format);
|
||||
break;
|
||||
|
||||
// Data access
|
||||
case 'GET_STATE':
|
||||
result = {success: true, data: API.getMapState()};
|
||||
break;
|
||||
case 'GET_DATA':
|
||||
result = {success: true, data: API.getData(payload?.key)};
|
||||
break;
|
||||
case 'GET_RIVERS':
|
||||
result = {success: true, data: API.getRivers()};
|
||||
break;
|
||||
case 'GET_CULTURES':
|
||||
result = {success: true, data: API.getCultures()};
|
||||
break;
|
||||
case 'GET_STATES':
|
||||
result = {success: true, data: API.getStates()};
|
||||
break;
|
||||
case 'GET_BURGS':
|
||||
result = {success: true, data: API.getBurgs()};
|
||||
break;
|
||||
|
||||
// Mutations
|
||||
case 'UPDATE_RIVERS':
|
||||
result = API.updateRivers(payload);
|
||||
break;
|
||||
case 'UPDATE_CULTURES':
|
||||
result = API.updateCultures(payload);
|
||||
break;
|
||||
case 'UPDATE_STATES':
|
||||
result = API.updateStates(payload);
|
||||
break;
|
||||
case 'UPDATE_BURGS':
|
||||
result = API.updateBurgs(payload);
|
||||
break;
|
||||
case 'ADD_BURG':
|
||||
result = API.addBurg(payload);
|
||||
break;
|
||||
|
||||
// Export
|
||||
case 'EXPORT_SVG':
|
||||
result = {success: true, data: API.exportSVG()};
|
||||
break;
|
||||
case 'EXPORT_PNG':
|
||||
const blob = await API.exportPNG(payload?.width, payload?.height);
|
||||
const reader = new FileReader();
|
||||
result = await new Promise((resolve) => {
|
||||
reader.onload = () => resolve({success: true, data: reader.result});
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
break;
|
||||
case 'EXPORT_JSON':
|
||||
result = {success: true, data: API.exportJSON(payload?.key)};
|
||||
break;
|
||||
|
||||
default:
|
||||
result = {success: false, error: `Unknown message type: ${type}`};
|
||||
}
|
||||
|
||||
// Send response
|
||||
this.sendMessage('RESPONSE', result, requestId);
|
||||
} catch (error) {
|
||||
this.sendMessage('ERROR', {
|
||||
success: false,
|
||||
error: error.message
|
||||
}, requestId);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Send message to parent window
|
||||
*/
|
||||
sendMessage(type, payload, requestId = null) {
|
||||
if (!this.enabled) return;
|
||||
if (window.parent === window) return; // Not in iframe
|
||||
|
||||
window.parent.postMessage({
|
||||
type,
|
||||
payload,
|
||||
requestId,
|
||||
timestamp: Date.now()
|
||||
}, this.targetOrigin);
|
||||
},
|
||||
|
||||
/**
|
||||
* Forward API events to parent window
|
||||
*/
|
||||
setupEventForwarding() {
|
||||
const events = [
|
||||
'map:created',
|
||||
'map:loaded',
|
||||
'map:changed',
|
||||
'rivers:updated',
|
||||
'cultures:updated',
|
||||
'states:updated',
|
||||
'burgs:updated',
|
||||
'burg:added'
|
||||
];
|
||||
|
||||
events.forEach(event => {
|
||||
API.on(event, (data) => {
|
||||
this.sendMessage('EVENT', {event, data});
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// INITIALIZATION
|
||||
// ============================================================================
|
||||
|
||||
// Initialize on DOMContentLoaded
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeChangeDetection();
|
||||
});
|
||||
} else {
|
||||
initializeChangeDetection();
|
||||
}
|
||||
|
||||
// Auto-enable PostMessage if in iframe
|
||||
if (window.self !== window.top) {
|
||||
setTimeout(() => PostMessageBridge.enable(), 1000);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXPORT API
|
||||
// ============================================================================
|
||||
|
||||
window.FMG_API = API;
|
||||
window.FMG_PostMessageBridge = PostMessageBridge;
|
||||
|
||||
console.log('[FMG API] External API initialized. Access via window.FMG_API');
|
||||
console.log('[FMG API] Available methods:', Object.keys(API));
|
||||
|
||||
})();
|
||||
|
|
@ -1129,3 +1129,363 @@ async function parseLoadedDataOnlyRivers(data) {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function createSharableRiverDropboxLink() {
|
||||
const mapFile = document.querySelector("#loadRiverFromDropbox select").value;
|
||||
const sharableLink = byId("sharableLink");
|
||||
const sharableLinkContainer = byId("sharableLinkContainer");
|
||||
|
||||
try {
|
||||
const previewLink = await Cloud.providers.dropbox.getLink(mapFile);
|
||||
const directLink = previewLink.replace("www.dropbox.com", "dl.dropboxusercontent.com"); // DL allows CORS
|
||||
const finalLink = `${location.origin}${location.pathname}?maplink=${directLink}`;
|
||||
|
||||
sharableLink.innerText = finalLink.slice(0, 45) + "...";
|
||||
sharableLink.setAttribute("href", finalLink);
|
||||
sharableLinkContainer.style.display = "block";
|
||||
} catch (error) {
|
||||
ERROR && console.error(error);
|
||||
return tip("Dropbox API error. Can not create link.", true, "error", 2000);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRiverFromDropbox() {
|
||||
const mapPath = byId("loadRiverFromDropboxSelect")?.value;
|
||||
|
||||
DEBUG && console.log("Loading map from Dropbox:", mapPath);
|
||||
const blob = await Cloud.providers.dropbox.load(mapPath);
|
||||
uploadRiversMap(blob);
|
||||
}
|
||||
|
||||
function uploadRiversMap(file, callback) {
|
||||
uploadRiversMap.timeStart = performance.now();
|
||||
const OLDEST_SUPPORTED_VERSION = 0.7;
|
||||
const currentVersion = parseFloat(version);
|
||||
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onloadend = async function (fileLoadedEvent) {
|
||||
if (callback) callback();
|
||||
byId("coas").innerHTML = ""; // remove auto-generated emblems
|
||||
const result = fileLoadedEvent.target.result;
|
||||
const [mapData, mapVersion] = await parseLoadedResult(result);
|
||||
|
||||
const isInvalid = !mapData || isNaN(mapVersion) || mapData.length < 26 || !mapData[5];
|
||||
const isUpdated = mapVersion === currentVersion;
|
||||
const isAncient = mapVersion < OLDEST_SUPPORTED_VERSION;
|
||||
const isNewer = mapVersion > currentVersion;
|
||||
const isOutdated = mapVersion < currentVersion;
|
||||
|
||||
if (isInvalid) return showUploadMessage("invalid", mapData, mapVersion);
|
||||
if (isUpdated) return parseLoadedDataOnlyRivers(mapData);
|
||||
if (isAncient) return showUploadMessage("ancient", mapData, mapVersion);
|
||||
if (isNewer) return showUploadMessage("newer", mapData, mapVersion);
|
||||
if (isOutdated) return showUploadMessage("outdated", mapData, mapVersion);
|
||||
};
|
||||
|
||||
fileReader.readAsArrayBuffer(file);
|
||||
}
|
||||
function showUploadRiverMessage(type, mapData, mapVersion) {
|
||||
const archive = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "archived version");
|
||||
let message, title, canBeLoaded;
|
||||
|
||||
if (type === "invalid") {
|
||||
message = `The file does not look like a valid save file.<br>Please check the data format`;
|
||||
title = "Invalid file";
|
||||
canBeLoaded = false;
|
||||
} else if (type === "ancient") {
|
||||
message = `The map version you are trying to load (${mapVersion}) is too old and cannot be updated to the current version.<br>Please keep using an ${archive}`;
|
||||
title = "Ancient file";
|
||||
canBeLoaded = false;
|
||||
} else if (type === "newer") {
|
||||
message = `The map version you are trying to load (${mapVersion}) is newer than the current version.<br>Please load the file in the appropriate version`;
|
||||
title = "Newer file";
|
||||
canBeLoaded = false;
|
||||
} else if (type === "outdated") {
|
||||
message = `The map version (${mapVersion}) does not match the Generator version (${version}).<br>That is fine, click OK to the get map <b style="color: #005000">auto-updated</b>.<br>In case of issues please keep using an ${archive} of the Generator`;
|
||||
title = "Outdated file";
|
||||
canBeLoaded = true;
|
||||
}
|
||||
|
||||
alertMessage.innerHTML = message;
|
||||
const buttons = {
|
||||
OK: function () {
|
||||
$(this).dialog("close");
|
||||
if (canBeLoaded) parseLoadedDataOnlyRiversData(mapData);
|
||||
}
|
||||
};
|
||||
$("#alert").dialog({title, buttons});
|
||||
}
|
||||
|
||||
async function parseLoadedDataOnlyRivers(data) {
|
||||
try {
|
||||
// exit customization
|
||||
if (window.closeDialogs) closeDialogs();
|
||||
customization = 0;
|
||||
if (customizationMenu.offsetParent) styleTab.click();
|
||||
|
||||
const params = data[0].split("|");
|
||||
|
||||
|
||||
INFO && console.group("Loaded Map " + seed);
|
||||
|
||||
|
||||
void (function parsePackData() {
|
||||
|
||||
pack.rivers = data[32] ? JSON.parse(data[32]) : [];
|
||||
|
||||
pack.cells.r = Uint16Array.from(data[22].split(","));
|
||||
|
||||
})();
|
||||
|
||||
void (function restoreLayersState() {
|
||||
const isVisible = selection => selection.node() && selection.style("display") !== "none";
|
||||
const isVisibleNode = node => node && node.style.display !== "none";
|
||||
const hasChildren = selection => selection.node()?.hasChildNodes();
|
||||
const hasChild = (selection, selector) => selection.node()?.querySelector(selector);
|
||||
const turnOn = el => byId(el).classList.remove("buttonoff");
|
||||
|
||||
toggleRivers();
|
||||
toggleRivers();
|
||||
// turn all layers off
|
||||
byId("mapLayers")
|
||||
.querySelectorAll("li")
|
||||
.forEach(el => el.classList.add("buttonoff"));
|
||||
|
||||
// turn on active layers
|
||||
if (hasChild(texture, "image")) turnOn("toggleTexture");
|
||||
if (hasChildren(terrs)) turnOn("toggleHeight");
|
||||
if (hasChildren(biomes)) turnOn("toggleBiomes");
|
||||
if (hasChildren(cells)) turnOn("toggleCells");
|
||||
if (hasChildren(gridOverlay)) turnOn("toggleGrid");
|
||||
if (hasChildren(coordinates)) turnOn("toggleCoordinates");
|
||||
if (isVisible(compass) && hasChild(compass, "use")) turnOn("toggleCompass");
|
||||
if (hasChildren(rivers)) turnOn("toggleRivers");
|
||||
if (isVisible(terrain) && hasChildren(terrain)) turnOn("toggleRelief");
|
||||
if (hasChildren(relig)) turnOn("toggleReligions");
|
||||
if (hasChildren(cults)) turnOn("toggleCultures");
|
||||
if (hasChildren(statesBody)) turnOn("toggleStates");
|
||||
if (hasChildren(provs)) turnOn("toggleProvinces");
|
||||
if (hasChildren(zones) && isVisible(zones)) turnOn("toggleZones");
|
||||
if (isVisible(borders) && hasChild(borders, "path")) turnOn("toggleBorders");
|
||||
if (isVisible(routes) && hasChild(routes, "path")) turnOn("toggleRoutes");
|
||||
if (hasChildren(temperature)) turnOn("toggleTemp");
|
||||
if (hasChild(population, "line")) turnOn("togglePopulation");
|
||||
if (hasChildren(ice)) turnOn("toggleIce");
|
||||
if (hasChild(prec, "circle")) turnOn("togglePrec");
|
||||
if (isVisible(emblems) && hasChild(emblems, "use")) turnOn("toggleEmblems");
|
||||
if (isVisible(labels)) turnOn("toggleLabels");
|
||||
if (isVisible(icons)) turnOn("toggleIcons");
|
||||
if (hasChildren(armies) && isVisible(armies)) turnOn("toggleMilitary");
|
||||
if (hasChildren(markers)) turnOn("toggleMarkers");
|
||||
if (isVisible(ruler)) turnOn("toggleRulers");
|
||||
if (isVisible(scaleBar)) turnOn("toggleScaleBar");
|
||||
if (isVisibleNode(byId("vignette"))) turnOn("toggleVignette");
|
||||
|
||||
getCurrentPreset();
|
||||
})();
|
||||
{
|
||||
// dynamically import and run auto-update script
|
||||
const versionNumber = parseFloat(params[0]);
|
||||
const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.95.00");
|
||||
resolveVersionConflicts(versionNumber);
|
||||
}
|
||||
|
||||
{
|
||||
// add custom heightmap color scheme if any
|
||||
const scheme = terrs.attr("scheme");
|
||||
if (!(scheme in heightmapColorSchemes)) {
|
||||
addCustomColorScheme(scheme);
|
||||
}
|
||||
}
|
||||
|
||||
fitMapToScreen();
|
||||
|
||||
void (function checkDataIntegrity() {
|
||||
const cells = pack.cells;
|
||||
|
||||
if (pack.cells.i.length !== pack.cells.state.length) {
|
||||
const message = "Data Integrity Check. Striping issue detected. To fix edit the heightmap in erase mode";
|
||||
ERROR && console.error(message);
|
||||
}
|
||||
|
||||
const invalidStates = [...new Set(cells.state)].filter(s => !pack.states[s] || pack.states[s].removed);
|
||||
invalidStates.forEach(s => {
|
||||
const invalidCells = cells.i.filter(i => cells.state[i] === s);
|
||||
invalidCells.forEach(i => (cells.state[i] = 0));
|
||||
ERROR && console.error("Data Integrity Check. Invalid state", s, "is assigned to cells", invalidCells);
|
||||
});
|
||||
|
||||
const invalidProvinces = [...new Set(cells.province)].filter(
|
||||
p => p && (!pack.provinces[p] || pack.provinces[p].removed)
|
||||
);
|
||||
invalidProvinces.forEach(p => {
|
||||
const invalidCells = cells.i.filter(i => cells.province[i] === p);
|
||||
invalidCells.forEach(i => (cells.province[i] = 0));
|
||||
ERROR && console.error("Data Integrity Check. Invalid province", p, "is assigned to cells", invalidCells);
|
||||
});
|
||||
|
||||
const invalidCultures = [...new Set(cells.culture)].filter(c => !pack.cultures[c] || pack.cultures[c].removed);
|
||||
invalidCultures.forEach(c => {
|
||||
const invalidCells = cells.i.filter(i => cells.culture[i] === c);
|
||||
invalidCells.forEach(i => (cells.province[i] = 0));
|
||||
ERROR && console.error("Data Integrity Check. Invalid culture", c, "is assigned to cells", invalidCells);
|
||||
});
|
||||
|
||||
const invalidReligions = [...new Set(cells.religion)].filter(
|
||||
r => !pack.religions[r] || pack.religions[r].removed
|
||||
);
|
||||
invalidReligions.forEach(r => {
|
||||
const invalidCells = cells.i.filter(i => cells.religion[i] === r);
|
||||
invalidCells.forEach(i => (cells.religion[i] = 0));
|
||||
ERROR && console.error("Data Integrity Check. Invalid religion", r, "is assigned to cells", invalidCells);
|
||||
});
|
||||
|
||||
const invalidFeatures = [...new Set(cells.f)].filter(f => f && !pack.features[f]);
|
||||
invalidFeatures.forEach(f => {
|
||||
const invalidCells = cells.i.filter(i => cells.f[i] === f);
|
||||
// No fix as for now
|
||||
ERROR && console.error("Data Integrity Check. Invalid feature", f, "is assigned to cells", invalidCells);
|
||||
});
|
||||
|
||||
const invalidBurgs = [...new Set(cells.burg)].filter(
|
||||
burgId => burgId && (!pack.burgs[burgId] || pack.burgs[burgId].removed)
|
||||
);
|
||||
invalidBurgs.forEach(burgId => {
|
||||
const invalidCells = cells.i.filter(i => cells.burg[i] === burgId);
|
||||
invalidCells.forEach(i => (cells.burg[i] = 0));
|
||||
ERROR && console.error("Data Integrity Check. Invalid burg", burgId, "is assigned to cells", invalidCells);
|
||||
});
|
||||
|
||||
const invalidRivers = [...new Set(cells.r)].filter(r => r && !pack.rivers.find(river => river.i === r));
|
||||
invalidRivers.forEach(r => {
|
||||
const invalidCells = cells.i.filter(i => cells.r[i] === r);
|
||||
invalidCells.forEach(i => (cells.r[i] = 0));
|
||||
rivers.select("river" + r).remove();
|
||||
ERROR && console.error("Data Integrity Check. Invalid river", r, "is assigned to cells", invalidCells);
|
||||
});
|
||||
|
||||
pack.burgs.forEach(burg => {
|
||||
if ((!burg.i || burg.removed) && burg.lock) {
|
||||
ERROR &&
|
||||
console.error(
|
||||
`Data Integrity Check. Burg ${burg.i || "0"} is removed or invalid but still locked. Unlocking the burg`
|
||||
);
|
||||
delete burg.lock;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!burg.i || burg.removed) return;
|
||||
if (burg.cell === undefined || burg.x === undefined || burg.y === undefined) {
|
||||
ERROR &&
|
||||
console.error(
|
||||
`Data Integrity Check. Burg ${burg.i} is missing cell info or coordinates. Removing the burg`
|
||||
);
|
||||
burg.removed = true;
|
||||
}
|
||||
|
||||
if (burg.port < 0) {
|
||||
ERROR && console.error("Data Integrity Check. Burg", burg.i, "has invalid port value", burg.port);
|
||||
burg.port = 0;
|
||||
}
|
||||
|
||||
if (burg.cell >= cells.i.length) {
|
||||
ERROR && console.error("Data Integrity Check. Burg", burg.i, "is linked to invalid cell", burg.cell);
|
||||
burg.cell = findCell(burg.x, burg.y);
|
||||
cells.i.filter(i => cells.burg[i] === burg.i).forEach(i => (cells.burg[i] = 0));
|
||||
cells.burg[burg.cell] = burg.i;
|
||||
}
|
||||
|
||||
if (burg.state && !pack.states[burg.state]) {
|
||||
ERROR && console.error("Data Integrity Check. Burg", burg.i, "is linked to invalid state", burg.state);
|
||||
burg.state = 0;
|
||||
}
|
||||
|
||||
if (burg.state && pack.states[burg.state].removed) {
|
||||
ERROR && console.error("Data Integrity Check. Burg", burg.i, "is linked to removed state", burg.state);
|
||||
burg.state = 0;
|
||||
}
|
||||
|
||||
if (burg.state === undefined) {
|
||||
ERROR && console.error("Data Integrity Check. Burg", burg.i, "has no state data");
|
||||
burg.state = 0;
|
||||
}
|
||||
});
|
||||
|
||||
pack.provinces.forEach(p => {
|
||||
if (!p.i || p.removed) return;
|
||||
if (pack.states[p.state] && !pack.states[p.state].removed) return;
|
||||
ERROR && console.error("Data Integrity Check. Province", p.i, "is linked to removed state", p.state);
|
||||
p.removed = true; // remove incorrect province
|
||||
});
|
||||
|
||||
{
|
||||
const markerIds = [];
|
||||
let nextId = last(pack.markers)?.i + 1 || 0;
|
||||
|
||||
pack.markers.forEach(marker => {
|
||||
if (markerIds[marker.i]) {
|
||||
ERROR && console.error("Data Integrity Check. Marker", marker.i, "has non-unique id. Changing to", nextId);
|
||||
|
||||
const domElements = document.querySelectorAll("#marker" + marker.i);
|
||||
if (domElements[1]) domElements[1].id = "marker" + nextId; // rename 2nd dom element
|
||||
|
||||
const noteElements = notes.filter(note => note.id === "marker" + marker.i);
|
||||
if (noteElements[1]) noteElements[1].id = "marker" + nextId; // rename 2nd note
|
||||
|
||||
marker.i = nextId;
|
||||
nextId += 1;
|
||||
} else {
|
||||
markerIds[marker.i] = true;
|
||||
}
|
||||
});
|
||||
|
||||
// sort markers by index
|
||||
pack.markers.sort((a, b) => a.i - b.i);
|
||||
}
|
||||
})();
|
||||
|
||||
fitMapToScreen();
|
||||
|
||||
// remove href from emblems, to trigger rendering on load
|
||||
emblems.selectAll("use").attr("href", null);
|
||||
|
||||
// draw data layers (no kept in svg)
|
||||
if (rulers && layerIsOn("toggleRulers")) rulers.draw();
|
||||
if (layerIsOn("toggleGrid")) drawGrid();
|
||||
|
||||
if (window.restoreDefaultEvents) restoreDefaultEvents();
|
||||
focusOn(); // based on searchParams focus on point, cell or burg
|
||||
invokeActiveZooming();
|
||||
|
||||
WARN && console.warn(`TOTAL: ${rn((performance.now() - uploadRiversMap.timeStart) / 1000, 2)}s`);
|
||||
showStatistics();
|
||||
INFO && console.groupEnd("Loaded Map " + seed);
|
||||
tip("Map is successfully loaded", true, "success", 7000);
|
||||
} catch (error) {
|
||||
ERROR && console.error(error);
|
||||
clearMainTip();
|
||||
|
||||
alertMessage.innerHTML = /* html */ `An error is occured on map loading. Select a different file to load, <br />generate a new random map or cancel the loading
|
||||
<p id="errorBox">${parseError(error)}</p>`;
|
||||
|
||||
$("#alert").dialog({
|
||||
resizable: false,
|
||||
title: "Loading error",
|
||||
maxWidth: "50em",
|
||||
buttons: {
|
||||
"Select file": function () {
|
||||
$(this).dialog("close");
|
||||
mapToLoad.click();
|
||||
},
|
||||
"New map": function () {
|
||||
$(this).dialog("close");
|
||||
regenerateMap("loading error");
|
||||
},
|
||||
Cancel: function () {
|
||||
$(this).dialog("close");
|
||||
}
|
||||
},
|
||||
position: {my: "center", at: "center", of: "svg"}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -797,12 +797,24 @@ function drawRivers() {
|
|||
|
||||
const riverPaths = pack.rivers.map(({cells, points, i, widthFactor, sourceWidth}) => {
|
||||
if (!cells || cells.length < 2) return;
|
||||
const {addMeandering, getRiverPath} = Rivers;
|
||||
lineGen.curve(d3.curveCatmullRom.alpha(0.1));
|
||||
|
||||
// PERFORMANCE OPTIMIZATION: Filter invalid rivers before processing
|
||||
const validRivers = pack.rivers.filter(r => r.cells && r.cells.length >= 2);
|
||||
|
||||
// PERFORMANCE OPTIMIZATION: Pre-allocate array with exact size
|
||||
const riverPaths = new Array(validRivers.length);
|
||||
|
||||
for (let idx = 0; idx < validRivers.length; idx++) {
|
||||
const {cells, points, i, widthFactor, sourceWidth} = validRivers[idx];
|
||||
let riverPoints = points;
|
||||
|
||||
if (points && points.length !== cells.length) {
|
||||
console.error(
|
||||
`River ${i} has ${cells.length} cells, but only ${points.length} points defined. Resetting points data`
|
||||
);
|
||||
points = undefined;
|
||||
riverPoints = undefined;
|
||||
}
|
||||
|
||||
const meanderedPoints = Rivers.addMeandering(cells, points);
|
||||
|
|
@ -810,6 +822,13 @@ function drawRivers() {
|
|||
return `<path id="river${i}" d="${path}"/>`;
|
||||
});
|
||||
rivers.html(riverPaths.join(""));
|
||||
const meanderedPoints = addMeandering(cells, riverPoints);
|
||||
const path = getRiverPath(meanderedPoints, widthFactor, sourceWidth);
|
||||
riverPaths[idx] = `<path id="river${i}" d="${path}"/>`;
|
||||
}
|
||||
|
||||
// PERFORMANCE: Use single innerHTML write
|
||||
rivers.node().innerHTML = riverPaths.join("");
|
||||
|
||||
TIME && console.timeEnd("drawRivers");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue