// Map initialization
let map;
let units = [];
let selectedUnit = null;
let isPaused = false;
let dayCounter = 1;
let hourCounter = 0;
let battleIntervals = [];
let timer;
// Battle statistics
let statistics = {
blueActive: 15,
redActive: 15,
blueCasualties: 0,
redCasualties: 0,
blueReinforcements: 0,
redReinforcements: 0,
activeBattles: 0,
blueControl: 50,
redControl: 50,
nextUnitId: 30 // Starting after initial 30 units (15 blue + 15 red)
};
// Unit types with their properties - expanded with new types that unlock over time
const unitTypes = {
// Base units available from start
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 },
// Units that unlock over time
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 }
};
// Initialize the map when the DOM is fully loaded
document.addEventListener('DOMContentLoaded', function() {
initializeMap();
generateUnits();
placeUnitsOnMap();
startSimulation();
// Set up event listener for pause button
document.getElementById('pause-button').addEventListener('click', togglePause);
});
function initializeMap() {
// Create map centered on a fictional battle area
map = L.map('map', {
dragging: false, // Disable map dragging
zoomControl: false, // Disable zoom control buttons
doubleClickZoom: false, // Disable double click zoom
scrollWheelZoom: false, // Disable scroll wheel zoom
touchZoom: false, // Disable touch zoom
boxZoom: false, // Disable box zoom
keyboard: false // Disable keyboard navigation
}).setView([40.7, -74.0], 13);
// Add the base map tiles (OpenStreetMap)
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors',
maxZoom: 19
}).addTo(map);
}
function generateUnits() {
// Generate blue forces
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, // 50-99
health: 100,
status: getRandomStatus(),
lat: 40.7 + (Math.random() * 0.05 - 0.025), // Around the center
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()
});
}
// Generate red forces
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, // 50-99
health: 100,
status: getRandomStatus(),
lat: 40.7 + (Math.random() * 0.05 - 0.025), // Around the center
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() {
// Filter unit types based on the current day
const availableTypes = Object.entries(unitTypes)
.filter(([type, properties]) => properties.unlockDay <= dayCounter)
.map(([type]) => type);
// Give higher probability to newly unlocked units to make them more noticeable
const newlyUnlockedTypes = availableTypes.filter(type =>
unitTypes[type].unlockDay === dayCounter);
// 30% chance to select a newly unlocked unit type if available
if (newlyUnlockedTypes.length > 0 && Math.random() < 0.3) {
const randomIndex = Math.floor(Math.random() * newlyUnlockedTypes.length);
return newlyUnlockedTypes[randomIndex];
}
// Otherwise select randomly from all available types
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',
// New unit types
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; // Size based on strength
// Create custom HTML element for the marker
const markerHtml = `
${unit.icon}
`;
// Create the icon
const customIcon = L.divIcon({
html: markerHtml,
className: '',
iconSize: [markerSize, markerSize],
iconAnchor: [markerSize/2, markerSize/2]
});
// Create marker and add to map
unit.marker = L.marker([unit.lat, unit.lng], { icon: customIcon })
.addTo(map)
.on('click', function() {
selectUnit(unit);
});
});
}
function selectUnit(unit) {
// Reset previous selection
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);
}
// Set new selection
selectedUnit = unit;
// Update marker to show selection
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);
// Update info panel
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 = `
${unit.name}
Type: ${getUnitName(unit.type)}
Strength: ${unit.strength} personnel
Status: ${unit.status}
Attack Power: ${unit.attackPower}
Range: ${(unit.range * 1000).toFixed(1)} meters
Speed: ${(unit.speed * 1000).toFixed(1)}
Kills: ${unit.kills}
Position: ${unit.lat.toFixed(4)}, ${unit.lng.toFixed(4)}
`;
// If the unit is in battle, show that information
if (unit.inBattle) {
unitInfoDiv.innerHTML += `
โ๏ธ ENGAGED IN BATTLE โ๏ธ
`;
}
}
function startSimulation() {
// Update time every second
timer = setInterval(() => {
if (!isPaused) {
updateTime();
// More frequent target assignments on busier days
if (hourCounter % (3 - Math.min(2, Math.floor(dayCounter / 5))) === 0) {
assignRandomTargets();
}
// Dynamic reinforcement spawning
// Base chance increases with time and if there are fewer active battles
const baseSpawnChance = 0.15 + (dayCounter / 50);
const battleAdjustment = Math.max(0, 0.1 - (statistics.activeBattles / 30));
if (Math.random() < baseSpawnChance + battleAdjustment) {
spawnReinforcements();
}
// Create sudden "hot zones" where units are redirected
if (Math.random() < 0.03) {
createHotZone();
}
moveUnits();
checkForBattles();
updateBattleStatistics();
// Increase frequency of random events for more action
if (Math.random() < 0.05) { // 5x more frequent events
triggerRandomEvent();
}
// Ensure battle never ends by checking unit counts and adding reinforcements if needed
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 either faction is below minimum threshold, add emergency reinforcements
const MIN_UNITS_PER_FACTION = 8; // Ensure at least 8 units per side
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');
}
}
// Update the selected unit's info if one is selected
if (selectedUnit) {
updateUnitInfo(selectedUnit);
}
}
}, 1000);
}
// Helper function to get a random unit type based on day counter
function getRandomUnitType() {
// Get all unit types that have been unlocked by the current day
const availableTypes = Object.entries(unitTypes)
.filter(([type, properties]) => properties.unlockDay <= dayCounter)
.map(([type]) => type);
// Select a random type from available ones
return availableTypes[Math.floor(Math.random() * availableTypes.length)];
}
// Helper function to get a formatted name for a unit type
function getUnitName(type) {
// Format the type name with proper capitalization and spacing
const formattedName = type
.replace(/([A-Z])/g, ' $1') // Add space before capital letters
.replace(/^./, str => str.toUpperCase()); // Capitalize first letter
return formattedName;
}
function createHotZone() {
// Create a random focal point on the map with faction control bias
// This makes hotspots appear more often in controlled territory
let hotspotLat, hotspotLng;
if (Math.random() < 0.5) {
// Neutral hotspot
hotspotLat = 40.7 + (Math.random() * 0.1 - 0.05);
hotspotLng = -74.0 + (Math.random() * 0.1 - 0.05);
} else {
// Faction-biased hotspot
const dominantFaction = statistics.blueControl > statistics.redControl ? 'blue' : 'red';
if (dominantFaction === 'blue') {
// Blue territory tends to be west/south
hotspotLat = 40.68 + (Math.random() * 0.06);
hotspotLng = -74.03 - (Math.random() * 0.04);
} else {
// Red territory tends to be east/north
hotspotLat = 40.72 + (Math.random() * 0.06);
hotspotLng = -73.97 - (Math.random() * 0.04);
}
}
// Divert some portion of units to this hotspot
units.forEach(unit => {
if (unit.health > 0 && Math.random() < 0.3) {
unit.targetLat = hotspotLat;
unit.targetLng = hotspotLng;
unit.movingToTarget = true;
unit.status = 'Redeploying';
}
});
// Determine the type of hotspot (battle, supply drop, strategic location)
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)' }
];
// Select a hotspot type based on probability
let hotspotType;
const random = Math.random();
let cumulativeProbability = 0;
for (const type of hotspotTypes) {
cumulativeProbability += type.probability;
if (random < cumulativeProbability) {
hotspotType = type;
break;
}
}
// Determine if this is a major event (less common, but more dramatic)
const isMajorEvent = Math.random() < 0.3;
const size = isMajorEvent ? 60 : 40;
const duration = isMajorEvent ? 8000 : 4000;
// Create visual effect for the hotspot
const point = map.latLngToLayerPoint([hotspotLat, hotspotLng]);
// Create main hotspot element
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`;
// Add pulsing effect to make it more dynamic
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);
// Add secondary ring effect for major events
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';
// Outward expansion animation
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);
// Add notification for major events
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);
// Remove the outer ring after the duration
setTimeout(() => {
if (outerRing.parentNode) {
outerRing.parentNode.removeChild(outerRing);
}
}, duration);
}
// Remove the hotspot element after the duration
setTimeout(() => {
if (hotspotEl.parentNode) {
hotspotEl.parentNode.removeChild(hotspotEl);
}
}, duration);
}
// Function to spawn a new unit for a given faction
function spawnNewUnit(faction) {
// Determine entry point based on faction
let entryLat, entryLng;
if (faction === 'blue') {
// Blue forces come from west or south
if (Math.random() < 0.5) {
entryLat = 40.7 + (Math.random() * 0.05 - 0.025);
entryLng = -74.05 - (Math.random() * 0.02); // West
} else {
entryLat = 40.65 - (Math.random() * 0.02); // South
entryLng = -74.0 + (Math.random() * 0.05 - 0.025);
}
} else {
// Red forces come from east or north
if (Math.random() < 0.5) {
entryLat = 40.7 + (Math.random() * 0.05 - 0.025);
entryLng = -73.95 + (Math.random() * 0.02); // East
} else {
entryLat = 40.75 + (Math.random() * 0.02); // North
entryLng = -74.0 + (Math.random() * 0.05 - 0.025);
}
}
// Create a new unit
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), // 50-99
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);
// Create marker for the new unit
const markerSize = newUnit.strength / 15 + 20;
const markerHtml = `
${newUnit.icon}
`;
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);
});
// Add an entrance effect
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';
// Add custom animation if not already defined
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);
// Remove effect after animation completes
setTimeout(() => {
if (entryEffect.parentNode) entryEffect.parentNode.removeChild(entryEffect);
}, 1500);
// Assign target to the new unit
assignTargetToUnit(newUnit);
// Update faction statistics
if (faction === 'blue') {
statistics.blueActive++;
statistics.blueReinforcements++;
} else {
statistics.redActive++;
statistics.redReinforcements++;
}
return newUnit;
}
function triggerRandomEvent() {
// Increment hour counter - every 12 hours is a new day
hourCounter++;
if (hourCounter >= 12) {
hourCounter = 0;
dayCounter++;
// Update the day counter in UI if it exists
const dayCounterElement = document.getElementById('day-counter');
if (dayCounterElement) {
dayCounterElement.textContent = `Day ${dayCounter}`;
} else {
// Create day counter element if it doesn't exist
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);
}
}
// Check if new unit types have been unlocked
const newlyUnlocked = Object.entries(unitTypes)
.filter(([type, properties]) => properties.unlockDay === dayCounter)
.map(([type]) => type);
if (newlyUnlocked.length > 0) {
// Announce new technology
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 = `
New Technology Unlocked!
${newlyUnlocked.map(type => `${unitTypes[type].icon} ${getUnitName(type)}`).join('
')}
`;
document.body.appendChild(techAnnouncement);
// Remove after 5 seconds
setTimeout(() => {
techAnnouncement.style.opacity = '0';
techAnnouncement.style.transition = 'opacity 1s';
setTimeout(() => {
if (techAnnouncement.parentNode) {
techAnnouncement.parentNode.removeChild(techAnnouncement);
}
}, 1000);
}, 5000);
}
}
// List of possible random events with weighted probabilities
const events = [
{ name: 'Reinforcement Surge', weight: 0.3, handler: () => {
// Spawn extra units for a random faction
const faction = Math.random() < 0.5 ? 'blue' : 'red';
for (let i = 0; i < 3 + Math.floor(Math.random() * 3); i++) {
spawnNewUnit(faction);
}
// Display notification
displayEventNotification(
'Reinforcement Surge',
`${faction.charAt(0).toUpperCase() + faction.slice(1)} forces receiving reinforcements`
);
}},
{ name: 'Supply Line Disruption', weight: 0.15, handler: () => {
// Randomly weaken some units of one faction
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);
});
// Display notification
displayEventNotification(
'Supply Line Disruption',
`${faction.charAt(0).toUpperCase() + faction.slice(1)} forces weakened by supply issues`
);
}
}},
{ name: 'Strategic Redeployment', weight: 0.2, handler: () => {
// Randomly reassign targets to create new battle lines
assignRandomTargets(0.7); // Higher chance of reassignment
// Display notification
displayEventNotification(
'Strategic Redeployment',
'Forces repositioning for tactical advantage'
);
}},
{ name: 'Environmental Event', weight: 0.2, handler: () => {
// Trigger a weather or environment event
triggerEnvironmentalEvent();
}},
{ name: 'Hot Zone', weight: 0.15, handler: () => {
// Create a hot zone where forces converge
createHotZone();
// Display notification
displayEventNotification(
'Strategic Hotspot',
'A critical location has been identified for control'
);
}}
];
// Weighted random selection
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;
}
}
// Execute the selected event
selectedEvent.handler();
// Auto-balance forces if one side is getting too dominant
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) {
// If either side is getting too low, reinforce them
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 to display event notifications
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 = `
${title}
${message}
`;
document.body.appendChild(notification);
// Fade in
setTimeout(() => {
notification.style.opacity = '1';
}, 100);
// Remove after 5 seconds
setTimeout(() => {
notification.style.opacity = '0';
setTimeout(() => {
if (notification.parentNode) {
notification.parentNode.removeChild(notification);
}
}, 500);
}, 5000);
}
// Environmental events function
function triggerEnvironmentalEvent() {
// Random selection of environmental events
const events = [
{
name: 'Fog of War',
probability: 0.3,
handler: () => {
// Reduce all units' range temporarily
units.forEach(unit => {
if (!unit.originalRange) unit.originalRange = unit.range;
unit.range *= 0.5; // 50% reduction in range
});
// Add a fog overlay to the map
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);
// Show event notification
displayEventNotification('Fog of War', 'Visibility reduced for all units');
// Restore normal visibility after duration
setTimeout(() => {
// Fade out fog
fogOverlay.style.opacity = '0';
// Remove fog and restore ranges
setTimeout(() => {
if (fogOverlay.parentNode) fogOverlay.parentNode.removeChild(fogOverlay);
units.forEach(unit => {
if (unit.originalRange) {
unit.range = unit.originalRange;
delete unit.originalRange;
}
});
}, 2000);
}, 15000); // 15 seconds for simulation
return true;
}
},
{
name: 'Artillery Barrage',
probability: 0.3,
handler: () => {
// Random faction launches barrage
const launchingFaction = Math.random() < 0.5 ? 'blue' : 'red';
const targetFaction = launchingFaction === 'blue' ? 'red' : 'blue';
// Find target units
const targetUnits = units.filter(u => u.faction === targetFaction && u.health > 0);
if (targetUnits.length > 0) {
// Calculate target area (center of enemy forces)
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`
);
// Visual effects - multiple explosions
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);
// Create explosion animation
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';
// Add custom animation if not already defined
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);
// Apply damage to units in radius
targetUnits.forEach(unit => {
const dLat = unit.lat - offsetLat;
const dLng = unit.lng - offsetLng;
const distance = Math.sqrt(dLat * dLat + dLng * dLng);
// Units within blast radius take damage
if (distance < 0.005) {
const damage = Math.round(20 * (1 - distance / 0.005));
unit.health = Math.max(0, unit.health - damage);
// Update unit appearance
updateUnitMarkerAppearance(unit);
// Check for death
if (unit.health <= 0) {
checkUnitDeath(unit);
}
}
});
// Remove explosion after animation
setTimeout(() => {
if (explosionEl.parentNode) {
explosionEl.parentNode.removeChild(explosionEl);
}
}, 1500);
}, i * 800); // Staggered explosions
}
return true;
}
return false;
}
},
{
name: 'Supply Drop',
probability: 0.4,
handler: () => {
// Random faction receives supplies
const targetFaction = Math.random() < 0.5 ? 'blue' : 'red';
// Find units that would benefit
const targetUnits = units.filter(u => u.faction === targetFaction && u.health < 80 && u.health > 0);
if (targetUnits.length > 0) {
// Calculate drop zone
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`
);
// Visual effect for supply drop
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';
// Add custom animation if not already defined
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);
// Apply healing to nearby units
selectedUnits.forEach(unit => {
const dLat = unit.lat - dropLat;
const dLng = unit.lng - dropLng;
const distance = Math.sqrt(dLat * dLat + dLng * dLng);
// Units near the drop get healed
if (distance < 0.008) {
const healAmount = 15 + Math.floor(Math.random() * 20);
unit.health = Math.min(100, unit.health + healAmount);
// Update unit appearance
updateUnitMarkerAppearance(unit);
// Show healing effect
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);
}
}
});
// Remove supply after animation
setTimeout(() => {
if (supplyEl.parentNode) {
supplyEl.parentNode.removeChild(supplyEl);
}
}, 2000);
return true;
}
return false;
}
}
];
// Randomly select an event based on probability
const randomValue = Math.random();
let cumulativeProbability = 0;
for (const event of events) {
cumulativeProbability += event.probability;
if (randomValue < cumulativeProbability) {
// Execute the selected event and check if it was successful
if (event.handler()) {
return event.name;
}
}
}
// If no event was triggered or all failed, default to a supply drop for a random faction
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 to update unit type counts display
function updateUnitTypeCounts() {
// Container for unit type statistics
let unitCountsContainer = document.getElementById('unit-counts-container');
if (!unitCountsContainer) {
// Create container if it doesn't exist
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) {
// Clear previous counts (except the title)
while (unitCountsContainer.childNodes.length > 1) {
unitCountsContainer.removeChild(unitCountsContainer.lastChild);
}
// Get counts by unit type for each faction
const blueUnitCounts = {};
const redUnitCounts = {};
units.forEach(unit => {
if (unit.health <= 0) return; // Skip dead units
if (unit.faction === 'blue') {
blueUnitCounts[unit.type] = (blueUnitCounts[unit.type] || 0) + 1;
} else {
redUnitCounts[unit.type] = (redUnitCounts[unit.type] || 0) + 1;
}
});
// Get all unit types currently in use
const activeTypes = Object.keys({...blueUnitCounts, ...redUnitCounts})
.filter(type => unitTypes[type].unlockDay <= dayCounter)
.sort();
// Create unit type count display
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 = `
${unitTypes[type].icon}
${getUnitName(type)}
${blueCount}
${redCount}
`;
unitCountsContainer.appendChild(unitTypeEl);
});
}
}
function updateBattleStatistics() {
// Update UI statistics
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;
// Update territory control visualization
document.getElementById('blue-control').style.width = `${statistics.blueControl}%`;
document.getElementById('red-control').style.width = `${statistics.redControl}%`;
// Update day counter or create it if it doesn't exist
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'; // Gold color for day counter
dayElement.textContent = `Day ${dayCounter}`;
statsContainer.prepend(dayElement);
// Add hour indicator
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}`;
// Update hour indicator if it exists, or create it
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})`;
}
}
// Update unit type counts
updateUnitTypeCounts();
// Randomize territory control slightly for visual effect
let blueVariation = Math.random() * 5 - 2.5; // -2.5 to +2.5
let newBlueControl = Math.min(Math.max(statistics.blueControl + blueVariation, 10), 90); // Keep between 10% and 90%
statistics.blueControl = newBlueControl;
statistics.redControl = 100 - newBlueControl;
}
function spawnReinforcements() {
// Ensure minimum active forces for each side
const minimumForceSize = 10 + Math.floor(dayCounter / 2); // Increases with time
// Calculate the total active units
const totalActive = statistics.blueActive + statistics.redActive;
const maxTotalUnits = 60 + (dayCounter * 5) + Math.floor(Math.random() * 20); // More variance in unit cap
// If we're under the maximum total, proceed with reinforcements
if (totalActive < maxTotalUnits) {
// Track reinforcements for notification
let blueSpawned = 0;
let redSpawned = 0;
// Always spawn at least one unit for each faction that's below minimum strength
if (statistics.blueActive < minimumForceSize) {
spawnNewUnit('blue');
blueSpawned++;
statistics.blueReinforcements++;
}
if (statistics.redActive < minimumForceSize) {
spawnNewUnit('red');
redSpawned++;
statistics.redReinforcements++;
}
// Additional random reinforcements (favor the losing side)
let spawnBlueSide = Math.random() < (statistics.redActive / (statistics.blueActive + statistics.redActive));
let faction = spawnBlueSide ? 'blue' : 'red';
// Determine how many extra units to spawn (1-4, increasing with time)
// Increased maximum from 3+1 to 5+1 for more variance
const extraUnitCount = Math.floor(Math.random() * 5) + 1 + Math.floor(dayCounter / 10);
// Generate units for the chosen faction
for (let i = 0; i < extraUnitCount; i++) {
// 50% chance to switch to the other faction for balance
if (i > 0 && Math.random() < 0.5) {
faction = faction === 'blue' ? 'red' : 'blue';
}
// 20% chance to use a specific unit type for variety
if (Math.random() < 0.2) {
// Get all available unit types
const availableTypes = Object.entries(unitTypes)
.filter(([type, props]) => props.unlockDay <= dayCounter)
.map(([type]) => type);
// Pick a random type
const randomType = availableTypes[Math.floor(Math.random() * availableTypes.length)];
spawnNewUnit(faction, randomType);
} else {
spawnNewUnit(faction);
}
// Count reinforcements
if (faction === 'blue') {
blueSpawned++;
statistics.blueReinforcements++;
} else {
redSpawned++;
statistics.redReinforcements++;
}
}
// Display notification if significant reinforcements arrive
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`
);
}
}
// Randomly vary some statistics to make the display more dynamic even if not much is happening
// This creates the impression of ongoing battle even if units are repositioning
// Randomize territory control slightly for visual effect
const controlVariation = Math.random() * 4 - 2; // -2 to +2 percent
statistics.blueControl = Math.min(Math.max(statistics.blueControl + controlVariation, 15), 85);
statistics.redControl = 100 - statistics.blueControl;
// Vary active battles count slightly
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++;
// Calculate spawn location - at the edges of the map with some randomness
let lat, lng;
if (faction === 'blue') {
// Blue forces spawn from the west
lat = 40.7 + (Math.random() * 0.1 - 0.05);
lng = -74.05 - (Math.random() * 0.02); // West of center
statistics.blueActive++;
statistics.blueReinforcements++;
} else {
// Red forces spawn from the east
lat = 40.7 + (Math.random() * 0.1 - 0.05);
lng = -73.95 + (Math.random() * 0.02); // East of center
statistics.redActive++;
statistics.redReinforcements++;
}
// Create the new unit
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, // 50-99
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);
// Create marker for the new unit
const markerSize = newUnit.strength / 15 + 20;
const markerHtml = `
${newUnit.icon}
`;
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);
});
// Immediately assign target to move toward
assignTargetToUnit(newUnit);
}
function assignTargetToUnit(unit) {
// Find a strategic position to move to
const enemyFaction = unit.faction === 'blue' ? 'red' : 'blue';
const enemies = units.filter(u => u.faction === enemyFaction && u.health > 0);
if (enemies.length > 0) {
// Choose random enemy as target
const target = enemies[Math.floor(Math.random() * enemies.length)];
unit.targetLat = target.lat;
unit.targetLng = target.lng;
unit.movingToTarget = true;
unit.status = 'Advancing';
} else {
// Move toward center of map if no enemies
unit.targetLat = 40.7;
unit.targetLng = -74.0;
unit.movingToTarget = true;
unit.status = 'Advancing';
}
}
function updateTime() {
hourCounter++;
if (hourCounter >= 24) {
hourCounter = 0;
dayCounter++;
// Check for newly unlocked units on day change
const newlyUnlockedTypes = Object.entries(unitTypes)
.filter(([type, props]) => props.unlockDay === dayCounter)
.map(([type]) => type);
if (newlyUnlockedTypes.length > 0) {
displayTechUnlockNotification(newlyUnlockedTypes);
}
// Add additional random reinforcements on new day
const blueSurvivors = units.filter(u => u.faction === 'blue' && u.health > 0).length;
const redSurvivors = units.filter(u => u.faction === 'red' && u.health > 0).length;
// Add 1-3 reinforcements for each faction at day change
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');
}
// Display day change notification
displayEventNotification(
`Day ${dayCounter} Begins`,
'Forces are repositioning and receiving reinforcements'
);
}
// Format hours with leading zero
const formattedHours = hourCounter.toString().padStart(2, '0');
// Make sure day-counter element exists before updating
const dayCounter_el = document.getElementById('day-counter');
if (dayCounter_el) {
dayCounter_el.textContent = `Day ${dayCounter} - ${formattedHours}:00 hours`;
}
}
// Display tech unlock notification
function displayTechUnlockNotification(unlockTypes) {
// Create notification element
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';
// Create content
let innerHTML = `
New Technology Unlocked!
`;
unlockTypes.forEach(type => {
innerHTML += `
${unitTypes[type].icon}
${getUnitName(type)}
`;
});
innerHTML += '
';
notification.innerHTML = innerHTML;
document.body.appendChild(notification);
// Add animation CSS if not already defined
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);
}
// Remove after delay
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'];
// Randomly select units to assign new targets
units.forEach(unit => {
// Only assign new targets to units that aren't in battle and at random (default 30% chance)
if (!unit.inBattle && Math.random() < chance) {
// Find potential enemy targets
const enemyFaction = unit.faction === 'blue' ? 'red' : 'blue';
const enemies = units.filter(u => u.faction === enemyFaction && u.health > 0);
if (enemies.length > 0) {
// Choose random enemy as target - prefer weaker enemies occasionally
let target;
if (Math.random() < 0.2) {
// Sort by health ascending and pick from the weakest third
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 {
// Random target
target = enemies[Math.floor(Math.random() * enemies.length)];
}
unit.targetLat = target.lat;
unit.targetLng = target.lng;
unit.movingToTarget = true;
// Occasionally give units different movement statuses for variety
const statuses = ['Advancing', 'Flanking', 'Pursuing', 'Engaging'];
unit.status = statuses[Math.floor(Math.random() * statuses.length)];
}
}
});
// Occasionally upgrade some units
if (Math.random() < 0.1) {
upgradeRandomUnit();
}
}
function upgradeRandomUnit() {
// Find a unit that's active and has been in battles
const eligibleUnits = units.filter(u => u.health > 50 && u.kills > 0);
if (eligibleUnits.length > 0) {
const unit = eligibleUnits[Math.floor(Math.random() * eligibleUnits.length)];
// Boost its stats
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);
// Visual upgrade - make it slightly larger and add a star if it's reached elite status
if (unit.marker) {
const markerSize = unit.strength / 15 + 20 + (unit.kills * 0.5);
// Add a visual indicator for veteran/elite units
let eliteIndicator = '';
if (unit.kills >= 10) {
eliteIndicator = 'โญ';
} else if (unit.kills >= 5) {
eliteIndicator = 'โ
';
}
const markerHtml = `
${unit.icon}${eliteIndicator}
`;
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 => {
// Only move units that are alive and have a target
if (unit.health > 0 && unit.movingToTarget && unit.targetLat && unit.targetLng) {
const timeDelta = (now - unit.lastMoveTimestamp) / 1000; // Time in seconds since last move
unit.lastMoveTimestamp = now;
// Calculate direction vector
const dLat = unit.targetLat - unit.lat;
const dLng = unit.targetLng - unit.lng;
// Calculate distance to target
const distance = Math.sqrt(dLat * dLat + dLng * dLng);
// If we've reached the target (or close enough)
if (distance < 0.0005) {
unit.movingToTarget = false;
unit.status = 'Operational';
return;
}
// Normalize direction vector
const magnitude = Math.sqrt(dLat * dLat + dLng * dLng);
const dirLat = dLat / magnitude;
const dirLng = dLng / magnitude;
// Move unit towards target with speed influenced by unit type
const moveDistance = unit.speed * timeDelta;
unit.lat += dirLat * moveDistance;
unit.lng += dirLng * moveDistance;
// Update marker position
if (unit.marker) {
unit.marker.setLatLng([unit.lat, unit.lng]);
}
}
});
}
function checkForBattles() {
// Clear any existing battle animations that are outdated
document.querySelectorAll('.battle-indicator').forEach(el => {
// Only remove indicators that have been around for more than 3 seconds
if (el.dataset.timestamp && (Date.now() - parseInt(el.dataset.timestamp)) > 3000) {
el.remove();
}
});
// Reset active battles counter
statistics.activeBattles = 0;
// Create a set to track unique battle pairs
const battlePairs = new Set();
// Check each unit against potential enemies
units.forEach(unit => {
if (unit.health <= 0) return; // Skip dead units
const enemyFaction = unit.faction === 'blue' ? 'red' : 'blue';
const enemies = units.filter(u => u.faction === enemyFaction && u.health > 0);
// Reset battle state
const wasInBattle = unit.inBattle;
unit.inBattle = false;
enemies.forEach(enemy => {
// Calculate distance between units
const dLat = unit.lat - enemy.lat;
const dLng = unit.lng - enemy.lng;
const distance = Math.sqrt(dLat * dLat + dLng * dLng);
// If within range, start battle
if (distance < unit.range) {
// Mark both units as in battle
unit.inBattle = true;
enemy.inBattle = true;
// Create a unique identifier for this battle pair
const battleId = [unit.id, enemy.id].sort().join('-');
// Only count unique battles
if (!battlePairs.has(battleId)) {
battlePairs.add(battleId);
statistics.activeBattles++;
// Create battle animation at midpoint with slight randomness
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);
// More frequent battle animations for increased visual impact
// but avoid too many by only creating on every other check or for elite units
if (Math.random() < 0.7 || unit.kills >= 3 || enemy.kills >= 3) {
createBattleAnimation(battleLat, battleLng);
}
}
// More varied battle statuses
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)];
// More dynamic damage calculation
// Base damage
let unitBaseDamage = unit.attackPower * 0.15 * (0.5 + Math.random());
let enemyBaseDamage = enemy.attackPower * 0.15 * (0.5 + Math.random());
// Modify damage based on unit kills/experience (veteran bonus)
unitBaseDamage *= (1 + (unit.kills * 0.05));
enemyBaseDamage *= (1 + (enemy.kills * 0.05));
// Health-based modifications (wounded units deal less damage)
unitBaseDamage *= (0.5 + (unit.health / 200));
enemyBaseDamage *= (0.5 + (enemy.health / 200));
// Final randomization and rounding
const unitDamage = Math.round(unitBaseDamage * (0.8 + Math.random() * 0.4));
const enemyDamage = Math.round(enemyBaseDamage * (0.8 + Math.random() * 0.4));
// Apply damage
enemy.health = Math.max(0, enemy.health - unitDamage);
unit.health = Math.max(0, unit.health - enemyDamage);
// Update marker appearance based on health
updateUnitMarkerAppearance(unit);
updateUnitMarkerAppearance(enemy);
// Critical hits for more drama (rare but powerful)
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);
// Visual indication of critical hit
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);
}
}
// Check if unit defeated enemy
if (enemy.health <= 0 && unitDamage > 0) {
unit.kills++;
checkUnitDeath(enemy);
// Victory effect
if (unit.marker) {
const point = map.latLngToLayerPoint([unit.lat, unit.lng]);
showVictoryEffect(point.x, point.y, unit.faction);
}
}
// Check if enemy defeated unit
if (unit.health <= 0 && enemyDamage > 0) {
enemy.kills++;
checkUnitDeath(unit);
// Victory effect
if (enemy.marker) {
const point = map.latLngToLayerPoint([enemy.lat, enemy.lng]);
showVictoryEffect(point.x, point.y, enemy.faction);
}
return; // Stop checking more enemies if this unit is dead
}
}
});
// If unit was in battle but no longer is, potentially assign new target
if (wasInBattle && !unit.inBattle) {
// Higher chance to pursue new targets for experienced units
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);
// Visual indicator for health status
let healthIndicator = '';
let glowColor = '';
if (unit.health < 30) {
healthIndicator = '๐ฉธ'; // Injured
glowColor = 'rgba(255, 0, 0, 0.5)';
} else if (unit.inBattle) {
healthIndicator = 'โ๏ธ'; // In battle
glowColor = 'rgba(255, 165, 0, 0.5)';
}
// Add a visual indicator for veteran/elite units
let eliteIndicator = '';
if (unit.kills >= 10) {
eliteIndicator = 'โญ';
} else if (unit.kills >= 5) {
eliteIndicator = 'โ
';
}
const markerHtml = `
${unit.icon}${healthIndicator}${eliteIndicator}
`;
const customIcon = L.divIcon({
html: markerHtml,
className: '',
iconSize: [markerSize, markerSize],
iconAnchor: [markerSize/2, markerSize/2]
});
unit.marker.setIcon(customIcon);
}
function createBattleAnimation(lat, lng) {
// Convert lat/lng to pixel coordinates
const point = map.latLngToLayerPoint([lat, lng]);
// Create multiple battle indicators for more intense visuals
const indicatorCount = 1 + Math.floor(Math.random() * 3); // 1-3 indicators per battle
for (let i = 0; i < indicatorCount; i++) {
// Add slight position variance for multiple indicators
const offsetX = Math.random() * 30 - 15;
const offsetY = Math.random() * 30 - 15;
// Choose a random battle animation type to add variety
const animationTypes = ['flash', 'expand', 'pulse', 'burst'];
const battleType = animationTypes[Math.floor(Math.random() * animationTypes.length)];
// Randomly vary the size and color of battle indicators
const size = 15 + Math.floor(Math.random() * 20); // 15-35px
// More varied colors including faction colors
const baseColors = ['#ffcc00', '#ff9900', '#ff3300', '#cc0000', '#3498db', '#e74c3c'];
const color = baseColors[Math.floor(Math.random() * baseColors.length)];
// Create battle indicator element with staggered timing
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;
// Add to map container
document.querySelector('#map').appendChild(battleEl);
// Remove element after animation completes
setTimeout(() => {
if (battleEl.parentNode) {
battleEl.parentNode.removeChild(battleEl);
}
}, 2000 + Math.random() * 1000);
}, i * 150); // Staggered start times
}
// Add combat symbols with more variety and quantity
const symbolCount = Math.floor(Math.random() * 4) + 1; // 1-4 symbols
for (let i = 0; i < symbolCount; i++) {
setTimeout(() => {
// Expanded symbol set with more variety
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);
// Random movement direction
const dirX = Math.random() * 30 - 15;
const dirY = -10 - Math.random() * 20; // Always move up somewhat
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); // Staggered symbols
}
// Occasionally add damage numbers for more battle feedback
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';
// Generate a random damage number
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) {
// Update the unit's appearance to show it's destroyed
if (unit.marker) {
// Change the marker to show a destroyed unit (smaller and gray)
const markerSize = 15; // Smaller size for destroyed units
const markerHtml = `
โ ๏ธ
`;
const destroyedIcon = L.divIcon({
html: markerHtml,
className: '',
iconSize: [markerSize, markerSize],
iconAnchor: [markerSize/2, markerSize/2]
});
unit.marker.setIcon(destroyedIcon);
}
// Update unit status
unit.status = 'Destroyed';
unit.movingToTarget = false;
unit.inBattle = false;
// Update statistics
if (unit.faction === 'blue') {
statistics.blueActive--;
statistics.blueCasualties++;
// Adjust territory control slightly toward red
statistics.redControl = Math.min(statistics.redControl + 2, 90);
statistics.blueControl = 100 - statistics.redControl;
} else {
statistics.redActive--;
statistics.redCasualties++;
// Adjust territory control slightly toward blue
statistics.blueControl = Math.min(statistics.blueControl + 2, 90);
statistics.redControl = 100 - statistics.blueControl;
}
// After a delay, remove the unit marker from the map
setTimeout(() => {
if (unit.marker) {
// Fade out effect (optional)
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);
// After another longer delay, units will disappear completely
// This creates a nice visual effect of "cleaning up" the battlefield
// Left commented out to keep destroyed units visible on the map
/*
setTimeout(() => {
map.removeLayer(unit.marker);
unit.marker = null;
}, 30000);
*/
}
}, 5000);
}
}