diff --git a/diplomacy/animation/simple-test.html b/diplomacy/animation/simple-test.html index db91f59..c186a2d 100644 --- a/diplomacy/animation/simple-test.html +++ b/diplomacy/animation/simple-test.html @@ -3,14 +3,12 @@ - Diplomacy Map Test - + Diplomacy Map (Fallback-Only) @@ -37,6 +35,7 @@ .map-view { flex-grow: 1; background-color: #87CEEB; + position: relative; } button { padding: 8px 16px; @@ -52,6 +51,7 @@ } #phase-display { font-weight: bold; + margin-left: 10px; } #file-input { display: none; @@ -90,777 +90,128 @@ import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; - // Basic variables + // --- CORE VARIABLES --- let scene, camera, renderer, controls; let gameData = null; let currentPhaseIndex = 0; - let coordinateData = null; // Will hold map coordinates - let unitMeshes = []; // Track unit meshes for cleanup - let mapGeometry; // Shared geometry for map components - - // DOM elements - const loadBtn = document.getElementById('load-btn'); - const fileInput = document.getElementById('file-input'); - const prevBtn = document.getElementById('prev-btn'); - const nextBtn = document.getElementById('next-btn'); - const phaseDisplay = document.getElementById('phase-display'); - const infoPanel = document.getElementById('info-panel'); - const mapView = document.getElementById('map-view'); - - // Initialize Three.js scene + let coordinateData = null; + let unitMeshes = []; // To store references for units + supply center 3D objects + let mapPlane = null; // The fallback map plane + + // --- DOM ELEMENTS --- + const loadBtn = document.getElementById('load-btn'); + const fileInput = document.getElementById('file-input'); + const prevBtn = document.getElementById('prev-btn'); + const nextBtn = document.getElementById('next-btn'); + const phaseDisplay = document.getElementById('phase-display'); + const infoPanel = document.getElementById('info-panel'); + const mapView = document.getElementById('map-view'); + + // --- INITIALIZE SCENE --- function initScene() { - // Create scene scene = new THREE.Scene(); - scene.background = new THREE.Color(0x87CEEB); // Sky blue background + scene.background = new THREE.Color(0x87CEEB); - // Setup camera + // Camera camera = new THREE.PerspectiveCamera( 60, mapView.clientWidth / mapView.clientHeight, 1, - 5000 + 3000 ); - camera.position.set(0, 500, 500); + camera.position.set(0, 800, 800); camera.lookAt(0, 0, 0); - // Setup renderer with improved quality - renderer = new THREE.WebGLRenderer({ - antialias: true, - alpha: true - }); + // Renderer + renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(mapView.clientWidth, mapView.clientHeight); renderer.setPixelRatio(window.devicePixelRatio); - renderer.shadowMap.enabled = true; - renderer.shadowMap.type = THREE.PCFSoftShadowMap; mapView.appendChild(renderer.domElement); - // Add controls with better defaults + // Controls controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.05; controls.screenSpacePanning = true; controls.minDistance = 100; - controls.maxDistance = 1000; - controls.maxPolarAngle = Math.PI / 2.1; // Limit rotation to prevent seeing under the map - - // Add camera preset buttons - createCameraPresetButtons(); - - // Enhanced lighting setup - setupLighting(); - - // Add an environment (skybox, fog) - setupEnvironment(); - - // Add a subtle grid for reference that fades with distance - const gridHelper = new THREE.GridHelper(1000, 50, 0x444444, 0x222222); - gridHelper.material.opacity = 0.2; - gridHelper.material.transparent = true; - scene.add(gridHelper); - - // Load the coordinate data - loadCoordinateData().then(() => { - // Create enhanced map with texture once coordinates are loaded - createMapWithTexture(); - - // Disable territory disks - we'll draw territories directly on the canvas map - // createTerritoryBoundaries(); - - // Add territory labels - if (coordinateData && coordinateData.coordinates) { - console.log('Adding territory labels...'); - for (const [province, position] of Object.entries(coordinateData.coordinates)) { - // Don't add labels for coast variants (they have underscores) - if (!province.includes('_')) { - addLabel(province, position.x, position.z); - } - } - } - - // Add supply centers - displaySupplyCenters(); - - // Add territory interactivity - addTerritoryInteractivity(); - - // Optimize label visibility - optimizeLabels(); - }); - - // Start animation loop + controls.maxDistance = 1500; + controls.maxPolarAngle = Math.PI / 2; // Limit so you don't flip under the map + + // Lighting (keep it simple) + const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); + scene.add(ambientLight); + + const dirLight = new THREE.DirectionalLight(0xffffff, 0.6); + dirLight.position.set(300, 400, 300); + scene.add(dirLight); + + // Load coordinate data, then build the fallback map + loadCoordinateData() + .then(() => { + createFallbackMap(); // Create the map plane from the start + }) + .catch(err => { + console.error("Error loading coordinates:", err); + infoPanel.textContent = `Error loading coords: ${err.message}`; + }); + + // Kick off animation loop animate(); - // Add window resize handler + // Handle resizing window.addEventListener('resize', onWindowResize); } - // Enhanced lighting setup - function setupLighting() { - // Clear existing lights if any - scene.children.forEach(child => { - if (child.isLight) scene.remove(child); - }); + // --- ANIMATION LOOP --- + function animate() { + requestAnimationFrame(animate); - // Add ambient light for base illumination - const ambientLight = new THREE.AmbientLight(0xCCDDFF, 0.4); - scene.add(ambientLight); - - // Main directional light (sun-like) - const mainLight = new THREE.DirectionalLight(0xFFFFCC, 0.8); - mainLight.position.set(300, 400, 300); - mainLight.castShadow = true; - - // Better shadow quality - mainLight.shadow.mapSize.width = 2048; - mainLight.shadow.mapSize.height = 2048; - mainLight.shadow.camera.near = 0.5; - mainLight.shadow.camera.far = 1000; - mainLight.shadow.camera.left = -500; - mainLight.shadow.camera.right = 500; - mainLight.shadow.camera.top = 500; - mainLight.shadow.camera.bottom = -500; - scene.add(mainLight); - - // Helper backlight - const backLight = new THREE.DirectionalLight(0x9999FF, 0.3); - backLight.position.set(-200, 200, -300); - scene.add(backLight); - - // Add some point lights for atmosphere - const colors = [0xFF6666, 0x66AAFF, 0x66FFAA, 0xFFFF66]; - - for (let i = 0; i < 4; i++) { - const pointLight = new THREE.PointLight(colors[i % colors.length], 0.4, 300); - const angle = (i / 4) * Math.PI * 2; - pointLight.position.set( - Math.cos(angle) * 400, - 100, - Math.sin(angle) * 400 - ); - scene.add(pointLight); - } - } - - // Create an environmental atmosphere - function setupEnvironment() { - // Add subtle fog for depth - scene.fog = new THREE.FogExp2(0xCCDDFF, 0.0005); - - // Store initial background color for day/night transitions - scene.userData.dayColor = new THREE.Color(0x87CEEB); - scene.userData.nightColor = new THREE.Color(0x001133); - scene.userData.currentTimeOfDay = 'day'; - } - - // Create camera preset buttons - function createCameraPresetButtons() { - const presetContainer = document.createElement('div'); - presetContainer.style.position = 'absolute'; - presetContainer.style.bottom = '20px'; - presetContainer.style.left = '20px'; - presetContainer.style.zIndex = '100'; - - const presets = [ - { name: 'Top View', position: [0, 600, 0], target: [0, 0, 0] }, - { name: 'Europe', position: [0, 400, 400], target: [0, 0, 0] }, - { name: 'West', position: [-500, 300, 0], target: [0, 0, 0] }, - { name: 'East', position: [500, 300, 0], target: [0, 0, 0] }, - { name: 'North', position: [0, 300, -500], target: [0, 0, 0] }, - { name: 'South', position: [0, 300, 500], target: [0, 0, 0] }, - { name: 'Toggle Day/Night', action: toggleDayNight } - ]; - - presets.forEach(preset => { - const btn = document.createElement('button'); - btn.innerText = preset.name; - btn.style.margin = '5px'; - btn.style.padding = '8px 12px'; - btn.style.backgroundColor = '#444'; - btn.style.color = 'white'; - btn.style.border = 'none'; - btn.style.borderRadius = '4px'; - btn.style.cursor = 'pointer'; - - btn.addEventListener('click', () => { - if (preset.action) { - preset.action(); - } else { - moveCamera(preset.position, preset.target); + // Update any pulsing or wave animations on supply centers or units + if (scene.userData.animatedObjects) { + scene.userData.animatedObjects.forEach(obj => { + if (obj.userData.pulseAnimation) { + const anim = obj.userData.pulseAnimation; + anim.time += anim.speed; + if (obj.userData.glowMesh) { + const pulseValue = Math.sin(anim.time) * anim.intensity + 0.5; + obj.userData.glowMesh.material.opacity = 0.2 + (pulseValue * 0.3); + obj.userData.glowMesh.scale.set( + 1 + (pulseValue * 0.1), + 1 + (pulseValue * 0.1), + 1 + (pulseValue * 0.1) + ); + } + // Subtle bobbing up/down + obj.position.y = 2 + Math.sin(anim.time) * 0.5; } }); - - presetContainer.appendChild(btn); - }); - - document.body.appendChild(presetContainer); - } - - // Smoothly move camera to new position and target - function moveCamera(position, target) { - const startPosition = camera.position.clone(); - const startTarget = controls.target.clone(); - const endPosition = new THREE.Vector3(...position); - const endTarget = new THREE.Vector3(...target); - - const duration = 1000; // ms - const startTime = Date.now(); - - function updateCamera() { - const elapsed = Date.now() - startTime; - const progress = Math.min(elapsed / duration, 1); - - // Use easing function for smoother motion - const easeProgress = 1 - Math.pow(1 - progress, 3); - - // Interpolate position and target - camera.position.lerpVectors(startPosition, endPosition, easeProgress); - controls.target.lerpVectors(startTarget, endTarget, easeProgress); - controls.update(); - - if (progress < 1) { - requestAnimationFrame(updateCamera); - } } - updateCamera(); + controls.update(); + renderer.render(scene, camera); } - // Toggle between day and night mode - function toggleDayNight() { - const isDay = scene.userData.currentTimeOfDay === 'day'; - const targetColor = isDay ? scene.userData.nightColor : scene.userData.dayColor; - const currentColor = scene.background.clone(); - - // Get all directional lights - const lights = scene.children.filter(child => child.isDirectionalLight); - const currentIntensities = lights.map(light => light.intensity); - const targetIntensities = isDay ? lights.map(light => light.intensity * 0.3) : lights.map(light => light.intensity / 0.3); - - // Get the ambient light - const ambientLight = scene.children.find(child => child.isAmbientLight); - const currentAmbientIntensity = ambientLight ? ambientLight.intensity : 0.4; - const targetAmbientIntensity = isDay ? currentAmbientIntensity * 0.5 : currentAmbientIntensity / 0.5; - - // Update fog - const currentFogColor = scene.fog.color.clone(); - const targetFogColor = isDay ? new THREE.Color(0x001133) : new THREE.Color(0xCCDDFF); - - const duration = 1000; // ms - const startTime = Date.now(); - - function updateLighting() { - const elapsed = Date.now() - startTime; - const progress = Math.min(elapsed / duration, 1); - - // Use easing function for smoother transition - const easeProgress = 1 - Math.pow(1 - progress, 3); - - // Interpolate background color - scene.background.lerpColors(currentColor, targetColor, easeProgress); - - // Interpolate light intensities - lights.forEach((light, index) => { - light.intensity = currentIntensities[index] + (targetIntensities[index] - currentIntensities[index]) * easeProgress; - }); - - // Interpolate ambient light intensity - if (ambientLight) { - ambientLight.intensity = currentAmbientIntensity + (targetAmbientIntensity - currentAmbientIntensity) * easeProgress; - } - - // Interpolate fog color - scene.fog.color.lerpColors(currentFogColor, targetFogColor, easeProgress); - - if (progress < 1) { - requestAnimationFrame(updateLighting); - } else { - // Update current time of day - scene.userData.currentTimeOfDay = isDay ? 'night' : 'day'; - } - } - - updateLighting(); + // --- RESIZE HANDLER --- + function onWindowResize() { + camera.aspect = mapView.clientWidth / mapView.clientHeight; + camera.updateProjectionMatrix(); + renderer.setSize(mapView.clientWidth, mapView.clientHeight); } - // Create territory boundaries based on coordinate data - function createTerritoryBoundaries() { - if (!coordinateData || !coordinateData.coordinates) { - console.warn('No coordinate data available for territory boundaries'); - return; - } - - // Group provinces by country - const countries = { - 'AUSTRIA': ['VIE', 'BUD', 'TRI', 'GAL', 'BOH', 'TYR'], - 'ENGLAND': ['LON', 'EDI', 'LVP', 'WAL', 'YOR', 'CLY'], - 'FRANCE': ['PAR', 'BRE', 'MAR', 'PIC', 'GAS', 'BUR'], - 'GERMANY': ['BER', 'MUN', 'KIE', 'RUH', 'PRU', 'SIL'], - 'ITALY': ['ROM', 'VEN', 'NAP', 'TUS', 'PIE', 'APU'], - 'RUSSIA': ['STP', 'MOS', 'WAR', 'SEV', 'UKR', 'LVN'], - 'TURKEY': ['CON', 'ANK', 'SMY', 'SYR', 'ARM'] - }; - - // Sea provinces for special styling - const seaProvinces = [ - 'NAO', 'NWG', 'BAR', 'NTH', 'SKA', 'HEL', 'BAL', 'BOT', 'ENG', 'IRI', 'MAO', - 'WES', 'LYO', 'TYS', 'ADR', 'ION', 'AEG', 'EAS', 'BLA' - ]; - - // Country colors with better contrast and theme - const countryColors = { - 'AUSTRIA': 0xd82b2b, - 'ENGLAND': 0x223a87, - 'FRANCE': 0x3699d4, - 'GERMANY': 0x232323, - 'ITALY': 0x35a335, - 'RUSSIA': 0xcccccc, - 'TURKEY': 0xe0c846 - }; - - // Create a group for all territory boundaries - const territoriesGroup = new THREE.Group(); - territoriesGroup.name = 'territories'; - - // Main group for land territories - const landGroup = new THREE.Group(); - landGroup.name = 'land_territories'; - - // Main group for sea territories - const seaGroup = new THREE.Group(); - seaGroup.name = 'sea_territories'; - - // Process each province - for (const [province, position] of Object.entries(coordinateData.coordinates)) { - if (!province.includes('_')) { // Skip coast variants - // Determine if this is a sea province - const isSea = seaProvinces.includes(province); - - // Determine country by checking each country's provinces - let country = null; - for (const [countryName, provinces] of Object.entries(countries)) { - if (provinces.includes(province)) { - country = countryName; - break; - } - } - - // Larger radius for territories - const radius = isSea ? 35 : 33; - - // Create appearance based on territory type - if (isSea) { - // Sea territory - blue with wave effect - const circleGeometry = new THREE.CircleGeometry(radius, 32); - const circleMaterial = new THREE.MeshStandardMaterial({ - color: 0x1a3c6e, - transparent: true, - opacity: 0.8, - side: THREE.DoubleSide, - metalness: 0.3, - roughness: 0.7 - }); - - const circleMesh = new THREE.Mesh(circleGeometry, circleMaterial); - circleMesh.position.set(position.x, 0.2, position.z); // Slightly lower than land - circleMesh.rotation.x = -Math.PI / 2; // Make it horizontal - - // Add a wave effect overlay - const waveGeometry = new THREE.CircleGeometry(radius - 2, 32); - const waveTexture = createWaveTexture(); - const waveMaterial = new THREE.MeshStandardMaterial({ - color: 0x66aadd, - transparent: true, - opacity: 0.7, - alphaMap: waveTexture, - side: THREE.DoubleSide - }); - - const waveMesh = new THREE.Mesh(waveGeometry, waveMaterial); - waveMesh.position.set(position.x, 0.3, position.z); - waveMesh.rotation.x = -Math.PI / 2; - - // Store province data - circleMesh.userData = { - province, - country, - isSea: true - }; - - // Add territory border (thicker for seas) - const borderGeometry = new THREE.RingGeometry(radius - 0.5, radius + 5, 32); - const borderMaterial = new THREE.MeshStandardMaterial({ - color: 0x0a1a3c, - transparent: true, - opacity: 0.8, - side: THREE.DoubleSide - }); - - const borderMesh = new THREE.Mesh(borderGeometry, borderMaterial); - borderMesh.position.set(position.x, 0.25, position.z); - borderMesh.rotation.x = -Math.PI / 2; // Make it horizontal - - // Add to sea group - seaGroup.add(circleMesh); - seaGroup.add(waveMesh); - seaGroup.add(borderMesh); - - // Add animation for waves - if (!scene.userData.animatedObjects) { - scene.userData.animatedObjects = []; - } - - waveMesh.userData.waveAnimation = { - speed: 0.0015 + Math.random() * 0.001, - time: Math.random() * Math.PI * 2 - }; - - scene.userData.animatedObjects.push(waveMesh); - - } else { - // Land territory - color based on country - const color = country ? countryColors[country] : 0xb19b69; // Default to tan for neutral provinces - - // Create a more natural territory shape - const circleGeometry = new THREE.CircleGeometry(radius, 32); - const circleMaterial = new THREE.MeshStandardMaterial({ - color, - transparent: false, - side: THREE.DoubleSide, - metalness: 0.1, - roughness: 0.9 - }); - - const circleMesh = new THREE.Mesh(circleGeometry, circleMaterial); - circleMesh.position.set(position.x, 0.5, position.z); // Higher than sea - circleMesh.rotation.x = -Math.PI / 2; // Make it horizontal - - // Add a raised border around the territory (thinner for land) - const borderGeometry = new THREE.RingGeometry(radius - 0.5, radius + 3.5, 32); - - // Use contrasting border color based on territory color brightness - const brightness = getBrightness(color); - const borderColor = brightness > 0.6 ? 0x333333 : 0xdddddd; - - const borderMaterial = new THREE.MeshStandardMaterial({ - color: borderColor, - transparent: true, - opacity: 0.9, - side: THREE.DoubleSide - }); - - const borderMesh = new THREE.Mesh(borderGeometry, borderMaterial); - borderMesh.position.set(position.x, 0.55, position.z); // Slightly above territory - borderMesh.rotation.x = -Math.PI / 2; // Make it horizontal - - // Add subtle terrain texture - const terrainGeometry = new THREE.CircleGeometry(radius - 2, 32); - const terrainTexture = createTerrainTexture(country); - const terrainMaterial = new THREE.MeshStandardMaterial({ - color, - transparent: true, - opacity: 0.5, - alphaMap: terrainTexture, - side: THREE.DoubleSide - }); - - const terrainMesh = new THREE.Mesh(terrainGeometry, terrainMaterial); - terrainMesh.position.set(position.x, 0.6, position.z); - terrainMesh.rotation.x = -Math.PI / 2; - - // Store province data - circleMesh.userData = { - province, - country, - isSea: false - }; - - // Add to land group - landGroup.add(circleMesh); - landGroup.add(borderMesh); - landGroup.add(terrainMesh); - } - } - } - - // Add land and sea groups to the main territories group - territoriesGroup.add(seaGroup); // Sea goes first (below land) - territoriesGroup.add(landGroup); // Land on top - - // Add to scene - scene.add(territoriesGroup); - } - - // Create wave texture for sea territories - function createWaveTexture() { - const canvas = document.createElement('canvas'); - canvas.width = 128; - canvas.height = 128; - const ctx = canvas.getContext('2d'); - - // Create gradient background - const gradient = ctx.createRadialGradient(64, 64, 0, 64, 64, 64); - gradient.addColorStop(0, 'rgba(255, 255, 255, 0.1)'); - gradient.addColorStop(0.7, 'rgba(255, 255, 255, 0.05)'); - gradient.addColorStop(1, 'rgba(255, 255, 255, 0)'); - - ctx.fillStyle = gradient; - ctx.fillRect(0, 0, 128, 128); - - // Add wave patterns - ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; - ctx.lineWidth = 2; - - // Draw wavy lines - for (let i = 0; i < 8; i++) { - const y = i * 16 + 8; - - ctx.beginPath(); - ctx.moveTo(0, y); - - for (let x = 0; x < 128; x += 16) { - const amplitude = 4; - const yOffset = Math.sin(x / 16 + i) * amplitude; - - ctx.quadraticCurveTo( - x + 8, y + yOffset, - x + 16, y - ); - } - - ctx.stroke(); - } - - const texture = new THREE.CanvasTexture(canvas); - texture.wrapS = THREE.RepeatWrapping; - texture.wrapT = THREE.RepeatWrapping; - return texture; - } - - // Create terrain texture for land territories - function createTerrainTexture(country) { - const canvas = document.createElement('canvas'); - canvas.width = 128; - canvas.height = 128; - const ctx = canvas.getContext('2d'); - - // Different patterns based on country - if (country) { - // Color based on country - switch(country) { - case 'AUSTRIA': - drawMountainTexture(ctx); - break; - case 'ITALY': - drawMediterraneanTexture(ctx); - break; - case 'RUSSIA': - drawTundraTexture(ctx); - break; - case 'GERMANY': - drawForestTexture(ctx); - break; - case 'FRANCE': - drawPlainsTexture(ctx); - break; - case 'ENGLAND': - drawIslandTexture(ctx); - break; - case 'TURKEY': - drawAridTexture(ctx); - break; - default: - drawGenericLandTexture(ctx); - } - } else { - // Generic neutral territory - drawGenericLandTexture(ctx); - } - - const texture = new THREE.CanvasTexture(canvas); - return texture; - } - - // Utility to get brightness of a color - function getBrightness(hexColor) { - const r = (hexColor >> 16) & 255; - const g = (hexColor >> 8) & 255; - const b = hexColor & 255; - - // Convert to relative luminance - return (0.299 * r + 0.587 * g + 0.114 * b) / 255; - } - - // Draw various terrain textures - function drawMountainTexture(ctx) { - // Draw small triangle patterns for mountains - ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'; - - for (let i = 0; i < 15; i++) { - const x = Math.random() * 128; - const y = Math.random() * 128; - const size = 5 + Math.random() * 10; - - ctx.beginPath(); - ctx.moveTo(x, y + size); - ctx.lineTo(x - size / 2, y - size / 2); - ctx.lineTo(x + size / 2, y - size / 2); - ctx.fill(); - } - } - - function drawMediterraneanTexture(ctx) { - // Olive/vineyard-like pattern - ctx.fillStyle = 'rgba(255, 255, 255, 0.15)'; - - for (let i = 0; i < 30; i++) { - const x = Math.random() * 128; - const y = Math.random() * 128; - const radius = 2 + Math.random() * 4; - - ctx.beginPath(); - ctx.arc(x, y, radius, 0, Math.PI * 2); - ctx.fill(); - } - } - - function drawTundraTexture(ctx) { - // Sparse dots for tundra - ctx.fillStyle = 'rgba(255, 255, 255, 0.2)'; - - for (let i = 0; i < 80; i++) { - const x = Math.random() * 128; - const y = Math.random() * 128; - const radius = 1 + Math.random() * 2; - - ctx.beginPath(); - ctx.arc(x, y, radius, 0, Math.PI * 2); - ctx.fill(); - } - } - - function drawForestTexture(ctx) { - // Small tree-like shapes - ctx.fillStyle = 'rgba(255, 255, 255, 0.15)'; - - for (let i = 0; i < 20; i++) { - const x = Math.random() * 128; - const y = Math.random() * 128; - const size = 3 + Math.random() * 6; - - // Tree top - ctx.beginPath(); - ctx.arc(x, y - size/2, size, 0, Math.PI * 2); - ctx.fill(); - - // Trunk - ctx.fillRect(x - 1, y, 2, size); - } - } - - function drawPlainsTexture(ctx) { - // Horizontal line patterns for fields - ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; - ctx.lineWidth = 1; - - for (let i = 0; i < 12; i++) { - const y = i * 10 + Math.random() * 5; - - ctx.beginPath(); - ctx.moveTo(0, y); - ctx.lineTo(128, y); - ctx.stroke(); - } - } - - function drawIslandTexture(ctx) { - // Small dots and lines for coastal features - ctx.fillStyle = 'rgba(255, 255, 255, 0.15)'; - ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; - ctx.lineWidth = 1; - - // Dots - for (let i = 0; i < 50; i++) { - const x = Math.random() * 128; - const y = Math.random() * 128; - const radius = 1 + Math.random(); - - ctx.beginPath(); - ctx.arc(x, y, radius, 0, Math.PI * 2); - ctx.fill(); - } - - // Curved lines for terrain contours - for (let i = 0; i < 5; i++) { - const x1 = Math.random() * 128; - const y1 = Math.random() * 128; - const x2 = x1 + (Math.random() * 40 - 20); - const y2 = y1 + (Math.random() * 40 - 20); - - ctx.beginPath(); - ctx.moveTo(x1, y1); - ctx.quadraticCurveTo( - (x1 + x2) / 2 + (Math.random() * 20 - 10), - (y1 + y2) / 2 + (Math.random() * 20 - 10), - x2, y2 - ); - ctx.stroke(); - } - } - - function drawAridTexture(ctx) { - // Sand dune-like patterns - ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)'; - ctx.lineWidth = 1; - - for (let i = 0; i < 10; i++) { - const yBase = i * 12 + Math.random() * 5; - - ctx.beginPath(); - ctx.moveTo(0, yBase); - - for (let x = 0; x < 128; x += 16) { - const amplitude = 2 + Math.random() * 2; - const yOffset = Math.sin(x / 16) * amplitude; - - ctx.quadraticCurveTo( - x + 8, yBase + yOffset, - x + 16, yBase - ); - } - - ctx.stroke(); - } - } - - function drawGenericLandTexture(ctx) { - // Simple noise pattern - ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; - - for (let i = 0; i < 60; i++) { - const x = Math.random() * 128; - const y = Math.random() * 128; - const radius = 1 + Math.random() * 3; - - ctx.beginPath(); - ctx.arc(x, y, radius, 0, Math.PI * 2); - ctx.fill(); - } - } - - // Load coordinate data + // --- LOAD COORDINATE DATA --- function loadCoordinateData() { return new Promise((resolve, reject) => { fetch('./assets/maps/standard_coords.json') .then(response => { if (!response.ok) { - // Try alternate path + // Try an alternate path if desired return fetch('../assets/maps/standard_coords.json'); } return response; }) .then(response => { if (!response.ok) { - // Try another alternate path + // Another fallback path return fetch('/diplomacy/animation/assets/maps/standard_coords.json'); } return response; @@ -873,2013 +224,465 @@ }) .then(data => { coordinateData = data; - infoPanel.textContent = 'Coordinate data loaded successfully!'; - console.log('Loaded coordinates for', Object.keys(coordinateData.coordinates).length, 'provinces'); + infoPanel.textContent = 'Coordinate data loaded!'; resolve(coordinateData); }) .catch(error => { - console.error('Error loading coordinates:', error); - infoPanel.textContent = `Error loading coordinates: ${error.message}. Using placeholder positions.`; - // Resolve anyway so we can continue with placeholder positions - resolve(null); + console.error(error); + reject(error); }); }); } - - // Animation loop - function animate() { - requestAnimationFrame(animate); - - // Update pulsing animations for supply centers - if (scene.userData.animatedObjects) { - scene.userData.animatedObjects.forEach(obj => { - // Handle supply center pulsing - if (obj.userData.pulseAnimation) { - const anim = obj.userData.pulseAnimation; - anim.time += anim.speed; - - // Pulsing effect on the glow - if (obj.userData.glowMesh) { - const pulseValue = Math.sin(anim.time) * anim.intensity + 0.5; - obj.userData.glowMesh.material.opacity = 0.2 + (pulseValue * 0.3); - obj.userData.glowMesh.scale.set( - 1 + (pulseValue * 0.1), - 1 + (pulseValue * 0.1), - 1 + (pulseValue * 0.1) - ); - } - - // Subtle height movement - obj.position.y = 2 + (Math.sin(anim.time) * 0.5); - } - - // Handle wave animation for sea territories - if (obj.userData.waveAnimation) { - const anim = obj.userData.waveAnimation; - anim.time += anim.speed; - - // Rotate texture coordinates for wave effect - if (obj.material && obj.material.map) { - obj.material.map.offset.x = Math.sin(anim.time) * 0.05; - obj.material.map.offset.y = Math.cos(anim.time) * 0.05; - obj.material.map.needsUpdate = true; - } - } - }); + + // --- CREATE THE FALLBACK MAP AS A PLANE --- + function createFallbackMap() { + // If there's already a plane from a previous phase re-draw, remove it + if (mapPlane) { + scene.remove(mapPlane); + mapPlane.geometry.dispose(); + mapPlane.material.dispose(); + mapPlane = null; } - - controls.update(); - renderer.render(scene, camera); + + const mapGeometry = new THREE.PlaneGeometry(1000, 1000); + + // We'll create a texture by drawing on a : + const texture = drawFallbackCanvas(); + const material = new THREE.MeshBasicMaterial({ map: texture, side: THREE.DoubleSide }); + + mapPlane = new THREE.Mesh(mapGeometry, material); + mapPlane.rotation.x = -Math.PI / 2; // Lay flat + mapPlane.position.y = -1; // Slightly below any 3D items + scene.add(mapPlane); + + infoPanel.textContent = "Fallback map loaded as primary texture!"; } - - // Handle window resize - function onWindowResize() { - camera.aspect = mapView.clientWidth / mapView.clientHeight; - camera.updateProjectionMatrix(); - renderer.setSize(mapView.clientWidth, mapView.clientHeight); - } - - // Create a map with texture - function createMapWithTexture() { - // Create a large plane for the map - mapGeometry = new THREE.PlaneGeometry(1000, 1000); - - // Display loading status - infoPanel.textContent = 'Loading map texture...'; - - // Use the SVG map directly - const svgMapPaths = [ - './diplomacy/maps/svg/standard.svg', - '../maps/svg/standard.svg', - '../../maps/svg/standard.svg', - '/maps/svg/standard.svg', - '/diplomacy/maps/svg/standard.svg', - './maps/svg/standard.svg' // Add another path to try - ]; - - // First try to load the SVG map - return fetchSVGMap(svgMapPaths) - .then(svgData => { - if (svgData) { - // Convert SVG to image texture - const texture = createTextureFromSVG(svgData); - const material = new THREE.MeshBasicMaterial({ - map: texture, - side: THREE.DoubleSide - }); - - const mapMesh = new THREE.Mesh(mapGeometry, material); - mapMesh.rotation.x = -Math.PI / 2; // Make it horizontal - mapMesh.position.y = -1; // Slightly below other elements - scene.add(mapMesh); - - infoPanel.textContent = 'Map loaded successfully using SVG!'; - return mapMesh; - } else { - // Fallback to trying to load image textures - return tryLoadImageTextures(); - } - }) - .catch(error => { - console.error('Error loading SVG map:', error); - // Fallback to trying to load image textures - return tryLoadImageTextures(); - }); - } - - // Fetch SVG map from various possible paths - function fetchSVGMap(paths) { - const fetchPromises = paths.map(path => - fetch(path) - .then(response => { - if (!response.ok) { - return null; - } - return response.text(); - }) - .catch(() => null) - ); - - return Promise.all(fetchPromises).then(results => { - // Find the first successful result - const svgData = results.find(data => data !== null); - return svgData; - }); - } - - // Create a texture from SVG data - function createTextureFromSVG(svgData) { - // Create a data URL from the SVG - const blob = new Blob([svgData], {type: 'image/svg+xml'}); - const url = URL.createObjectURL(blob); - - // Load the texture - const texture = new THREE.TextureLoader().load(url, - // onLoad callback - function(texture) { - // Cleanup - URL.revokeObjectURL(url); - // Configure texture settings - texture.wrapS = THREE.ClampToEdgeWrapping; - texture.wrapT = THREE.ClampToEdgeWrapping; - texture.magFilter = THREE.LinearFilter; - texture.minFilter = THREE.LinearMipmapLinearFilter; - console.log('SVG texture loaded successfully'); - }, - // onProgress callback (not used) - undefined, - // onError callback - function(error) { - console.error('Error loading SVG texture:', error); - URL.revokeObjectURL(url); - } - ); - - return texture; - } - - // Try to load map image textures as fallback - function tryLoadImageTextures() { - // Try to load the map texture - const textureLoader = new THREE.TextureLoader(); - - // List of possible paths to try - const texturePaths = [ - './assets/maps/standard_map.jpg', - '../assets/maps/standard_map.jpg', - '/diplomacy/animation/assets/maps/standard_map.jpg', - './diplomacy/assets/maps/standard_map.jpg', // Add another path to try - // Remove the problematic base64 image that's causing errors - ]; - - // Add a resource checker that runs after all attempts - let textureLoadSuccess = false; - - // Attempt loading each texture until one works - return new Promise((resolve) => { - let currentIndex = 0; - - function tryNextTexture() { - if (currentIndex >= texturePaths.length) { - // If all paths failed, use enhanced fallback - return resolve(createFallbackMap()); - } - - console.log(`Attempting to load map texture from ${texturePaths[currentIndex]}`); - textureLoader.load( - texturePaths[currentIndex], - (texture) => { - // Check if texture is valid (has dimensions) - if (texture.image && texture.image.width > 0 && texture.image.height > 0) { - console.log(`Successfully loaded map texture from ${texturePaths[currentIndex]}`); - textureLoadSuccess = true; - infoPanel.textContent = 'Map texture loaded successfully'; - - // Configure texture settings for better appearance - texture.wrapS = THREE.ClampToEdgeWrapping; - texture.wrapT = THREE.ClampToEdgeWrapping; - texture.magFilter = THREE.LinearFilter; - texture.minFilter = THREE.LinearMipmapLinearFilter; - - const material = new THREE.MeshBasicMaterial({ - map: texture, - side: THREE.DoubleSide - }); - - // Create mesh with the texture - const mapMesh = new THREE.Mesh(mapGeometry, material); - mapMesh.rotation.x = -Math.PI / 2; // Make it horizontal - mapMesh.position.y = -1; // Slightly below other elements - scene.add(mapMesh); - - resolve(mapMesh); - } else { - console.warn(`Loaded texture from ${texturePaths[currentIndex]} appears invalid (empty or corrupt)`); - currentIndex++; - tryNextTexture(); - } - }, - undefined, // onProgress callback not needed - (error) => { - console.warn(`Failed to load texture from ${texturePaths[currentIndex]}: ${error.message}, trying next path`); - currentIndex++; - tryNextTexture(); - } - ); - } - - tryNextTexture(); - }); - } - - // Fix optimizeLabels to make labels always visible at normal zoom levels - function optimizeLabels() { - // Find all label sprites - const labels = scene.children.filter(child => - child.isSprite && child.userData && child.userData.isLabel); - - // First set all labels to visible by default and enlarge them - labels.forEach(label => { - label.visible = true; - // Make labels more visible by default - label.scale.set(100, 50, 1); // Larger size for better readability - }); - - // Register a camera change callback - controls.addEventListener('change', () => { - const distance = camera.position.y; // Height is a good approximation of distance for top-down view - - labels.forEach(label => { - // Only hide labels at very far distances (>800 instead of 400) - if (distance > 800) { - label.visible = false; - } else { - label.visible = true; - - // Adjust size based on distance for readable text - const scale = Math.max(0.8, Math.min(1.2, 800 / distance)); - label.scale.set(scale * 100, scale * 50, 1); - } - }); - }); - } - - // Improve the label function to create more visible labels - function addLabel(text, x, z) { + + // --- DRAW THE FALLBACK MAP ON A CANVAS --- + function drawFallbackCanvas() { const canvas = document.createElement('canvas'); - canvas.width = 128; - canvas.height = 64; + canvas.width = 2048; + canvas.height = 2048; + const ctx = canvas.getContext('2d'); - const context = canvas.getContext('2d'); + // Fill background with a radial gradient (sea-like) + const seaGradient = ctx.createRadialGradient( + canvas.width / 2, canvas.height / 2, 0, + canvas.width / 2, canvas.height / 2, canvas.width / 1.5 + ); + seaGradient.addColorStop(0, '#1a3c6e'); + seaGradient.addColorStop(0.7, '#2a5d9e'); + seaGradient.addColorStop(0.9, '#3973ac'); + seaGradient.addColorStop(1, '#4b8bc5'); + ctx.fillStyle = seaGradient; + ctx.fillRect(0, 0, canvas.width, canvas.height); - // Make label background more opaque for better readability - context.fillStyle = 'rgba(255, 255, 255, 0.9)'; - context.fillRect(0, 0, canvas.width, canvas.height); - context.strokeStyle = 'rgba(0, 0, 0, 0.8)'; - context.lineWidth = 3; // Thicker border - context.strokeRect(2, 2, canvas.width - 4, canvas.height - 4); - - // Use larger, bolder text for better visibility - context.font = 'bold 28px Arial'; - context.fillStyle = 'black'; - context.textAlign = 'center'; - context.textBaseline = 'middle'; - context.fillText(text, canvas.width / 2, canvas.height / 2); - - // Create texture and sprite - const texture = new THREE.CanvasTexture(canvas); - texture.minFilter = THREE.LinearFilter; // Prevent blurry text - const material = new THREE.SpriteMaterial({ - map: texture, - transparent: true, - depthTest: false, // Always show on top - sizeAttenuation: false // Keep consistent size regardless of distance - }); - const sprite = new THREE.Sprite(material); - - sprite.position.set(x, 40, z); // Position higher above the map for better visibility - sprite.scale.set(100, 50, 1); // Larger size for better readability - - // Mark as label for optimization - sprite.userData.isLabel = true; - sprite.userData.text = text; - - scene.add(sprite); - unitMeshes.push(sprite); // Add to unit meshes array for proper cleanup - - return sprite; + // If we have coordinateData, we can draw an "accurate" map: + if (coordinateData && coordinateData.coordinates) { + drawAccurateMap(ctx, canvas.width, canvas.height); + } else { + // Otherwise, just some placeholder + drawSimplifiedOcean(ctx, canvas.width, canvas.height); + } + + // If we have current game data (and a chosen phase), we can reflect supply center ownership: + // But note, this fallback draw is only called once here, so it won't "live-update" supply center + // colors as you press next/prev. If you want that, you can call createFallbackMap() again inside + // displayPhase() each time. For now, we just do it once on load. + + return new THREE.CanvasTexture(canvas); } - - // Display a unit on the map - function displayUnit(unitData) { - // Create more sophisticated unit representations - let geometry; - let color; - - // Color based on power - switch(unitData.power) { - case 'AUSTRIA': color = 0xFF0000; break; - case 'ENGLAND': color = 0x0000FF; break; - case 'FRANCE': color = 0x00FFFF; break; - case 'GERMANY': color = 0x000000; break; - case 'ITALY': color = 0x00FF00; break; - case 'RUSSIA': color = 0xFFFFFF; break; - case 'TURKEY': color = 0xFFFF00; break; - default: color = 0xAAAAAA; + + // --- MINIMAL EXAMPLE: Draw a quick "accidental polygons" map --- + // (This is the same core logic from the original fallback code, just shortened.) + function drawAccurateMap(ctx, width, height) { + // Borrowed from the original: scaling & offset for province coordinates + const scaleX = width / 1000; + const scaleY = height / 1000; + const offsetX = width / 2; + const offsetY = height / 2; + + // Fill ocean pattern + drawOceanBackground(ctx, width, height); + + // We can do "approx polygon" logic for each province, or do circles. + // For brevity, let's just do circles to represent each land province, ignoring seas: + for (const [prov, pos] of Object.entries(coordinateData.coordinates)) { + if (!prov.includes('_')) { + const x = pos.x * scaleX + offsetX; + const y = pos.z * scaleY + offsetY; + + // Draw a circle + ctx.beginPath(); + ctx.arc(x, y, 25, 0, Math.PI * 2); + ctx.fillStyle = '#b19b69'; // Land color + ctx.fill(); + ctx.strokeStyle = '#333'; + ctx.lineWidth = 2; + ctx.stroke(); + + // Province label + ctx.font = 'bold 20px Arial'; + ctx.fillStyle = '#000'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(prov, x, y); + } } - - // Different shapes for army vs fleet with more details - if (unitData.type === 'A') { // Army - // Create a more complex army unit (soldier-like) - geometry = new THREE.Group(); - - // Body (main cube) - const body = new THREE.Mesh( - new THREE.BoxGeometry(15, 20, 10), - new THREE.MeshStandardMaterial({ - color, - emissive: new THREE.Color(color).multiplyScalar(0.2), - metalness: 0.3, - roughness: 0.7 - }) - ); - geometry.add(body); - - // Head (smaller cube on top) - const head = new THREE.Mesh( - new THREE.BoxGeometry(8, 8, 8), - new THREE.MeshStandardMaterial({ - color, - emissive: new THREE.Color(color).multiplyScalar(0.2), - metalness: 0.3, - roughness: 0.7 - }) - ); - head.position.set(0, 14, 0); - geometry.add(head); - - // Add a spear or flag - const spearPole = new THREE.Mesh( - new THREE.CylinderGeometry(1, 1, 30, 8), - new THREE.MeshStandardMaterial({ color: 0x8B4513 }) - ); - spearPole.position.set(10, 5, 0); - spearPole.rotation.z = Math.PI / 6; // Tilt - geometry.add(spearPole); - - // Add a flag to the pole - const flag = new THREE.Mesh( - new THREE.BoxGeometry(15, 8, 1), - new THREE.MeshStandardMaterial({ - color, - emissive: new THREE.Color(color).multiplyScalar(0.4), - metalness: 0.1, - roughness: 0.9, - side: THREE.DoubleSide - }) - ); - flag.position.set(15, 15, 0); - flag.rotation.z = Math.PI / 6; // Match spear tilt - geometry.add(flag); - - // Add a base - const base = new THREE.Mesh( - new THREE.CylinderGeometry(12, 12, 3, 16), - new THREE.MeshStandardMaterial({ - color: 0x333333, - metalness: 0.5, - roughness: 0.5 - }) - ); - base.position.set(0, -10, 0); - geometry.add(base); - - } else { // Fleet - // Create a more sophisticated ship - geometry = new THREE.Group(); - - // Ship hull - const hull = new THREE.Mesh( - new THREE.BoxGeometry(30, 8, 15), - new THREE.MeshStandardMaterial({ - color: 0x8B4513, - metalness: 0.3, - roughness: 0.7 - }) - ); - geometry.add(hull); - - // Front point (bow) - const bow = new THREE.Mesh( - new THREE.ConeGeometry(7.5, 15, 4, 1), - new THREE.MeshStandardMaterial({ - color: 0x8B4513, - metalness: 0.3, - roughness: 0.7 - }) - ); - bow.rotation.set(0, 0, -Math.PI / 2); - bow.position.set(22.5, 0, 0); - geometry.add(bow); - - // Sail - const sail = new THREE.Mesh( - new THREE.PlaneGeometry(20, 25), - new THREE.MeshStandardMaterial({ - color, - emissive: new THREE.Color(color).multiplyScalar(0.2), - transparent: true, - opacity: 0.9, - side: THREE.DoubleSide - }) - ); - sail.rotation.set(0, Math.PI / 2, 0); - sail.position.set(0, 15, 0); - geometry.add(sail); - - // Mast - const mast = new THREE.Mesh( - new THREE.CylinderGeometry(1, 1, 30, 8), - new THREE.MeshStandardMaterial({ color: 0x8B4513 }) - ); - mast.position.set(0, 15, 0); - geometry.add(mast); - - // Base - const base = new THREE.Mesh( - new THREE.CylinderGeometry(12, 12, 3, 16), - new THREE.MeshStandardMaterial({ - color: 0x333333, - metalness: 0.5, - roughness: 0.5 - }) - ); - base.position.set(0, -8, 0); - geometry.add(base); + + // Optionally draw supply centers if coordinateData.provinces has them: + if (coordinateData.provinces) { + for (const [province, data] of Object.entries(coordinateData.provinces)) { + if (data.isSupplyCenter && coordinateData.coordinates[province]) { + const pos = coordinateData.coordinates[province]; + const x = pos.x * scaleX + offsetX; + const y = pos.z * scaleY + offsetY; + // Little star + ctx.beginPath(); + // simple star + starPath(ctx, x, y, 5, 10, 5); + ctx.fillStyle = '#FFD700'; + ctx.fill(); + ctx.strokeStyle = '#000'; + ctx.stroke(); + } + } } - - // Get position for this province - const position = getProvincePosition(unitData.location); - - // Create a unit container for the geometry - let unitMesh; - - // For simple meshes, the geometry is the mesh - if (unitData.type === 'A' || unitData.type === 'F') { - unitMesh = geometry; - unitMesh.position.set(position.x, 15, position.z); // Higher position for better visibility - } else { - // If something went wrong and we don't have proper geometry - const fallbackMaterial = new THREE.MeshStandardMaterial({ - color, - emissive: new THREE.Color(color).multiplyScalar(0.2), - metalness: 0.3, - roughness: 0.7 + } + + // Just a helper for the star shape + function starPath(ctx, cx, cy, spikes, outerR, innerR) { + let rot = Math.PI / 2 * 3; + let x = cx; + let y = cy; + const step = Math.PI / spikes; + + ctx.moveTo(cx, cy - outerR); + for (let i = 0; i < spikes; i++) { + x = cx + Math.cos(rot) * outerR; + y = cy + Math.sin(rot) * outerR; + ctx.lineTo(x, y); + rot += step; + + x = cx + Math.cos(rot) * innerR; + y = cy + Math.sin(rot) * innerR; + ctx.lineTo(x, y); + rot += step; + } + ctx.lineTo(cx, cy - outerR); + ctx.closePath(); + } + + // Draw some faint wave lines + function drawOceanBackground(ctx, width, height) { + ctx.save(); + ctx.strokeStyle = 'rgba(255, 255, 255, 0.05)'; + ctx.lineWidth = 2; + for (let i = 0; i < 40; i++) { + const x1 = Math.random() * width; + const y1 = Math.random() * height; + const len = 40 + Math.random() * 60; + const angle = Math.random() * Math.PI; + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo( + x1 + Math.cos(angle) * len, + y1 + Math.sin(angle) * len + ); + ctx.stroke(); + } + ctx.restore(); + } + + // If coordinate data isn't available, just do a big watery rectangle + function drawSimplifiedOcean(ctx, width, height) { + ctx.fillStyle = '#1a3c6e'; + ctx.fillRect(0, 0, width, height); + } + + // --- 3D SUPPLY CENTERS (Optional) --- + // If you want supply centers to be dynamic, keep these next two functions. + // If you'd rather rely on the fallback canvas's "star icons," remove them. + function displaySupplyCenters() { + if (!coordinateData || !coordinateData.provinces) return; + for (const [province, data] of Object.entries(coordinateData.provinces)) { + if (data.isSupplyCenter && coordinateData.coordinates[province]) { + const pos = getProvincePosition(province); + + // Build a small pillar + star in 3D + const scGroup = new THREE.Group(); + + const baseGeom = new THREE.CylinderGeometry(12, 12, 3, 16); + const baseMat = new THREE.MeshStandardMaterial({ color: 0x333333 }); + const base = new THREE.Mesh(baseGeom, baseMat); + base.position.y = 1.5; + scGroup.add(base); + + const pillarGeom = new THREE.CylinderGeometry(2.5, 2.5, 12, 8); + const pillarMat = new THREE.MeshStandardMaterial({ color: 0xcccccc }); + const pillar = new THREE.Mesh(pillarGeom, pillarMat); + pillar.position.y = 7.5; + scGroup.add(pillar); + + // We'll just do a cone star for simplicity + const starGeom = new THREE.ConeGeometry(6, 10, 5); + const starMat = new THREE.MeshStandardMaterial({ color: 0xFFD700 }); + const starMesh = new THREE.Mesh(starGeom, starMat); + starMesh.rotation.x = Math.PI; // point upwards + starMesh.position.y = 14; + scGroup.add(starMesh); + + // Optionally add a glow disc + const glowGeom = new THREE.CircleGeometry(15, 32); + const glowMat = new THREE.MeshBasicMaterial({ color: 0xFFFFAA, transparent: true, opacity: 0.3, side: THREE.DoubleSide }); + const glowMesh = new THREE.Mesh(glowGeom, glowMat); + glowMesh.rotation.x = -Math.PI / 2; + glowMesh.position.y = 2; + scGroup.add(glowMesh); + + // Store userData for ownership changes + scGroup.userData = { + province, + isSupplyCenter: true, + owner: null, + starMesh, + glowMesh + }; + + scGroup.position.set(pos.x, 2, pos.z); + scene.add(scGroup); + unitMeshes.push(scGroup); + } + } + } + + function updateSupplyCenterOwnership(centers) { + if (!centers) return; + const ownershipMap = {}; + // centers is typically { "AUSTRIA":["VIE","BUD"], "FRANCE":["PAR","MAR"], ... } + for (const [power, provinces] of Object.entries(centers)) { + provinces.forEach(p => { + ownershipMap[p.toUpperCase()] = power.toUpperCase(); }); - unitMesh = new THREE.Mesh(new THREE.BoxGeometry(20, 20, 20), fallbackMaterial); - unitMesh.position.set(position.x, 10, position.z); } - - // For fleets, rotate them to face a consistent direction - if (unitData.type === 'F') { - unitMesh.rotation.y = Math.PI / 4; + + // Basic color scheme + const powerColors = { + 'AUSTRIA': 0xFF0000, + 'ENGLAND': 0x0000FF, + 'FRANCE': 0x00FFFF, + 'GERMANY': 0x000000, + 'ITALY': 0x00FF00, + 'RUSSIA': 0xFFFFFF, + 'TURKEY': 0xFFFF00 + }; + + unitMeshes.forEach(obj => { + if (obj.userData && obj.userData.isSupplyCenter) { + const prov = obj.userData.province; + const owner = ownershipMap[prov]; + if (owner) { + const c = powerColors[owner] || 0xFFD700; + obj.userData.starMesh.material.color.setHex(c); + + // Add a pulsing animation + if (!obj.userData.pulseAnimation) { + obj.userData.pulseAnimation = { + speed: 0.003 + Math.random() * 0.002, + intensity: 0.3, + time: Math.random() * Math.PI * 2 + }; + if (!scene.userData.animatedObjects) scene.userData.animatedObjects = []; + scene.userData.animatedObjects.push(obj); + } + } else { + // Neutral + obj.userData.starMesh.material.color.setHex(0xFFD700); + // remove pulse + obj.userData.pulseAnimation = null; + } + } + }); + } + + // --- UNITS --- + function displayUnit(unitData) { + // Choose color by power + const powerColors = { + 'AUSTRIA': 0xFF0000, + 'ENGLAND': 0x0000FF, + 'FRANCE': 0x00FFFF, + 'GERMANY': 0x000000, + 'ITALY': 0x00FF00, + 'RUSSIA': 0xFFFFFF, + 'TURKEY': 0xFFFF00 + }; + const color = powerColors[unitData.power] || 0xAAAAAA; + + let group = new THREE.Group(); + // Minimal shape difference for armies vs fleets + if (unitData.type === 'A') { + // Army: a simple block + small flag + const body = new THREE.Mesh( + new THREE.BoxGeometry(15, 15, 15), + new THREE.MeshStandardMaterial({ color }) + ); + body.position.y = 7.5; + group.add(body); + } else { + // Fleet: a rectangle + a mast + const hull = new THREE.Mesh( + new THREE.BoxGeometry(30, 10, 15), + new THREE.MeshStandardMaterial({ color: 0x8B4513 }) + ); + hull.position.y = 5; + group.add(hull); + const sail = new THREE.Mesh( + new THREE.PlaneGeometry(20, 20), + new THREE.MeshStandardMaterial({ color, side: THREE.DoubleSide }) + ); + sail.position.y = 15; + sail.rotation.y = Math.PI / 2; + group.add(sail); } - - // Add to scene - scene.add(unitMesh); - - // Store metadata about the unit - unitMesh.userData = { - id: unitData.id, + + // Position + const pos = getProvincePosition(unitData.location); + group.position.set(pos.x, 10, pos.z); + + // Store meta + group.userData = { power: unitData.power, type: unitData.type, location: unitData.location }; - - // Add outline for highlighting (optional) - const outlineMaterial = new THREE.MeshBasicMaterial({ - color: 0xFFFFFF, - side: THREE.BackSide - }); - - // Track for cleanup - unitMeshes.push(unitMesh); - - return unitMesh; + + scene.add(group); + unitMeshes.push(group); } - - // Get proper position for a province using coordinate data - function getProvincePosition(location) { - // Handle both formats for special coast locations - const normalizedLocation = location.toUpperCase().replace('/', '_'); - const baseLocation = normalizedLocation.split('_')[0]; - - // If we have coordinate data, use it + + function getProvincePosition(loc) { + // Convert e.g. "Spa/sc" to "SPA_SC" if needed + const normalized = loc.toUpperCase().replace('/', '_'); + const base = normalized.split('_')[0]; + if (coordinateData && coordinateData.coordinates) { - // Try exact match first - if (coordinateData.coordinates[normalizedLocation]) { - return coordinateData.coordinates[normalizedLocation]; + if (coordinateData.coordinates[normalized]) { + return coordinateData.coordinates[normalized]; } - - // Then try base location - if (coordinateData.coordinates[baseLocation]) { - return coordinateData.coordinates[baseLocation]; + if (coordinateData.coordinates[base]) { + return coordinateData.coordinates[base]; } } - - // Fallback to hash function if we don't have the coordinates - console.warn(`No coordinates found for ${location}, using hash position`); - return hashStringToPosition(location); + // Fallback if missing + return hashStringToPosition(loc); } - - // Fallback hash function to position units when coordinates aren't available + function hashStringToPosition(str) { let hash = 0; for (let i = 0; i < str.length; i++) { - hash = ((hash << 5) - hash) + str.charCodeAt(i); - hash |= 0; // Convert to 32bit integer + hash = (hash << 5) - hash + str.charCodeAt(i); + hash |= 0; } - - // Use modulo to keep within bounds of map (-500 to 500) const x = (hash % 800) - 400; const z = ((hash >> 8) % 800) - 400; - return { x, y: 0, z }; } - - // Load a game file + + // --- LOADING & DISPLAYING GAME PHASES --- function loadGame(file) { const reader = new FileReader(); - - reader.onload = function(e) { + reader.onload = e => { try { - // Parse the JSON - let data = JSON.parse(e.target.result); - - console.log('Parsed game data:', data); - infoPanel.textContent = 'Loading game data...'; - - // Just use the data directly without conversion - gameData = data; - - - // Set phase index to start + gameData = JSON.parse(e.target.result); + infoPanel.textContent = `Game data loaded: ${gameData.phases?.length || 0} phases found.`; currentPhaseIndex = 0; - - // Check if phases exist - if (!gameData.phases || !Array.isArray(gameData.phases) || gameData.phases.length === 0) { - infoPanel.textContent = 'Error: No phases found in game data'; - return; + if (gameData.phases?.length) { + prevBtn.disabled = false; + nextBtn.disabled = false; + displayPhase(currentPhaseIndex); } - - // Enable navigation buttons - prevBtn.disabled = false; - nextBtn.disabled = false; - - // Display the first phase - displayPhase(currentPhaseIndex); - - infoPanel.textContent = `Game loaded successfully!\n${gameData.phases.length} phases found.`; - } catch (error) { - infoPanel.textContent = `Error loading game: ${error.message}`; - console.error('Error loading game:', error); + } catch (err) { + infoPanel.textContent = "Error parsing JSON: " + err.message; } }; - - reader.onerror = function() { - infoPanel.textContent = 'Error reading file'; + reader.onerror = () => { + infoPanel.textContent = "Error reading file."; }; - reader.readAsText(file); } - - // Display a specific phase + function displayPhase(index) { if (!gameData || !gameData.phases || index < 0 || index >= gameData.phases.length) { - infoPanel.textContent = 'Invalid phase index'; + infoPanel.textContent = "Invalid phase index."; return; } - - // Clean up previous unit meshes - unitMeshes.forEach(mesh => { - scene.remove(mesh); - }); + + // Clear old units + unitMeshes.forEach(m => scene.remove(m)); unitMeshes = []; - - // Get the current phase + + // Possibly re-draw the fallback if you want the canvas to show updated supply center ownership text: + // createFallbackMap(); // <-- you could call this here if needed + const phase = gameData.phases[index]; - infoPanel.textContent = `Displaying phase: ${phase.name || 'Unknown'}`; - - // Update phase display header - phaseDisplay.textContent = `${phase.name} (${index + 1}/${gameData.phases.length})`; - - // Check if we have state data - if (!phase.state) { - infoPanel.textContent += '\nNo state data found in this phase'; - return; - } - - // Update supply centers if available - if (phase.state.centers) { + phaseDisplay.textContent = `${phase.name || 'Unknown Phase'} (${index + 1}/${gameData.phases.length})`; + + // 1) Show supply centers (3D approach) + displaySupplyCenters(); + + // 2) If phase has supply center ownership data + if (phase.state?.centers) { updateSupplyCenterOwnership(phase.state.centers); - infoPanel.textContent += `\nSupply centers updated`; } - - // Process units - check if exists in the state object - if (phase.state.units) { - const unitsList = []; - - // Process units from state (format is power -> array of unit strings) - // Example: {"AUSTRIA":["A VIE","A BUD","F TRI"],"ENGLAND":["F LON","F EDI","A LVP"], ... } - for (const [power, units] of Object.entries(phase.state.units)) { - if (Array.isArray(units)) { - units.forEach(unitStr => { - // Parse unit string (format: "A VIE" or "F LON") - const match = unitStr.match(/^([AF])\s+(.+)$/); - if (match) { - const unitType = match[1]; // 'A' or 'F' - const location = match[2]; // 'VIE', 'LON', etc. - - unitsList.push({ - power: power.toUpperCase(), - type: unitType, - location: location, - id: `${power}-${unitType}-${location}` - }); - } - }); - } + + // 3) Show units + if (phase.state?.units) { + for (const [power, unitArr] of Object.entries(phase.state.units)) { + unitArr.forEach(unitStr => { + const match = unitStr.match(/^([AF])\s+(.+)$/); + if (match) { + displayUnit({ + power: power.toUpperCase(), + type: match[1], + location: match[2], + }); + } + }); } - - console.log(`Found ${unitsList.length} units to display:`, unitsList); - - // Display each unit - unitsList.forEach(unit => { - try { - displayUnit(unit); - } catch (error) { - console.error(`Error displaying unit ${unit.power} ${unit.type} ${unit.location}:`, error); - } - }); - - infoPanel.textContent += `\nDisplayed ${unitsList.length} units on the map`; - } else { - infoPanel.textContent += '\nNo units found in this phase'; } - - // Process orders if available - if (phase.orders) { - let orderInfo = 'Orders:\n'; - let orderCount = 0; - - // Handle object format of orders - for (const [power, orders] of Object.entries(phase.orders)) { - if (Array.isArray(orders)) { - orders.forEach(order => { - orderInfo += `${power.toUpperCase()}: ${order}\n`; - orderCount++; - }); - } - } - - if (orderCount > 0) { - infoPanel.textContent = orderInfo; - } else { - infoPanel.textContent += '\nNo orders found in this phase'; - } - } else { - infoPanel.textContent += '\nNo orders found in this phase'; - } - - // Log info about this phase - console.log(`Displaying phase ${index + 1}/${gameData.phases.length}:`, phase); - console.log(`- Units: ${unitsList ? unitsList.length : 0}`); - console.log(`- Orders: ${phase.orders ? Object.values(phase.orders).flat().length : 0}`); + + // Show some info in the panel + infoPanel.textContent = `Phase: ${phase.name}\nSupply centers: ${ + phase.state?.centers ? JSON.stringify(phase.state.centers) : 'None' + }\nUnits: ${ + phase.state?.units ? JSON.stringify(phase.state.units) : 'None' + }`; } - - // Display supply centers as yellow cylinders - function displaySupplyCenters() { - if (!coordinateData || !coordinateData.provinces) { - console.warn('No province data available for supply centers'); - return; - } - - // Create a more sophisticated supply center representation - for (const [province, data] of Object.entries(coordinateData.provinces)) { - // Check if this province is a supply center - if (data.isSupplyCenter) { - // Get the position for this province - const position = getProvincePosition(province); - - // Create a group for the supply center elements - const scGroup = new THREE.Group(); - - // Base (platform) - const baseGeometry = new THREE.CylinderGeometry(15, 18, 3, 16); - const baseMaterial = new THREE.MeshStandardMaterial({ - color: 0x333333, - metalness: 0.6, - roughness: 0.3 - }); - const base = new THREE.Mesh(baseGeometry, baseMaterial); - base.position.y = 1; - scGroup.add(base); - - // Center pillar - const pillarGeometry = new THREE.CylinderGeometry(3, 3, 12, 8); - const pillarMaterial = new THREE.MeshStandardMaterial({ - color: 0xCCCCCC, - metalness: 0.8, - roughness: 0.2 - }); - const pillar = new THREE.Mesh(pillarGeometry, pillarMaterial); - pillar.position.y = 7; - scGroup.add(pillar); - - // Star (using custom geometry) - const starPoints = 5; - const starOuterRadius = 10; - const starInnerRadius = 5; - const starGeometry = new THREE.BufferGeometry(); - - // Create star shape vertices - const vertices = []; - const angle = Math.PI * 2 / starPoints; - - for (let i = 0; i < starPoints; i++) { - // Outer point - const outerAngle = i * angle; - vertices.push( - Math.cos(outerAngle) * starOuterRadius, - 0, - Math.sin(outerAngle) * starOuterRadius - ); - - // Inner point - const innerAngle = outerAngle + angle / 2; - vertices.push( - Math.cos(innerAngle) * starInnerRadius, - 0, - Math.sin(innerAngle) * starInnerRadius - ); - } - - // Create faces (triangles) - const indices = []; - for (let i = 0; i < starPoints * 2 - 2; i++) { - indices.push(0, i + 1, i + 2); - } - indices.push(0, starPoints * 2 - 1, 1); - - starGeometry.setIndex(indices); - starGeometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3)); - starGeometry.computeVertexNormals(); - - // Star material (yellow by default, will update based on ownership) - const starMaterial = new THREE.MeshStandardMaterial({ - color: 0xFFD700, - emissive: 0x666600, - metalness: 0.8, - roughness: 0.2, - side: THREE.DoubleSide - }); - - const starMesh = new THREE.Mesh(starGeometry, starMaterial); - starMesh.rotation.x = -Math.PI / 2; // Make it horizontal - starMesh.position.y = 14; // Above the pillar - scGroup.add(starMesh); - - // Add a subtle glow effect - const glowGeometry = new THREE.CircleGeometry(15, 32); - const glowMaterial = new THREE.MeshBasicMaterial({ - color: 0xFFFFAA, - transparent: true, - opacity: 0.3, - side: THREE.DoubleSide - }); - const glowMesh = new THREE.Mesh(glowGeometry, glowMaterial); - glowMesh.rotation.x = -Math.PI / 2; // Make it horizontal - glowMesh.position.y = 1.5; // Just above the base - scGroup.add(glowMesh); - - // Position the entire group - scGroup.position.set(position.x, 2, position.z); - - // Store center data for ownership updates - scGroup.userData = { - province: province, - isSupplyCenter: true, - owner: null, - starMesh: starMesh, - glowMesh: glowMesh - }; - - // Add to the scene - scene.add(scGroup); - unitMeshes.push(scGroup); // Add to unit meshes for cleanup - } - } - } - - // Update supply center ownership - function updateSupplyCenterOwnership(centers) { - if (!centers) return; - - // Create a map of province to power - const centerOwnership = {}; - - // Process centers from the game data - for (const [power, powerCenters] of Object.entries(centers)) { - if (Array.isArray(powerCenters)) { - powerCenters.forEach(center => { - centerOwnership[center.toUpperCase()] = power.toUpperCase(); - }); - } - } - - // Power-specific colors for consistency - const powerColors = { - 'AUSTRIA': 0xFF0000, - 'ENGLAND': 0x0000FF, - 'FRANCE': 0x00FFFF, - 'GERMANY': 0x000000, - 'ITALY': 0x00FF00, - 'RUSSIA': 0xFFFFFF, - 'TURKEY': 0xFFFF00 - }; - - // Update the colors of the supply center markers - unitMeshes.forEach(mesh => { - if (mesh.userData.isSupplyCenter) { - const province = mesh.userData.province; - const owner = centerOwnership[province]; - - // Store the owner for future reference - mesh.userData.owner = owner; - - if (owner && mesh.userData.starMesh) { - // Get color for this power - const color = powerColors[owner] || 0xAAAAAA; - - // Update star color to match owner - mesh.userData.starMesh.material.color.set(color); - mesh.userData.starMesh.material.emissive.set(new THREE.Color(color).multiplyScalar(0.3)); - - // Update glow color to match owner (with transparency) - if (mesh.userData.glowMesh) { - mesh.userData.glowMesh.material.color.set(color); - } - - // Add a pulsing animation to owned supply centers - if (!mesh.userData.pulseAnimation) { - mesh.userData.pulseAnimation = { - speed: 0.003 + Math.random() * 0.002, // Slightly randomized speed - intensity: 0.3, - time: Math.random() * Math.PI * 2 // Random starting phase - }; - - // Add this mesh to a list of animated objects - if (!scene.userData.animatedObjects) { - scene.userData.animatedObjects = []; - } - scene.userData.animatedObjects.push(mesh); - } - } else { - // Reset to default yellow for unowned centers - if (mesh.userData.starMesh) { - mesh.userData.starMesh.material.color.set(0xFFD700); - mesh.userData.starMesh.material.emissive.set(0x666600); - } - if (mesh.userData.glowMesh) { - mesh.userData.glowMesh.material.color.set(0xFFFFAA); - } - - // Remove any animation - mesh.userData.pulseAnimation = null; - - // Remove from animated objects list if it exists - if (scene.userData.animatedObjects) { - const index = scene.userData.animatedObjects.indexOf(mesh); - if (index !== -1) { - scene.userData.animatedObjects.splice(index, 1); - } - } - } - } - }); - } - - // Event listeners - loadBtn.addEventListener('click', () => { - fileInput.click(); - }); - - fileInput.addEventListener('change', (event) => { - const file = event.target.files[0]; + + // --- EVENT HANDLERS --- + loadBtn.addEventListener('click', () => fileInput.click()); + fileInput.addEventListener('change', e => { + const file = e.target.files[0]; if (file) { loadGame(file); } }); - + prevBtn.addEventListener('click', () => { if (currentPhaseIndex > 0) { currentPhaseIndex--; displayPhase(currentPhaseIndex); } }); - nextBtn.addEventListener('click', () => { if (gameData && currentPhaseIndex < gameData.phases.length - 1) { currentPhaseIndex++; displayPhase(currentPhaseIndex); } }); - - // Add interactive hovering and selection for territories - function addTerritoryInteractivity() { - // Create a raycaster for mouse interaction - const raycaster = new THREE.Raycaster(); - const mouse = new THREE.Vector2(); - - // Track hovering and selection - let hoveredObject = null; - let selectedObject = null; - - // Detail panel for territory information - const detailPanel = document.createElement('div'); - detailPanel.style.position = 'absolute'; - detailPanel.style.top = '80px'; - detailPanel.style.left = '20px'; - detailPanel.style.backgroundColor = 'rgba(0, 0, 0, 0.7)'; - detailPanel.style.color = 'white'; - detailPanel.style.padding = '10px'; - detailPanel.style.borderRadius = '5px'; - detailPanel.style.display = 'none'; - detailPanel.style.zIndex = '100'; - detailPanel.style.maxWidth = '300px'; - document.body.appendChild(detailPanel); - - // Handle mouse move for hovering - mapView.addEventListener('mousemove', (event) => { - // Calculate mouse position in normalized device coordinates - const rect = renderer.domElement.getBoundingClientRect(); - mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; - mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; - - // Cast a ray from the camera through the mouse position - raycaster.setFromCamera(mouse, camera); - - // Find intersections with territory objects - const territories = scene.getObjectByName('territories'); - let intersects = []; - - if (territories) { - // Only check for intersections with territory circles (not borders) - const territoryCircles = territories.children.filter(child => - child.geometry instanceof THREE.CircleGeometry); - - intersects = raycaster.intersectObjects(territoryCircles); - } - - // Add supply centers to intersect check - const supplyCenters = scene.children.filter(child => - child.userData && child.userData.isSupplyCenter); - - intersects = intersects.concat(raycaster.intersectObjects(supplyCenters)); - - // Handle hover highlights - if (intersects.length > 0) { - const intersectedObject = intersects[0].object; - - // Skip if we're already hovering this object - if (hoveredObject === intersectedObject) return; - - // Remove highlight from previous hovered object - if (hoveredObject) { - if (hoveredObject.userData.originalScale) { - hoveredObject.scale.copy(hoveredObject.userData.originalScale); - } - if (hoveredObject.material && hoveredObject.userData.originalEmissive) { - hoveredObject.material.emissive.copy(hoveredObject.userData.originalEmissive); - } - } - - // Add highlight to new hovered object - hoveredObject = intersectedObject; - - // Store original scale for restoration - hoveredObject.userData.originalScale = hoveredObject.scale.clone(); - - // Slightly enlarge the hovered object - hoveredObject.scale.multiplyScalar(1.1); - - // Show detail panel with territory information - const territoryData = getObjectData(hoveredObject); - if (territoryData) { - detailPanel.innerHTML = territoryData; - detailPanel.style.display = 'block'; - - // Position the panel near the mouse but avoid edge clipping - detailPanel.style.left = `${Math.min(event.clientX + 20, window.innerWidth - 320)}px`; - detailPanel.style.top = `${Math.min(event.clientY - 30, window.innerHeight - 200)}px`; - } - - // Add glow effect if material has emissive property - if (hoveredObject.material && hoveredObject.material.emissive) { - hoveredObject.userData.originalEmissive = hoveredObject.material.emissive.clone(); - hoveredObject.material.emissive.set(0x444444); - } - - // Change cursor to indicate interactive element - renderer.domElement.style.cursor = 'pointer'; - } else { - // No intersections, restore previous hovered object - if (hoveredObject) { - if (hoveredObject.userData.originalScale) { - hoveredObject.scale.copy(hoveredObject.userData.originalScale); - } - if (hoveredObject.material && hoveredObject.userData.originalEmissive) { - hoveredObject.material.emissive.copy(hoveredObject.userData.originalEmissive); - } - hoveredObject = null; - - // Hide detail panel - detailPanel.style.display = 'none'; - - // Reset cursor - renderer.domElement.style.cursor = 'auto'; - } - } - }); - - // Handle click for selection - mapView.addEventListener('click', () => { - if (hoveredObject) { - if (selectedObject) { - // Deselect if clicking the same object - if (selectedObject === hoveredObject) { - if (selectedObject.material) { - selectedObject.material.emissive.copy(selectedObject.userData.originalEmissive || new THREE.Color(0x000000)); - } - selectedObject = null; - return; - } - - // Deselect previous selection - if (selectedObject.material) { - selectedObject.material.emissive.copy(selectedObject.userData.originalEmissive || new THREE.Color(0x000000)); - } - } - - // Select new object - selectedObject = hoveredObject; - - // Add permanent highlight to selected object - if (selectedObject.material) { - selectedObject.material.emissive.set(0x666666); - } - - // Update info panel with details - if (selectedObject.userData.province) { - // Show province details in the main info panel - showProvinceDetails(selectedObject.userData.province); - } - } - }); - - function getObjectData(object) { - // Get data from territory or supply center - let html = ''; - - // For territories - if (object.userData.province) { - const province = object.userData.province; - const country = object.userData.country; - - html = `
${province}
`; - - if (country) { - html += `
Country: ${country}
`; - } - - if (object.userData.isSupplyCenter || - (object.parent && object.parent.userData && object.parent.userData.isSupplyCenter)) { - html += `
Supply Center
`; - - // If owned, show owner - const owner = object.userData.owner || (object.parent && object.parent.userData.owner); - if (owner) { - html += `
Controlled by: ${owner}
`; - } - } - - // Add any units in this location - const units = unitMeshes.filter(unit => - unit.userData && unit.userData.location === province); - - if (units.length > 0) { - html += `
Units:
`; - - units.forEach(unit => { - if (unit.userData.power && unit.userData.type) { - html += `
${unit.userData.power} ${unit.userData.type}
`; - } - }); - } - } - - return html; - } - - function showProvinceDetails(province) { - // Prepare details for the main info panel - let details = `Province: ${province}
`; - - // Look for units at this location in the current phase - if (gameData && gameData.phases && currentPhaseIndex >= 0) { - const phase = gameData.phases[currentPhaseIndex]; - - // Check for units - if (phase.state && phase.state.units) { - for (const [power, units] of Object.entries(phase.state.units)) { - if (Array.isArray(units)) { - units.forEach(unitStr => { - const match = unitStr.match(/^([AF])\s+(.+)$/); - if (match && match[2] === province) { - details += `
Unit: ${power.toUpperCase()} ${match[1]} (${match[1] === 'A' ? 'Army' : 'Fleet'})
`; - } - }); - } - } - } - - // Check for supply center ownership - if (phase.state && phase.state.centers) { - for (const [power, centers] of Object.entries(phase.state.centers)) { - if (Array.isArray(centers) && centers.includes(province)) { - details += `
Supply Center owned by: ${power.toUpperCase()}
`; - break; - } - } - } - - // Check for orders affecting this province - if (phase.orders) { - details += `
Orders:
`; - let ordersFound = false; - - for (const [power, orders] of Object.entries(phase.orders)) { - if (Array.isArray(orders)) { - orders.forEach(order => { - if (order.includes(province)) { - details += `${power.toUpperCase()}: ${order}
`; - ordersFound = true; - } - }); - } - } - - if (!ordersFound) { - details += "No orders involving this province
"; - } - } - } - - // Update the info panel - infoPanel.innerHTML = details; - } - } - - // Create fallback texture with enhanced visual appearance - function createFallbackMap() { - console.warn('Using enhanced fallback map due to texture loading failure'); - infoPanel.textContent = 'Using enhanced diplomacy map (custom rendered)'; - - // Create a canvas for dynamic map rendering - const canvas = document.createElement('canvas'); - canvas.width = 2048; - canvas.height = 2048; - const ctx = canvas.getContext('2d'); - - // Fill with a gradient sea background - const seaGradient = ctx.createRadialGradient( - canvas.width / 2, canvas.height / 2, 0, - canvas.width / 2, canvas.height / 2, canvas.width / 1.5 - ); - seaGradient.addColorStop(0, '#1a3c6e'); // Deep blue center - seaGradient.addColorStop(0.7, '#2a5d9e'); // Mid-tone blue - seaGradient.addColorStop(0.9, '#3973ac'); // Lighter blue edges - seaGradient.addColorStop(1, '#4b8bc5'); // Very light blue at extremes - - ctx.fillStyle = seaGradient; - ctx.fillRect(0, 0, canvas.width, canvas.height); - - // Draw accurate map with province boundaries - if (coordinateData && coordinateData.coordinates) { - // Draw the map with accurate territory boundaries - drawAccurateMap(ctx, canvas.width, canvas.height); - } else { - // Fallback to simplified country outlines if no coordinate data - drawSimplifiedMap(ctx, canvas.width, canvas.height); - } - - // Create texture from canvas - const texture = new THREE.CanvasTexture(canvas); - texture.wrapS = THREE.ClampToEdgeWrapping; - texture.wrapT = THREE.ClampToEdgeWrapping; - texture.magFilter = THREE.LinearFilter; - texture.minFilter = THREE.LinearMipmapLinearFilter; - - const material = new THREE.MeshBasicMaterial({ - map: texture, - side: THREE.DoubleSide - }); - - // Make sure we have geometry available - if (!mapGeometry) { - mapGeometry = new THREE.PlaneGeometry(1000, 1000); - } - - const mapMesh = new THREE.Mesh(mapGeometry, material); - mapMesh.rotation.x = -Math.PI / 2; // Make it horizontal - mapMesh.position.y = -1; // Slightly below other elements - scene.add(mapMesh); - - // Create invisible territory hitboxes for interaction - createInvisibleTerritoryHitboxes(); - - return mapMesh; - } - - // Draw accurate map with territory boundaries - function drawAccurateMap(ctx, width, height) { - const scaleX = width / 1000; - const scaleY = height / 1000; - const offsetX = width / 2; - const offsetY = height / 2; - - // Group provinces by country (for default coloring) - const countries = { - 'AUSTRIA': ['VIE', 'BUD', 'TRI', 'GAL', 'BOH', 'TYR'], - 'ENGLAND': ['LON', 'EDI', 'LVP', 'WAL', 'YOR', 'CLY'], - 'FRANCE': ['PAR', 'BRE', 'MAR', 'PIC', 'GAS', 'BUR'], - 'GERMANY': ['BER', 'MUN', 'KIE', 'RUH', 'PRU', 'SIL'], - 'ITALY': ['ROM', 'VEN', 'NAP', 'TUS', 'PIE', 'APU'], - 'RUSSIA': ['STP', 'MOS', 'WAR', 'SEV', 'UKR', 'LVN'], - 'TURKEY': ['CON', 'ANK', 'SMY', 'SYR', 'ARM'] - }; - - const seaProvinces = [ - 'NAO', 'NWG', 'BAR', 'NTH', 'SKA', 'HEL', 'BAL', 'BOT', 'ENG', 'IRI', 'MAO', - 'WES', 'LYO', 'TYS', 'ADR', 'ION', 'AEG', 'EAS', 'BLA' - ]; - - const countryColors = { - 'AUSTRIA': '#d82b2b', - 'ENGLAND': '#223a87', - 'FRANCE': '#3699d4', - 'GERMANY': '#232323', - 'ITALY': '#35a335', - 'RUSSIA': '#cccccc', - 'TURKEY': '#e0c846' - }; - - // Map power to color (consistent with unit colors in displayUnit) - const powerColors = { - 'AUSTRIA': '#FF0000', - 'ENGLAND': '#0000FF', - 'FRANCE': '#00FFFF', - 'GERMANY': '#000000', - 'ITALY': '#00FF00', - 'RUSSIA': '#FFFFFF', - 'TURKEY': '#FFFF00' - }; - - const neutralLandColor = '#b19b69'; - - // Get current phase data - let supplyCenterOwnership = {}; - let unitOccupancy = {}; - - if (gameData && gameData.phases && currentPhaseIndex >= 0) { - const phase = gameData.phases[currentPhaseIndex]; - - // Supply center ownership - if (phase.state && phase.state.centers) { - for (const [power, centers] of Object.entries(phase.state.centers)) { - if (Array.isArray(centers)) { - centers.forEach(center => { - supplyCenterOwnership[center.toUpperCase()] = power.toUpperCase(); - }); - } - } - } - - // Unit occupancy - if (phase.state && phase.state.units) { - for (const [power, units] of Object.entries(phase.state.units)) { - if (Array.isArray(units)) { - units.forEach(unitStr => { - const match = unitStr.match(/^([AF])\s+(.+)$/); - if (match) { - const location = match[2].toUpperCase(); - unitOccupancy[location] = power.toUpperCase(); - } - }); - } - } - } - } - - // Draw sea territories - for (const province of seaProvinces) { - if (coordinateData.coordinates[province]) { - const position = coordinateData.coordinates[province]; - const x = (position.x * scaleX) + offsetX; - const y = (position.z * scaleY) + offsetY; - - const connectedProvinces = getConnectedProvinces(province); - - // Color sea based on unit occupancy, if any - const occupyingPower = unitOccupancy[province]; - const color = occupyingPower ? powerColors[occupyingPower] : '#1a3c6e'; - const opacity = occupyingPower ? 0.9 : 0.7; - - if (connectedProvinces.length > 0) { - drawTerritoryPolygon(ctx, province, connectedProvinces, color, opacity); - } else { - const radius = 35; - const seaGradient = ctx.createRadialGradient(x, y, 0, x, y, radius); - seaGradient.addColorStop(0, color); - seaGradient.addColorStop(1, '#1a3c6e'); - ctx.beginPath(); - ctx.arc(x, y, radius, 0, Math.PI * 2); - ctx.fillStyle = seaGradient; - ctx.fill(); - ctx.strokeStyle = '#0a1a3c'; - ctx.lineWidth = 2; - ctx.stroke(); - - drawWavePattern(ctx, x, y, 30); - } - } - } - - // Draw land territories - const drawnProvinces = new Set(); - - // First, draw by country for default coloring - for (const [country, provinces] of Object.entries(countries)) { - const defaultColor = countryColors[country]; - - for (const province of provinces) { - if (coordinateData.coordinates[province] && !drawnProvinces.has(province)) { - const position = coordinateData.coordinates[province]; - const x = (position.x * scaleX) + offsetX; - const y = (position.z * scaleY) + offsetY; - - // Determine color based on supply center or unit - let color = defaultColor; - if (supplyCenterOwnership[province]) { - color = powerColors[supplyCenterOwnership[province]]; - } else if (unitOccupancy[province]) { - color = powerColors[unitOccupancy[province]]; - } - - const connectedProvinces = getConnectedProvinces(province); - - if (connectedProvinces.length > 0) { - drawTerritoryPolygon(ctx, province, connectedProvinces, color, 0.8); - } else { - drawNaturalTerritory(ctx, x, y, 33, color); - } - - drawnProvinces.add(province); - } - } - } - - // Draw remaining neutral provinces - for (const [province, position] of Object.entries(coordinateData.coordinates)) { - if (!province.includes('_') && !drawnProvinces.has(province) && !seaProvinces.includes(province)) { - const x = (position.x * scaleX) + offsetX; - const y = (position.z * scaleY) + offsetY; - - // Determine color based on supply center or unit - let color = neutralLandColor; - if (supplyCenterOwnership[province]) { - color = powerColors[supplyCenterOwnership[province]]; - } else if (unitOccupancy[province]) { - color = powerColors[unitOccupancy[province]]; - } - - const connectedProvinces = getConnectedProvinces(province); - - if (connectedProvinces.length > 0) { - drawTerritoryPolygon(ctx, province, connectedProvinces, color, 0.8); - } else { - drawNaturalTerritory(ctx, x, y, 33, color); - } - - drawnProvinces.add(province); - } - } - - drawTerritoryBorders(ctx, width, height); - drawSupplyCenters(ctx, width, height); - drawProvinceNames(ctx, width, height); - } - - // Helper function to get connected provinces (uses adjacency data or approximates) - function getConnectedProvinces(province) { - // If we have explicit adjacency data, use it - const connections = { - // These are approximate connections to create more realistic territory shapes - // Generally, each territory connects to 3-6 neighbors - 'LON': ['WAL', 'YOR', 'NTH', 'ENG'], - 'EDI': ['CLY', 'YOR', 'NWG', 'NTH'], - 'LVP': ['CLY', 'EDI', 'WAL', 'IRI'], - 'WAL': ['LON', 'LVP', 'IRI', 'ENG'], - 'YOR': ['LON', 'EDI', 'LVP', 'NTH'], - 'CLY': ['EDI', 'LVP', 'NAO', 'NWG'], - - 'BRE': ['PIC', 'PAR', 'GAS', 'MAO', 'ENG'], - 'PAR': ['BRE', 'PIC', 'BUR', 'GAS'], - 'MAR': ['GAS', 'BUR', 'PIE', 'SPA', 'LYO'], - 'PIC': ['BRE', 'PAR', 'BUR', 'BEL', 'ENG'], - 'GAS': ['BRE', 'PAR', 'BUR', 'MAR', 'SPA', 'MAO'], - 'BUR': ['PIC', 'PAR', 'GAS', 'MAR', 'MUN', 'RUH', 'BEL'], - - // Add more connections for other territories - // This is just a sample - a complete map would define all connections - }; - - if (connections[province]) { - return connections[province]; - } - - // If we don't have explicit connections, approximate based on coordinates - const result = []; - - if (!coordinateData || !coordinateData.coordinates || !coordinateData.coordinates[province]) { - return result; - } - - const pos = coordinateData.coordinates[province]; - const threshold = 150; // Distance threshold to consider provinces connected - - for (const [otherProvince, otherPos] of Object.entries(coordinateData.coordinates)) { - if (otherProvince !== province && !otherProvince.includes('_')) { - // Calculate distance between provinces - const dx = pos.x - otherPos.x; - const dz = pos.z - otherPos.z; - const distance = Math.sqrt(dx * dx + dz * dz); - - if (distance < threshold) { - result.push(otherProvince); - } - } - } - - return result; - } - - // Draw a territory polygon based on connected provinces - function drawTerritoryPolygon(ctx, province, connectedProvinces, color, opacity) { - if (!coordinateData || !coordinateData.coordinates) return; - - const position = coordinateData.coordinates[province]; - if (!position) return; - - const scaleX = ctx.canvas.width / 1000; - const scaleY = ctx.canvas.height / 1000; - const offsetX = ctx.canvas.width / 2; - const offsetY = ctx.canvas.height / 2; - - const x = (position.x * scaleX) + offsetX; - const y = (position.z * scaleY) + offsetY; - - const isSea = ['NAO', 'NWG', 'BAR', 'NTH', 'SKA', 'HEL', 'BAL', 'BOT', 'ENG', 'IRI', 'MAO', - 'WES', 'LYO', 'TYS', 'ADR', 'ION', 'AEG', 'EAS', 'BLA'].includes(province); - - // Create points for the polygon - const points = []; - const center = { x, y }; - - for (const connectedProvince of connectedProvinces) { - if (coordinateData.coordinates[connectedProvince]) { - const connPos = coordinateData.coordinates[connectedProvince]; - const connX = (connPos.x * scaleX) + offsetX; - const connY = (connPos.z * scaleY) + offsetY; - - const midX = x + (connX - x) * 0.4; - const midY = y + (connY - y) * 0.4; - - const dx = connX - x; - const dy = connY - y; - const dist = Math.sqrt(dx * dx + dy * dy); - - const perpX = dy / dist; - const perpY = -dx / dist; - - const scale = 40; - const pointX = midX + perpX * scale; - const pointY = midY + perpY * scale; - - points.push({ x: pointX, y: pointY, angle: Math.atan2(pointY - y, pointX - x) }); - } - } - - points.sort((a, b) => a.angle - b.angle); - - if (points.length > 2) { - ctx.beginPath(); - ctx.moveTo(points[0].x, points[0].y); - - for (let i = 1; i < points.length; i++) { - ctx.lineTo(points[i].x, points[i].y); - } - - ctx.closePath(); - - if (isSea) { - // Deeper sea color with gradient - const seaGradient = ctx.createRadialGradient(x, y, 0, x, y, 50); - seaGradient.addColorStop(0, '#0a2a5c'); // Very dark blue center - seaGradient.addColorStop(1, '#1a3c6e'); // Dark blue edge - ctx.fillStyle = seaGradient; - ctx.fill(); - - // Subtle sea border - ctx.strokeStyle = '#0a1a3c'; - ctx.lineWidth = 2; - ctx.stroke(); - - // Add wave pattern for sea - drawWavePattern(ctx, x, y, 30); - } else { - // Land territory - drawNaturalTerritory(ctx, x, y, 33, color); - } - } else { - if (isSea) { - const radius = 35; - const seaGradient = ctx.createRadialGradient(x, y, 0, x, y, radius); - seaGradient.addColorStop(0, '#0a2a5c'); - seaGradient.addColorStop(1, '#1a3c6e'); - ctx.beginPath(); - ctx.arc(x, y, radius, 0, Math.PI * 2); - ctx.fillStyle = seaGradient; - ctx.fill(); - ctx.strokeStyle = '#0a1a3c'; - ctx.lineWidth = 2; - ctx.stroke(); - - drawWavePattern(ctx, x, y, 30); - } else { - drawNaturalTerritory(ctx, x, y, 33, color); - } - } - } - - // Draw a more natural territory shape (not a perfect circle) - function drawNaturalTerritory(ctx, x, y, radius, color) { - ctx.beginPath(); - - const points = 16; // More points for a smoother, yet irregular shape - const angleStep = (Math.PI * 2) / points; - - for (let i = 0; i < points; i++) { - const angle = i * angleStep; - // Vary the radius more significantly for each point - const variation = 0.7 + (Math.random() * 0.6); // Wider variation - const pointRadius = radius * variation; - - // Add a slight distortion using Perlin-like noise simulation - const noise = Math.sin(angle * 3 + i) * 0.2; - const distortedRadius = pointRadius * (1 + noise); - - const px = x + Math.cos(angle) * distortedRadius; - const py = y + Math.sin(angle) * distortedRadius; - - if (i === 0) { - ctx.moveTo(px, py); - } else { - // Use quadratic curves for smoother transitions between points - const midAngle = angle - angleStep / 2; - const midRadius = (pointRadius + (radius * (0.7 + Math.random() * 0.6))) / 2; - const midX = x + Math.cos(midAngle) * midRadius; - const midY = y + Math.sin(midAngle) * midRadius; - ctx.quadraticCurveTo(midX, midY, px, py); - } - } - - ctx.closePath(); - - // Add a gradient fill for land to make it more visually distinct - const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius); - gradient.addColorStop(0, color + 'FF'); // Opaque center - gradient.addColorStop(1, color + 'AA'); // Slightly transparent edge - - ctx.fillStyle = gradient; - ctx.fill(); - - // Add a subtle border - ctx.strokeStyle = '#333333'; - ctx.lineWidth = 2; - ctx.stroke(); - } - - // Draw wave patterns for sea territories - function drawWavePattern(ctx, x, y, size) { - ctx.strokeStyle = 'rgba(255, 255, 255, 0.15)'; // Slightly more visible waves - ctx.lineWidth = 1.5; - - for (let i = 0; i < 4; i++) { // More waves for better texture - const offset = i * (size / 4) - size / 2; - - ctx.beginPath(); - ctx.moveTo(x - size / 2, y + offset); - - for (let j = 0; j < 3; j++) { - const segX = x - size / 2 + ((j + 1) * size / 3); - const segY = y + offset + (j % 2 === 0 ? 6 : -6); - - ctx.quadraticCurveTo( - x - size / 2 + (j * size / 3) + (size / 6), - y + offset + (j % 2 === 0 ? -6 : 6), - segX, segY - ); - } - - ctx.stroke(); - } - } - - // Draw borders between territories - function drawTerritoryBorders(ctx, width, height) { - const scaleX = width / 1000; - const scaleY = height / 1000; - const offsetX = width / 2; - const offsetY = height / 2; - - // Sea provinces for identifying land-sea borders - const seaProvinces = [ - 'NAO', 'NWG', 'BAR', 'NTH', 'SKA', 'HEL', 'BAL', 'BOT', 'ENG', 'IRI', 'MAO', - 'WES', 'LYO', 'TYS', 'ADR', 'ION', 'AEG', 'EAS', 'BLA' - ]; - - // For each province, draw borders with its neighbors - for (const [province, position] of Object.entries(coordinateData.coordinates)) { - if (!province.includes('_')) { // Skip coast variants - const x1 = (position.x * scaleX) + offsetX; - const y1 = (position.z * scaleY) + offsetY; - - const isSea = seaProvinces.includes(province); - - // Get connected neighbors - const neighbors = getConnectedProvinces(province); - - for (const neighbor of neighbors) { - if (coordinateData.coordinates[neighbor] && !neighbor.includes('_')) { - const neighborPos = coordinateData.coordinates[neighbor]; - const x2 = (neighborPos.x * scaleX) + offsetX; - const y2 = (neighborPos.z * scaleY) + offsetY; - - const isNeighborSea = seaProvinces.includes(neighbor); - - // Only draw each border once (when province < neighbor) - if (province < neighbor) { - ctx.beginPath(); - ctx.moveTo(x1, y1); - - // Make borders slightly curved for more natural appearance - const midX = (x1 + x2) / 2; - const midY = (y1 + y2) / 2; - - // Add a slight curve - const dx = x2 - x1; - const dy = y2 - y1; - const dist = Math.sqrt(dx * dx + dy * dy); - - // Calculate perpendicular direction for the curve - const perpX = -dy / dist; - const perpY = dx / dist; - - // Apply a small curve - const curveSize = 10 + Math.random() * 10; - const curveX = midX + perpX * curveSize; - const curveY = midY + perpY * curveSize; - - // Draw the curved line - ctx.quadraticCurveTo(curveX, curveY, x2, y2); - - // Style based on type of border - if (isSea !== isNeighborSea) { - // Land-sea border (coastline) - ctx.strokeStyle = '#000000'; - ctx.lineWidth = 2.5; - } else if (isSea && isNeighborSea) { - // Sea-sea border (lighter) - ctx.strokeStyle = 'rgba(20, 60, 120, 0.5)'; - ctx.lineWidth = 1.5; - } else { - // Land-land border - ctx.strokeStyle = '#333333'; - ctx.lineWidth = 2; - } - - ctx.stroke(); - } - } - } - } - } - } - - // Draw supply centers on the map - function drawSupplyCenters(ctx, width, height) { - if (!coordinateData || !coordinateData.provinces) return; - - const scaleX = width / 1000; - const scaleY = height / 1000; - const offsetX = width / 2; - const offsetY = height / 2; - - // Draw supply centers as star symbols - for (const [province, data] of Object.entries(coordinateData.provinces)) { - if (data.isSupplyCenter && coordinateData.coordinates[province]) { - const position = coordinateData.coordinates[province]; - const x = (position.x * scaleX) + offsetX; - const y = (position.z * scaleY) + offsetY; - - // Draw a star symbol - const starPoints = 5; - const outerRadius = 12; - const innerRadius = 6; - - ctx.beginPath(); - - for (let i = 0; i < starPoints * 2; i++) { - const radius = i % 2 === 0 ? outerRadius : innerRadius; - const angle = (Math.PI * i) / starPoints; - - const px = x + Math.cos(angle) * radius; - const py = y + Math.sin(angle) * radius; - - if (i === 0) { - ctx.moveTo(px, py); - } else { - ctx.lineTo(px, py); - } - } - - ctx.closePath(); - - // Fill and stroke - ctx.fillStyle = '#FFD700'; // Gold - ctx.fill(); - ctx.strokeStyle = '#333333'; - ctx.lineWidth = 1; - ctx.stroke(); - } - } - } - - // Draw province names on the map - function drawProvinceNames(ctx, width, height) { - const scaleX = width / 1000; - const scaleY = height / 1000; - const offsetX = width / 2; - const offsetY = height / 2; - - for (const [province, position] of Object.entries(coordinateData.coordinates)) { - if (!province.includes('_')) { // Skip coast variants - const x = (position.x * scaleX) + offsetX; - const y = (position.z * scaleY) + offsetY; - - // Add the province name - ctx.font = 'bold 20px Arial'; - ctx.fillStyle = '#000000'; - ctx.strokeStyle = '#FFFFFF'; - ctx.lineWidth = 3; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - - // Draw text outline for better visibility - ctx.strokeText(province, x, y); - ctx.fillText(province, x, y); - } - } - } - - // Create invisible hitboxes for territory interaction - function createInvisibleTerritoryHitboxes() { - if (!coordinateData || !coordinateData.coordinates) return; - - // Create a group for all territory hitboxes - const territoriesGroup = new THREE.Group(); - territoriesGroup.name = 'territories'; - - // Sea provinces for special styling - const seaProvinces = [ - 'NAO', 'NWG', 'BAR', 'NTH', 'SKA', 'HEL', 'BAL', 'BOT', 'ENG', 'IRI', 'MAO', - 'WES', 'LYO', 'TYS', 'ADR', 'ION', 'AEG', 'EAS', 'BLA' - ]; - - // Group provinces by country - const countries = { - 'AUSTRIA': ['VIE', 'BUD', 'TRI', 'GAL', 'BOH', 'TYR'], - 'ENGLAND': ['LON', 'EDI', 'LVP', 'WAL', 'YOR', 'CLY'], - 'FRANCE': ['PAR', 'BRE', 'MAR', 'PIC', 'GAS', 'BUR'], - 'GERMANY': ['BER', 'MUN', 'KIE', 'RUH', 'PRU', 'SIL'], - 'ITALY': ['ROM', 'VEN', 'NAP', 'TUS', 'PIE', 'APU'], - 'RUSSIA': ['STP', 'MOS', 'WAR', 'SEV', 'UKR', 'LVN'], - 'TURKEY': ['CON', 'ANK', 'SMY', 'SYR', 'ARM'] - }; - - // Determine country for each province - const provinceToCountry = {}; - for (const [country, provinces] of Object.entries(countries)) { - for (const province of provinces) { - provinceToCountry[province] = country; - } - } - - // Create hitboxes for each province - for (const [province, position] of Object.entries(coordinateData.coordinates)) { - if (!province.includes('_')) { // Skip coast variants - // Determine if this is a sea province - const isSea = seaProvinces.includes(province); - - // Determine country - const country = provinceToCountry[province]; - - // Create a circular hitbox (nearly invisible) - const radius = 30; // Slightly smaller than visual territory - const geometry = new THREE.CircleGeometry(radius, 32); - const material = new THREE.MeshBasicMaterial({ - transparent: true, - opacity: 0.01, // Nearly invisible - side: THREE.DoubleSide - }); - - const circleMesh = new THREE.Mesh(geometry, material); - circleMesh.position.set(position.x, 0.5, position.z); - circleMesh.rotation.x = -Math.PI / 2; // Make it horizontal - - // Store province data for interaction - circleMesh.userData = { - province, - country, - isSea - }; - - // Add to the territories group - territoriesGroup.add(circleMesh); - } - } - - // Add to scene - scene.add(territoriesGroup); - } - - // Draw a simplified map with country outlines for the fallback - function drawSimplifiedMap(ctx, width, height) { - // Sea opacity fill - ctx.fillStyle = 'rgba(30, 70, 150, 0.1)'; - ctx.fillRect(0, 0, width, height); - - // Country regions - approximate simplified polygons for each major power - const regions = { - 'ENGLAND': [ - [0.35, 0.25], [0.38, 0.2], [0.42, 0.22], [0.43, 0.25], - [0.40, 0.27], [0.37, 0.3], [0.35, 0.25] - ], - 'FRANCE': [ - [0.4, 0.35], [0.35, 0.4], [0.3, 0.45], [0.35, 0.5], - [0.4, 0.48], [0.45, 0.45], [0.42, 0.40], [0.4, 0.35] - ], - 'GERMANY': [ - [0.48, 0.31], [0.5, 0.36], [0.55, 0.38], [0.5, 0.43], - [0.45, 0.4], [0.43, 0.35], [0.48, 0.31] - ], - 'ITALY': [ - [0.45, 0.48], [0.5, 0.5], [0.53, 0.53], [0.5, 0.58], - [0.45, 0.55], [0.43, 0.5], [0.45, 0.48] - ], - 'AUSTRIA': [ - [0.55, 0.42], [0.6, 0.45], [0.58, 0.5], [0.53, 0.48], - [0.52, 0.45], [0.55, 0.42] - ], - 'RUSSIA': [ - [0.6, 0.25], [0.7, 0.3], [0.75, 0.4], [0.7, 0.5], - [0.62, 0.45], [0.58, 0.4], [0.55, 0.35], [0.6, 0.25] - ], - 'TURKEY': [ - [0.65, 0.5], [0.7, 0.55], [0.75, 0.6], [0.65, 0.6], - [0.6, 0.55], [0.65, 0.5] - ] - }; - - // Color palette for countries - const countryColors = { - 'ENGLAND': '#223a87', - 'FRANCE': '#3699d4', - 'GERMANY': '#232323', - 'ITALY': '#35a335', - 'AUSTRIA': '#d82b2b', - 'RUSSIA': '#dad6d6', - 'TURKEY': '#e0c846' - }; - - // Draw each country region - Object.entries(regions).forEach(([country, points]) => { - ctx.beginPath(); - - // Scale points to canvas size - const scaledPoints = points.map(([x, y]) => [x * width, y * height]); - - // Draw the country shape - ctx.moveTo(scaledPoints[0][0], scaledPoints[0][1]); - for (let i = 1; i < scaledPoints.length; i++) { - ctx.lineTo(scaledPoints[i][0], scaledPoints[i][1]); - } - ctx.closePath(); - - // Fill with country color (semi-transparent) - ctx.fillStyle = countryColors[country] + '88'; // Add alpha - ctx.fill(); - - // Add a border - ctx.strokeStyle = '#333333'; - ctx.lineWidth = 8; - ctx.stroke(); - - // Add country label - ctx.font = 'bold 48px Arial'; - ctx.fillStyle = '#000000'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - - // Calculate center point for label - const centerX = scaledPoints.reduce((sum, [x]) => sum + x, 0) / scaledPoints.length; - const centerY = scaledPoints.reduce((sum, [_, y]) => sum + y, 0) / scaledPoints.length; - - ctx.fillText(country, centerX, centerY); - }); - - // Add province markers if coordinate data is available - if (coordinateData && coordinateData.coordinates) { - const scaleX = width / 1000; - const scaleY = height / 1000; - const offsetX = width / 2; - const offsetY = height / 2; - - // Draw province markers and names - for (const [province, position] of Object.entries(coordinateData.coordinates)) { - if (!province.includes('_')) { // Skip coast variants - const x = (position.x * scaleX) + offsetX; - const y = (position.z * scaleY) + offsetY; - - // Draw a dot for the province - ctx.beginPath(); - ctx.arc(x, y, 6, 0, Math.PI * 2); - ctx.fillStyle = '#000000'; - ctx.fill(); - - // Add the province name - ctx.font = '20px Arial'; - ctx.fillStyle = '#000000'; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText(province, x, y + 25); - } - } - } - } - - // Initialize the scene when page loads + + // --- BOOTSTRAP ON PAGE LOAD --- window.addEventListener('load', initScene);