| |
| let map; |
| let units = []; |
| let selectedUnit = null; |
| let isPaused = false; |
| let dayCounter = 1; |
| let hourCounter = 0; |
| let battleIntervals = []; |
| let timer; |
|
|
| |
| let statistics = { |
| blueActive: 15, |
| redActive: 15, |
| blueCasualties: 0, |
| redCasualties: 0, |
| blueReinforcements: 0, |
| redReinforcements: 0, |
| activeBattles: 0, |
| blueControl: 50, |
| redControl: 50, |
| nextUnitId: 30 |
| }; |
|
|
| |
| const unitTypes = { |
| |
| infantry: { speed: 0.001, attackPower: 10, range: 0.01, icon: '👤', unlockDay: 0 }, |
| armor: { speed: 0.002, attackPower: 25, range: 0.015, icon: '🛡️', unlockDay: 0 }, |
| artillery: { speed: 0.0005, attackPower: 30, range: 0.03, icon: '💥', unlockDay: 0 }, |
| recon: { speed: 0.003, attackPower: 5, range: 0.02, icon: '🔍', unlockDay: 0 }, |
| airDefense: { speed: 0.0008, attackPower: 20, range: 0.025, icon: '🚀', unlockDay: 0 }, |
| command: { speed: 0.0006, attackPower: 5, range: 0.015, icon: '⭐', unlockDay: 0 }, |
| medical: { speed: 0.001, attackPower: 0, range: 0.008, icon: '🩺', unlockDay: 0 }, |
| |
| |
| airborne: { speed: 0.0025, attackPower: 15, range: 0.02, icon: '🪂', unlockDay: 2 }, |
| specialForces: { speed: 0.0035, attackPower: 18, range: 0.015, icon: '🥷', unlockDay: 3 }, |
| mechanized: { speed: 0.0015, attackPower: 22, range: 0.018, icon: '🚜', unlockDay: 4 }, |
| engineers: { speed: 0.001, attackPower: 8, range: 0.01, icon: '🔧', unlockDay: 5, special: 'fortify' }, |
| naval: { speed: 0.0008, attackPower: 35, range: 0.04, icon: '⚓', unlockDay: 6 }, |
| airForce: { speed: 0.004, attackPower: 28, range: 0.05, icon: '✈️', unlockDay: 7 }, |
| missile: { speed: 0.0005, attackPower: 40, range: 0.06, icon: '🚀', unlockDay: 10 }, |
| elite: { speed: 0.002, attackPower: 35, range: 0.03, icon: '👑', unlockDay: 15 } |
| }; |
|
|
| |
| document.addEventListener('DOMContentLoaded', function() { |
| initializeMap(); |
| generateUnits(); |
| placeUnitsOnMap(); |
| startSimulation(); |
| |
| |
| document.getElementById('pause-button').addEventListener('click', togglePause); |
| }); |
|
|
| function initializeMap() { |
| |
| map = L.map('map', { |
| dragging: false, |
| zoomControl: false, |
| doubleClickZoom: false, |
| scrollWheelZoom: false, |
| touchZoom: false, |
| boxZoom: false, |
| keyboard: false |
| }).setView([40.7, -74.0], 13); |
| |
| |
| L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { |
| attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', |
| maxZoom: 19 |
| }).addTo(map); |
| } |
|
|
| function generateUnits() { |
| |
| for (let i = 0; i < 15; i++) { |
| const type = getRandomUnitType(); |
| units.push({ |
| id: `blue-${i}`, |
| name: `Blue Force ${getUnitName(type)} ${i + 1}`, |
| type: type, |
| faction: 'blue', |
| strength: Math.floor(Math.random() * 50) + 50, |
| health: 100, |
| status: getRandomStatus(), |
| lat: 40.7 + (Math.random() * 0.05 - 0.025), |
| lng: -74.0 + (Math.random() * 0.05 - 0.025), |
| targetLat: null, |
| targetLng: null, |
| marker: null, |
| icon: unitTypes[type].icon, |
| speed: unitTypes[type].speed, |
| attackPower: unitTypes[type].attackPower, |
| range: unitTypes[type].range, |
| inBattle: false, |
| movingToTarget: false, |
| kills: 0, |
| lastMoveTimestamp: Date.now() |
| }); |
| } |
| |
| |
| for (let i = 0; i < 15; i++) { |
| const type = getRandomUnitType(); |
| units.push({ |
| id: `red-${i}`, |
| name: `Red Force ${getUnitName(type)} ${i + 1}`, |
| type: type, |
| faction: 'red', |
| strength: Math.floor(Math.random() * 50) + 50, |
| health: 100, |
| status: getRandomStatus(), |
| lat: 40.7 + (Math.random() * 0.05 - 0.025), |
| lng: -74.0 + (Math.random() * 0.05 - 0.025), |
| targetLat: null, |
| targetLng: null, |
| marker: null, |
| icon: unitTypes[type].icon, |
| speed: unitTypes[type].speed, |
| attackPower: unitTypes[type].attackPower, |
| range: unitTypes[type].range, |
| inBattle: false, |
| movingToTarget: false, |
| kills: 0, |
| lastMoveTimestamp: Date.now() |
| }); |
| } |
| } |
|
|
| function getRandomUnitType() { |
| |
| const availableTypes = Object.entries(unitTypes) |
| .filter(([type, properties]) => properties.unlockDay <= dayCounter) |
| .map(([type]) => type); |
| |
| |
| const newlyUnlockedTypes = availableTypes.filter(type => |
| unitTypes[type].unlockDay === dayCounter); |
| |
| |
| if (newlyUnlockedTypes.length > 0 && Math.random() < 0.3) { |
| const randomIndex = Math.floor(Math.random() * newlyUnlockedTypes.length); |
| return newlyUnlockedTypes[randomIndex]; |
| } |
| |
| |
| const randomIndex = Math.floor(Math.random() * availableTypes.length); |
| return availableTypes[randomIndex]; |
| } |
|
|
| function getUnitName(type) { |
| const names = { |
| infantry: 'Battalion', |
| armor: 'Tank Division', |
| artillery: 'Artillery Battery', |
| recon: 'Reconnaissance Unit', |
| airDefense: 'Air Defense Unit', |
| command: 'Command Center', |
| medical: 'Medical Corps', |
| |
| airborne: 'Airborne Division', |
| specialForces: 'Special Forces', |
| mechanized: 'Mechanized Infantry', |
| engineers: 'Combat Engineers', |
| naval: 'Naval Support', |
| airForce: 'Air Squadron', |
| missile: 'Missile Battery', |
| elite: 'Elite Force' |
| }; |
| return names[type] || 'Unit'; |
| } |
|
|
| function getRandomStatus() { |
| const statuses = ['Operational', 'Engaged', 'Advancing', 'Defending', 'Flanking', 'Retreating']; |
| return statuses[Math.floor(Math.random() * statuses.length)]; |
| } |
|
|
| function placeUnitsOnMap() { |
| units.forEach(unit => { |
| const markerSize = unit.strength / 15 + 20; |
| |
| |
| const markerHtml = ` |
| <div class="unit-marker ${unit.faction}-unit" style="width: ${markerSize}px; height: ${markerSize}px; line-height: ${markerSize}px; font-size: ${markerSize/2}px;"> |
| ${unit.icon} |
| </div> |
| `; |
| |
| |
| const customIcon = L.divIcon({ |
| html: markerHtml, |
| className: '', |
| iconSize: [markerSize, markerSize], |
| iconAnchor: [markerSize/2, markerSize/2] |
| }); |
| |
| |
| unit.marker = L.marker([unit.lat, unit.lng], { icon: customIcon }) |
| .addTo(map) |
| .on('click', function() { |
| selectUnit(unit); |
| }); |
| }); |
| } |
|
|
| function selectUnit(unit) { |
| |
| if (selectedUnit) { |
| const prevMarker = selectedUnit.marker; |
| const prevIcon = prevMarker.options.icon; |
| const prevHtml = prevIcon.options.html.replace(' selected', ''); |
| |
| const newIcon = L.divIcon({ |
| html: prevHtml, |
| className: prevIcon.options.className, |
| iconSize: prevIcon.options.iconSize, |
| iconAnchor: prevIcon.options.iconAnchor |
| }); |
| |
| prevMarker.setIcon(newIcon); |
| } |
| |
| |
| selectedUnit = unit; |
| |
| |
| const marker = unit.marker; |
| const icon = marker.options.icon; |
| const newHtml = icon.options.html.replace('unit-marker', 'unit-marker selected'); |
| |
| const newIcon = L.divIcon({ |
| html: newHtml, |
| className: icon.options.className, |
| iconSize: icon.options.iconSize, |
| iconAnchor: icon.options.iconAnchor |
| }); |
| |
| marker.setIcon(newIcon); |
| |
| |
| updateUnitInfo(unit); |
| } |
|
|
| function updateUnitInfo(unit) { |
| const unitInfoDiv = document.getElementById('unit-info'); |
| |
| let healthClass = unit.health > 70 ? '' : (unit.health > 30 ? 'health-warning' : 'health-critical'); |
| |
| unitInfoDiv.innerHTML = ` |
| <div class="unit-name">${unit.name}</div> |
| <div class="unit-type">Type: ${getUnitName(unit.type)}</div> |
| <div class="unit-strength">Strength: ${unit.strength} personnel</div> |
| <div class="unit-status">Status: ${unit.status}</div> |
| <div class="unit-health"> |
| Health: ${unit.health}% |
| <div class="health-bar ${healthClass}"> |
| <div class="health-fill" style="width: ${unit.health}%"></div> |
| </div> |
| </div> |
| <div class="unit-stats"> |
| <div>Attack Power: ${unit.attackPower}</div> |
| <div>Range: ${(unit.range * 1000).toFixed(1)} meters</div> |
| <div>Speed: ${(unit.speed * 1000).toFixed(1)}</div> |
| <div>Kills: ${unit.kills}</div> |
| </div> |
| <div class="unit-position"> |
| Position: ${unit.lat.toFixed(4)}, ${unit.lng.toFixed(4)} |
| </div> |
| `; |
| |
| |
| if (unit.inBattle) { |
| unitInfoDiv.innerHTML += ` |
| <div class="unit-battle-status"> |
| <span style="color: red; font-weight: bold;">⚔️ ENGAGED IN BATTLE ⚔️</span> |
| </div> |
| `; |
| } |
| } |
|
|
| function startSimulation() { |
| |
| timer = setInterval(() => { |
| if (!isPaused) { |
| updateTime(); |
| |
| |
| if (hourCounter % (3 - Math.min(2, Math.floor(dayCounter / 5))) === 0) { |
| assignRandomTargets(); |
| } |
| |
| |
| |
| const baseSpawnChance = 0.15 + (dayCounter / 50); |
| const battleAdjustment = Math.max(0, 0.1 - (statistics.activeBattles / 30)); |
| |
| if (Math.random() < baseSpawnChance + battleAdjustment) { |
| spawnReinforcements(); |
| } |
| |
| |
| if (Math.random() < 0.03) { |
| createHotZone(); |
| } |
| |
| moveUnits(); |
| checkForBattles(); |
| updateBattleStatistics(); |
| |
| |
| if (Math.random() < 0.05) { |
| triggerRandomEvent(); |
| } |
| |
| |
| const blueSurvivors = units.filter(u => u.faction === 'blue' && u.health > 0).length; |
| const redSurvivors = units.filter(u => u.faction === 'red' && u.health > 0).length; |
| |
| |
| const MIN_UNITS_PER_FACTION = 8; |
| if (blueSurvivors < MIN_UNITS_PER_FACTION) { |
| for (let i = 0; i < (MIN_UNITS_PER_FACTION - blueSurvivors); i++) { |
| spawnNewUnit('blue'); |
| } |
| } |
| if (redSurvivors < MIN_UNITS_PER_FACTION) { |
| for (let i = 0; i < (MIN_UNITS_PER_FACTION - redSurvivors); i++) { |
| spawnNewUnit('red'); |
| } |
| } |
| |
| |
| if (selectedUnit) { |
| updateUnitInfo(selectedUnit); |
| } |
| } |
| }, 1000); |
| } |
|
|
| |
| function getRandomUnitType() { |
| |
| const availableTypes = Object.entries(unitTypes) |
| .filter(([type, properties]) => properties.unlockDay <= dayCounter) |
| .map(([type]) => type); |
| |
| |
| return availableTypes[Math.floor(Math.random() * availableTypes.length)]; |
| } |
|
|
| |
| function getUnitName(type) { |
| |
| const formattedName = type |
| .replace(/([A-Z])/g, ' $1') |
| .replace(/^./, str => str.toUpperCase()); |
| |
| return formattedName; |
| } |
|
|
| function createHotZone() { |
| |
| |
| let hotspotLat, hotspotLng; |
| |
| if (Math.random() < 0.5) { |
| |
| hotspotLat = 40.7 + (Math.random() * 0.1 - 0.05); |
| hotspotLng = -74.0 + (Math.random() * 0.1 - 0.05); |
| } else { |
| |
| const dominantFaction = statistics.blueControl > statistics.redControl ? 'blue' : 'red'; |
| |
| if (dominantFaction === 'blue') { |
| |
| hotspotLat = 40.68 + (Math.random() * 0.06); |
| hotspotLng = -74.03 - (Math.random() * 0.04); |
| } else { |
| |
| hotspotLat = 40.72 + (Math.random() * 0.06); |
| hotspotLng = -73.97 - (Math.random() * 0.04); |
| } |
| } |
| |
| |
| units.forEach(unit => { |
| if (unit.health > 0 && Math.random() < 0.3) { |
| unit.targetLat = hotspotLat; |
| unit.targetLng = hotspotLng; |
| unit.movingToTarget = true; |
| unit.status = 'Redeploying'; |
| } |
| }); |
| |
| |
| const hotspotTypes = [ |
| { name: 'battle', probability: 0.5, color: 'rgba(255, 0, 0, 0.2)', shadowColor: 'rgba(255, 0, 0, 0.5)' }, |
| { name: 'supply', probability: 0.3, color: 'rgba(0, 255, 0, 0.2)', shadowColor: 'rgba(0, 255, 0, 0.5)' }, |
| { name: 'strategic', probability: 0.2, color: 'rgba(255, 255, 0, 0.2)', shadowColor: 'rgba(255, 255, 0, 0.5)' } |
| ]; |
| |
| |
| let hotspotType; |
| const random = Math.random(); |
| let cumulativeProbability = 0; |
| |
| for (const type of hotspotTypes) { |
| cumulativeProbability += type.probability; |
| if (random < cumulativeProbability) { |
| hotspotType = type; |
| break; |
| } |
| } |
| |
| |
| const isMajorEvent = Math.random() < 0.3; |
| const size = isMajorEvent ? 60 : 40; |
| const duration = isMajorEvent ? 8000 : 4000; |
| |
| |
| const point = map.latLngToLayerPoint([hotspotLat, hotspotLng]); |
| |
| |
| const hotspotEl = document.createElement('div'); |
| hotspotEl.style.position = 'absolute'; |
| hotspotEl.style.left = (point.x - size/2) + 'px'; |
| hotspotEl.style.top = (point.y - size/2) + 'px'; |
| hotspotEl.style.width = size + 'px'; |
| hotspotEl.style.height = size + 'px'; |
| hotspotEl.style.backgroundColor = hotspotType.color; |
| hotspotEl.style.borderRadius = '50%'; |
| hotspotEl.style.zIndex = '600'; |
| hotspotEl.style.boxShadow = `0 0 ${size/2}px ${hotspotType.shadowColor}`; |
| hotspotEl.style.animation = `battle-expand ${duration/1000}s forwards`; |
| |
| |
| const pulseAnimation = document.createElement('style'); |
| pulseAnimation.textContent = ` |
| @keyframes pulse-${Date.now()} { |
| 0% { transform: scale(1); opacity: 1; } |
| 50% { transform: scale(1.2); opacity: 0.8; } |
| 100% { transform: scale(1); opacity: 1; } |
| } |
| `; |
| document.head.appendChild(pulseAnimation); |
| hotspotEl.style.animation = `pulse-${Date.now()} 1.5s infinite`; |
| |
| document.querySelector('#map').appendChild(hotspotEl); |
| |
| |
| if (isMajorEvent) { |
| const outerRing = document.createElement('div'); |
| outerRing.style.position = 'absolute'; |
| outerRing.style.left = (point.x - size) + 'px'; |
| outerRing.style.top = (point.y - size) + 'px'; |
| outerRing.style.width = (size * 2) + 'px'; |
| outerRing.style.height = (size * 2) + 'px'; |
| outerRing.style.border = `2px solid ${hotspotType.color}`; |
| outerRing.style.borderRadius = '50%'; |
| outerRing.style.zIndex = '599'; |
| outerRing.style.opacity = '0.7'; |
| |
| |
| const expandAnimation = document.createElement('style'); |
| expandAnimation.textContent = ` |
| @keyframes expand-${Date.now()} { |
| 0% { transform: scale(0.5); opacity: 0.7; } |
| 100% { transform: scale(2); opacity: 0; } |
| } |
| `; |
| document.head.appendChild(expandAnimation); |
| outerRing.style.animation = `expand-${Date.now()} 4s infinite`; |
| |
| document.querySelector('#map').appendChild(outerRing); |
| |
| |
| let eventTitle = ''; |
| let eventDesc = ''; |
| |
| if (hotspotType.name === 'battle') { |
| eventTitle = 'Major Battle Erupting'; |
| eventDesc = 'Heavy fighting reported in sector'; |
| } else if (hotspotType.name === 'supply') { |
| eventTitle = 'Supply Drop Zone'; |
| eventDesc = 'Critical supplies have been airdropped'; |
| } else { |
| eventTitle = 'Strategic Location'; |
| eventDesc = 'Forces moving to secure key position'; |
| } |
| |
| displayEventNotification(eventTitle, eventDesc); |
| |
| |
| setTimeout(() => { |
| if (outerRing.parentNode) { |
| outerRing.parentNode.removeChild(outerRing); |
| } |
| }, duration); |
| } |
| |
| |
| setTimeout(() => { |
| if (hotspotEl.parentNode) { |
| hotspotEl.parentNode.removeChild(hotspotEl); |
| } |
| }, duration); |
| } |
|
|
| |
| function spawnNewUnit(faction) { |
| |
| let entryLat, entryLng; |
| |
| if (faction === 'blue') { |
| |
| if (Math.random() < 0.5) { |
| entryLat = 40.7 + (Math.random() * 0.05 - 0.025); |
| entryLng = -74.05 - (Math.random() * 0.02); |
| } else { |
| entryLat = 40.65 - (Math.random() * 0.02); |
| entryLng = -74.0 + (Math.random() * 0.05 - 0.025); |
| } |
| } else { |
| |
| if (Math.random() < 0.5) { |
| entryLat = 40.7 + (Math.random() * 0.05 - 0.025); |
| entryLng = -73.95 + (Math.random() * 0.02); |
| } else { |
| entryLat = 40.75 + (Math.random() * 0.02); |
| entryLng = -74.0 + (Math.random() * 0.05 - 0.025); |
| } |
| } |
| |
| |
| const type = getRandomUnitType(); |
| const unitId = `${faction}-${statistics.nextUnitId++}`; |
| |
| const newUnit = { |
| id: unitId, |
| name: `${faction.charAt(0).toUpperCase() + faction.slice(1)} Force ${getUnitName(type)} ${statistics.nextUnitId}`, |
| type: type, |
| faction: faction, |
| strength: 50 + Math.floor(Math.random() * 50), |
| health: 100, |
| status: 'Reinforcing', |
| lat: entryLat, |
| lng: entryLng, |
| targetLat: null, |
| targetLng: null, |
| marker: null, |
| icon: unitTypes[type].icon, |
| speed: unitTypes[type].speed, |
| attackPower: unitTypes[type].attackPower, |
| range: unitTypes[type].range, |
| inBattle: false, |
| movingToTarget: false, |
| kills: 0, |
| lastMoveTimestamp: Date.now() |
| }; |
| |
| units.push(newUnit); |
| |
| |
| const markerSize = newUnit.strength / 15 + 20; |
| |
| const markerHtml = ` |
| <div class="unit-marker ${newUnit.faction}-unit new-unit" style="width: ${markerSize}px; height: ${markerSize}px; line-height: ${markerSize}px; font-size: ${markerSize/2}px;"> |
| ${newUnit.icon} |
| </div> |
| `; |
| |
| const customIcon = L.divIcon({ |
| html: markerHtml, |
| className: '', |
| iconSize: [markerSize, markerSize], |
| iconAnchor: [markerSize/2, markerSize/2] |
| }); |
| |
| newUnit.marker = L.marker([newUnit.lat, newUnit.lng], { icon: customIcon }) |
| .addTo(map) |
| .on('click', function() { |
| selectUnit(newUnit); |
| }); |
| |
| |
| const point = map.latLngToLayerPoint([newUnit.lat, newUnit.lng]); |
| |
| const entryEffect = document.createElement('div'); |
| entryEffect.style.position = 'absolute'; |
| entryEffect.style.left = point.x + 'px'; |
| entryEffect.style.top = point.y + 'px'; |
| entryEffect.style.width = '30px'; |
| entryEffect.style.height = '30px'; |
| entryEffect.style.backgroundColor = faction === 'blue' ? 'rgba(0, 100, 255, 0.5)' : 'rgba(255, 0, 0, 0.5)'; |
| entryEffect.style.borderRadius = '50%'; |
| entryEffect.style.zIndex = '700'; |
| entryEffect.style.boxShadow = `0 0 15px ${faction === 'blue' ? 'blue' : 'red'}`; |
| entryEffect.style.animation = 'unit-entry 1.5s forwards'; |
| |
| |
| if (!document.querySelector('#unit-entry-animation')) { |
| const style = document.createElement('style'); |
| style.id = 'unit-entry-animation'; |
| style.textContent = ` |
| @keyframes unit-entry { |
| 0% { transform: scale(0); opacity: 1; } |
| 70% { transform: scale(1.5); opacity: 0.7; } |
| 100% { transform: scale(2); opacity: 0; } |
| } |
| |
| .new-unit { |
| animation: highlight-new 2s; |
| } |
| |
| @keyframes highlight-new { |
| 0% { box-shadow: 0 0 15px gold; transform: scale(1.2); } |
| 100% { box-shadow: none; transform: scale(1); } |
| } |
| `; |
| document.head.appendChild(style); |
| } |
| |
| document.querySelector('#map').appendChild(entryEffect); |
| |
| |
| setTimeout(() => { |
| if (entryEffect.parentNode) entryEffect.parentNode.removeChild(entryEffect); |
| }, 1500); |
| |
| |
| assignTargetToUnit(newUnit); |
| |
| |
| if (faction === 'blue') { |
| statistics.blueActive++; |
| statistics.blueReinforcements++; |
| } else { |
| statistics.redActive++; |
| statistics.redReinforcements++; |
| } |
| |
| return newUnit; |
| } |
|
|
| function triggerRandomEvent() { |
| |
| hourCounter++; |
| if (hourCounter >= 12) { |
| hourCounter = 0; |
| dayCounter++; |
| |
| |
| const dayCounterElement = document.getElementById('day-counter'); |
| if (dayCounterElement) { |
| dayCounterElement.textContent = `Day ${dayCounter}`; |
| } else { |
| |
| const statsContainer = document.querySelector('.statistics-container'); |
| if (statsContainer) { |
| const dayElement = document.createElement('div'); |
| dayElement.id = 'day-counter'; |
| dayElement.style.fontSize = '18px'; |
| dayElement.style.fontWeight = 'bold'; |
| dayElement.style.marginBottom = '10px'; |
| dayElement.textContent = `Day ${dayCounter}`; |
| statsContainer.prepend(dayElement); |
| } |
| } |
| |
| |
| const newlyUnlocked = Object.entries(unitTypes) |
| .filter(([type, properties]) => properties.unlockDay === dayCounter) |
| .map(([type]) => type); |
| |
| if (newlyUnlocked.length > 0) { |
| |
| const techAnnouncement = document.createElement('div'); |
| techAnnouncement.className = 'tech-announcement'; |
| techAnnouncement.style.position = 'absolute'; |
| techAnnouncement.style.top = '50%'; |
| techAnnouncement.style.left = '50%'; |
| techAnnouncement.style.transform = 'translate(-50%, -50%)'; |
| techAnnouncement.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; |
| techAnnouncement.style.color = 'white'; |
| techAnnouncement.style.padding = '20px'; |
| techAnnouncement.style.borderRadius = '10px'; |
| techAnnouncement.style.zIndex = '2000'; |
| techAnnouncement.style.textAlign = 'center'; |
| techAnnouncement.style.boxShadow = '0 0 20px gold'; |
| |
| techAnnouncement.innerHTML = ` |
| <h3>New Technology Unlocked!</h3> |
| <div>${newlyUnlocked.map(type => `${unitTypes[type].icon} ${getUnitName(type)}`).join('<br>')}</div> |
| `; |
| |
| document.body.appendChild(techAnnouncement); |
| |
| |
| setTimeout(() => { |
| techAnnouncement.style.opacity = '0'; |
| techAnnouncement.style.transition = 'opacity 1s'; |
| setTimeout(() => { |
| if (techAnnouncement.parentNode) { |
| techAnnouncement.parentNode.removeChild(techAnnouncement); |
| } |
| }, 1000); |
| }, 5000); |
| } |
| } |
| |
| |
| const events = [ |
| { name: 'Reinforcement Surge', weight: 0.3, handler: () => { |
| |
| const faction = Math.random() < 0.5 ? 'blue' : 'red'; |
| for (let i = 0; i < 3 + Math.floor(Math.random() * 3); i++) { |
| spawnNewUnit(faction); |
| } |
| |
| |
| displayEventNotification( |
| 'Reinforcement Surge', |
| `${faction.charAt(0).toUpperCase() + faction.slice(1)} forces receiving reinforcements` |
| ); |
| }}, |
| { name: 'Supply Line Disruption', weight: 0.15, handler: () => { |
| |
| const faction = Math.random() < 0.5 ? 'blue' : 'red'; |
| const affectedUnits = units.filter(u => u.faction === faction && u.health > 30); |
| |
| if (affectedUnits.length > 0) { |
| affectedUnits.forEach(unit => { |
| unit.health = Math.max(30, unit.health - 20); |
| updateUnitMarkerAppearance(unit); |
| }); |
| |
| |
| displayEventNotification( |
| 'Supply Line Disruption', |
| `${faction.charAt(0).toUpperCase() + faction.slice(1)} forces weakened by supply issues` |
| ); |
| } |
| }}, |
| { name: 'Strategic Redeployment', weight: 0.2, handler: () => { |
| |
| assignRandomTargets(0.7); |
| |
| |
| displayEventNotification( |
| 'Strategic Redeployment', |
| 'Forces repositioning for tactical advantage' |
| ); |
| }}, |
| { name: 'Environmental Event', weight: 0.2, handler: () => { |
| |
| triggerEnvironmentalEvent(); |
| }}, |
| { name: 'Hot Zone', weight: 0.15, handler: () => { |
| |
| createHotZone(); |
| |
| |
| displayEventNotification( |
| 'Strategic Hotspot', |
| 'A critical location has been identified for control' |
| ); |
| }} |
| ]; |
| |
| |
| const totalWeight = events.reduce((sum, event) => sum + event.weight, 0); |
| let random = Math.random() * totalWeight; |
| let selectedEvent = null; |
| |
| for (const event of events) { |
| random -= event.weight; |
| if (random <= 0) { |
| selectedEvent = event; |
| break; |
| } |
| } |
| |
| |
| selectedEvent.handler(); |
| |
| |
| const blueSurvivors = units.filter(u => u.faction === 'blue' && u.health > 0).length; |
| const redSurvivors = units.filter(u => u.faction === 'red' && u.health > 0).length; |
| |
| if (blueSurvivors < 5 || redSurvivors < 5) { |
| |
| const weakerFaction = blueSurvivors < redSurvivors ? 'blue' : 'red'; |
| const reinforcementCount = 3 + Math.floor(Math.random() * 3); |
| |
| for (let i = 0; i < reinforcementCount; i++) { |
| spawnNewUnit(weakerFaction); |
| } |
| |
| displayEventNotification( |
| 'Emergency Reinforcements', |
| `${weakerFaction.charAt(0).toUpperCase() + weakerFaction.slice(1)} forces receiving critical support` |
| ); |
| } |
| } |
|
|
| |
| function displayEventNotification(title, message) { |
| const notification = document.createElement('div'); |
| notification.className = 'event-notification'; |
| notification.style.position = 'absolute'; |
| notification.style.top = '20px'; |
| notification.style.right = '20px'; |
| notification.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'; |
| notification.style.color = 'white'; |
| notification.style.padding = '10px 15px'; |
| notification.style.borderRadius = '5px'; |
| notification.style.zIndex = '2000'; |
| notification.style.maxWidth = '250px'; |
| notification.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.5)'; |
| notification.style.opacity = '0'; |
| notification.style.transition = 'opacity 0.5s'; |
| |
| notification.innerHTML = ` |
| <div style="font-weight: bold; font-size: 14px; margin-bottom: 5px;">${title}</div> |
| <div style="font-size: 12px;">${message}</div> |
| `; |
| |
| document.body.appendChild(notification); |
| |
| |
| setTimeout(() => { |
| notification.style.opacity = '1'; |
| }, 100); |
| |
| |
| setTimeout(() => { |
| notification.style.opacity = '0'; |
| setTimeout(() => { |
| if (notification.parentNode) { |
| notification.parentNode.removeChild(notification); |
| } |
| }, 500); |
| }, 5000); |
| } |
|
|
| |
| function triggerEnvironmentalEvent() { |
| |
| const events = [ |
| { |
| name: 'Fog of War', |
| probability: 0.3, |
| handler: () => { |
| |
| units.forEach(unit => { |
| if (!unit.originalRange) unit.originalRange = unit.range; |
| unit.range *= 0.5; |
| }); |
| |
| |
| const fogOverlay = document.createElement('div'); |
| fogOverlay.id = 'fog-overlay'; |
| fogOverlay.style.position = 'absolute'; |
| fogOverlay.style.top = '0'; |
| fogOverlay.style.left = '0'; |
| fogOverlay.style.width = '100%'; |
| fogOverlay.style.height = '100%'; |
| fogOverlay.style.backgroundColor = 'rgba(255, 255, 255, 0.5)'; |
| fogOverlay.style.pointerEvents = 'none'; |
| fogOverlay.style.zIndex = '500'; |
| fogOverlay.style.transition = 'opacity 2s'; |
| |
| document.querySelector('#map').appendChild(fogOverlay); |
| |
| |
| displayEventNotification('Fog of War', 'Visibility reduced for all units'); |
| |
| |
| setTimeout(() => { |
| |
| fogOverlay.style.opacity = '0'; |
| |
| |
| setTimeout(() => { |
| if (fogOverlay.parentNode) fogOverlay.parentNode.removeChild(fogOverlay); |
| units.forEach(unit => { |
| if (unit.originalRange) { |
| unit.range = unit.originalRange; |
| delete unit.originalRange; |
| } |
| }); |
| }, 2000); |
| }, 15000); |
| |
| return true; |
| } |
| }, |
| { |
| name: 'Artillery Barrage', |
| probability: 0.3, |
| handler: () => { |
| |
| const launchingFaction = Math.random() < 0.5 ? 'blue' : 'red'; |
| const targetFaction = launchingFaction === 'blue' ? 'red' : 'blue'; |
| |
| |
| const targetUnits = units.filter(u => u.faction === targetFaction && u.health > 0); |
| |
| if (targetUnits.length > 0) { |
| |
| let centerLat = 0, centerLng = 0; |
| targetUnits.forEach(unit => { |
| centerLat += unit.lat; |
| centerLng += unit.lng; |
| }); |
| centerLat /= targetUnits.length; |
| centerLng /= targetUnits.length; |
| |
| displayEventNotification( |
| 'Artillery Barrage', |
| `${launchingFaction.charAt(0).toUpperCase() + launchingFaction.slice(1)} forces launching artillery strike` |
| ); |
| |
| |
| for (let i = 0; i < 5; i++) { |
| setTimeout(() => { |
| const offsetLat = centerLat + (Math.random() * 0.01 - 0.005); |
| const offsetLng = centerLng + (Math.random() * 0.01 - 0.005); |
| |
| |
| const point = map.latLngToLayerPoint([offsetLat, offsetLng]); |
| |
| const explosionEl = document.createElement('div'); |
| explosionEl.style.position = 'absolute'; |
| explosionEl.style.left = point.x + 'px'; |
| explosionEl.style.top = point.y + 'px'; |
| explosionEl.style.width = '40px'; |
| explosionEl.style.height = '40px'; |
| explosionEl.style.backgroundColor = '#ff4500'; |
| explosionEl.style.borderRadius = '50%'; |
| explosionEl.style.zIndex = '700'; |
| explosionEl.style.boxShadow = '0 0 30px red'; |
| explosionEl.style.animation = 'explosion 1.5s forwards'; |
| |
| |
| if (!document.querySelector('#explosion-animation')) { |
| const style = document.createElement('style'); |
| style.id = 'explosion-animation'; |
| style.textContent = ` |
| @keyframes explosion { |
| 0% { transform: scale(0.2); opacity: 0.8; } |
| 50% { transform: scale(1.5); opacity: 0.9; } |
| 100% { transform: scale(2); opacity: 0; } |
| } |
| `; |
| document.head.appendChild(style); |
| } |
| |
| document.querySelector('#map').appendChild(explosionEl); |
| |
| |
| targetUnits.forEach(unit => { |
| const dLat = unit.lat - offsetLat; |
| const dLng = unit.lng - offsetLng; |
| const distance = Math.sqrt(dLat * dLat + dLng * dLng); |
| |
| |
| if (distance < 0.005) { |
| const damage = Math.round(20 * (1 - distance / 0.005)); |
| unit.health = Math.max(0, unit.health - damage); |
| |
| |
| updateUnitMarkerAppearance(unit); |
| |
| |
| if (unit.health <= 0) { |
| checkUnitDeath(unit); |
| } |
| } |
| }); |
| |
| |
| setTimeout(() => { |
| if (explosionEl.parentNode) { |
| explosionEl.parentNode.removeChild(explosionEl); |
| } |
| }, 1500); |
| }, i * 800); |
| } |
| |
| return true; |
| } |
| |
| return false; |
| } |
| }, |
| { |
| name: 'Supply Drop', |
| probability: 0.4, |
| handler: () => { |
| |
| const targetFaction = Math.random() < 0.5 ? 'blue' : 'red'; |
| |
| |
| const targetUnits = units.filter(u => u.faction === targetFaction && u.health < 80 && u.health > 0); |
| |
| if (targetUnits.length > 0) { |
| |
| let dropLat = 0, dropLng = 0; |
| const selectedUnits = targetUnits.slice(0, Math.min(5, targetUnits.length)); |
| |
| selectedUnits.forEach(unit => { |
| dropLat += unit.lat; |
| dropLng += unit.lng; |
| }); |
| dropLat /= selectedUnits.length; |
| dropLng /= selectedUnits.length; |
| |
| displayEventNotification( |
| 'Supply Drop', |
| `${targetFaction.charAt(0).toUpperCase() + targetFaction.slice(1)} forces receiving supplies` |
| ); |
| |
| |
| const point = map.latLngToLayerPoint([dropLat, dropLng]); |
| |
| const supplyEl = document.createElement('div'); |
| supplyEl.style.position = 'absolute'; |
| supplyEl.style.left = point.x + 'px'; |
| supplyEl.style.top = point.y + 'px'; |
| supplyEl.style.fontSize = '30px'; |
| supplyEl.style.zIndex = '700'; |
| supplyEl.textContent = '📦'; |
| supplyEl.style.animation = 'supply-drop 2s forwards'; |
| |
| |
| if (!document.querySelector('#supply-drop-animation')) { |
| const style = document.createElement('style'); |
| style.id = 'supply-drop-animation'; |
| style.textContent = ` |
| @keyframes supply-drop { |
| 0% { transform: translateY(-50px); opacity: 0; } |
| 80% { transform: translateY(0); opacity: 1; } |
| 100% { transform: translateY(0); opacity: 0; } |
| } |
| `; |
| document.head.appendChild(style); |
| } |
| |
| document.querySelector('#map').appendChild(supplyEl); |
| |
| |
| selectedUnits.forEach(unit => { |
| const dLat = unit.lat - dropLat; |
| const dLng = unit.lng - dropLng; |
| const distance = Math.sqrt(dLat * dLat + dLng * dLng); |
| |
| |
| if (distance < 0.008) { |
| const healAmount = 15 + Math.floor(Math.random() * 20); |
| unit.health = Math.min(100, unit.health + healAmount); |
| |
| |
| updateUnitMarkerAppearance(unit); |
| |
| |
| if (unit.marker) { |
| const unitPoint = map.latLngToLayerPoint([unit.lat, unit.lng]); |
| |
| const healEl = document.createElement('div'); |
| healEl.style.position = 'absolute'; |
| healEl.style.left = unitPoint.x + 'px'; |
| healEl.style.top = unitPoint.y + 'px'; |
| healEl.style.fontSize = '14px'; |
| healEl.style.fontWeight = 'bold'; |
| healEl.style.color = '#2ecc71'; |
| healEl.style.textShadow = '0 0 3px white'; |
| healEl.style.zIndex = '1000'; |
| healEl.style.transition = 'transform 1.5s, opacity 1.5s'; |
| healEl.style.opacity = '1'; |
| healEl.textContent = `+${healAmount}`; |
| |
| document.querySelector('#map').appendChild(healEl); |
| |
| setTimeout(() => { |
| healEl.style.transform = 'translateY(-20px)'; |
| healEl.style.opacity = '0'; |
| }, 100); |
| |
| setTimeout(() => { |
| if (healEl.parentNode) { |
| healEl.parentNode.removeChild(healEl); |
| } |
| }, 1600); |
| } |
| } |
| }); |
| |
| |
| setTimeout(() => { |
| if (supplyEl.parentNode) { |
| supplyEl.parentNode.removeChild(supplyEl); |
| } |
| }, 2000); |
| |
| return true; |
| } |
| |
| return false; |
| } |
| } |
| ]; |
| |
| |
| const randomValue = Math.random(); |
| let cumulativeProbability = 0; |
| |
| for (const event of events) { |
| cumulativeProbability += event.probability; |
| if (randomValue < cumulativeProbability) { |
| |
| if (event.handler()) { |
| return event.name; |
| } |
| } |
| } |
| |
| |
| const faction = Math.random() < 0.5 ? 'blue' : 'red'; |
| for (let i = 0; i < 2; i++) { |
| spawnNewUnit(faction); |
| } |
| |
| displayEventNotification( |
| 'Tactical Reinforcements', |
| `${faction.charAt(0).toUpperCase() + faction.slice(1)} forces receiving light support` |
| ); |
| |
| return 'Tactical Reinforcements'; |
| } |
|
|
| |
| function updateUnitTypeCounts() { |
| |
| let unitCountsContainer = document.getElementById('unit-counts-container'); |
| |
| if (!unitCountsContainer) { |
| |
| const statsContainer = document.querySelector('.statistics-container'); |
| if (statsContainer) { |
| unitCountsContainer = document.createElement('div'); |
| unitCountsContainer.id = 'unit-counts-container'; |
| unitCountsContainer.style.marginTop = '15px'; |
| unitCountsContainer.style.padding = '10px'; |
| unitCountsContainer.style.borderTop = '1px solid #444'; |
| unitCountsContainer.style.display = 'flex'; |
| unitCountsContainer.style.flexWrap = 'wrap'; |
| unitCountsContainer.style.justifyContent = 'space-between'; |
| |
| const title = document.createElement('div'); |
| title.style.width = '100%'; |
| title.style.fontWeight = 'bold'; |
| title.style.marginBottom = '5px'; |
| title.textContent = 'Active Unit Types:'; |
| unitCountsContainer.appendChild(title); |
| |
| statsContainer.appendChild(unitCountsContainer); |
| } |
| } |
| |
| if (unitCountsContainer) { |
| |
| while (unitCountsContainer.childNodes.length > 1) { |
| unitCountsContainer.removeChild(unitCountsContainer.lastChild); |
| } |
| |
| |
| const blueUnitCounts = {}; |
| const redUnitCounts = {}; |
| |
| units.forEach(unit => { |
| if (unit.health <= 0) return; |
| |
| if (unit.faction === 'blue') { |
| blueUnitCounts[unit.type] = (blueUnitCounts[unit.type] || 0) + 1; |
| } else { |
| redUnitCounts[unit.type] = (redUnitCounts[unit.type] || 0) + 1; |
| } |
| }); |
| |
| |
| const activeTypes = Object.keys({...blueUnitCounts, ...redUnitCounts}) |
| .filter(type => unitTypes[type].unlockDay <= dayCounter) |
| .sort(); |
| |
| |
| activeTypes.forEach(type => { |
| const blueCount = blueUnitCounts[type] || 0; |
| const redCount = redUnitCounts[type] || 0; |
| |
| const unitTypeEl = document.createElement('div'); |
| unitTypeEl.style.display = 'flex'; |
| unitTypeEl.style.alignItems = 'center'; |
| unitTypeEl.style.margin = '3px 0'; |
| unitTypeEl.style.width = '48%'; |
| |
| unitTypeEl.innerHTML = ` |
| <span style="font-size: 16px; margin-right: 5px;">${unitTypes[type].icon}</span> |
| <span style="flex-grow: 1;">${getUnitName(type)}</span> |
| <span style="color: #3498db; margin-right: 5px;">${blueCount}</span> |
| <span style="color: #e74c3c;">${redCount}</span> |
| `; |
| |
| unitCountsContainer.appendChild(unitTypeEl); |
| }); |
| } |
| } |
|
|
| function updateBattleStatistics() { |
| |
| document.getElementById('blue-active').textContent = statistics.blueActive; |
| document.getElementById('red-active').textContent = statistics.redActive; |
| document.getElementById('blue-casualties').textContent = statistics.blueCasualties; |
| document.getElementById('red-casualties').textContent = statistics.redCasualties; |
| document.getElementById('blue-reinforcements').textContent = statistics.blueReinforcements; |
| document.getElementById('red-reinforcements').textContent = statistics.redReinforcements; |
| document.getElementById('active-battles').textContent = statistics.activeBattles; |
| |
| |
| document.getElementById('blue-control').style.width = `${statistics.blueControl}%`; |
| document.getElementById('red-control').style.width = `${statistics.redControl}%`; |
| |
| |
| if (!document.getElementById('day-counter')) { |
| const statsContainer = document.querySelector('.statistics-container'); |
| if (statsContainer) { |
| const dayElement = document.createElement('div'); |
| dayElement.id = 'day-counter'; |
| dayElement.style.fontSize = '18px'; |
| dayElement.style.fontWeight = 'bold'; |
| dayElement.style.marginBottom = '10px'; |
| dayElement.style.textAlign = 'center'; |
| dayElement.style.color = '#FFD700'; |
| dayElement.textContent = `Day ${dayCounter}`; |
| statsContainer.prepend(dayElement); |
| |
| |
| const hourElement = document.createElement('span'); |
| hourElement.id = 'hour-indicator'; |
| hourElement.style.fontSize = '14px'; |
| hourElement.style.marginLeft = '10px'; |
| hourElement.style.color = '#AAAAAA'; |
| hourElement.textContent = `(Hour ${hourCounter})`; |
| dayElement.appendChild(hourElement); |
| } |
| } else { |
| document.getElementById('day-counter').textContent = `Day ${dayCounter}`; |
| |
| |
| if (!document.getElementById('hour-indicator')) { |
| const dayCounterElement = document.getElementById('day-counter'); |
| const hourElement = document.createElement('span'); |
| hourElement.id = 'hour-indicator'; |
| hourElement.style.fontSize = '14px'; |
| hourElement.style.marginLeft = '10px'; |
| hourElement.style.color = '#AAAAAA'; |
| hourElement.textContent = `(Hour ${hourCounter})`; |
| dayCounterElement.appendChild(hourElement); |
| } else { |
| document.getElementById('hour-indicator').textContent = `(Hour ${hourCounter})`; |
| } |
| } |
| |
| |
| updateUnitTypeCounts(); |
| |
| |
| let blueVariation = Math.random() * 5 - 2.5; |
| let newBlueControl = Math.min(Math.max(statistics.blueControl + blueVariation, 10), 90); |
| |
| statistics.blueControl = newBlueControl; |
| statistics.redControl = 100 - newBlueControl; |
| } |
|
|
| function spawnReinforcements() { |
| |
| const minimumForceSize = 10 + Math.floor(dayCounter / 2); |
| |
| |
| const totalActive = statistics.blueActive + statistics.redActive; |
| const maxTotalUnits = 60 + (dayCounter * 5) + Math.floor(Math.random() * 20); |
| |
| |
| if (totalActive < maxTotalUnits) { |
| |
| let blueSpawned = 0; |
| let redSpawned = 0; |
| |
| |
| if (statistics.blueActive < minimumForceSize) { |
| spawnNewUnit('blue'); |
| blueSpawned++; |
| statistics.blueReinforcements++; |
| } |
| |
| if (statistics.redActive < minimumForceSize) { |
| spawnNewUnit('red'); |
| redSpawned++; |
| statistics.redReinforcements++; |
| } |
| |
| |
| let spawnBlueSide = Math.random() < (statistics.redActive / (statistics.blueActive + statistics.redActive)); |
| let faction = spawnBlueSide ? 'blue' : 'red'; |
| |
| |
| |
| const extraUnitCount = Math.floor(Math.random() * 5) + 1 + Math.floor(dayCounter / 10); |
| |
| |
| for (let i = 0; i < extraUnitCount; i++) { |
| |
| if (i > 0 && Math.random() < 0.5) { |
| faction = faction === 'blue' ? 'red' : 'blue'; |
| } |
| |
| |
| if (Math.random() < 0.2) { |
| |
| const availableTypes = Object.entries(unitTypes) |
| .filter(([type, props]) => props.unlockDay <= dayCounter) |
| .map(([type]) => type); |
| |
| |
| const randomType = availableTypes[Math.floor(Math.random() * availableTypes.length)]; |
| spawnNewUnit(faction, randomType); |
| } else { |
| spawnNewUnit(faction); |
| } |
| |
| |
| if (faction === 'blue') { |
| blueSpawned++; |
| statistics.blueReinforcements++; |
| } else { |
| redSpawned++; |
| statistics.redReinforcements++; |
| } |
| } |
| |
| |
| if (blueSpawned >= 3) { |
| displayEventNotification( |
| 'Blue Reinforcements', |
| `${blueSpawned} Blue units have arrived as reinforcements` |
| ); |
| } |
| |
| if (redSpawned >= 3) { |
| displayEventNotification( |
| 'Red Reinforcements', |
| `${redSpawned} Red units have arrived as reinforcements` |
| ); |
| } |
| } |
| |
| |
| |
| |
| |
| const controlVariation = Math.random() * 4 - 2; |
| statistics.blueControl = Math.min(Math.max(statistics.blueControl + controlVariation, 15), 85); |
| statistics.redControl = 100 - statistics.blueControl; |
| |
| |
| const battleDelta = Math.random() < 0.3 ? (Math.random() < 0.5 ? 1 : -1) : 0; |
| statistics.activeBattles = Math.max(1, statistics.activeBattles + battleDelta); |
| } |
|
|
| function spawnNewUnit(faction) { |
| const type = getRandomUnitType(); |
| const unitId = statistics.nextUnitId++; |
| |
| |
| let lat, lng; |
| |
| if (faction === 'blue') { |
| |
| lat = 40.7 + (Math.random() * 0.1 - 0.05); |
| lng = -74.05 - (Math.random() * 0.02); |
| statistics.blueActive++; |
| statistics.blueReinforcements++; |
| } else { |
| |
| lat = 40.7 + (Math.random() * 0.1 - 0.05); |
| lng = -73.95 + (Math.random() * 0.02); |
| statistics.redActive++; |
| statistics.redReinforcements++; |
| } |
| |
| |
| const newUnit = { |
| id: `${faction}-${unitId}`, |
| name: `${faction.charAt(0).toUpperCase() + faction.slice(1)} Force ${getUnitName(type)} ${unitId}`, |
| type: type, |
| faction: faction, |
| strength: Math.floor(Math.random() * 50) + 50, |
| health: 100, |
| status: 'Reinforcement', |
| lat: lat, |
| lng: lng, |
| targetLat: null, |
| targetLng: null, |
| marker: null, |
| icon: unitTypes[type].icon, |
| speed: unitTypes[type].speed, |
| attackPower: unitTypes[type].attackPower, |
| range: unitTypes[type].range, |
| inBattle: false, |
| movingToTarget: false, |
| kills: 0, |
| lastMoveTimestamp: Date.now() |
| }; |
| |
| units.push(newUnit); |
| |
| |
| const markerSize = newUnit.strength / 15 + 20; |
| |
| const markerHtml = ` |
| <div class="unit-marker ${newUnit.faction}-unit" style="width: ${markerSize}px; height: ${markerSize}px; line-height: ${markerSize}px; font-size: ${markerSize/2}px;"> |
| ${newUnit.icon} |
| </div> |
| `; |
| |
| const customIcon = L.divIcon({ |
| html: markerHtml, |
| className: '', |
| iconSize: [markerSize, markerSize], |
| iconAnchor: [markerSize/2, markerSize/2] |
| }); |
| |
| newUnit.marker = L.marker([newUnit.lat, newUnit.lng], { icon: customIcon }) |
| .addTo(map) |
| .on('click', function() { |
| selectUnit(newUnit); |
| }); |
| |
| |
| assignTargetToUnit(newUnit); |
| } |
|
|
| function assignTargetToUnit(unit) { |
| |
| const enemyFaction = unit.faction === 'blue' ? 'red' : 'blue'; |
| const enemies = units.filter(u => u.faction === enemyFaction && u.health > 0); |
| |
| if (enemies.length > 0) { |
| |
| const target = enemies[Math.floor(Math.random() * enemies.length)]; |
| unit.targetLat = target.lat; |
| unit.targetLng = target.lng; |
| unit.movingToTarget = true; |
| unit.status = 'Advancing'; |
| } else { |
| |
| unit.targetLat = 40.7; |
| unit.targetLng = -74.0; |
| unit.movingToTarget = true; |
| unit.status = 'Advancing'; |
| } |
| } |
|
|
| function updateTime() { |
| hourCounter++; |
| if (hourCounter >= 24) { |
| hourCounter = 0; |
| dayCounter++; |
| |
| |
| const newlyUnlockedTypes = Object.entries(unitTypes) |
| .filter(([type, props]) => props.unlockDay === dayCounter) |
| .map(([type]) => type); |
| |
| if (newlyUnlockedTypes.length > 0) { |
| displayTechUnlockNotification(newlyUnlockedTypes); |
| } |
| |
| |
| const blueSurvivors = units.filter(u => u.faction === 'blue' && u.health > 0).length; |
| const redSurvivors = units.filter(u => u.faction === 'red' && u.health > 0).length; |
| |
| |
| const blueReinforcements = 1 + Math.floor(Math.random() * 3); |
| const redReinforcements = 1 + Math.floor(Math.random() * 3); |
| |
| for (let i = 0; i < blueReinforcements; i++) { |
| spawnNewUnit('blue'); |
| } |
| |
| for (let i = 0; i < redReinforcements; i++) { |
| spawnNewUnit('red'); |
| } |
| |
| |
| displayEventNotification( |
| `Day ${dayCounter} Begins`, |
| 'Forces are repositioning and receiving reinforcements' |
| ); |
| } |
| |
| |
| const formattedHours = hourCounter.toString().padStart(2, '0'); |
| |
| |
| const dayCounter_el = document.getElementById('day-counter'); |
| if (dayCounter_el) { |
| dayCounter_el.textContent = `Day ${dayCounter} - ${formattedHours}:00 hours`; |
| } |
| } |
|
|
| |
| function displayTechUnlockNotification(unlockTypes) { |
| |
| const notification = document.createElement('div'); |
| notification.className = 'tech-unlock-notification'; |
| notification.style.position = 'absolute'; |
| notification.style.top = '50%'; |
| notification.style.left = '50%'; |
| notification.style.transform = 'translate(-50%, -50%)'; |
| notification.style.backgroundColor = 'rgba(0, 0, 0, 0.85)'; |
| notification.style.color = 'white'; |
| notification.style.padding = '20px'; |
| notification.style.borderRadius = '10px'; |
| notification.style.zIndex = '2000'; |
| notification.style.minWidth = '300px'; |
| notification.style.textAlign = 'center'; |
| notification.style.boxShadow = '0 0 30px gold'; |
| notification.style.border = '2px solid gold'; |
| |
| |
| let innerHTML = ` |
| <h2 style="margin: 0 0 15px 0; color: gold;">New Technology Unlocked!</h2> |
| <div style="display: flex; flex-direction: column; gap: 10px;"> |
| `; |
| |
| unlockTypes.forEach(type => { |
| innerHTML += ` |
| <div style="display: flex; align-items: center; justify-content: center; gap: 10px;"> |
| <span style="font-size: 24px;">${unitTypes[type].icon}</span> |
| <span style="font-size: 18px;">${getUnitName(type)}</span> |
| </div> |
| `; |
| }); |
| |
| innerHTML += '</div>'; |
| notification.innerHTML = innerHTML; |
| |
| document.body.appendChild(notification); |
| |
| |
| if (!document.getElementById('notification-animations')) { |
| const styleEl = document.createElement('style'); |
| styleEl.id = 'notification-animations'; |
| styleEl.textContent = ` |
| @keyframes fadeIn { |
| from { opacity: 0; transform: translate(-50%, -60%); } |
| to { opacity: 1; transform: translate(-50%, -50%); } |
| } |
| |
| @keyframes fadeOut { |
| from { opacity: 1; transform: translate(-50%, -50%); } |
| to { opacity: 0; transform: translate(-50%, -40%); } |
| } |
| |
| .tech-unlock-notification { |
| animation: fadeIn 0.8s forwards; |
| } |
| |
| .tech-unlock-notification.fadeout { |
| animation: fadeOut 0.8s forwards; |
| } |
| `; |
| document.head.appendChild(styleEl); |
| } |
| |
| |
| setTimeout(() => { |
| notification.classList.add('fadeout'); |
| |
| setTimeout(() => { |
| if (notification.parentNode) { |
| notification.parentNode.removeChild(notification); |
| } |
| }, 800); |
| }, 5000); |
| } |
|
|
| function togglePause() { |
| isPaused = !isPaused; |
| const pauseButton = document.getElementById('pause-button'); |
| pauseButton.textContent = isPaused ? 'Resume' : 'Pause'; |
| } |
|
|
| function assignRandomTargets(chance = 0.3) { |
| const activeFactions = ['blue', 'red']; |
| |
| |
| units.forEach(unit => { |
| |
| if (!unit.inBattle && Math.random() < chance) { |
| |
| const enemyFaction = unit.faction === 'blue' ? 'red' : 'blue'; |
| const enemies = units.filter(u => u.faction === enemyFaction && u.health > 0); |
| |
| if (enemies.length > 0) { |
| |
| let target; |
| if (Math.random() < 0.2) { |
| |
| const sortedEnemies = [...enemies].sort((a, b) => a.health - b.health); |
| const weakEnemyIndex = Math.floor(Math.random() * Math.ceil(sortedEnemies.length / 3)); |
| target = sortedEnemies[weakEnemyIndex]; |
| } else { |
| |
| target = enemies[Math.floor(Math.random() * enemies.length)]; |
| } |
| |
| unit.targetLat = target.lat; |
| unit.targetLng = target.lng; |
| unit.movingToTarget = true; |
| |
| |
| const statuses = ['Advancing', 'Flanking', 'Pursuing', 'Engaging']; |
| unit.status = statuses[Math.floor(Math.random() * statuses.length)]; |
| } |
| } |
| }); |
| |
| |
| if (Math.random() < 0.1) { |
| upgradeRandomUnit(); |
| } |
| } |
|
|
| function upgradeRandomUnit() { |
| |
| const eligibleUnits = units.filter(u => u.health > 50 && u.kills > 0); |
| |
| if (eligibleUnits.length > 0) { |
| const unit = eligibleUnits[Math.floor(Math.random() * eligibleUnits.length)]; |
| |
| |
| unit.attackPower = Math.min(100, unit.attackPower + 5); |
| unit.range = Math.min(0.05, unit.range + 0.002); |
| unit.speed = Math.min(0.005, unit.speed * 1.1); |
| |
| |
| if (unit.marker) { |
| const markerSize = unit.strength / 15 + 20 + (unit.kills * 0.5); |
| |
| |
| let eliteIndicator = ''; |
| if (unit.kills >= 10) { |
| eliteIndicator = '⭐'; |
| } else if (unit.kills >= 5) { |
| eliteIndicator = '★'; |
| } |
| |
| const markerHtml = ` |
| <div class="unit-marker ${unit.faction}-unit" style="width: ${markerSize}px; height: ${markerSize}px; line-height: ${markerSize}px; font-size: ${markerSize/2}px;"> |
| ${unit.icon}${eliteIndicator} |
| </div> |
| `; |
| |
| const customIcon = L.divIcon({ |
| html: markerHtml, |
| className: '', |
| iconSize: [markerSize, markerSize], |
| iconAnchor: [markerSize/2, markerSize/2] |
| }); |
| |
| unit.marker.setIcon(customIcon); |
| } |
| } |
| } |
|
|
| function moveUnits() { |
| const now = Date.now(); |
| |
| units.forEach(unit => { |
| |
| if (unit.health > 0 && unit.movingToTarget && unit.targetLat && unit.targetLng) { |
| const timeDelta = (now - unit.lastMoveTimestamp) / 1000; |
| unit.lastMoveTimestamp = now; |
| |
| |
| const dLat = unit.targetLat - unit.lat; |
| const dLng = unit.targetLng - unit.lng; |
| |
| |
| const distance = Math.sqrt(dLat * dLat + dLng * dLng); |
| |
| |
| if (distance < 0.0005) { |
| unit.movingToTarget = false; |
| unit.status = 'Operational'; |
| return; |
| } |
| |
| |
| const magnitude = Math.sqrt(dLat * dLat + dLng * dLng); |
| const dirLat = dLat / magnitude; |
| const dirLng = dLng / magnitude; |
| |
| |
| const moveDistance = unit.speed * timeDelta; |
| unit.lat += dirLat * moveDistance; |
| unit.lng += dirLng * moveDistance; |
| |
| |
| if (unit.marker) { |
| unit.marker.setLatLng([unit.lat, unit.lng]); |
| } |
| } |
| }); |
| } |
|
|
| function checkForBattles() { |
| |
| document.querySelectorAll('.battle-indicator').forEach(el => { |
| |
| if (el.dataset.timestamp && (Date.now() - parseInt(el.dataset.timestamp)) > 3000) { |
| el.remove(); |
| } |
| }); |
| |
| |
| statistics.activeBattles = 0; |
| |
| |
| const battlePairs = new Set(); |
| |
| |
| units.forEach(unit => { |
| if (unit.health <= 0) return; |
| |
| const enemyFaction = unit.faction === 'blue' ? 'red' : 'blue'; |
| const enemies = units.filter(u => u.faction === enemyFaction && u.health > 0); |
| |
| |
| const wasInBattle = unit.inBattle; |
| unit.inBattle = false; |
| |
| enemies.forEach(enemy => { |
| |
| const dLat = unit.lat - enemy.lat; |
| const dLng = unit.lng - enemy.lng; |
| const distance = Math.sqrt(dLat * dLat + dLng * dLng); |
| |
| |
| if (distance < unit.range) { |
| |
| unit.inBattle = true; |
| enemy.inBattle = true; |
| |
| |
| const battleId = [unit.id, enemy.id].sort().join('-'); |
| |
| |
| if (!battlePairs.has(battleId)) { |
| battlePairs.add(battleId); |
| statistics.activeBattles++; |
| |
| |
| const battleLat = (unit.lat + enemy.lat) / 2 + (Math.random() * 0.002 - 0.001); |
| const battleLng = (unit.lng + enemy.lng) / 2 + (Math.random() * 0.002 - 0.001); |
| |
| |
| |
| if (Math.random() < 0.7 || unit.kills >= 3 || enemy.kills >= 3) { |
| createBattleAnimation(battleLat, battleLng); |
| } |
| } |
| |
| |
| const battleStatuses = ['Engaged', 'In Combat', 'Fighting', 'Attacking', 'Defending']; |
| unit.status = battleStatuses[Math.floor(Math.random() * battleStatuses.length)]; |
| enemy.status = battleStatuses[Math.floor(Math.random() * battleStatuses.length)]; |
| |
| |
| |
| let unitBaseDamage = unit.attackPower * 0.15 * (0.5 + Math.random()); |
| let enemyBaseDamage = enemy.attackPower * 0.15 * (0.5 + Math.random()); |
| |
| |
| unitBaseDamage *= (1 + (unit.kills * 0.05)); |
| enemyBaseDamage *= (1 + (enemy.kills * 0.05)); |
| |
| |
| unitBaseDamage *= (0.5 + (unit.health / 200)); |
| enemyBaseDamage *= (0.5 + (enemy.health / 200)); |
| |
| |
| const unitDamage = Math.round(unitBaseDamage * (0.8 + Math.random() * 0.4)); |
| const enemyDamage = Math.round(enemyBaseDamage * (0.8 + Math.random() * 0.4)); |
| |
| |
| enemy.health = Math.max(0, enemy.health - unitDamage); |
| unit.health = Math.max(0, unit.health - enemyDamage); |
| |
| |
| updateUnitMarkerAppearance(unit); |
| updateUnitMarkerAppearance(enemy); |
| |
| |
| if (Math.random() < 0.05) { |
| const criticalTarget = Math.random() < 0.5 ? unit : enemy; |
| const criticalDamage = Math.round(criticalTarget.health * 0.3); |
| criticalTarget.health = Math.max(0, criticalTarget.health - criticalDamage); |
| |
| |
| if (criticalTarget.marker) { |
| const point = map.latLngToLayerPoint([criticalTarget.lat, criticalTarget.lng]); |
| |
| const criticalEl = document.createElement('div'); |
| criticalEl.style.position = 'absolute'; |
| criticalEl.style.left = point.x + 'px'; |
| criticalEl.style.top = point.y + 'px'; |
| criticalEl.style.fontSize = '16px'; |
| criticalEl.style.fontWeight = 'bold'; |
| criticalEl.style.color = '#ff0000'; |
| criticalEl.style.textShadow = '0 0 3px #ffffff'; |
| criticalEl.style.zIndex = '1000'; |
| criticalEl.style.transition = 'transform 1s, opacity 1s'; |
| criticalEl.style.opacity = '1'; |
| criticalEl.textContent = "CRITICAL!"; |
| |
| document.querySelector('#map').appendChild(criticalEl); |
| |
| setTimeout(() => { |
| criticalEl.style.transform = 'translateY(-20px) scale(1.5)'; |
| criticalEl.style.opacity = '0'; |
| }, 100); |
| |
| setTimeout(() => { |
| if (criticalEl.parentNode) { |
| criticalEl.parentNode.removeChild(criticalEl); |
| } |
| }, 1100); |
| } |
| } |
| |
| |
| if (enemy.health <= 0 && unitDamage > 0) { |
| unit.kills++; |
| checkUnitDeath(enemy); |
| |
| if (unit.marker) { |
| const point = map.latLngToLayerPoint([unit.lat, unit.lng]); |
| showVictoryEffect(point.x, point.y, unit.faction); |
| } |
| } |
| |
| |
| if (unit.health <= 0 && enemyDamage > 0) { |
| enemy.kills++; |
| checkUnitDeath(unit); |
| |
| if (enemy.marker) { |
| const point = map.latLngToLayerPoint([enemy.lat, enemy.lng]); |
| showVictoryEffect(point.x, point.y, enemy.faction); |
| } |
| return; |
| } |
| } |
| }); |
| |
| |
| if (wasInBattle && !unit.inBattle) { |
| |
| const reassignChance = 0.3 + (unit.kills * 0.05); |
| if (Math.random() < reassignChance) { |
| assignTargetToUnit(unit); |
| } |
| } |
| }); |
| } |
|
|
| function showVictoryEffect(x, y, faction) { |
| const victoryEl = document.createElement('div'); |
| victoryEl.style.position = 'absolute'; |
| victoryEl.style.left = x + 'px'; |
| victoryEl.style.top = y + 'px'; |
| victoryEl.style.fontSize = '14px'; |
| victoryEl.style.fontWeight = 'bold'; |
| victoryEl.style.color = faction === 'blue' ? '#3498db' : '#e74c3c'; |
| victoryEl.style.textShadow = '0 0 3px white'; |
| victoryEl.style.zIndex = '1000'; |
| victoryEl.style.transition = 'transform 1.5s, opacity 1.5s'; |
| victoryEl.style.opacity = '1'; |
| victoryEl.textContent = "Victory!"; |
| |
| document.querySelector('#map').appendChild(victoryEl); |
| |
| setTimeout(() => { |
| victoryEl.style.transform = 'translateY(-25px) scale(1.2)'; |
| victoryEl.style.opacity = '0'; |
| }, 100); |
| |
| setTimeout(() => { |
| if (victoryEl.parentNode) { |
| victoryEl.parentNode.removeChild(victoryEl); |
| } |
| }, 1600); |
| } |
|
|
| function updateUnitMarkerAppearance(unit) { |
| if (!unit.marker || unit.health <= 0) return; |
| |
| const markerSize = unit.strength / 15 + 20 + (unit.kills * 0.5); |
| |
| |
| let healthIndicator = ''; |
| let glowColor = ''; |
| |
| if (unit.health < 30) { |
| healthIndicator = '🩸'; |
| glowColor = 'rgba(255, 0, 0, 0.5)'; |
| } else if (unit.inBattle) { |
| healthIndicator = '⚔️'; |
| glowColor = 'rgba(255, 165, 0, 0.5)'; |
| } |
| |
| |
| let eliteIndicator = ''; |
| if (unit.kills >= 10) { |
| eliteIndicator = '⭐'; |
| } else if (unit.kills >= 5) { |
| eliteIndicator = '★'; |
| } |
| |
| const markerHtml = ` |
| <div class="unit-marker ${unit.faction}-unit" |
| style="width: ${markerSize}px; height: ${markerSize}px; |
| line-height: ${markerSize}px; |
| font-size: ${markerSize/2}px; |
| box-shadow: 0 0 8px ${glowColor};"> |
| ${unit.icon}${healthIndicator}${eliteIndicator} |
| </div> |
| `; |
| |
| const customIcon = L.divIcon({ |
| html: markerHtml, |
| className: '', |
| iconSize: [markerSize, markerSize], |
| iconAnchor: [markerSize/2, markerSize/2] |
| }); |
| |
| unit.marker.setIcon(customIcon); |
| } |
|
|
| function createBattleAnimation(lat, lng) { |
| |
| const point = map.latLngToLayerPoint([lat, lng]); |
| |
| |
| const indicatorCount = 1 + Math.floor(Math.random() * 3); |
| |
| for (let i = 0; i < indicatorCount; i++) { |
| |
| const offsetX = Math.random() * 30 - 15; |
| const offsetY = Math.random() * 30 - 15; |
| |
| |
| const animationTypes = ['flash', 'expand', 'pulse', 'burst']; |
| const battleType = animationTypes[Math.floor(Math.random() * animationTypes.length)]; |
| |
| |
| const size = 15 + Math.floor(Math.random() * 20); |
| |
| |
| const baseColors = ['#ffcc00', '#ff9900', '#ff3300', '#cc0000', '#3498db', '#e74c3c']; |
| const color = baseColors[Math.floor(Math.random() * baseColors.length)]; |
| |
| |
| setTimeout(() => { |
| const battleEl = document.createElement('div'); |
| battleEl.className = `battle-indicator battle-${battleType}`; |
| battleEl.style.left = (point.x + offsetX) + 'px'; |
| battleEl.style.top = (point.y + offsetY) + 'px'; |
| battleEl.style.width = size + 'px'; |
| battleEl.style.height = size + 'px'; |
| battleEl.style.backgroundColor = color; |
| |
| |
| document.querySelector('#map').appendChild(battleEl); |
| |
| |
| setTimeout(() => { |
| if (battleEl.parentNode) { |
| battleEl.parentNode.removeChild(battleEl); |
| } |
| }, 2000 + Math.random() * 1000); |
| }, i * 150); |
| } |
| |
| |
| const symbolCount = Math.floor(Math.random() * 4) + 1; |
| |
| for (let i = 0; i < symbolCount; i++) { |
| setTimeout(() => { |
| |
| const symbols = ['💥', '🔥', '⚔️', '💣', '🧨', '💀', '⚡', '🛡️', '🏹', '🗡️', '🔫']; |
| const symbol = symbols[Math.floor(Math.random() * symbols.length)]; |
| |
| const symbolEl = document.createElement('div'); |
| symbolEl.style.position = 'absolute'; |
| symbolEl.style.left = (point.x + Math.random() * 40 - 20) + 'px'; |
| symbolEl.style.top = (point.y + Math.random() * 40 - 20) + 'px'; |
| symbolEl.style.fontSize = (12 + Math.random() * 16) + 'px'; |
| symbolEl.style.zIndex = '1000'; |
| symbolEl.style.transition = 'transform 1.5s, opacity 1.5s'; |
| symbolEl.style.transform = 'translateY(0) rotate(0deg)'; |
| symbolEl.style.opacity = '1'; |
| symbolEl.textContent = symbol; |
| |
| document.querySelector('#map').appendChild(symbolEl); |
| |
| |
| const dirX = Math.random() * 30 - 15; |
| const dirY = -10 - Math.random() * 20; |
| const rotation = Math.random() * 360; |
| |
| setTimeout(() => { |
| symbolEl.style.transform = `translate(${dirX}px, ${dirY}px) rotate(${rotation}deg)`; |
| symbolEl.style.opacity = '0'; |
| }, 100); |
| |
| setTimeout(() => { |
| if (symbolEl.parentNode) { |
| symbolEl.parentNode.removeChild(symbolEl); |
| } |
| }, 1600); |
| }, i * 200); |
| } |
| |
| |
| if (Math.random() < 0.4) { |
| setTimeout(() => { |
| const damageEl = document.createElement('div'); |
| damageEl.style.position = 'absolute'; |
| damageEl.style.left = (point.x + Math.random() * 20 - 10) + 'px'; |
| damageEl.style.top = (point.y + Math.random() * 20 - 10) + 'px'; |
| damageEl.style.fontSize = '14px'; |
| damageEl.style.fontWeight = 'bold'; |
| damageEl.style.color = 'red'; |
| damageEl.style.textShadow = '0 0 3px white'; |
| damageEl.style.zIndex = '1000'; |
| damageEl.style.transition = 'transform 1.5s, opacity 1.5s'; |
| damageEl.style.opacity = '1'; |
| |
| |
| const damage = Math.floor(Math.random() * 20) + 5; |
| damageEl.textContent = `-${damage}`; |
| |
| document.querySelector('#map').appendChild(damageEl); |
| |
| setTimeout(() => { |
| damageEl.style.transform = 'translateY(-20px)'; |
| damageEl.style.opacity = '0'; |
| }, 100); |
| |
| setTimeout(() => { |
| if (damageEl.parentNode) { |
| damageEl.parentNode.removeChild(damageEl); |
| } |
| }, 1600); |
| }, Math.random() * 500); |
| } |
| } |
|
|
| function checkUnitDeath(unit) { |
| if (unit.health <= 0) { |
| |
| if (unit.marker) { |
| |
| const markerSize = 15; |
| |
| const markerHtml = ` |
| <div class="unit-marker ${unit.faction}-unit" style="width: ${markerSize}px; height: ${markerSize}px; line-height: ${markerSize}px; font-size: ${markerSize/2}px; opacity: 0.5; background-color: #888;"> |
| ☠️ |
| </div> |
| `; |
| |
| const destroyedIcon = L.divIcon({ |
| html: markerHtml, |
| className: '', |
| iconSize: [markerSize, markerSize], |
| iconAnchor: [markerSize/2, markerSize/2] |
| }); |
| |
| unit.marker.setIcon(destroyedIcon); |
| } |
| |
| |
| unit.status = 'Destroyed'; |
| unit.movingToTarget = false; |
| unit.inBattle = false; |
| |
| |
| if (unit.faction === 'blue') { |
| statistics.blueActive--; |
| statistics.blueCasualties++; |
| |
| |
| statistics.redControl = Math.min(statistics.redControl + 2, 90); |
| statistics.blueControl = 100 - statistics.redControl; |
| } else { |
| statistics.redActive--; |
| statistics.redCasualties++; |
| |
| |
| statistics.blueControl = Math.min(statistics.blueControl + 2, 90); |
| statistics.redControl = 100 - statistics.blueControl; |
| } |
| |
| |
| setTimeout(() => { |
| if (unit.marker) { |
| |
| const icon = unit.marker.options.icon; |
| const html = icon.options.html.replace('opacity: 0.5', 'opacity: 0.2'); |
| |
| const fadedIcon = L.divIcon({ |
| html: html, |
| className: icon.options.className, |
| iconSize: icon.options.iconSize, |
| iconAnchor: icon.options.iconAnchor |
| }); |
| |
| unit.marker.setIcon(fadedIcon); |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| } |
| }, 5000); |
| } |
| } |