mirror of
https://github.com/GoodStartLabs/AI_Diplomacy.git
synced 2026-04-19 12:58:09 +00:00
405 lines
No EOL
12 KiB
JavaScript
405 lines
No EOL
12 KiB
JavaScript
// ==============================================================================
|
|
// Copyright (C) 2023
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify it under
|
|
// the terms of the GNU Affero General Public License as published by the Free
|
|
// Software Foundation, either version 3 of the License, or (at your option) any
|
|
// later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful, but WITHOUT
|
|
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
// details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License along
|
|
// with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
// ==============================================================================
|
|
|
|
/**
|
|
* Map Conversion Utility
|
|
*
|
|
* This script extracts province coordinates, supply centers and other data
|
|
* from existing SVG map files and prepares them for use with the Three.js
|
|
* animation system.
|
|
*
|
|
* Usage:
|
|
* node convert_svg_maps.js <map_name> [output_dir]
|
|
*
|
|
* Where:
|
|
* <map_name> is one of: standard, ancmed, modern, pure
|
|
* [output_dir] is an optional output directory (defaults to ../assets/maps/)
|
|
*
|
|
* This will:
|
|
* 1. Load the SVG map file from the maps directory
|
|
* 2. Extract province data (positions, borders, etc.)
|
|
* 3. Generate a texture map (high-res JPG)
|
|
* 4. Create a coordinate file (JSON)
|
|
* 5. Save both to the output directory
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { createCanvas, loadImage } = require('canvas');
|
|
const { DOMParser } = require('xmldom');
|
|
const { optimize } = require('svgo');
|
|
|
|
// Map sources - adjust these paths for your codebase
|
|
const MAP_SOURCES = {
|
|
standard: '../../maps/svg/standard.svg',
|
|
ancmed: '../../maps/svg/ancmed.svg',
|
|
modern: '../../maps/svg/modern.svg',
|
|
pure: '../../maps/svg/pure.svg'
|
|
};
|
|
|
|
// Supply centers for standard map (others will be determined from SVG)
|
|
const SUPPLY_CENTERS = {
|
|
standard: [
|
|
'EDI', 'LVP', 'LON', 'BRE', 'PAR', 'MAR', 'SPA', 'POR', 'BEL', 'HOL', 'DEN',
|
|
'NWY', 'SWE', 'KIE', 'BER', 'MUN', 'VEN', 'ROM', 'NAP', 'TUN', 'VIE', 'BUD',
|
|
'TRI', 'SER', 'RUM', 'BUL', 'GRE', 'ANK', 'SMY', 'CON', 'SEV', 'WAR', 'MOS', 'STP'
|
|
],
|
|
ancmed: [], // Will be extracted from SVG
|
|
modern: [], // Will be extracted from SVG
|
|
pure: [] // Will be extracted from SVG
|
|
};
|
|
|
|
// Map dimensions in 3D space
|
|
const MAP_DIMENSIONS = {
|
|
standard: { width: 1000, height: 1000 },
|
|
ancmed: { width: 1000, height: 1000 },
|
|
modern: { width: 1000, height: 1000 },
|
|
pure: { width: 1000, height: 1000 }
|
|
};
|
|
|
|
/**
|
|
* Main function
|
|
*/
|
|
async function main() {
|
|
// Parse command line arguments
|
|
const args = process.argv.slice(2);
|
|
const mapName = args[0]?.toLowerCase();
|
|
const outputDir = args[1] || path.join(__dirname, '../assets/maps');
|
|
|
|
// Validate map name
|
|
if (!mapName || !MAP_SOURCES[mapName]) {
|
|
console.error('Error: Invalid map name');
|
|
console.error('Usage: node convert_svg_maps.js <map_name> [output_dir]');
|
|
console.error(' where <map_name> is one of: standard, ancmed, modern, pure');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Create output directory if it doesn't exist
|
|
if (!fs.existsSync(outputDir)) {
|
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
}
|
|
|
|
console.log(`Converting map: ${mapName}`);
|
|
|
|
try {
|
|
// Extract SVG data
|
|
const svgData = await extractSvgFromSource(mapName);
|
|
|
|
// Generate texture map
|
|
await generateTextureMap(svgData, mapName, outputDir);
|
|
|
|
// Generate coordinate file
|
|
await generateCoordinateFile(svgData, mapName, outputDir);
|
|
|
|
console.log('Conversion complete!');
|
|
} catch (error) {
|
|
console.error('Error during conversion:', error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extract SVG data from source file
|
|
* @param {string} mapName - The map name
|
|
* @returns {Object} - SVG DOM and metadata
|
|
*/
|
|
async function extractSvgFromSource(mapName) {
|
|
const sourcePath = path.join(__dirname, MAP_SOURCES[mapName]);
|
|
|
|
console.log(`Reading SVG from: ${sourcePath}`);
|
|
|
|
// Read SVG file
|
|
const svgContent = fs.readFileSync(sourcePath, 'utf8');
|
|
|
|
// Parse SVG
|
|
const parser = new DOMParser();
|
|
const svgDoc = parser.parseFromString(svgContent, 'image/svg+xml');
|
|
|
|
// Get SVG dimensions
|
|
const svgElement = svgDoc.documentElement;
|
|
const viewBox = svgElement.getAttribute('viewBox')?.split(' ').map(Number) || [0, 0, 800, 600];
|
|
const width = parseInt(svgElement.getAttribute('width') || viewBox[2], 10);
|
|
const height = parseInt(svgElement.getAttribute('height') || viewBox[3], 10);
|
|
|
|
return {
|
|
doc: svgDoc,
|
|
content: svgContent,
|
|
width,
|
|
height,
|
|
viewBox
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Generate texture map from SVG
|
|
* @param {Object} svgData - SVG data
|
|
* @param {string} mapName - Map name
|
|
* @param {string} outputDir - Output directory
|
|
*/
|
|
async function generateTextureMap(svgData, mapName, outputDir) {
|
|
console.log('Generating texture map...');
|
|
|
|
// Optimize SVG for rendering
|
|
const optimizedSvg = optimize(svgData.content, {
|
|
multipass: true,
|
|
plugins: [
|
|
'removeViewBox',
|
|
'removeDimensions',
|
|
'removeUnknownsAndDefaults',
|
|
'removeUselessStrokeAndFill',
|
|
'mergeStyles',
|
|
'inlineStyles'
|
|
]
|
|
}).data;
|
|
|
|
// Set output dimensions (high resolution for texture)
|
|
const outputWidth = 2048;
|
|
const outputHeight = Math.round((svgData.height / svgData.width) * outputWidth);
|
|
|
|
// Create canvas
|
|
const canvas = createCanvas(outputWidth, outputHeight);
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
// Fill background
|
|
ctx.fillStyle = '#E6EBF4'; // Light blue-gray background
|
|
ctx.fillRect(0, 0, outputWidth, outputHeight);
|
|
|
|
// Load and draw the SVG
|
|
const image = await loadSvgToImage(optimizedSvg, outputWidth, outputHeight);
|
|
ctx.drawImage(image, 0, 0, outputWidth, outputHeight);
|
|
|
|
// Save as JPEG
|
|
const outputPath = path.join(outputDir, `${mapName}_map.jpg`);
|
|
const out = fs.createWriteStream(outputPath);
|
|
const stream = canvas.createJPEGStream({ quality: 0.9 });
|
|
stream.pipe(out);
|
|
|
|
await new Promise((resolve, reject) => {
|
|
out.on('finish', resolve);
|
|
out.on('error', reject);
|
|
});
|
|
|
|
console.log(`Texture map saved to: ${outputPath}`);
|
|
}
|
|
|
|
/**
|
|
* Load SVG to Image
|
|
* @param {string} svg - SVG content
|
|
* @param {number} width - Target width
|
|
* @param {number} height - Target height
|
|
* @returns {Promise<Image>} - Image object
|
|
*/
|
|
async function loadSvgToImage(svg, width, height) {
|
|
// Create a data URL from the SVG
|
|
const dataUrl = `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`;
|
|
|
|
// Load the image
|
|
const image = await loadImage(dataUrl);
|
|
return image;
|
|
}
|
|
|
|
/**
|
|
* Generate coordinate file from SVG
|
|
* @param {Object} svgData - SVG data
|
|
* @param {string} mapName - Map name
|
|
* @param {string} outputDir - Output directory
|
|
*/
|
|
async function generateCoordinateFile(svgData, mapName, outputDir) {
|
|
console.log('Extracting coordinate data...');
|
|
|
|
const svgDoc = svgData.doc;
|
|
const viewBox = svgData.viewBox;
|
|
const mapDimensions = MAP_DIMENSIONS[mapName];
|
|
|
|
// Result object
|
|
const coordinateData = {
|
|
name: mapName,
|
|
dimensions: mapDimensions,
|
|
provinces: {}
|
|
};
|
|
|
|
// Extract province elements
|
|
const provinces = findProvinceElements(svgDoc, mapName);
|
|
|
|
// Process each province
|
|
for (const [province, element] of Object.entries(provinces)) {
|
|
// Get province center/position
|
|
const position = calculateProvinceCenter(element, viewBox, mapDimensions);
|
|
|
|
// Determine if it's a supply center
|
|
const isSupplyCenter = SUPPLY_CENTERS[mapName].includes(province);
|
|
|
|
// Determine province type (sea, land, coast)
|
|
const type = determineProvinceType(element, province);
|
|
|
|
// Add to coordinate data
|
|
coordinateData.provinces[province] = {
|
|
position,
|
|
isSupplyCenter,
|
|
type
|
|
};
|
|
}
|
|
|
|
// Save coordinate data as JSON
|
|
const outputPath = path.join(outputDir, `${mapName}_coords.json`);
|
|
fs.writeFileSync(outputPath, JSON.stringify(coordinateData, null, 2));
|
|
|
|
console.log(`Coordinate data saved to: ${outputPath}`);
|
|
console.log(`Extracted ${Object.keys(coordinateData.provinces).length} provinces`);
|
|
}
|
|
|
|
/**
|
|
* Find province elements in the SVG
|
|
* @param {Document} svgDoc - SVG Document
|
|
* @param {string} mapName - Map name
|
|
* @returns {Object} - Map of province ID to element
|
|
*/
|
|
function findProvinceElements(svgDoc, mapName) {
|
|
const provinces = {};
|
|
|
|
// Different maps have different conventions for province elements
|
|
let elements;
|
|
|
|
switch (mapName) {
|
|
case 'standard':
|
|
// Standard map typically has provinces as specific elements with IDs
|
|
elements = svgDoc.getElementsByTagName('path');
|
|
for (let i = 0; i < elements.length; i++) {
|
|
const element = elements[i];
|
|
const id = element.getAttribute('id');
|
|
|
|
// Check if this is a province ID (usually uppercase, 3 letters)
|
|
if (id && /^[A-Z]{3}(_[NS]C)?$/.test(id)) {
|
|
const baseId = id.split('_')[0]; // Handle coast variants
|
|
provinces[baseId] = element;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 'ancmed':
|
|
case 'modern':
|
|
case 'pure':
|
|
// For other maps, we'd need to adapt the extraction logic
|
|
// This is a placeholder - actual implementation would depend on the SVG structure
|
|
elements = svgDoc.getElementsByTagName('g');
|
|
for (let i = 0; i < elements.length; i++) {
|
|
const element = elements[i];
|
|
const id = element.getAttribute('id');
|
|
|
|
if (id && (id.includes('province') || id.includes('territory'))) {
|
|
// Extract province code from the element or its children
|
|
const label = findProvinceLabel(element);
|
|
if (label) {
|
|
provinces[label] = element;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
return provinces;
|
|
}
|
|
|
|
/**
|
|
* Find province label in an element or its children
|
|
* @param {Element} element - SVG element
|
|
* @returns {string|null} - Province label or null
|
|
*/
|
|
function findProvinceLabel(element) {
|
|
// This would need to be adapted based on the specific SVG structure
|
|
// For now, return a placeholder
|
|
return element.getAttribute('data-province') || element.getAttribute('id');
|
|
}
|
|
|
|
/**
|
|
* Calculate center position of a province
|
|
* @param {Element} element - SVG element
|
|
* @param {number[]} viewBox - SVG viewBox
|
|
* @param {Object} mapDimensions - Target map dimensions
|
|
* @returns {Object} - Position as {x, y, z}
|
|
*/
|
|
function calculateProvinceCenter(element, viewBox, mapDimensions) {
|
|
// Get the bounding box of the element
|
|
const bbox = element.getBBox();
|
|
|
|
// Calculate center in SVG coordinates
|
|
const centerX = bbox.x + bbox.width / 2;
|
|
const centerY = bbox.y + bbox.height / 2;
|
|
|
|
// Map to 3D coordinates
|
|
// SVG coordinate system has origin at top-left, Y increases downward
|
|
// 3D coordinate system for our map has origin at center, Y is up, Z is depth
|
|
|
|
// Normalize to 0-1 range based on viewBox
|
|
const normalizedX = (centerX - viewBox[0]) / viewBox[2];
|
|
const normalizedY = (centerY - viewBox[1]) / viewBox[3];
|
|
|
|
// Map to 3D space (X: -halfWidth to halfWidth, Z: -halfHeight to halfHeight)
|
|
const halfWidth = mapDimensions.width / 2;
|
|
const halfHeight = mapDimensions.height / 2;
|
|
|
|
return {
|
|
x: (normalizedX * mapDimensions.width) - halfWidth,
|
|
y: 0, // Flat map at y=0
|
|
z: (normalizedY * mapDimensions.height) - halfHeight
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Determine the type of province (sea, land, coast)
|
|
* @param {Element} element - SVG element
|
|
* @param {string} province - Province ID
|
|
* @returns {string} - Province type
|
|
*/
|
|
function determineProvinceType(element, province) {
|
|
// Based on typical conventions in Diplomacy maps
|
|
|
|
// Check element class or style for hints
|
|
const className = element.getAttribute('class') || '';
|
|
const style = element.getAttribute('style') || '';
|
|
|
|
if (className.includes('water') || className.includes('sea') || style.includes('fill:blue')) {
|
|
return 'sea';
|
|
}
|
|
|
|
// If we can't determine from element, use heuristics based on province code
|
|
|
|
// Most sea spaces in standard map are 3-letter codes
|
|
const seaSpaces = [
|
|
'NAO', 'MAO', 'IRI', 'ENG', 'NTH', 'HEL', 'SKA', 'BAL', 'BOT', 'BAR',
|
|
'NWG', 'WES', 'LYO', 'TYS', 'ADR', 'ION', 'AEG', 'EAS', 'BLA'
|
|
];
|
|
|
|
if (seaSpaces.includes(province)) {
|
|
return 'sea';
|
|
}
|
|
|
|
// Default to land
|
|
return 'land';
|
|
}
|
|
|
|
// Run the main function
|
|
if (require.main === module) {
|
|
main().catch(console.error);
|
|
}
|
|
|
|
module.exports = {
|
|
convertMap: main,
|
|
extractSvgFromSource,
|
|
generateTextureMap,
|
|
generateCoordinateFile
|
|
};
|