AI_Diplomacy/diplomacy/animation/utils/CoordinateMapper.js
2025-03-04 11:35:02 -08:00

751 lines
No EOL
28 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/>.
// ==============================================================================
/**
* Maps Diplomacy game coordinates to 3D positions.
* This class handles the translation between game location names (e.g. "LON", "PAR")
* and 3D coordinates for the Three.js renderer.
*/
export class CoordinateMapper {
/**
* Creates a new CoordinateMapper for a specific map
* @param {string} mapName - The name of the map (e.g. "standard", "ancmed")
*/
constructor(mapName) {
this.mapName = mapName;
this.coordinates = {};
this.provinceData = {};
this.mapWidth = 1000; // Default map dimensions
this.mapHeight = 1000;
// Initialize with placeholder coordinates until we load the real ones
this._initializeCoordinates();
}
/**
* Public initialize method that returns a Promise
* @returns {Promise} A promise that resolves when coordinates are loaded
*/
initialize() {
return new Promise((resolve, reject) => {
// We're already initialized, just resolve
setTimeout(resolve, 0);
});
}
/**
* Initialize coordinate mappings
* @private
*/
_initializeCoordinates() {
// Try to load coordinates from the JSON file
try {
this._loadCoordinatesFromJson();
} catch (error) {
console.warn(`[CoordinateMapper] Could not load coordinates from JSON: ${error.message}`);
console.warn('[CoordinateMapper] Using built-in placeholder coordinates');
this._useBuiltinCoordinates();
}
}
/**
* Load coordinates from a JSON file
* @private
*/
_loadCoordinatesFromJson() {
// Use relative paths for browser environment
const jsonPath = `./diplomacy/animation/assets/maps/${this.mapName}_coords.json`;
console.log(`[CoordinateMapper] Attempting to load coordinates from ${jsonPath}`);
// In a browser environment, we need to use fetch to load the JSON
if (typeof fetch !== 'undefined') {
fetch(jsonPath)
.then(response => {
if (!response.ok) {
// Try alternate path format
const altPath = `./assets/maps/${this.mapName}_coords.json`;
console.log(`[CoordinateMapper] First attempt failed, trying ${altPath}`);
return fetch(altPath);
}
return response;
})
.then(response => {
if (!response.ok) {
throw new Error(`Could not load coordinates: ${response.status}`);
}
return response.json();
})
.then(data => {
this._processCoordinateData(data);
})
.catch(error => {
console.warn(`[CoordinateMapper] JSON fetch error: ${error.message}`);
console.log('[CoordinateMapper] Falling back to built-in coordinates');
this._useBuiltinCoordinates();
});
} else {
// In a Node.js environment, we can use require
// But in our browser context, this will likely fall back to built-in coords
try {
const data = require(`../assets/maps/${this.mapName}_coords.json`);
this._processCoordinateData(data);
} catch (error) {
console.warn(`[CoordinateMapper] Could not load coordinate file: ${error.message}`);
this._useBuiltinCoordinates();
}
}
}
/**
* Process coordinate data from JSON
* @param {Object} data - Coordinate data
* @private
*/
_processCoordinateData(data) {
if (!data) {
console.warn('[CoordinateMapper] Empty coordinate data, using built-in fallback');
this._useBuiltinCoordinates();
return;
}
try {
console.log('[CoordinateMapper] Processing coordinate data');
// Set map dimensions if provided
if (data.mapWidth && data.mapHeight) {
this.mapWidth = data.mapWidth;
this.mapHeight = data.mapHeight;
}
// Process provinces
if (data.provinces) {
this.provinceData = data.provinces;
}
// Process coordinates
if (data.coordinates) {
this.coordinates = data.coordinates;
console.log(`[CoordinateMapper] Loaded ${Object.keys(this.coordinates).length} locations`);
} else {
console.warn('[CoordinateMapper] No coordinates found in data, using built-in');
this._useBuiltinCoordinates();
}
} catch (error) {
console.error(`[CoordinateMapper] Error processing coordinate data: ${error.message}`);
this._useBuiltinCoordinates();
}
}
/**
* Use built-in placeholder coordinates
* @private
*/
_useBuiltinCoordinates() {
// For Phase 2, we provide detailed coordinates for the standard map
// Standard map coordinates, optimized for better layout
const standardMapCoordinates = {
// Western Europe
'BRE': { x: -350, y: 0, z: 150 }, // Brest
'PAR': { x: -250, y: 0, z: 100 }, // Paris
'PIC': { x: -250, y: 0, z: 50 }, // Picardy
'BUR': { x: -200, y: 0, z: 100 }, // Burgundy
'GAS': { x: -300, y: 0, z: 200 }, // Gascony
'BEL': { x: -200, y: 0, z: 0 }, // Belgium
'RUH': { x: -150, y: 0, z: 0 }, // Ruhr
'HOL': { x: -150, y: 0, z: -50 }, // Holland
// Great Britain
'LON': { x: -300, y: 0, z: -100 }, // London
'WAL': { x: -350, y: 0, z: -100 }, // Wales
'YOR': { x: -300, y: 0, z: -150 }, // Yorkshire
'EDI': { x: -300, y: 0, z: -200 }, // Edinburgh
'LVP': { x: -350, y: 0, z: -170 }, // Liverpool
'CLY': { x: -330, y: 0, z: -250 }, // Clyde
// Northern Europe
'DEN': { x: -50, y: 0, z: -200 }, // Denmark
'NWY': { x: 0, y: 0, z: -280 }, // Norway
'SWE': { x: 50, y: 0, z: -250 }, // Sweden
'FIN': { x: 150, y: 0, z: -300 }, // Finland
'STP': { x: 200, y: 0, z: -300 }, // St. Petersburg
'STP/NC': { x: 200, y: 0, z: -350 }, // St. Petersburg (North Coast)
'STP/SC': { x: 250, y: 0, z: -250 }, // St. Petersburg (South Coast)
// Central Europe
'MUN': { x: -80, y: 0, z: 50 }, // Munich
'BER': { x: -50, y: 0, z: -100 }, // Berlin
'KIE': { x: -100, y: 0, z: -100 }, // Kiel
'SIL': { x: 0, y: 0, z: -50 }, // Silesia
'BOH': { x: 0, y: 0, z: 50 }, // Bohemia
'TYR': { x: -20, y: 0, z: 100 }, // Tyrolia
'VIE': { x: 50, y: 0, z: 100 }, // Vienna
'TRI': { x: 50, y: 0, z: 150 }, // Trieste
'BUD': { x: 100, y: 0, z: 100 }, // Budapest
'GAL': { x: 120, y: 0, z: 50 }, // Galicia
// Southern Europe
'MAR': { x: -200, y: 0, z: 200 }, // Marseilles
'PIE': { x: -100, y: 0, z: 150 }, // Piedmont
'VEN': { x: -50, y: 0, z: 150 }, // Venice
'TUS': { x: -50, y: 0, z: 200 }, // Tuscany
'ROM': { x: 0, y: 0, z: 250 }, // Rome
'NAP': { x: 50, y: 0, z: 300 }, // Naples
'APU': { x: 100, y: 0, z: 250 }, // Apulia
// Iberian Peninsula
'SPA': { x: -350, y: 0, z: 300 }, // Spain
'SPA/NC': { x: -380, y: 0, z: 250 }, // Spain (North Coast)
'SPA/SC': { x: -330, y: 0, z: 350 }, // Spain (South Coast)
'POR': { x: -450, y: 0, z: 350 }, // Portugal
// Eastern Europe
'WAR': { x: 100, y: 0, z: 0 }, // Warsaw
'UKR': { x: 170, y: 0, z: 50 }, // Ukraine
'LVN': { x: 150, y: 0, z: -200 }, // Livonia
'MOS': { x: 250, y: 0, z: -100 }, // Moscow
'SEV': { x: 300, y: 0, z: 100 }, // Sevastopol
// Balkans
'SER': { x: 150, y: 0, z: 180 }, // Serbia
'ALB': { x: 130, y: 0, z: 230 }, // Albania
'GRE': { x: 150, y: 0, z: 280 }, // Greece
'BUL': { x: 200, y: 0, z: 200 }, // Bulgaria
'BUL/EC': { x: 250, y: 0, z: 200 }, // Bulgaria (East Coast)
'BUL/SC': { x: 200, y: 0, z: 230 }, // Bulgaria (South Coast)
'RUM': { x: 200, y: 0, z: 150 }, // Rumania
// Ottoman Empire
'CON': { x: 250, y: 0, z: 250 }, // Constantinople
'ANK': { x: 300, y: 0, z: 200 }, // Ankara
'SMY': { x: 250, y: 0, z: 300 }, // Smyrna
'ARM': { x: 350, y: 0, z: 150 }, // Armenia
'SYR': { x: 350, y: 0, z: 250 }, // Syria
// North Africa
'TUN': { x: 50, y: 0, z: 400 }, // Tunisia
'NAF': { x: -200, y: 0, z: 400 }, // North Africa
// Seas
'NAO': { x: -450, y: 0, z: -300 }, // North Atlantic Ocean
'NWG': { x: -100, y: 0, z: -350 }, // Norwegian Sea
'BAR': { x: 200, y: 0, z: -400 }, // Barents Sea
'IRI': { x: -400, y: 0, z: -150 }, // Irish Sea
'NTH': { x: -200, y: 0, z: -200 }, // North Sea
'SKA': { x: 0, y: 0, z: -230 }, // Skagerrak
'HEL': { x: -100, y: 0, z: -150 }, // Helgoland Bight
'BAL': { x: 50, y: 0, z: -150 }, // Baltic Sea
'BOT': { x: 100, y: 0, z: -250 }, // Gulf of Bothnia
'ENG': { x: -270, y: 0, z: -20 }, // English Channel
'MAO': { x: -450, y: 0, z: 200 }, // Mid-Atlantic Ocean
'WES': { x: -100, y: 0, z: 350 }, // Western Mediterranean
'LYO': { x: -150, y: 0, z: 250 }, // Gulf of Lyon
'TYS': { x: 0, y: 0, z: 300 }, // Tyrrhenian Sea
'ION': { x: 120, y: 0, z: 330 }, // Ionian Sea
'ADR': { x: 80, y: 0, z: 200 }, // Adriatic Sea
'AEG': { x: 200, y: 0, z: 300 }, // Aegean Sea
'EAS': { x: 300, y: 0, z: 300 }, // Eastern Mediterranean
'BLA': { x: 270, y: 0, z: 170 } // Black Sea
};
// Ancient Mediterranean map coordinates (simplified)
const ancmedMapCoordinates = {
'ROM': { x: 0, y: 0, z: 0 }, // Rome
'ATH': { x: 200, y: 0, z: 100 }, // Athens
'CAR': { x: -100, y: 0, z: 150 }, // Carthage
// ... more for ancmed
};
// Modern map coordinates (simplified)
const modernMapCoordinates = {
'WAS': { x: -300, y: 0, z: 0 }, // Washington
'MOS': { x: 300, y: 0, z: -100 }, // Moscow
'PEK': { x: 400, y: 0, z: 50 }, // Beijing
// ... more for modern
};
// Pure map coordinates (simplified)
const pureMapCoordinates = {
'A1': { x: -300, y: 0, z: -300 },
'B1': { x: -200, y: 0, z: -300 },
'C1': { x: -100, y: 0, z: -300 },
// ... more for pure
};
// Select the appropriate map coordinates based on map name
const mapCoordinates = {
standard: standardMapCoordinates,
ancmed: ancmedMapCoordinates,
modern: modernMapCoordinates,
pure: pureMapCoordinates
};
// Set the coordinates for the specified map
this.coordinates = mapCoordinates[this.mapName] || standardMapCoordinates;
// Define sea provinces for standard map
const seaProvinces = [
'NAO', 'NWG', 'BAR', 'IRI', 'NTH', 'SKA', 'HEL', 'BAL', 'BOT',
'ENG', 'MAO', 'WES', 'LYO', 'TYS', 'ION', 'ADR', 'AEG', 'EAS', 'BLA'
];
// Define supply centers for standard map
const supplyCenters = [
'EDI', 'LVP', 'LON', 'PAR', 'BRE', 'MAR', 'KIE', 'BER', 'MUN',
'ROM', 'VEN', 'NAP', 'VIE', 'TRI', 'BUD', 'MOS', 'WAR', 'SEV',
'STP', 'ANK', 'CON', 'SMY', 'NWY', 'SWE', 'DEN', 'HOL', 'BEL',
'SPA', 'POR', 'TUN', 'SER', 'RUM', 'BUL', 'GRE'
];
// Define coastal provinces that have special coast designations
const coastalProvinces = {
'SPA': ['NC', 'SC'],
'STP': ['NC', 'SC'],
'BUL': ['EC', 'SC']
};
// Generate detailed province data
this.provinceData = {};
// Process all coordinates
for (const [province, position] of Object.entries(this.coordinates)) {
// Skip special coast provinces, they will be handled with their base province
if (province.includes('/')) continue;
// Determine if it's a supply center
const isSupplyCenter = supplyCenters.includes(province);
// Determine province type
let type = 'land';
if (seaProvinces.includes(province)) {
type = 'sea';
} else if (coastalProvinces[province]) {
type = 'coast';
}
// Create province data
this.provinceData[province] = {
position,
isSupplyCenter,
type,
coasts: coastalProvinces[province] || []
};
// Add coast-specific positions if this is a coastal province
if (coastalProvinces[province]) {
this.provinceData[province].coastPositions = {};
coastalProvinces[province].forEach(coast => {
const coastKey = `${province}/${coast}`;
if (this.coordinates[coastKey]) {
this.provinceData[province].coastPositions[coast] = this.coordinates[coastKey];
}
});
}
}
}
/**
* Get the 3D position for a location
* @param {string} location - The location name (e.g. "LON", "PAR")
* @returns {Object|null} The position as {x, y, z} or null if not found
*/
getPositionForLocation(location) {
if (!location) {
console.warn(`[CoordinateMapper] Invalid location provided: ${location}`);
return null;
}
// Trim and uppercase the location to standardize
let normalizedLocation = location.trim().toUpperCase();
// Handle both slash and underscore formats for coast locations
// Convert slash format to underscore format for lookup
if (normalizedLocation.includes('/')) {
normalizedLocation = normalizedLocation.replace('/', '_');
}
// Direct lookup first
if (this.coordinates[normalizedLocation]) {
return { ...this.coordinates[normalizedLocation] };
}
// Try without coast designation
const baseLocation = normalizedLocation.split('_')[0];
if (baseLocation !== normalizedLocation && this.coordinates[baseLocation]) {
console.log(`[CoordinateMapper] Using base location ${baseLocation} for ${normalizedLocation}`);
return { ...this.coordinates[baseLocation] };
}
// For locations with coasts (e.g. "STP_SC"), check if we have a specific coast position
if (normalizedLocation.includes('_')) {
const parts = normalizedLocation.split('_');
const baseLocationPart = parts[0];
const coast = parts.slice(1).join('_'); // In case there are multiple underscores
// Try different coast separator formats
// Try slash format (STP/SC)
const slashFormat = `${baseLocationPart}/${coast}`;
if (this.coordinates[slashFormat]) {
return { ...this.coordinates[slashFormat] };
}
// Check if we have province data with coast positions
const provinceInfo = this.getProvinceInfo(baseLocationPart);
if (provinceInfo && provinceInfo.coastPositions && provinceInfo.coastPositions[coast]) {
return { ...provinceInfo.coastPositions[coast] };
}
// If we don't have a specific coast position, use the base province
if (this.coordinates[baseLocationPart]) {
console.log(`[CoordinateMapper] Falling back to base position for ${normalizedLocation}`);
return { ...this.coordinates[baseLocationPart] };
}
}
console.warn(`[CoordinateMapper] Location not found: ${location} (normalized: ${normalizedLocation})`);
return null;
}
/**
* Get province information
* @param {string} location - The province name
* @returns {Object|null} The province data or null if not found
*/
getProvinceInfo(location) {
if (!location) return null;
let normalizedLocation = location.trim().toUpperCase();
// Handle both slash and underscore formats
if (normalizedLocation.includes('/')) {
normalizedLocation = normalizedLocation.replace('/', '_');
}
// Get the base location (without coast designation)
const baseLocation = normalizedLocation.split('_')[0];
// First try with the full normalized location
if (this.provinceData[normalizedLocation]) {
return { ...this.provinceData[normalizedLocation] };
}
// Then try with the base location (no coast)
if (this.provinceData[baseLocation]) {
return { ...this.provinceData[baseLocation] };
}
// Try with slash format instead of underscore
if (normalizedLocation.includes('_')) {
const slashFormat = normalizedLocation.replace('_', '/');
if (this.provinceData[slashFormat]) {
return { ...this.provinceData[slashFormat] };
}
}
console.log(`[CoordinateMapper] Province info not found for: ${location}`);
return null;
}
/**
* Check if a province is a supply center
* @param {string} location - The province name
* @returns {boolean} Whether the province is a supply center
*/
isSupplyCenter(location) {
const info = this.getProvinceInfo(location);
return info ? info.isSupplyCenter : false;
}
/**
* Get the type of a province (land, sea)
* @param {string} location - The province name
* @returns {string|null} The province type or null if not found
*/
getProvinceType(location) {
const info = this.getProvinceInfo(location);
return info ? info.type : null;
}
/**
* Get all available locations
* @returns {string[]} An array of all location names
*/
getAllLocations() {
return Object.keys(this.coordinates);
}
/**
* Calculate the midpoint between two locations, for animations
* @param {string} location1 - The first location
* @param {string} location2 - The second location
* @returns {Object|null} The midpoint as {x, y, z} or null if either location not found
*/
getMidpoint(location1, location2) {
const pos1 = this.getPositionForLocation(location1);
const pos2 = this.getPositionForLocation(location2);
if (!pos1 || !pos2) return null;
return {
x: (pos1.x + pos2.x) / 2,
y: (pos1.y + pos2.y) / 2 + 30, // Add some height for arcing movement
z: (pos1.z + pos2.z) / 2
};
}
/**
* Check if a province has specific coasts
* @param {string} location - The province name
* @returns {string[]|null} Array of coast codes or null if not a coastal province
*/
getProvinceCoasts(location) {
const info = this.getProvinceInfo(location);
return info && info.coasts ? [...info.coasts] : null;
}
/**
* Check if a location is a valid coast of a province
* @param {string} location - The full location with coast (e.g. "SPA/NC")
* @returns {boolean} Whether the coast is valid
*/
isValidCoast(location) {
if (!location.includes('/')) return false;
const [province, coast] = location.split('/');
const coasts = this.getProvinceCoasts(province);
return coasts ? coasts.includes(coast) : false;
}
/**
* Get adjacent provinces for a given province
* @param {string} location - The province name
* @returns {string[]} Array of adjacent provinces
*/
getAdjacentProvinces(location) {
const baseLocation = location.split('/')[0];
// This is a partial adjacency list for the standard map
// In a full implementation, this would be loaded from a configuration file
const adjacencyList = {
// Western Europe
'BRE': ['PAR', 'PIC', 'ENG', 'MAO', 'GAS'],
'PAR': ['BRE', 'PIC', 'BUR', 'GAS'],
'PIC': ['BRE', 'PAR', 'BUR', 'BEL', 'ENG'],
'BUR': ['PAR', 'PIC', 'BEL', 'RUH', 'MUN', 'MAR', 'GAS'],
'GAS': ['BRE', 'PAR', 'BUR', 'MAR', 'SPA', 'MAO'],
'MAR': ['GAS', 'BUR', 'PIE', 'LYO', 'SPA'],
'BEL': ['PIC', 'BUR', 'RUH', 'HOL', 'NTH', 'ENG'],
'RUH': ['BEL', 'BUR', 'MUN', 'KIE', 'HOL'],
'HOL': ['BEL', 'RUH', 'KIE', 'HEL', 'NTH'],
// Great Britain
'LON': ['YOR', 'WAL', 'ENG', 'NTH'],
'YOR': ['LON', 'WAL', 'EDI', 'NTH'],
'EDI': ['YOR', 'CLY', 'NTH', 'NWG'],
'LVP': ['WAL', 'YOR', 'EDI', 'CLY', 'IRI', 'NAO'],
'WAL': ['LON', 'YOR', 'LVP', 'IRI', 'ENG'],
'CLY': ['EDI', 'LVP', 'NAO', 'NWG'],
// Northern Europe
'NWY': ['NTH', 'NWG', 'BAR', 'STP', 'FIN', 'SKA', 'SWE'],
'SWE': ['NWY', 'FIN', 'BOT', 'BAL', 'DEN', 'SKA'],
'DEN': ['HEL', 'BAL', 'SKA', 'SWE', 'KIE', 'NTH'],
'FIN': ['NWY', 'STP', 'BOT', 'SWE'],
// Central Europe
'KIE': ['DEN', 'BAL', 'BER', 'MUN', 'RUH', 'HOL', 'HEL'],
'BER': ['KIE', 'BAL', 'PRU', 'SIL', 'MUN'],
'MUN': ['KIE', 'BER', 'SIL', 'BOH', 'TYR', 'BUR', 'RUH'],
'SIL': ['MUN', 'BER', 'PRU', 'WAR', 'GAL', 'BOH'],
'BOH': ['MUN', 'SIL', 'GAL', 'VIE', 'TYR'],
'TYR': ['MUN', 'BOH', 'VIE', 'TRI', 'VEN', 'PIE'],
'VIE': ['TYR', 'BOH', 'GAL', 'BUD', 'TRI'],
'BUD': ['VIE', 'GAL', 'RUM', 'SER', 'TRI'],
'TRI': ['VEN', 'TYR', 'VIE', 'BUD', 'SER', 'ALB', 'ADR'],
'GAL': ['BOH', 'SIL', 'WAR', 'UKR', 'RUM', 'BUD', 'VIE'],
// Eastern Europe
'WAR': ['PRU', 'SIL', 'GAL', 'UKR', 'LVN'],
'MOS': ['STP', 'LVN', 'UKR', 'SEV'],
'UKR': ['WAR', 'GAL', 'RUM', 'SEV', 'MOS'],
'LVN': ['PRU', 'BAL', 'BOT', 'STP', 'MOS', 'WAR'],
'STP': ['BAR', 'NWY', 'FIN', 'BOT', 'LVN', 'MOS'],
'STP/NC': ['BAR', 'NWY'],
'STP/SC': ['FIN', 'BOT', 'LVN'],
'SEV': ['UKR', 'RUM', 'BLA', 'ARM', 'MOS'],
// Italy
'PIE': ['MAR', 'TYR', 'VEN', 'TUS', 'LYO'],
'VEN': ['PIE', 'TYR', 'TRI', 'ADR', 'APU', 'ROM', 'TUS'],
'TUS': ['PIE', 'VEN', 'ROM', 'LYO', 'TYS'],
'ROM': ['TUS', 'VEN', 'APU', 'NAP', 'TYS'],
'NAP': ['ROM', 'APU', 'ION', 'TYS'],
'APU': ['VEN', 'ADR', 'ION', 'NAP', 'ROM'],
// Balkans
'ALB': ['TRI', 'SER', 'GRE', 'ION', 'ADR'],
'SER': ['TRI', 'BUD', 'RUM', 'BUL', 'GRE', 'ALB'],
'GRE': ['ALB', 'SER', 'BUL', 'AEG', 'ION'],
'BUL': ['SER', 'RUM', 'BLA', 'CON', 'AEG', 'GRE'],
'BUL/EC': ['BLA', 'CON'],
'BUL/SC': ['AEG', 'CON'],
'RUM': ['GAL', 'UKR', 'SEV', 'BLA', 'BUL', 'SER', 'BUD'],
// Ottoman Empire
'CON': ['BUL', 'BLA', 'ANK', 'SMY', 'AEG'],
'ANK': ['BLA', 'ARM', 'SMY', 'CON'],
'SMY': ['CON', 'ANK', 'ARM', 'SYR', 'EAS', 'AEG'],
'ARM': ['SEV', 'BLA', 'ANK', 'SMY', 'SYR'],
'SYR': ['ARM', 'SMY', 'EAS'],
// North Africa
'TUN': ['NAF', 'WES', 'TYS', 'ION'],
'NAF': ['MAO', 'WES', 'TUN'],
// Iberian Peninsula
'SPA': ['GAS', 'MAR', 'LYO', 'WES', 'MAO', 'POR'],
'SPA/NC': ['GAS', 'MAO', 'POR'],
'SPA/SC': ['MAR', 'LYO', 'WES', 'MAO'],
'POR': ['SPA', 'MAO'],
// Seas
'NAO': ['MAO', 'IRI', 'LVP', 'CLY', 'NWG'],
'NWG': ['NAO', 'CLY', 'EDI', 'NTH', 'NWY', 'BAR'],
'BAR': ['NWG', 'NWY', 'STP', 'STP/NC'],
'IRI': ['NAO', 'MAO', 'ENG', 'WAL', 'LVP'],
'NTH': ['ENG', 'BEL', 'HOL', 'HEL', 'DEN', 'SKA', 'NWY', 'EDI', 'YOR', 'LON'],
'SKA': ['NTH', 'DEN', 'SWE', 'NWY'],
'HEL': ['NTH', 'DEN', 'KIE', 'HOL'],
'BAL': ['DEN', 'KIE', 'BER', 'PRU', 'LVN', 'BOT', 'SWE'],
'BOT': ['BAL', 'SWE', 'FIN', 'STP', 'STP/SC', 'LVN'],
'ENG': ['IRI', 'WAL', 'LON', 'NTH', 'BEL', 'PIC', 'BRE', 'MAO'],
'MAO': ['NAO', 'IRI', 'ENG', 'BRE', 'GAS', 'SPA', 'SPA/NC', 'SPA/SC', 'POR', 'NAF', 'WES'],
'WES': ['MAO', 'SPA', 'SPA/SC', 'LYO', 'TYS', 'TUN', 'NAF'],
'LYO': ['SPA', 'SPA/SC', 'MAR', 'PIE', 'TUS', 'TYS', 'WES'],
'TYS': ['LYO', 'TUS', 'ROM', 'NAP', 'ION', 'TUN', 'WES'],
'ION': ['TYS', 'NAP', 'APU', 'ADR', 'ALB', 'GRE', 'AEG', 'EAS', 'TUN'],
'ADR': ['VEN', 'TRI', 'ALB', 'ION', 'APU'],
'AEG': ['GRE', 'BUL', 'BUL/SC', 'CON', 'SMY', 'EAS', 'ION'],
'EAS': ['ION', 'AEG', 'SMY', 'SYR'],
'BLA': ['RUM', 'SEV', 'ARM', 'ANK', 'CON', 'BUL', 'BUL/EC']
};
return adjacencyList[baseLocation] || [];
}
/**
* Check if two provinces are adjacent
* @param {string} location1 - First province
* @param {string} location2 - Second province
* @returns {boolean} Whether the provinces are adjacent
*/
areAdjacent(location1, location2) {
const baseLocation1 = location1.split('/')[0];
const baseLocation2 = location2.split('/')[0];
// For provinces with coasts, first check if base provinces are adjacent
if (baseLocation1 !== location1 || baseLocation2 !== location2) {
// For coast-specific adjacency, we need to check special cases
// Special case: St. Petersburg north coast only connects to Barents Sea,
// while south coast connects to Gulf of Bothnia and Livonia
if (location1 === 'STP/NC' && location2 === 'BOT') return false;
if (location1 === 'STP/SC' && location2 === 'BAR') return false;
if (location2 === 'STP/NC' && location1 === 'BOT') return false;
if (location2 === 'STP/SC' && location1 === 'BAR') return false;
// Similar special cases for Spain and Bulgaria coasts
// ...
}
const adjacent = this.getAdjacentProvinces(baseLocation1);
return adjacent.includes(baseLocation2);
}
/**
* Calculate a path of positions between two locations
* @param {string} fromLocation - The starting location
* @param {string} toLocation - The destination location
* @param {number} steps - The number of points to generate
* @param {number} arcHeight - The height of the arc in the y-direction
* @returns {THREE.Vector3[]|null} Array of THREE.Vector3 positions or null if either location not found
*/
getPathBetween(fromLocation, toLocation, steps = 10, arcHeight = 30) {
const startPos = this.getPositionForLocation(fromLocation);
const endPos = this.getPositionForLocation(toLocation);
if (!startPos || !endPos) return null;
const path = [];
// Determine path type based on province types
const fromType = this.getProvinceType(fromLocation.split('/')[0]);
const toType = this.getProvinceType(toLocation.split('/')[0]);
// Higher arc for land movement, flatter for sea movement
const actualArcHeight = (fromType === 'sea' && toType === 'sea') ?
arcHeight / 3 : arcHeight;
// Import THREE if needed
const THREE = window.THREE || (typeof global !== 'undefined' ? global.THREE : null);
if (!THREE) {
console.error('[CoordinateMapper] THREE.js not available');
return null;
}
// Add intermediate points for better path
if (this.areAdjacent(fromLocation, toLocation)) {
// Direct path for adjacent provinces
for (let i = 0; i <= steps; i++) {
const t = i / steps;
// Interpolate x and z linearly
const x = startPos.x + (endPos.x - startPos.x) * t;
const z = startPos.z + (endPos.z - startPos.z) * t;
// Add an arc in the y direction using a sin curve
// sin(π * t) gives a nice arc that starts and ends at 0
const y = startPos.y + (endPos.y - startPos.y) * t + Math.sin(Math.PI * t) * actualArcHeight;
// Create a THREE.Vector3 object
path.push(new THREE.Vector3(x, y, z));
}
} else {
// For non-adjacent provinces, try to find a path through adjacent provinces
// This is a simplified approach; in a real implementation, you'd use a pathfinding algorithm
// Just create a direct path for now, but with a higher arc
for (let i = 0; i <= steps; i++) {
const t = i / steps;
// Interpolate x and z linearly
const x = startPos.x + (endPos.x - startPos.x) * t;
const z = startPos.z + (endPos.z - startPos.z) * t;
// Add a higher arc for longer paths
const y = startPos.y + (endPos.y - startPos.y) * t + Math.sin(Math.PI * t) * (actualArcHeight * 2);
// Create a THREE.Vector3 object
path.push(new THREE.Vector3(x, y, z));
}
}
return path;
}
}