WAR-Game-Simul / index.html
openfree's picture
Update index.html
e401758 verified
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Advanced Tactical Terrain Simulator - 6 Nations Military</title>
<style>
.discord-badge {
position: fixed;
top: 10px;
left: 10px;
z-index: 9999;
}
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background:#0a0a0a; color:#fff; overflow:hidden; }
#container { display:flex; height:100vh; }
#sidebar {
width:420px; background:linear-gradient(180deg,#1a1a1a 0%,#2a2a2a 100%);
padding:15px; overflow-y:auto; border-right:3px solid #444;
}
#map-container { flex:1; position:relative; background:#000; min-width:0; min-height:480px; }
#map-canvas, #terrain-background, #contour-canvas {
width:100%; height:100%; position:absolute; top:0; left:0;
image-rendering:auto;
}
#terrain-background { z-index:1; }
#contour-canvas { z-index:2; pointer-events:none; }
#map-canvas { cursor:crosshair; z-index:3; }
.battlefield-divider {
position:absolute; left:0; right:0; height:3px;
background:linear-gradient(90deg,transparent 0%,#ff0000 10%,#ffff00 50%,#ff0000 90%,transparent 100%);
z-index:10; pointer-events:none; box-shadow:0 0 10px rgba(255,0,0,0.5);
}
#blue-zone, #red-zone {
position:absolute; left:50%; transform:translateX(-50%); font-weight:bold; font-size:16px;
text-shadow:2px 2px 4px rgba(0,0,0,0.8); z-index:20;
}
#blue-zone { top:10px; color:#2196F3; }
#red-zone { bottom:10px; color:#f44336; }
.control-section { margin-bottom:20px; padding:12px; background:rgba(40,40,40,0.9); border-radius:8px; border:1px solid #555; }
.control-section h3 {
margin-bottom:10px; color:#4CAF50; font-size:13px; text-transform:uppercase; letter-spacing:1px;
border-bottom:1px solid #333; padding-bottom:5px;
}
.terrain-selector { display:grid; grid-template-columns:repeat(2, 1fr); gap:8px; margin-bottom:15px; }
.terrain-btn {
padding:10px; background:#333; border:2px solid #555; color:#fff; cursor:pointer;
border-radius:5px; transition:all .3s; font-size:11px; text-align:center;
}
.terrain-btn:hover { background:#444; border-color:#4CAF50; }
.terrain-btn.active { background:#4CAF50; border-color:#4CAF50; }
.country-selector { display:grid; grid-template-columns:repeat(2,1fr); gap:8px; margin-bottom:15px; }
.country-btn {
padding:10px; border:2px solid #666; background:#444; color:#fff; cursor:pointer;
border-radius:5px; transition:all .3s; font-weight:bold; font-size:11px; text-align:center;
}
.country-btn:hover { transform:translateY(-2px); box-shadow:0 3px 10px rgba(0,0,0,0.5); }
.country-btn.active { border-width:3px; }
.country-btn.roka { border-color:#2196F3; }
.country-btn.kpa { border-color:#f44336; }
.country-btn.usa { border-color:#1976D2; }
.country-btn.russia { border-color:#D32F2F; }
.country-btn.ukraine { border-color:#FFD700; }
.country-btn.china { border-color:#FF5722; }
.country-btn.active.roka { background:linear-gradient(135deg,#2196F3 0%,#1976D2 100%); }
.country-btn.active.kpa { background:linear-gradient(135deg,#f44336 0%,#d32f2f 100%); }
.country-btn.active.usa { background:linear-gradient(135deg,#1976D2 0%,#0D47A1 100%); }
.country-btn.active.russia { background:linear-gradient(135deg,#D32F2F 0%,#B71C1C 100%); }
.country-btn.active.ukraine { background:linear-gradient(135deg,#FFD700 0%,#FFC107 100%); color:#000; }
.country-btn.active.china { background:linear-gradient(135deg,#FF5722 0%,#E64A19 100%); }
.side-selector { display:flex; gap:10px; margin-bottom:10px; }
.side-btn {
flex:1; padding:8px; border:2px solid #666; background:#333; color:#fff; cursor:pointer;
border-radius:5px; transition:all .3s; font-size:11px; text-align:center;
}
.side-btn.active { background:#4CAF50; border-color:#4CAF50; }
.unit-hierarchy { margin-bottom:15px; max-height:300px; overflow-y:auto; }
.unit-level-title { font-size:12px; color:#888; margin-bottom:5px; font-weight:bold; }
.unit-options { display:grid; grid-template-columns:repeat(2,1fr); gap:5px; }
.unit-btn {
padding:8px; background:#3a3a3a; border:2px solid #555; color:#fff; cursor:pointer;
border-radius:4px; transition:all .3s; font-size:10px; text-align:center;
}
.unit-btn:hover { background:#4a4a4a; border-color:#4CAF50; transform:translateY(-1px); }
.unit-btn.active { background:#4CAF50; border-color:#4CAF50; }
#info-panel { padding:12px; background:rgba(50,50,50,0.9); border-radius:6px; font-size:11px; line-height:1.6; }
#info-panel div { margin:4px 0; padding:4px; background:rgba(30,30,30,0.8); border-radius:3px; }
.weapons-list { font-size:10px; color:#aaa; margin-left:10px; }
.status-bar {
position:absolute; bottom:0; left:0; right:0; background:rgba(0,0,0,0.9);
padding:10px 20px; display:flex; justify-content:space-between; align-items:center;
backdrop-filter:blur(10px); z-index:20; border-top:1px solid #333;
}
.coordinates { color:#4CAF50; font-family:'Courier New', monospace; font-size:12px; }
.control-buttons { display:grid; grid-template-columns:repeat(2,1fr); gap:8px; }
.control-btn {
padding:10px; background:linear-gradient(135deg,#555 0%,#444 100%);
border:1px solid #666; color:#fff; cursor:pointer; border-radius:5px; transition:all .3s; font-size:11px;
}
.control-btn:hover { background:linear-gradient(135deg,#666 0%,#555 100%); transform:translateY(-1px); }
.ai-deploy-btn {
width:100%; padding:15px; background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);
border:none; color:#fff; cursor:pointer; border-radius:5px; font-size:13px; font-weight:bold; transition:all .3s; margin-bottom:10px;
}
.ai-deploy-btn:hover { transform:scale(1.02); box-shadow:0 5px 15px rgba(102,126,234,0.4); }
.stats { display:grid; grid-template-columns:repeat(2,1fr); gap:8px; margin-top:10px; }
.stat-item { padding:8px; background:rgba(30,30,30,0.8); border-radius:5px; text-align:center; border:1px solid #333; }
.stat-value { font-size:16px; font-weight:bold; color:#4CAF50; }
.stat-label { font-size:10px; color:#888; margin-top:2px; }
.legend {
position:absolute; top:20px; right:20px; background:rgba(0,0,0,0.9);
padding:12px; border-radius:8px; backdrop-filter:blur(10px); z-index:20; border:1px solid #333; font-size:11px;
}
.legend-title { color:#4CAF50; font-weight:bold; margin-bottom:8px; border-bottom:1px solid #333; padding-bottom:5px; }
.legend-item { display:flex; align-items:center; margin:5px 0; }
.legend-symbol { width:25px; height:20px; margin-right:8px; display:flex; align-items:center; justify-content:center; font-weight:bold; font-size:14px; }
.scale-indicator {
position:absolute; bottom:50px; right:20px; background:rgba(0,0,0,0.9);
padding:10px; border-radius:5px; z-index:20; border:1px solid #333; font-size:11px;
color:#4CAF50;
}
.terrain-controls {
display:flex; gap:8px; margin-top:10px; flex-wrap:wrap;
}
.terrain-control-btn {
padding:6px 12px; background:#444; border:1px solid #666; color:#fff;
cursor:pointer; border-radius:4px; font-size:10px; transition:all .3s;
}
.terrain-control-btn:hover { background:#555; border-color:#4CAF50; }
.terrain-control-btn.active { background:#4CAF50; border-color:#4CAF50; }
.speed-control {
margin-top:15px; padding:15px; background:rgba(30,30,30,0.9); border-radius:8px; border:1px solid #666;
}
.speed-control-title {
font-size:12px; color:#FFC107; margin-bottom:10px; font-weight:bold; text-transform:uppercase;
display:flex; align-items:center; justify-content:space-between;
}
.speed-slider-container { position:relative; margin:15px 0; }
.speed-slider {
width:100%; -webkit-appearance:none; appearance:none; height:8px;
background:linear-gradient(90deg, #333 0%, #666 100%); outline:none; opacity:0.9;
border-radius:4px; cursor:pointer;
}
.speed-slider:hover { opacity:1; }
.speed-slider::-webkit-slider-thumb {
-webkit-appearance:none; appearance:none; width:20px; height:20px;
background:linear-gradient(135deg, #FFC107 0%, #FF9800 100%); cursor:pointer;
border-radius:50%; border:2px solid #fff; box-shadow:0 2px 6px rgba(0,0,0,0.5);
}
.speed-slider::-moz-range-thumb {
width:20px; height:20px; background:linear-gradient(135deg, #FFC107 0%, #FF9800 100%);
cursor:pointer; border-radius:50%; border:2px solid #fff; box-shadow:0 2px 6px rgba(0,0,0,0.5);
}
.speed-marks {
display:flex; justify-content:space-between; margin-top:8px; font-size:10px; color:#888;
}
.speed-mark { text-align:center; cursor:pointer; transition:color 0.3s; }
.speed-mark:hover { color:#FFC107; }
.speed-mark.active { color:#FFC107; font-weight:bold; }
.current-speed {
margin-top:10px; padding:8px; background:rgba(255,193,7,0.1); border:1px solid #FFC107;
border-radius:4px; text-align:center; font-size:12px; color:#FFC107;
}
.time-display {
display:grid; grid-template-columns:repeat(2,1fr); gap:8px; margin-top:10px;
}
.time-item {
padding:6px; background:rgba(40,40,40,0.8); border-radius:4px; text-align:center;
border:1px solid #444;
}
.time-label { font-size:9px; color:#888; }
.time-value { font-size:11px; color:#4CAF50; font-weight:bold; }
.zoom-controls {
position:absolute; top:20px; left:20px; background:rgba(0,0,0,0.9);
padding:8px; border-radius:8px; z-index:20; border:1px solid #333;
display:flex; gap:5px; align-items:center;
}
.zoom-btn {
width:30px; height:30px; background:#444; border:1px solid #666; color:#fff;
cursor:pointer; border-radius:4px; font-size:16px; transition:all .3s;
display:flex; align-items:center; justify-content:center;
}
.zoom-btn:hover { background:#555; border-color:#4CAF50; }
.zoom-level { color:#4CAF50; font-size:11px; margin:0 8px; min-width:50px; text-align:center; }
::-webkit-scrollbar { width:8px; }
::-webkit-scrollbar-track { background:#1a1a1a; }
::-webkit-scrollbar-thumb { background:#555; border-radius:4px; }
::-webkit-scrollbar-thumb:hover { background:#666; }
</style>
</head>
<body>
<a href="https://discord.gg/openfreeai" target="_blank" class="discord-badge">
<img src="https://img.shields.io/static/v1?label=Discord&message=Openfree%20AI&color=%230000ff&labelColor=%23800080&logo=discord&logoColor=white&style=for-the-badge" alt="badge">
</a>
<div id="container">
<div id="sidebar">
<div class="control-section">
<h3>šŸ—ŗļø OPERATION TERRAIN</h3>
<div class="terrain-selector">
<button class="terrain-btn active" data-terrain="urban">šŸ™ļø Urban Warfare<br><small>Cities, Roads, Villages</small></button>
<button class="terrain-btn" data-terrain="mountain">ā›°ļø Mountain Highland<br><small>Rough Terrain, Villages</small></button>
</div>
<div class="terrain-controls">
<button class="terrain-control-btn active" id="toggle-contours">šŸ“Š Contours</button>
<button class="terrain-control-btn active" id="toggle-roads">šŸ›£ļø Roads</button>
<button class="terrain-control-btn active" id="toggle-rivers">šŸ’§ Rivers</button>
<button class="terrain-control-btn" id="toggle-heatmap">šŸŒ”ļø Heatmap</button>
<button class="terrain-control-btn" id="regenerate-terrain">šŸ”„ New Terrain</button>
</div>
</div>
<div class="control-section">
<h3>āš”ļø MISSION TYPE</h3>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px; margin-bottom:10px;">
<div style="padding:10px; background:rgba(33,150,243,0.2); border-radius:5px; border:1px solid #2196F3;">
<div style="font-size:11px; color:#2196F3; margin-bottom:5px;">BLUE TEAM MISSION</div>
<select id="blue-mission" style="width:100%; padding:5px; background:#333; color:#fff; border:1px solid #2196F3; border-radius:3px;">
<option value="attack">šŸ—”ļø ATTACK</option>
<option value="defend">šŸ›”ļø DEFEND</option>
</select>
</div>
<div style="padding:10px; background:rgba(244,67,54,0.2); border-radius:5px; border:1px solid #f44336;">
<div style="font-size:11px; color:#f44336; margin-bottom:5px;">RED TEAM MISSION</div>
<select id="red-mission" style="width:100%; padding:5px; background:#333; color:#fff; border:1px solid #f44336; border-radius:3px;">
<option value="attack">šŸ—”ļø ATTACK</option>
<option value="defend" selected>šŸ›”ļø DEFEND</option>
</select>
</div>
</div>
<div style="padding:8px; background:rgba(76,175,80,0.2); border-radius:5px; border:1px solid #4CAF50;">
<div style="font-size:10px; color:#4CAF50; line-height:1.4;">
<strong>Lanchester's Law Applied:</strong><br>
Defenders get 3:1 advantage ratio<br>
High ground provides additional bonus
</div>
</div>
</div>
<div class="control-section">
<h3>šŸŽ–ļø NATION & FORCE SELECTION</h3>
<div class="side-selector">
<button class="side-btn active" data-side="blue">BLUE TEAM (South)</button>
<button class="side-btn" data-side="red">RED TEAM (North)</button>
</div>
<div class="country-selector">
<button class="country-btn roka active" data-country="roka">šŸ‡°šŸ‡· South Korea<br/>ROK</button>
<button class="country-btn kpa" data-country="kpa">šŸ‡°šŸ‡µ North Korea<br/>DPRK</button>
<button class="country-btn usa" data-country="usa">šŸ‡ŗšŸ‡ø United States<br/>USA</button>
<button class="country-btn russia" data-country="russia">šŸ‡·šŸ‡ŗ Russia<br/>RUS</button>
<button class="country-btn ukraine" data-country="ukraine">šŸ‡ŗšŸ‡¦ Ukraine<br/>UKR</button>
<button class="country-btn china" data-country="china">šŸ‡ØšŸ‡³ China<br/>PRC</button>
</div>
<div class="unit-hierarchy" id="units-list">
<!-- Unit list will be dynamically generated -->
</div>
</div>
<div class="control-section">
<h3>šŸŽ² QUICK DEPLOYMENT</h3>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:8px;">
<button class="control-btn" id="random-blue" style="background:linear-gradient(135deg,#2196F3 0%,#1976D2 100%);">
šŸŽ² Deploy Blue Regiment
</button>
<button class="control-btn" id="random-red" style="background:linear-gradient(135deg,#f44336 0%,#d32f2f 100%);">
šŸŽ² Deploy Red Regiment
</button>
</div>
<button class="control-btn" id="random-both" style="width:100%; margin-top:8px; background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);">
āš”ļø Full Battle Setup
</button>
<p style="font-size:10px; color:#888; text-align:center; margin-top:10px;">
Deploys full regiment with proper organization
</p>
</div>
<div class="control-section">
<h3>šŸ“Š COMBAT POWER ANALYSIS</h3>
<div class="stats">
<div class="stat-item"><div class="stat-value" id="blue-units">0</div><div class="stat-label">Blue Forces</div></div>
<div class="stat-item"><div class="stat-value" id="red-units">0</div><div class="stat-label">Red Forces</div></div>
<div class="stat-item"><div class="stat-value" id="firepower-ratio">0%</div><div class="stat-label">Fire Superiority</div></div>
<div class="stat-item"><div class="stat-value" id="coverage">0%</div><div class="stat-label">Fire Coverage</div></div>
</div>
</div>
<div class="control-section">
<h3>šŸ“‹ SELECTED UNIT INFO</h3>
<div id="info-panel">
<div>Unit: <span id="unit-name">-</span></div>
<div>Nation: <span id="unit-country">-</span></div>
<div>Size: <span id="unit-size">-</span></div>
<div>Strength: <span id="unit-strength">-</span></div>
<div>Weapons:<div class="weapons-list" id="unit-weapons">-</div></div>
<div>Effective Range: <span id="unit-range">-</span></div>
<div>Position: <span id="unit-position">-</span></div>
<div>Elevation: <span id="unit-elevation">-</span></div>
</div>
</div>
<div class="control-section">
<h3>āš”ļø OPERATION TOOLS</h3>
<div class="control-buttons">
<button class="control-btn" id="analyze-coverage">Fire Coverage Analysis</button>
<button class="control-btn" id="analyze-terrain">Terrain Analysis</button>
<button class="control-btn" id="run-simulation">Combat Simulation</button>
<button class="control-btn" id="clear-all">Clear All</button>
</div>
</div>
<div class="control-section">
<h3>šŸ”„ BATTLE COMMAND</h3>
<button class="control-btn" id="start-battle" style="width:100%; padding:15px; background:linear-gradient(135deg,#ff5252 0%,#ff1744 100%); font-size:14px; font-weight:bold;">
āš”ļø START ENGAGEMENT
</button>
<button class="control-btn" id="pause-battle" style="width:100%; padding:10px; margin-top:5px; background:#666; display:none;">
āøļø PAUSE BATTLE
</button>
<div class="speed-control">
<div class="speed-control-title">
<span>ā±ļø BATTLE TIME SPEED</span>
<span id="speed-display">1 MIN</span>
</div>
<div class="speed-slider-container">
<input type="range" class="speed-slider" id="speed-slider" min="0" max="4" value="0" step="1">
<div class="speed-marks">
<div class="speed-mark active" data-speed="0">1M</div>
<div class="speed-mark" data-speed="1">10M</div>
<div class="speed-mark" data-speed="2">1H</div>
<div class="speed-mark" data-speed="3">6H</div>
<div class="speed-mark" data-speed="4">1D</div>
</div>
</div>
<div class="current-speed" id="current-speed">
1 second = 1 minute of battle time
</div>
<div class="time-display">
<div class="time-item">
<div class="time-label">Battle Time</div>
<div class="time-value" id="battle-time">00:00:00</div>
</div>
<div class="time-item">
<div class="time-label">Real Time</div>
<div class="time-value" id="real-time">00:00:00</div>
</div>
</div>
</div>
<div style="margin-top:10px; padding:10px; background:rgba(30,30,30,0.8); border-radius:5px;">
<div style="font-size:11px; color:#888;">Battle Status</div>
<div id="battle-status" style="font-size:12px; color:#4CAF50; margin-top:5px;">Standby</div>
<div style="margin-top:10px;">
<div style="font-size:10px; color:#888;">Blue Casualties</div>
<div id="blue-casualties" style="font-size:14px; color:#2196F3;">0</div>
</div>
<div style="margin-top:5px;">
<div style="font-size:10px; color:#888;">Red Casualties</div>
<div id="red-casualties" style="font-size:14px; color:#f44336;">0</div>
</div>
</div>
</div>
</div>
<div id="map-container">
<canvas id="terrain-background"></canvas>
<canvas id="contour-canvas"></canvas>
<canvas id="map-canvas"></canvas>
<div class="zoom-controls">
<button class="zoom-btn" id="zoom-in">+</button>
<div class="zoom-level" id="zoom-level">100%</div>
<button class="zoom-btn" id="zoom-out">āˆ’</button>
</div>
<div class="battlefield-divider" style="top:40%;"></div>
<div id="blue-zone">[ BLUE FORCE SECTOR ]</div>
<div id="red-zone">[ RED FORCE SECTOR ]</div>
<div class="legend">
<div class="legend-title">Unit Symbols</div>
<div class="legend-item"><div class="legend-symbol" style="color:#2196F3;">ā– </div><span>Blue Battalion</span></div>
<div class="legend-item"><div class="legend-symbol" style="color:#2196F3;">ā—</div><span>Blue Company</span></div>
<div class="legend-item"><div class="legend-symbol" style="color:#2196F3;">ā–²</div><span>Blue Platoon</span></div>
<div class="legend-item"><div class="legend-symbol" style="color:#f44336;">ā– </div><span>Red Battalion</span></div>
<div class="legend-item"><div class="legend-symbol" style="color:#f44336;">ā—</div><span>Red Company</span></div>
<div class="legend-item"><div class="legend-symbol" style="color:#f44336;">ā–²</div><span>Red Platoon</span></div>
</div>
<div class="scale-indicator" id="scale-indicator">
Battle Area: 20km Ɨ 15km<br>
Scale: 1:5000<br>
Grid: 100m
</div>
<div class="status-bar">
<div class="coordinates">
Pos: X=<span id="mouse-x">0</span>, Y=<span id="mouse-y">0</span> |
Real: <span id="real-coords">0,0</span>km |
Elev: <span id="elevation">0</span>m |
Slope: <span id="slope">0</span>° |
Terrain: <span id="terrain-type">-</span>
</div>
<button class="control-btn" id="generate-report" style="padding:8px 20px;">šŸ“„ Operation Report</button>
</div>
</div>
</div>
<script>
// ===== Constants/Globals =====
let METERS_PER_PIXEL = 25;
const CONTACT_RADIUS = 20;
const SCALE = 5000;
const CONTOUR_INTERVAL = 0.05;
const TICK_INTERVAL = 100; // 100ms per tick
let canvas, ctx, terrainCanvas, terrainCtx, contourCanvas, contourCtx;
let selectedCountry = 'roka';
let selectedSide = 'blue';
let selectedUnitType = 'infantry_company';
let selectedTerrain = 'urban';
let units = [];
let heightMap = [];
let roads = [];
let rivers = [];
let settlements = [];
let mapWidth = 0, mapHeight = 0;
let battlefieldDivider = 0;
let zoomLevel = 1;
let offsetX = 0, offsetY = 0;
let isDragging = false;
let dragStartClientX = 0, dragStartClientY = 0;
let dragStartOffsetX = 0, dragStartOffsetY = 0;
let seed = Math.random() * 10000;
let blueMission = 'attack';
let redMission = 'defend';
let showContours = true;
let showRoads = true;
let showRivers = true;
let showHeatmap = false;
let battleActive = false;
let battleInterval = null;
let battleAnimationFrame = null;
let blueCasualties = 0;
let redCasualties = 0;
let explosions = [];
let bulletTrails = [];
let battleSpeed = 0;
let battleTimeElapsed = 0;
let realTimeElapsed = 0;
let battleStartTime = 0;
const speedMultipliers = [60, 600, 3600, 21600, 86400];
const speedLabels = ['1 MIN', '10 MIN', '1 HOUR', '6 HOURS', '1 DAY'];
const speedDescriptions = [
'1 second = 1 minute of battle time',
'1 second = 10 minutes of battle time',
'1 second = 1 hour of battle time',
'1 second = 6 hours of battle time',
'1 second = 1 day of battle time'
];
// ===== Country Unit Specifications =====
const countryUnits = {
roka: {
hq:{name:'HQ Battalion',symbol:'⬟',size:'battalion',strength:50,weapons:['Command Systems','K2 Rifle','Intel Equipment'],range:1000,firepower:30,speed:50},
infantry_company:{name:'Infantry Company',symbol:'ā—',size:'company',strength:140,weapons:['K2 Rifle','K201 Grenade Launcher','K3 LMG','K6 HMG'],range:800,firepower:100,speed:100},
infantry_platoon:{name:'Infantry Platoon',symbol:'ā–²',size:'platoon',strength:40,weapons:['K2 Rifle','K201 GL','K3 LMG'],range:600,firepower:40,speed:100},
mortar_81mm:{name:'81mm Mortar Company',symbol:'ā—ˆ',size:'company',strength:80,weapons:['KM29A1 81mm Mortar','K2 Rifle'],range:5700,firepower:150,speed:80},
antitank_platoon:{name:'AT Platoon',symbol:'ā—‡',size:'platoon',strength:35,weapons:['Hyungung ATGM','Panzerfaust 3','K2 Rifle'],range:2500,firepower:120,speed:100},
tank_company:{name:'Tank Company',symbol:'ā– ',size:'company',strength:60,weapons:['K2 Black Panther (120mm)','K6 HMG'],range:4000,firepower:350,speed:500},
mech_company:{name:'Mechanized Company',symbol:'ā–£',size:'company',strength:120,weapons:['K21 IFV (40mm)','K6 HMG','K2 Rifle'],range:3000,firepower:250,speed:800},
artillery_155mm:{name:'K9 SPH Battalion',symbol:'⬢',size:'battalion',strength:150,weapons:['K9 Thunder 155mm SPH','K2 Rifle'],range:18000,firepower:500,speed:400},
mlrs_chunmoo:{name:'Chunmoo MLRS Company',symbol:'ā—‰',size:'company',strength:90,weapons:['K239 Chunmoo MLRS','K2 Rifle'],range:20000,firepower:600,speed:400},
attack_heli:{name:'AH-64E Company',symbol:'✈',size:'company',strength:40,weapons:['AH-64E Apache (30mm)','Hellfire Missile','Hydra 70 Rocket'],range:8000,firepower:450,speed:3000},
attack_drone:{name:'Attack Drone Company',symbol:'ā—Ž',size:'company',strength:30,weapons:['Loitering Munition','Recon Drone','Small Guided Missile'],range:10000,firepower:200,speed:1500},
air_defense:{name:'Cheonma AD Platoon',symbol:'✦',size:'platoon',strength:40,weapons:['Cheonma SAM','Shingung MANPADS','K2 Rifle'],range:3500,firepower:100,speed:400},
support_company:{name:'Support Company',symbol:'ā—‹',size:'company',strength:150,weapons:['Transport Vehicles','Maintenance Equipment','K2 Rifle'],range:500,firepower:30,speed:500}
},
kpa: {
hq:{name:'HQ Battalion',symbol:'⬟',size:'battalion',strength:45,weapons:['Command Systems','AK-74','Intel Equipment'],range:800,firepower:25,speed:50},
infantry_company:{name:'Infantry Company',symbol:'ā—',size:'company',strength:130,weapons:['AK-74/Type-88','RPK LMG','RPG-7','PKM MG'],range:700,firepower:85,speed:100},
infantry_platoon:{name:'Infantry Platoon',symbol:'ā–²',size:'platoon',strength:35,weapons:['AK-74/Type-88','RPK LMG'],range:500,firepower:35,speed:100},
light_infantry:{name:'Light Infantry Platoon',symbol:'ā–½',size:'platoon',strength:30,weapons:['AK-74','RPG-7','LMG'],range:600,firepower:40,speed:120},
mortar_82mm:{name:'82mm Mortar Company',symbol:'ā—ˆ',size:'company',strength:75,weapons:['82mm Mortar','AK-74'],range:4000,firepower:130,speed:80},
antitank_platoon:{name:'AT Platoon',symbol:'ā—‡',size:'platoon',strength:30,weapons:['Bulsae-3 ATGM','RPG-7','AK-74'],range:2000,firepower:100,speed:100},
tank_company:{name:'Tank Company',symbol:'ā– ',size:'company',strength:55,weapons:['Chonma/Songun Tank (125mm)','PKT MG'],range:3500,firepower:300,speed:400},
mech_company:{name:'Mechanized Company',symbol:'ā–£',size:'company',strength:110,weapons:['BMP-2 (30mm)','PKT MG','AK-74'],range:2500,firepower:200,speed:700},
mrls_company:{name:'BM-21 MLRS Company',symbol:'ā—‰',size:'company',strength:90,weapons:['BM-21 122mm MLRS','AK-74'],range:20000,firepower:400,speed:400},
artillery_152mm:{name:'152mm SPH Battalion',symbol:'⬢',size:'battalion',strength:140,weapons:['2S3 Akatsiya 152mm SPH','AK-74'],range:17300,firepower:400,speed:350},
attack_heli:{name:'Mi-24 Attack Heli',symbol:'✈',size:'company',strength:35,weapons:['Mi-24 Hind (12.7mm)','AT-2 Missile','S-5 Rocket'],range:6000,firepower:350,speed:2500},
antiair_platoon:{name:'AAA Platoon',symbol:'✦',size:'platoon',strength:35,weapons:['ZPU-4 14.5mm AAA','SA-7 MANPADS','AK-74'],range:3000,firepower:80,speed:300},
support_company:{name:'Support Company',symbol:'ā—‹',size:'company',strength:140,weapons:['Transport Vehicles','Maintenance Equipment','AK-74'],range:400,firepower:25,speed:400}
},
usa: {
hq:{name:'Battalion HQ',symbol:'⬟',size:'battalion',strength:60,weapons:['Command Systems','M4A1 Carbine','SINCGARS Radio'],range:1200,firepower:35,speed:50},
infantry_company:{name:'Infantry Company',symbol:'ā—',size:'company',strength:150,weapons:['M4A1 Carbine','M249 SAW','M240B MG','M320 GL','AT4'],range:900,firepower:120,speed:100},
infantry_platoon:{name:'Infantry Platoon',symbol:'ā–²',size:'platoon',strength:45,weapons:['M4A1 Carbine','M249 SAW','M320 GL'],range:700,firepower:50,speed:100},
ranger_platoon:{name:'Ranger Platoon',symbol:'ā—†',size:'platoon',strength:40,weapons:['M4A1 SOPMOD','Mk48 MG','Carl Gustaf','M320 GL'],range:1000,firepower:80,speed:150},
mortar_120mm:{name:'120mm Mortar Platoon',symbol:'ā—ˆ',size:'platoon',strength:60,weapons:['M120 120mm Mortar','M4A1 Carbine'],range:7200,firepower:180,speed:80},
javelin_team:{name:'Javelin Team',symbol:'ā—‡',size:'squad',strength:12,weapons:['FGM-148 Javelin','M4A1 Carbine'],range:4750,firepower:150,speed:100},
abrams_platoon:{name:'M1A2 Platoon',symbol:'ā– ',size:'platoon',strength:16,weapons:['M1A2 SEPv3 Abrams (120mm)','M2 .50 Cal','M240 MG'],range:4000,firepower:400,speed:600},
bradley_platoon:{name:'Bradley Platoon',symbol:'ā–£',size:'platoon',strength:28,weapons:['M2A3 Bradley (25mm)','TOW Missile','M240C MG'],range:3000,firepower:280,speed:900},
stryker_platoon:{name:'Stryker Platoon',symbol:'ā–¢',size:'platoon',strength:36,weapons:['M1296 Stryker ICV (30mm)','M2 .50 Cal','M4A1 Carbine'],range:2500,firepower:220,speed:1000},
paladin_battery:{name:'M109A7 Battery',symbol:'⬢',size:'battery',strength:100,weapons:['M109A7 Paladin 155mm SPH','M4A1 Carbine'],range:20000,firepower:550,speed:400},
himars_battery:{name:'HIMARS Battery',symbol:'ā—‰',size:'battery',strength:80,weapons:['M142 HIMARS','GMLRS Rockets','M4A1 Carbine'],range:20000,firepower:700,speed:500},
apache_company:{name:'AH-64E Company',symbol:'✈',size:'company',strength:45,weapons:['AH-64E Apache (30mm)','AGM-114 Hellfire','Hydra 70 Rockets'],range:8000,firepower:500,speed:3000},
reaper_team:{name:'MQ-9 Reaper',symbol:'ā—Ž',size:'team',strength:10,weapons:['MQ-9 Reaper','AGM-114 Hellfire','GBU-12 Paveway'],range:15000,firepower:300,speed:2000},
patriot_battery:{name:'Patriot Battery',symbol:'✦',size:'battery',strength:90,weapons:['MIM-104 Patriot','PAC-3 MSE','M4A1 Carbine'],range:20000,firepower:200,speed:400},
support_company:{name:'Support Company',symbol:'ā—‹',size:'company',strength:160,weapons:['M1083 FMTV','M4A1 Carbine'],range:500,firepower:35,speed:600}
},
russia: {
hq:{name:'Battalion HQ',symbol:'⬟',size:'battalion',strength:55,weapons:['Command Systems','AK-12','R-187 Azart Radio'],range:1000,firepower:30,speed:50},
motor_rifle_company:{name:'Motor Rifle Company',symbol:'ā—',size:'company',strength:135,weapons:['AK-12','PKP Pecheneg','RPG-7V2','AGS-30'],range:800,firepower:95,speed:100},
motor_rifle_platoon:{name:'Motor Rifle Platoon',symbol:'ā–²',size:'platoon',strength:38,weapons:['AK-12','PKP Pecheneg','RPG-7V2'],range:600,firepower:40,speed:100},
vdv_platoon:{name:'VDV Platoon',symbol:'ā—†',size:'platoon',strength:35,weapons:['AK-12','PKP Pecheneg','RPG-28','AGS-30'],range:800,firepower:65,speed:150},
mortar_120mm:{name:'120mm Mortar',symbol:'ā—ˆ',size:'battery',strength:70,weapons:['2S12 Sani 120mm Mortar','AK-12'],range:7100,firepower:160,speed:80},
kornet_team:{name:'Kornet-EM Team',symbol:'ā—‡',size:'squad',strength:10,weapons:['9M133 Kornet-EM ATGM','AK-12'],range:8000,firepower:140,speed:100},
t90_platoon:{name:'T-90M Platoon',symbol:'ā– ',size:'platoon',strength:12,weapons:['T-90M Proryv (125mm)','Kord 12.7mm','PKT 7.62mm'],range:4000,firepower:380,speed:500},
t72_company:{name:'T-72B3 Company',symbol:'ā– ',size:'company',strength:39,weapons:['T-72B3M (125mm)','Kord 12.7mm','PKT 7.62mm'],range:3500,firepower:320,speed:450},
bmp3_platoon:{name:'BMP-3 Platoon',symbol:'ā–£',size:'platoon',strength:30,weapons:['BMP-3 (100mm+30mm)','PKT 7.62mm','AK-12'],range:3000,firepower:260,speed:750},
btr82_platoon:{name:'BTR-82A Platoon',symbol:'ā–¢',size:'platoon',strength:32,weapons:['BTR-82A (30mm)','PKT 7.62mm','AK-12'],range:2000,firepower:180,speed:900},
msta_battery:{name:'2S19 Msta-S',symbol:'⬢',size:'battery',strength:120,weapons:['2S19 Msta-S 152mm SPH','AK-12'],range:20000,firepower:480,speed:400},
grad_battery:{name:'BM-21 Grad',symbol:'ā—‰',size:'battery',strength:90,weapons:['BM-21 Grad 122mm MLRS','AK-12'],range:20000,firepower:450,speed:400},
ka52_squadron:{name:'Ka-52 Squadron',symbol:'✈',size:'squadron',strength:30,weapons:['Ka-52 Alligator (30mm)','Vikhr ATGM','S-8 Rockets'],range:10000,firepower:420,speed:2800},
orlan_team:{name:'Orlan-10 UAV',symbol:'ā—Ž',size:'team',strength:8,weapons:['Orlan-10 UAV','Laser Designator'],range:12000,firepower:50,speed:1200},
pantsir_battery:{name:'Pantsir-S1',symbol:'✦',size:'battery',strength:50,weapons:['Pantsir-S1 (30mm)','57E6 SAM','AK-12'],range:20000,firepower:150,speed:400},
support_company:{name:'Support Company',symbol:'ā—‹',size:'company',strength:150,weapons:['KamAZ-5350','AK-12'],range:400,firepower:25,speed:500}
},
ukraine: {
hq:{name:'Battalion HQ',symbol:'⬟',size:'battalion',strength:50,weapons:['Command Systems','AK-74M','Harris Radio'],range:1000,firepower:30,speed:50},
mech_infantry_company:{name:'Mech Infantry Company',symbol:'ā—',size:'company',strength:130,weapons:['AK-74M','Fort-221','PKM','RPG-7'],range:750,firepower:90,speed:100},
infantry_platoon:{name:'Infantry Platoon',symbol:'ā–²',size:'platoon',strength:36,weapons:['AK-74M','Fort-221','PKM'],range:600,firepower:38,speed:100},
azov_platoon:{name:'Azov Platoon',symbol:'ā—†',size:'platoon',strength:35,weapons:['M4 WAC-47','Mk48 MG','NLAW','Carl Gustaf'],range:900,firepower:70,speed:120},
mortar_120mm:{name:'120mm Mortar',symbol:'ā—ˆ',size:'battery',strength:65,weapons:['M120-15 Molot 120mm','AK-74M'],range:7000,firepower:155,speed:80},
stugna_team:{name:'Stugna-P Team',symbol:'ā—‡',size:'squad',strength:8,weapons:['Stugna-P ATGM','AK-74M'],range:5000,firepower:130,speed:100},
nlaw_team:{name:'NLAW Team',symbol:'ā—‡',size:'squad',strength:6,weapons:['NLAW','AK-74M'],range:800,firepower:100,speed:100},
t64_company:{name:'T-64BV Company',symbol:'ā– ',size:'company',strength:39,weapons:['T-64BV (125mm)','NSVT 12.7mm','PKT 7.62mm'],range:3500,firepower:300,speed:450},
bmp2_platoon:{name:'BMP-2 Platoon',symbol:'ā–£',size:'platoon',strength:28,weapons:['BMP-2 (30mm)','Konkurs ATGM','PKT 7.62mm'],range:2500,firepower:200,speed:700},
btr4_platoon:{name:'BTR-4E Platoon',symbol:'ā–¢',size:'platoon',strength:30,weapons:['BTR-4E (30mm)','Barrier ATGM','KT-7.62 MG'],range:2500,firepower:210,speed:850},
m777_battery:{name:'M777 Battery',symbol:'⬢',size:'battery',strength:100,weapons:['M777 155mm Howitzer','AK-74M'],range:20000,firepower:450,speed:300},
caesar_battery:{name:'CAESAR Battery',symbol:'⬢',size:'battery',strength:80,weapons:['CAESAR 155mm SPG','AK-74M'],range:20000,firepower:470,speed:450},
himars_battery:{name:'M142 HIMARS',symbol:'ā—‰',size:'battery',strength:70,weapons:['M142 HIMARS','GMLRS','AK-74M'],range:20000,firepower:650,speed:500},
bayraktar_team:{name:'Bayraktar TB2',symbol:'ā—Ž',size:'team',strength:12,weapons:['Bayraktar TB2','MAM-L','MAM-C'],range:15000,firepower:250,speed:1800},
gepard_platoon:{name:'Gepard SPAAG',symbol:'✦',size:'platoon',strength:20,weapons:['Flakpanzer Gepard (35mm)','AK-74M'],range:5500,firepower:120,speed:400},
support_company:{name:'Support Company',symbol:'ā—‹',size:'company',strength:140,weapons:['KrAZ-6322','AK-74M'],range:400,firepower:25,speed:500}
},
china: {
hq:{name:'Battalion HQ',symbol:'⬟',size:'battalion',strength:55,weapons:['Command Systems','QBZ-95','Digital Radio'],range:1100,firepower:32,speed:50},
infantry_company:{name:'Infantry Company',symbol:'ā—',size:'company',strength:145,weapons:['QBZ-95','QJY-88 MG','PF-98 RPG','QLZ-87 GL'],range:850,firepower:105,speed:100},
infantry_platoon:{name:'Infantry Platoon',symbol:'ā–²',size:'platoon',strength:42,weapons:['QBZ-95','QJY-88 MG','PF-89 RPG'],range:650,firepower:45,speed:100},
special_forces:{name:'Special Forces',symbol:'ā—†',size:'platoon',strength:30,weapons:['QBZ-95-1','CS/LR4 Sniper','HJ-12 ATGM','QJY-201 MG'],range:1200,firepower:85,speed:150},
mortar_120mm:{name:'120mm Mortar Company',symbol:'ā—ˆ',size:'company',strength:75,weapons:['PP87 120mm Mortar','QBZ-95'],range:6800,firepower:165,speed:80},
hj12_team:{name:'HJ-12 AT Team',symbol:'ā—‡',size:'squad',strength:8,weapons:['HJ-12 ATGM','QBZ-95'],range:4000,firepower:135,speed:100},
type99a_company:{name:'Type 99A Tank Company',symbol:'ā– ',size:'company',strength:39,weapons:['ZTZ-99A (125mm)','QJC-88 12.7mm','Type 86 MG'],range:4000,firepower:370,speed:550},
zbd04_platoon:{name:'ZBD-04A IFV Platoon',symbol:'ā–£',size:'platoon',strength:30,weapons:['ZBD-04A (100mm+30mm)','HJ-8 ATGM','Type 86 MG'],range:3000,firepower:270,speed:800},
zbl08_platoon:{name:'ZBL-08 APC Platoon',symbol:'ā–¢',size:'platoon',strength:33,weapons:['ZBL-08 (30mm)','HJ-73C ATGM','QJC-88 MG'],range:2500,firepower:220,speed:950},
plz05_battery:{name:'PLZ-05 SPH Battalion',symbol:'⬢',size:'battalion',strength:120,weapons:['PLZ-05 155mm SPH','QBZ-95'],range:20000,firepower:520,speed:400},
pcl191_battery:{name:'PCL-191 MLRS',symbol:'ā—‰',size:'battery',strength:90,weapons:['PCL-191 MLRS System','QBZ-95'],range:20000,firepower:750,speed:450},
wz10_squadron:{name:'WZ-10 Squadron',symbol:'✈',size:'squadron',strength:35,weapons:['WZ-10 (23mm)','HJ-10 ATGM','TY-90 AAM'],range:8000,firepower:430,speed:2700},
ch5_team:{name:'CH-5 UAV',symbol:'ā—Ž',size:'team',strength:10,weapons:['CH-5 UAV','AR-1 Missile','AR-2 Missile'],range:14000,firepower:280,speed:1600},
hq17_battery:{name:'HQ-17 SAM',symbol:'✦',size:'battery',strength:60,weapons:['HQ-17 SAM System','35mm AAA','QBZ-95'],range:15000,firepower:140,speed:400},
support_company:{name:'Support Company',symbol:'ā—‹',size:'company',strength:155,weapons:['Dongfeng Mengshi','QBZ-95'],range:450,firepower:28,speed:600}
}
};
const countryInfo = {
roka: { name: 'ROK Army', color: '#2196F3' },
kpa: { name: 'Korean People\'s Army', color: '#f44336' },
usa: { name: 'US Army', color: '#1976D2' },
russia: { name: 'Russian Ground Forces', color: '#D32F2F' },
ukraine: { name: 'Ukrainian Army', color: '#FFD700' },
china: { name: 'PLA Ground Force', color: '#FF5722' }
};
// Mission type controls
document.getElementById('blue-mission').addEventListener('change', function() {
blueMission = this.value;
if (blueMission === 'defend' && redMission === 'defend') {
redMission = 'attack';
document.getElementById('red-mission').value = 'attack';
} else if (blueMission === 'attack' && redMission === 'attack') {
redMission = 'defend';
document.getElementById('red-mission').value = 'defend';
}
});
document.getElementById('red-mission').addEventListener('change', function() {
redMission = this.value;
if (redMission === 'defend' && blueMission === 'defend') {
blueMission = 'attack';
document.getElementById('blue-mission').value = 'attack';
} else if (redMission === 'attack' && blueMission === 'attack') {
blueMission = 'defend';
document.getElementById('blue-mission').value = 'defend';
}
});
// ===== CRITICAL FIX: Common View Transform =====
function applyViewTransform(context) {
context.save();
context.translate(mapWidth/2, mapHeight/2);
context.scale(zoomLevel, zoomLevel);
context.translate(-mapWidth/2 + offsetX, -mapHeight/2 + offsetY);
}
function restoreViewTransform(context) {
context.restore();
}
// ===== Enhanced Terrain System =====
function noise(x, y, scale, octaves) {
let value = 0;
let amplitude = 1;
let frequency = scale;
let maxValue = 0;
for (let i = 0; i < octaves; i++) {
value += amplitude * simpleNoise(x * frequency + seed, y * frequency + seed);
maxValue += amplitude;
amplitude *= 0.5;
frequency *= 2;
}
return value / maxValue;
}
function simpleNoise(x, y) {
const n = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
return (n - Math.floor(n)) * 2 - 1;
}
function generateTerrain() {
console.log('Generating terrain:', selectedTerrain);
seed = Math.random() * 10000;
heightMap = [];
roads = [];
rivers = [];
settlements = [];
const gridSize = 50;
for (let y = 0; y < gridSize; y++) {
heightMap[y] = [];
for (let x = 0; x < gridSize; x++) {
let height = 0;
switch(selectedTerrain) {
case 'mountain':
height = noise(x, y, 0.03, 4) * 0.6;
height = Math.pow(Math.abs(height), 1.2) * Math.sign(height);
break;
case 'urban':
height = noise(x, y, 0.015, 2) * 0.1 + 0.15;
break;
default:
height = 0.3;
}
height = Math.max(0, Math.min(1, (height + 1) / 2));
heightMap[y][x] = height;
}
}
generateRoads(gridSize);
generateRivers(gridSize);
generateSettlements(gridSize);
console.log('Terrain generated successfully');
}
function generateRoads(gridSize) {
roads = [];
// ė„ė”œė„¼ ė” ė§Žģ“ ģƒģ„± (기씓볓다 2ė°°)
const numRoads = selectedTerrain === 'urban' ? 16 : 8;
// ģ£¼ģš” ė„ė”œ ė„¤ķŠøģ›Œķ¬ ģƒģ„±
for (let i = 0; i < numRoads; i++) {
const road = [];
let x, y, targetX, targetY;
if (i < 4) {
// 남북 ģ—°ź²° ģ£¼ģš” ė„ė”œ
x = (gridSize / 4) * (i + 0.5) + (Math.random() - 0.5) * 10;
y = 0;
targetX = x + (Math.random() - 0.5) * 20;
targetY = gridSize - 1;
} else if (i < 8) {
// ė™ģ„œ ģ—°ź²° ģ£¼ģš” ė„ė”œ
x = 0;
y = (gridSize / 4) * (i - 3.5) + (Math.random() - 0.5) * 10;
targetX = gridSize - 1;
targetY = y + (Math.random() - 0.5) * 20;
} else {
// ėŒ€ź°ģ„  ė° 볓씰 ė„ė”œ
x = Math.random() * gridSize;
y = Math.random() * gridSize;
targetX = Math.random() * gridSize;
targetY = Math.random() * gridSize;
}
const maxSteps = 100;
for (let step = 0; step < maxSteps; step++) {
road.push({ x: Math.floor(x), y: Math.floor(y) });
const dx = targetX - x;
const dy = targetY - y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 2) break;
const angle = Math.atan2(dy, dx);
// ė„ė”œė„¼ 좀 ė” ģ§ģ„ ģ ģœ¼ė”œ
x += Math.cos(angle) * 1.5 + (Math.random() - 0.5) * 0.3;
y += Math.sin(angle) * 1.5 + (Math.random() - 0.5) * 0.3;
x = Math.max(0, Math.min(gridSize - 1, x));
y = Math.max(0, Math.min(gridSize - 1, y));
}
if (road.length > 5) {
roads.push(road);
}
}
}
function generateRivers(gridSize) {
rivers = [];
const numRivers = selectedTerrain === 'mountain' ? 3 : 1;
for (let i = 0; i < numRivers; i++) {
const river = [];
let x = Math.random() * gridSize;
let y = Math.random() * gridSize;
const maxSteps = 100;
for (let step = 0; step < maxSteps; step++) {
const ix = Math.floor(x);
const iy = Math.floor(y);
if (ix < 0 || ix >= gridSize || iy < 0 || iy >= gridSize) break;
river.push({ x: ix, y: iy });
let moved = false;
const currentHeight = heightMap[iy] && heightMap[iy][ix] ? heightMap[iy][ix] : 0;
for (let attempts = 0; attempts < 8; attempts++) {
const angle = Math.random() * Math.PI * 2;
const nx = x + Math.cos(angle) * 1.5;
const ny = y + Math.sin(angle) * 1.5;
const nix = Math.floor(nx);
const niy = Math.floor(ny);
if (nix >= 0 && nix < gridSize && niy >= 0 && niy < gridSize) {
const nextHeight = heightMap[niy] && heightMap[niy][nix] ? heightMap[niy][nix] : 0;
if (nextHeight <= currentHeight) {
x = nx;
y = ny;
moved = true;
break;
}
}
}
if (!moved) {
x += (Math.random() - 0.5) * 2;
y += (Math.random() - 0.5) * 2;
}
x = Math.max(0, Math.min(gridSize - 1, x));
y = Math.max(0, Math.min(gridSize - 1, y));
if (currentHeight < 0.2) break;
}
if (river.length > 5) {
rivers.push(river);
}
}
}
function generateSettlements(gridSize) {
settlements = [];
// ė„ė”œ 교차점과 ė„ė”œė³€ģ— ė§ˆģ„ 배치
const villagePositions = [];
// ė„ė”œ 교차점 찾기
for (let i = 0; i < roads.length - 1; i++) {
for (let j = i + 1; j < roads.length; j++) {
for (let p1 of roads[i]) {
for (let p2 of roads[j]) {
const dist = Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
if (dist < 3) {
villagePositions.push({
x: Math.floor((p1.x + p2.x) / 2),
y: Math.floor((p1.y + p2.y) / 2),
importance: 2 // źµģ°Øģ ģ€ ģ¤‘ģš”ė„ ė†’ģŒ
});
}
}
}
}
}
// ė„ė”œė³€ģ— 추가 ė§ˆģ„ 배치
for (let road of roads) {
const numVillages = Math.floor(road.length / 15) + 1;
for (let i = 0; i < numVillages; i++) {
const idx = Math.floor(Math.random() * road.length);
const point = road[idx];
if (point) {
villagePositions.push({
x: point.x + Math.floor((Math.random() - 0.5) * 2),
y: point.y + Math.floor((Math.random() - 0.5) * 2),
importance: 1
});
}
}
}
// 중복 제거 ė° ģµœģ¢… ė§ˆģ„ ģƒģ„±
const uniquePositions = [];
for (let pos of villagePositions) {
let tooClose = false;
for (let existing of uniquePositions) {
const dist = Math.sqrt(Math.pow(pos.x - existing.x, 2) + Math.pow(pos.y - existing.y, 2));
if (dist < 5) {
tooClose = true;
break;
}
}
if (!tooClose && pos.x >= 0 && pos.x < gridSize && pos.y >= 0 && pos.y < gridSize) {
uniquePositions.push(pos);
}
}
// ė§ˆģ„ ģƒģ„±
for (let pos of uniquePositions) {
const height = heightMap[pos.y][pos.x];
if (height > 0.15 && height < 0.8) { // 물과 ė†’ģ€ ģ‚° ģ œģ™ø
let type, size;
if (selectedTerrain === 'urban') {
if (pos.importance === 2 && Math.random() < 0.5) {
type = 'city';
size = 12 + Math.random() * 8;
} else if (pos.importance === 2 || Math.random() < 0.3) {
type = 'town';
size = 8 + Math.random() * 4;
} else {
type = 'village';
size = 4 + Math.random() * 3;
}
} else { // mountain
if (pos.importance === 2 && Math.random() < 0.3) {
type = 'town';
size = 6 + Math.random() * 3;
} else {
type = 'village';
size = 3 + Math.random() * 2;
}
}
settlements.push({
x: pos.x,
y: pos.y,
type: type,
size: size,
importance: pos.importance
});
}
}
console.log(`Generated ${settlements.length} settlements along ${roads.length} roads`);
}
// ===== FIXED: drawTerrain with View Transform =====
function drawTerrain() {
if (!terrainCtx || !heightMap || heightMap.length === 0) return;
const gridSize = heightMap.length;
const cellWidth = mapWidth / gridSize;
const cellHeight = mapHeight / gridSize;
terrainCtx.clearRect(0, 0, mapWidth, mapHeight);
applyViewTransform(terrainCtx);
try {
for (let y = 0; y < gridSize; y++) {
for (let x = 0; x < gridSize; x++) {
if (!heightMap[y] || heightMap[y][x] === undefined) continue;
const height = heightMap[y][x];
if (showHeatmap) {
const hue = (1 - height) * 240;
terrainCtx.fillStyle = `hsl(${hue}, 70%, 50%)`;
} else {
terrainCtx.fillStyle = getTerrainColor(height);
}
terrainCtx.fillRect(
x * cellWidth,
y * cellHeight,
cellWidth + 1,
cellHeight + 1
);
}
}
if (showRivers && rivers && rivers.length > 0) {
rivers.forEach(river => {
if (!river || river.length === 0) return;
terrainCtx.strokeStyle = '#4682B4';
terrainCtx.lineWidth = 3;
terrainCtx.beginPath();
river.forEach((point, index) => {
if (!point) return;
const x = point.x * cellWidth;
const y = point.y * cellHeight;
if (index === 0) {
terrainCtx.moveTo(x, y);
} else {
terrainCtx.lineTo(x, y);
}
});
terrainCtx.stroke();
});
}
if (showRoads && roads && roads.length > 0) {
roads.forEach(road => {
if (!road || road.length === 0) return;
terrainCtx.strokeStyle = '#555';
terrainCtx.lineWidth = 2;
terrainCtx.setLineDash([5, 3]);
terrainCtx.beginPath();
road.forEach((point, index) => {
if (!point) return;
const x = point.x * cellWidth;
const y = point.y * cellHeight;
if (index === 0) {
terrainCtx.moveTo(x, y);
} else {
terrainCtx.lineTo(x, y);
}
});
terrainCtx.stroke();
terrainCtx.setLineDash([]);
});
}
if (settlements && settlements.length > 0) {
settlements.forEach(settlement => {
if (!settlement) return;
const x = settlement.x * cellWidth;
const y = settlement.y * cellHeight;
if (settlement.type === 'city') {
terrainCtx.fillStyle = '#666';
terrainCtx.fillRect(x - settlement.size/2, y - settlement.size/2, settlement.size, settlement.size);
terrainCtx.strokeStyle = '#333';
terrainCtx.strokeRect(x - settlement.size/2, y - settlement.size/2, settlement.size, settlement.size);
} else if (settlement.type === 'town') {
terrainCtx.fillStyle = '#7A6A5A';
terrainCtx.fillRect(x - settlement.size/2, y - settlement.size/2, settlement.size, settlement.size);
} else if (settlement.type === 'village') {
terrainCtx.fillStyle = '#8B7355';
terrainCtx.beginPath();
terrainCtx.arc(x, y, settlement.size, 0, Math.PI * 2);
terrainCtx.fill();
}
});
}
} catch(error) {
console.error('Error drawing terrain:', error);
}
restoreViewTransform(terrainCtx);
}
// ===== FIXED: drawContours with View Transform =====
function drawContours() {
if (!contourCtx || !showContours || !heightMap || heightMap.length === 0) return;
const gridSize = heightMap.length;
const cellWidth = mapWidth / gridSize;
const cellHeight = mapHeight / gridSize;
contourCtx.clearRect(0, 0, mapWidth, mapHeight);
applyViewTransform(contourCtx);
const contourLevels = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
try {
for (let level of contourLevels) {
contourCtx.beginPath();
for (let y = 0; y < gridSize - 1; y++) {
for (let x = 0; x < gridSize - 1; x++) {
if (!heightMap[y] || !heightMap[y+1] ||
heightMap[y][x] === undefined || heightMap[y][x+1] === undefined ||
heightMap[y+1][x] === undefined || heightMap[y+1][x+1] === undefined) {
continue;
}
const corners = [
heightMap[y][x],
heightMap[y][x + 1],
heightMap[y + 1][x + 1],
heightMap[y + 1][x]
];
drawContourCell(
x * cellWidth,
y * cellHeight,
cellWidth,
cellHeight,
corners,
level
);
}
}
if (level % 0.2 === 0) {
contourCtx.lineWidth = 2;
contourCtx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
} else {
contourCtx.lineWidth = 1;
contourCtx.strokeStyle = 'rgba(0, 0, 0, 0.3)';
}
contourCtx.stroke();
}
} catch(error) {
console.error('Error drawing contours:', error);
}
restoreViewTransform(contourCtx);
}
function drawContourCell(x, y, width, height, corners, level) {
let state = 0;
if (corners[0] > level) state |= 1;
if (corners[1] > level) state |= 2;
if (corners[2] > level) state |= 4;
if (corners[3] > level) state |= 8;
switch(state) {
case 1: case 14:
drawLine(contourCtx, x, y + height * interpolate(corners[0], corners[3], level),
x + width * interpolate(corners[0], corners[1], level), y);
break;
case 2: case 13:
drawLine(contourCtx, x + width * interpolate(corners[0], corners[1], level), y,
x + width, y + height * interpolate(corners[1], corners[2], level));
break;
case 3: case 12:
drawLine(contourCtx, x, y + height * interpolate(corners[0], corners[3], level),
x + width, y + height * interpolate(corners[1], corners[2], level));
break;
case 4: case 11:
drawLine(contourCtx, x + width, y + height * interpolate(corners[1], corners[2], level),
x + width * interpolate(corners[3], corners[2], level), y + height);
break;
case 5:
drawLine(contourCtx, x, y + height * interpolate(corners[0], corners[3], level),
x + width * interpolate(corners[0], corners[1], level), y);
drawLine(contourCtx, x + width, y + height * interpolate(corners[1], corners[2], level),
x + width * interpolate(corners[3], corners[2], level), y + height);
break;
case 6: case 9:
drawLine(contourCtx, x + width * interpolate(corners[0], corners[1], level), y,
x + width * interpolate(corners[3], corners[2], level), y + height);
break;
case 7: case 8:
drawLine(contourCtx, x, y + height * interpolate(corners[0], corners[3], level),
x + width * interpolate(corners[3], corners[2], level), y + height);
break;
case 10:
drawLine(contourCtx, x + width * interpolate(corners[0], corners[1], level), y,
x + width, y + height * interpolate(corners[1], corners[2], level));
drawLine(contourCtx, x, y + height * interpolate(corners[0], corners[3], level),
x + width * interpolate(corners[3], corners[2], level), y + height);
break;
}
}
function interpolate(v1, v2, level) {
if (v1 === v2) return 0;
return (level - v1) / (v2 - v1);
}
function drawLine(context, x1, y1, x2, y2) {
context.moveTo(x1, y1);
context.lineTo(x2, y2);
}
function getTerrainColor(height) {
if (height < 0.15) return '#0d4f8b';
if (height < 0.25) return '#1e7eb8';
if (height < 0.35) return '#61a861';
if (height < 0.45) return '#8fc68f';
if (height < 0.55) return '#c4d4aa';
if (height < 0.65) return '#f5deb3';
if (height < 0.75) return '#d2b48c';
if (height < 0.85) return '#8b5a2b';
return '#d9d9d9'; // ģ—°ķ•œ ķšŒģƒ‰ģœ¼ė”œ 변경
}
function getHeightAt(x, y) {
if (!heightMap || heightMap.length === 0) return 0.3;
const gridSize = heightMap.length;
const cellWidth = mapWidth / gridSize;
const cellHeight = mapHeight / gridSize;
const gridX = Math.floor(x / cellWidth);
const gridY = Math.floor(y / cellHeight);
if (gridX >= 0 && gridX < gridSize && gridY >= 0 && gridY < gridSize) {
if (heightMap[gridY] && heightMap[gridY][gridX] !== undefined) {
return heightMap[gridY][gridX];
}
}
return 0.3;
}
function getSlope(x, y) {
if (!heightMap || heightMap.length === 0) return 0;
const gridSize = heightMap.length;
const cellWidth = mapWidth / gridSize;
const cellHeight = mapHeight / gridSize;
const gx = Math.floor(x / cellWidth);
const gy = Math.floor(y / cellHeight);
if (gx <= 0 || gx >= gridSize - 1 || gy <= 0 || gy >= gridSize - 1) return 0;
// ź³ ė„ ģŠ¤ģ¼€ģ¼: 0~1 -> 0~1000m
const h = (ix, iy) => (heightMap[iy]?.[ix] ?? 0) * 1000;
const dzdx = (h(gx+1, gy) - h(gx-1, gy)) / (2 * cellWidth * METERS_PER_PIXEL);
const dzdy = (h(gx, gy+1) - h(gx, gy-1)) / (2 * cellHeight * METERS_PER_PIXEL);
const gradient = Math.sqrt(dzdx*dzdx + dzdy*dzdy); // ģˆ˜ķ‰ 1m당 ź³ ė„ 변화(m/m)
const angleRad = Math.atan(gradient);
return Math.round(angleRad * 180 / Math.PI); // ė„ ė‹Øģœ„
}
// ===== Initialization =====
window.addEventListener('DOMContentLoaded', () => {
console.log('Initializing simulator...');
canvas = document.getElementById('map-canvas');
terrainCanvas = document.getElementById('terrain-background');
contourCanvas = document.getElementById('contour-canvas');
if (!canvas || !terrainCanvas || !contourCanvas) {
console.error('Canvas elements not found');
return;
}
ctx = canvas.getContext('2d');
terrainCtx = terrainCanvas.getContext('2d');
contourCtx = contourCanvas.getContext('2d');
if (!ctx || !terrainCtx || !contourCtx) {
console.error('Canvas context initialization failed');
return;
}
// Initialize canvas size first
setCanvasSizeWithDPR();
// Setup event listeners
setupEventListeners();
setupSpeedControl();
updateUnitsList();
// Generate and draw terrain
try {
generateTerrain();
drawTerrain();
drawContours();
drawMap();
console.log('Terrain generated and drawn successfully');
} catch(error) {
console.error('Error during initialization:', error);
}
});
function setCanvasSizeWithDPR(){
const container = document.getElementById('map-container');
if (!container) {
console.error('Map container not found');
return;
}
const rect = container.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
// Ensure minimum size
mapWidth = Math.max(600, Math.floor(rect.width));
mapHeight = Math.max(400, Math.floor(rect.height));
battlefieldDivider = Math.floor(mapHeight * 0.4);
METERS_PER_PIXEL = ((20000 / mapWidth) + (15000 / mapHeight)) / 2;
console.log(`Canvas size: ${mapWidth}x${mapHeight}, DPR: ${dpr}, Scale: ${METERS_PER_PIXEL}m/px`);
// Set canvas display size
terrainCanvas.style.width = mapWidth + 'px';
terrainCanvas.style.height = mapHeight + 'px';
contourCanvas.style.width = mapWidth + 'px';
contourCanvas.style.height = mapHeight + 'px';
canvas.style.width = mapWidth + 'px';
canvas.style.height = mapHeight + 'px';
// Set canvas actual size with DPR
terrainCanvas.width = mapWidth * dpr;
terrainCanvas.height = mapHeight * dpr;
contourCanvas.width = mapWidth * dpr;
contourCanvas.height = mapHeight * dpr;
canvas.width = mapWidth * dpr;
canvas.height = mapHeight * dpr;
// Scale context for DPR
terrainCtx.scale(dpr, dpr);
contourCtx.scale(dpr, dpr);
ctx.scale(dpr, dpr);
const scaleText = `Battle Area: 20km Ɨ 15km<br>Scale: 1:${SCALE}<br>Grid: ${Math.round(METERS_PER_PIXEL)}m/px`;
document.getElementById('scale-indicator').innerHTML = scaleText;
}
function setupEventListeners() {
document.querySelectorAll('.terrain-btn').forEach(btn=>{
btn.addEventListener('click', function(){ selectTerrain(this.getAttribute('data-terrain'), this); });
});
document.getElementById('toggle-contours').addEventListener('click', function() {
showContours = !showContours;
this.classList.toggle('active');
drawContours();
});
document.getElementById('toggle-roads').addEventListener('click', function() {
showRoads = !showRoads;
this.classList.toggle('active');
drawTerrain();
});
document.getElementById('toggle-rivers').addEventListener('click', function() {
showRivers = !showRivers;
this.classList.toggle('active');
drawTerrain();
});
document.getElementById('toggle-heatmap').addEventListener('click', function() {
showHeatmap = !showHeatmap;
this.classList.toggle('active');
drawTerrain();
});
document.getElementById('regenerate-terrain').addEventListener('click', function() {
generateTerrain();
drawTerrain();
drawContours();
});
document.querySelectorAll('.side-btn').forEach(btn=>{
btn.addEventListener('click', function(){ selectSide(this.getAttribute('data-side')); });
});
document.querySelectorAll('.country-btn').forEach(btn=>{
btn.addEventListener('click', function(){ selectCountry(this.getAttribute('data-country')); });
});
document.getElementById('zoom-in').addEventListener('click', zoomIn);
document.getElementById('zoom-out').addEventListener('click', zoomOut);
document.getElementById('random-blue').addEventListener('click', () => randomRegimentDeployment('blue'));
document.getElementById('random-red').addEventListener('click', () => randomRegimentDeployment('red'));
document.getElementById('random-both').addEventListener('click', randomFullBattleSetup);
document.getElementById('analyze-coverage').addEventListener('click', analyzeCoverage);
document.getElementById('analyze-terrain').addEventListener('click', analyzeTerrain);
document.getElementById('run-simulation').addEventListener('click', runSimulation);
document.getElementById('clear-all').addEventListener('click', clearAll);
document.getElementById('generate-report').addEventListener('click', generateReport);
document.getElementById('start-battle').addEventListener('click', startBattle);
document.getElementById('pause-battle').addEventListener('click', pauseBattle);
canvas.addEventListener('click', handleMapClick);
canvas.addEventListener('mousemove', handleMouseMove);
canvas.addEventListener('mousedown', handleMouseDown);
canvas.addEventListener('mouseup', handleMouseUp);
canvas.addEventListener('wheel', handleWheel, { passive: false });
canvas.addEventListener('contextmenu', (e) => e.preventDefault()); // 우큓릭 메뉓 ė°©ģ§€
// Window resize handler
window.addEventListener('resize', () => {
setCanvasSizeWithDPR();
drawTerrain();
drawContours();
drawMap();
});
}
function setupSpeedControl() {
const slider = document.getElementById('speed-slider');
const speedDisplay = document.getElementById('speed-display');
const currentSpeed = document.getElementById('current-speed');
const speedMarks = document.querySelectorAll('.speed-mark');
slider.addEventListener('input', function() {
battleSpeed = parseInt(this.value);
updateSpeedDisplay();
});
speedMarks.forEach(mark => {
mark.addEventListener('click', function() {
const speed = parseInt(this.getAttribute('data-speed'));
slider.value = speed;
battleSpeed = speed;
updateSpeedDisplay();
});
});
function updateSpeedDisplay() {
speedDisplay.textContent = speedLabels[battleSpeed];
currentSpeed.textContent = speedDescriptions[battleSpeed];
speedMarks.forEach(mark => {
mark.classList.remove('active');
if (parseInt(mark.getAttribute('data-speed')) === battleSpeed) {
mark.classList.add('active');
}
});
}
}
function formatTime(seconds) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
}
function updateTimeDisplay() {
if (battleActive && battleStartTime) {
const now = Date.now();
const realDelta = (now - battleStartTime) / 1000; // seconds
realTimeElapsed = realDelta;
// Calculate battle time based on speed multiplier
battleTimeElapsed = realDelta * speedMultipliers[battleSpeed];
document.getElementById('battle-time').textContent = formatTime(battleTimeElapsed);
document.getElementById('real-time').textContent = formatTime(realTimeElapsed);
}
}
// ===== Zoom Functions with Cursor-centered Zoom =====
function zoomAt(cx, cy, factor) {
const prevZoom = zoomLevel;
const newZoom = Math.min(4, Math.max(0.5, zoomLevel * factor));
if (newZoom === prevZoom) return;
const rect = canvas.getBoundingClientRect();
const scaleX = mapWidth / rect.width;
const scaleY = mapHeight / rect.height;
// 화멓 ģ¢Œķ‘œ -> ė§µ ģ¢Œķ‘œ
const x = ((cx - rect.left) * scaleX - mapWidth/2) / prevZoom + mapWidth/2 - offsetX;
const y = ((cy - rect.top) * scaleY - mapHeight/2) / prevZoom + mapHeight/2 - offsetY;
zoomLevel = newZoom;
// ģ»¤ģ„œź°€ ź°€ė¦¬ķ‚¤ėŠ” ģ›”ė“œ ķ¬ģøķŠøź°€ ķ™”ė©“ģƒ ź³ ģ •ė˜ė„ė” ģ˜¤ķ”„ģ…‹ 볓정
const x2 = ((cx - rect.left) * scaleX - mapWidth/2) / zoomLevel + mapWidth/2 - offsetX;
const y2 = ((cy - rect.top) * scaleY - mapHeight/2) / zoomLevel + mapHeight/2 - offsetY;
offsetX += (x2 - x);
offsetY += (y2 - y);
updateZoomDisplay();
drawMap();
drawTerrain();
drawContours();
}
function zoomIn(e) {
const rect = canvas.getBoundingClientRect();
const cx = e?.clientX ?? (rect.left + rect.width/2);
const cy = e?.clientY ?? (rect.top + rect.height/2);
zoomAt(cx, cy, 1.2);
}
function zoomOut(e) {
const rect = canvas.getBoundingClientRect();
const cx = e?.clientX ?? (rect.left + rect.width/2);
const cy = e?.clientY ?? (rect.top + rect.height/2);
zoomAt(cx, cy, 1/1.2);
}
function updateZoomDisplay() {
document.getElementById('zoom-level').textContent = Math.round(zoomLevel * 100) + '%';
}
function handleWheel(e) {
e.preventDefault();
if (e.deltaY < 0) {
zoomIn(e);
} else {
zoomOut(e);
}
}
function handleMouseDown(e) {
// ģ¤‘ė²„ķŠ¼, ģš°ė²„ķŠ¼, ė˜ėŠ” Shift+좌큓릭 ķ—ˆģš©
if (e.button === 1 || e.button === 2 || (e.button === 0 && e.shiftKey)) {
isDragging = true;
dragStartClientX = e.clientX;
dragStartClientY = e.clientY;
dragStartOffsetX = offsetX;
dragStartOffsetY = offsetY;
canvas.style.cursor = 'grabbing';
e.preventDefault();
}
}
function handleMouseUp() {
isDragging = false;
canvas.style.cursor = 'crosshair';
}
// ===== Selection Functions =====
function selectTerrain(type, element){
selectedTerrain = type;
document.querySelectorAll('.terrain-btn').forEach(b=>b.classList.remove('active'));
if(element) element.classList.add('active');
if(battleActive) {
pauseBattle();
}
try {
generateTerrain();
drawTerrain();
drawContours();
drawMap();
} catch(error) {
console.error('Error generating terrain:', error);
alert('Error generating terrain. Please try again.');
}
}
function selectSide(side){
selectedSide = side;
document.querySelectorAll('.side-btn').forEach(b=>b.classList.remove('active'));
document.querySelector(`.side-btn[data-side="${side}"]`).classList.add('active');
}
function selectCountry(country){
selectedCountry = country;
document.querySelectorAll('.country-btn').forEach(b=>b.classList.remove('active'));
document.querySelector(`.country-btn[data-country="${country}"]`).classList.add('active');
updateUnitsList();
}
function updateUnitsList(){
const unitsList = document.getElementById('units-list');
const units = countryUnits[selectedCountry];
const info = countryInfo[selectedCountry];
let html = `<div class="unit-level-title" style="color:${info.color}; margin-bottom:10px;">${info.name} Order of Battle</div>`;
html += '<div class="unit-options">';
for(const [key, unit] of Object.entries(units)){
html += `<button class="unit-btn${key === 'infantry_company' ? ' active' : ''}" data-unit="${key}">${unit.name}</button>`;
}
html += '</div>';
unitsList.innerHTML = html;
selectedUnitType = 'infantry_company';
document.querySelectorAll('.unit-btn').forEach(btn=>{
btn.addEventListener('click', function(){ selectUnit(this.getAttribute('data-unit'), this); });
});
}
function selectUnit(type, element){
selectedUnitType = type;
document.querySelectorAll('.unit-btn').forEach(b=>b.classList.remove('active'));
if(element) element.classList.add('active');
}
// ===== Map Drawing =====
function drawMap(){
if(!ctx || !mapWidth || !mapHeight) {
console.error('Canvas not ready for drawing');
return;
}
ctx.clearRect(0, 0, mapWidth, mapHeight);
applyViewTransform(ctx);
ctx.strokeStyle = 'rgba(255,255,0,0.5)';
ctx.lineWidth = 2;
ctx.setLineDash([15, 5]);
ctx.beginPath();
ctx.moveTo(0, battlefieldDivider);
ctx.lineTo(mapWidth, battlefieldDivider);
ctx.stroke();
ctx.setLineDash([]);
units.forEach(u => {
drawUnit(u);
if(u.selected) drawUnitRange(u);
});
restoreViewTransform(ctx);
}
function drawUnit(unit){
if(unit.health<=0) return;
const spec = countryUnits[unit.country][unit.type];
const unitColor = unit.side === 'blue' ? '#2196F3' : '#f44336';
ctx.save();
ctx.globalAlpha = Math.max(0.3, unit.health/100);
ctx.font = `bold ${spec.size==='battalion'?'20px':spec.size==='company'?'16px':'14px'} Arial`;
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillStyle = unit.isRetreating? '#888' : unitColor;
ctx.fillText(spec.symbol, unit.x, unit.y);
ctx.font='8px Arial';
ctx.fillStyle='rgba(255,255,255,0.7)';
const countryEmoji = {roka:'šŸ‡°šŸ‡·',kpa:'šŸ‡°šŸ‡µ',usa:'šŸ‡ŗšŸ‡ø',russia:'šŸ‡·šŸ‡ŗ',ukraine:'šŸ‡ŗšŸ‡¦',china:'šŸ‡ØšŸ‡³'};
ctx.fillText(countryEmoji[unit.country], unit.x, unit.y-20);
ctx.font='10px Arial'; ctx.fillStyle='rgba(255,255,255,0.85)';
ctx.fillText(spec.name, unit.x, unit.y+15);
ctx.fillStyle='rgba(0,0,0,0.5)'; ctx.fillRect(unit.x-20, unit.y-25, 40, 4);
const hpColor = unit.health>60? '#4CAF50' : unit.health>30? '#FFC107' : '#F44336';
ctx.fillStyle=hpColor; ctx.fillRect(unit.x-20, unit.y-25, 40*(unit.health/100), 4);
if(unit.inCombat && !unit.isRetreating){ ctx.strokeStyle='#FF5722'; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(unit.x,unit.y,25,0,Math.PI*2); ctx.stroke(); }
if(unit.selected){ ctx.strokeStyle='#ffeb3b'; ctx.lineWidth=2; ctx.beginPath(); ctx.arc(unit.x,unit.y,20,0,Math.PI*2); ctx.stroke(); }
ctx.restore();
}
function drawUnitRange(unit){
const spec = countryUnits[unit.country][unit.type];
const r = spec.range / METERS_PER_PIXEL;
ctx.strokeStyle = unit.side === 'blue' ? 'rgba(33,150,243,0.2)' : 'rgba(244,67,54,0.2)';
ctx.lineWidth=1; ctx.setLineDash([5,5]);
ctx.beginPath(); ctx.arc(unit.x, unit.y, r, 0, Math.PI*2); ctx.stroke(); ctx.setLineDash([]);
}
// ===== FIXED: Input Handling with proper coordinate transformation =====
function handleMapClick(e){
if (isDragging) return;
const rect = canvas.getBoundingClientRect();
const scaleX = mapWidth / rect.width;
const scaleY = mapHeight / rect.height;
const x = ((e.clientX - rect.left) * scaleX - mapWidth/2) / zoomLevel + mapWidth/2 - offsetX;
const y = ((e.clientY - rect.top) * scaleY - mapHeight/2) / zoomLevel + mapHeight/2 - offsetY;
console.log(`Click at: ${x}, ${y}, Side: ${selectedSide}, Divider: ${battlefieldDivider}`);
let clicked = null;
units.forEach(u => {
const d = Math.hypot(u.x - x, u.y - y);
if(d < 20) clicked = u;
u.selected = false;
});
if(clicked){
clicked.selected = true;
updateUnitInfo(clicked);
console.log('Selected unit:', clicked);
} else {
if(selectedSide === 'red' && y > battlefieldDivider){
alert('āš ļø Red Team units can only be deployed in the northern sector!');
return;
}
if(selectedSide === 'blue' && y < battlefieldDivider){
alert('āš ļø Blue Team units can only be deployed in the southern sector!');
return;
}
const height = getHeightAt(x, y);
if(height < 0.15){
alert('āš ļø Cannot deploy units on water!');
return;
}
console.log('Placing unit at:', x, y);
placeUnit(x, y);
}
drawMap();
}
function placeUnit(x, y){
const newUnit = {
id: Date.now(),
type: selectedUnitType,
country: selectedCountry,
side: selectedSide,
x: x,
y: y,
targetX: x,
targetY: y,
selected: false,
health: 100,
morale: 100,
ammo: 100,
isRetreating: false,
inCombat: false,
lastFired: 0,
kills: 0,
currentTarget: null,
mission: selectedSide === 'blue' ? blueMission : redMission
};
units.push(newUnit);
console.log('Unit placed:', newUnit);
updateStats();
drawMap();
}
// ===== FIXED: Mouse Move with drag redraw =====
function handleMouseMove(e){
const rect = canvas.getBoundingClientRect();
const scaleX = mapWidth / rect.width;
const scaleY = mapHeight / rect.height;
if (isDragging) {
// ģ¤Œģ„ 고려핓 ģ“ė™ėŸ‰ ģ¶•ģ†Œ
const dx = (e.clientX - dragStartClientX) * scaleX / zoomLevel;
const dy = (e.clientY - dragStartClientY) * scaleY / zoomLevel;
offsetX = dragStartOffsetX + dx;
offsetY = dragStartOffsetY + dy;
drawMap();
drawTerrain();
drawContours();
return;
}
const x = ((e.clientX - rect.left) * scaleX - mapWidth/2) / zoomLevel + mapWidth/2 - offsetX;
const y = ((e.clientY - rect.top) * scaleY - mapHeight/2) / zoomLevel + mapHeight/2 - offsetY;
const realX = Math.round(x * METERS_PER_PIXEL / 1000);
const realY = Math.round(y * METERS_PER_PIXEL / 1000);
const height = getHeightAt(x, y);
const elevation = Math.round(height * 1000);
const slope = getSlope(x, y);
document.getElementById('mouse-x').textContent = Math.round(x);
document.getElementById('mouse-y').textContent = Math.round(y);
document.getElementById('real-coords').textContent = `${realX},${realY}`;
document.getElementById('elevation').textContent = elevation;
document.getElementById('slope').textContent = slope;
let terrainType = 'Plains';
if (height < 0.2) terrainType = 'Water';
else if (height < 0.35) terrainType = 'Lowland';
else if (height < 0.55) terrainType = 'Hills';
else if (height < 0.75) terrainType = 'Highland';
else terrainType = 'Mountain';
document.getElementById('terrain-type').textContent = terrainType;
}
function updateUnitInfo(unit){
const spec = countryUnits[unit.country][unit.type];
const info = countryInfo[unit.country];
const elevation = Math.round(getHeightAt(unit.x, unit.y) * 1000);
document.getElementById('unit-name').textContent = spec.name;
document.getElementById('unit-country').textContent = info.name;
document.getElementById('unit-size').textContent = spec.size==='battalion'?'Battalion':spec.size==='company'?'Company':spec.size==='platoon'?'Platoon':'Squad';
document.getElementById('unit-strength').textContent = spec.strength+' troops';
document.getElementById('unit-weapons').innerHTML = spec.weapons.join('<br>');
document.getElementById('unit-range').textContent = spec.range+'m';
document.getElementById('unit-position').textContent = `(${Math.round(unit.x)}, ${Math.round(unit.y)})`;
document.getElementById('unit-elevation').textContent = elevation+'m';
}
function updateStats(){
const blueCount = units.filter(u=>u.side==='blue').length;
const redCount = units.filter(u=>u.side==='red').length;
document.getElementById('blue-units').textContent = blueCount;
document.getElementById('red-units').textContent = redCount;
let blueF=0, redF=0;
units.forEach(u=>{
const spec = countryUnits[u.country][u.type];
if(u.side==='blue') blueF += spec.firepower;
else redF += spec.firepower;
});
const total = blueF+redF;
const ratio = total>0? Math.round((blueF/total)*100):0;
document.getElementById('firepower-ratio').textContent = ratio+'%';
}
// ===== Random Deployment Functions (Fixed rendering issue) =====
function randomRegimentDeployment(side) {
const existingUnits = units.filter(u => u.side === side);
if (existingUnits.length > 0) {
if (!confirm(`Remove existing ${side} units and deploy new regiment?`)) return;
units = units.filter(u => u.side !== side);
}
const country = side === 'blue' ?
['roka', 'usa', 'ukraine'][Math.floor(Math.random() * 3)] :
['kpa', 'russia', 'china'][Math.floor(Math.random() * 3)];
const deployment = [];
deployment.push({ type: 'hq', x: mapWidth/2, y: side === 'blue' ? mapHeight * 0.85 : mapHeight * 0.15 });
for (let b = 0; b < 3; b++) {
const bx = (mapWidth / 4) * (b + 1);
const by = side === 'blue' ? mapHeight * 0.7 : mapHeight * 0.3;
for (let c = 0; c < 3; c++) {
const cx = bx + (Math.random() - 0.5) * 100;
const cy = by + (side === 'blue' ? 1 : -1) * c * 40;
deployment.push({ type: 'infantry_company', x: cx, y: cy });
}
}
// Fixed: Use correct unit types for each country
const mortarType = country === 'roka' ? 'mortar_81mm' :
country === 'kpa' ? 'mortar_82mm' :
country === 'usa' ? 'mortar_120mm' :
country === 'russia' ? 'mortar_120mm' :
country === 'ukraine' ? 'mortar_120mm' :
'mortar_120mm';
// Fixed: Country-specific AT types
const atTypeMap = {
roka: 'antitank_platoon',
kpa: 'antitank_platoon',
usa: 'javelin_team',
russia: 'kornet_team',
ukraine: 'stugna_team',
china: 'hj12_team'
};
const atType = atTypeMap[country];
deployment.push({ type: mortarType, x: mapWidth * 0.2, y: side === 'blue' ? mapHeight * 0.8 : mapHeight * 0.2 });
deployment.push({ type: atType, x: mapWidth * 0.8, y: side === 'blue' ? mapHeight * 0.8 : mapHeight * 0.2 });
// Deploy all units at once and then redraw
deployment.forEach((d, idx) => {
units.push({
id: Date.now() + idx,
type: d.type,
country: country,
side: side,
x: d.x,
y: d.y,
targetX: d.x,
targetY: d.y,
selected: false,
health: 100,
morale: 100,
ammo: 100,
isRetreating: false,
inCombat: false,
lastFired: 0,
kills: 0,
currentTarget: null,
mission: side === 'blue' ? blueMission : redMission
});
});
// Update and redraw everything
updateStats();
drawMap();
console.log(`Deployed ${deployment.length} ${side} units`);
}
function randomFullBattleSetup() {
if (units.length > 0) {
if (!confirm('Clear all units and deploy full battle setup?')) return;
units = [];
}
// Deploy both teams
randomRegimentDeployment('red');
randomRegimentDeployment('blue');
// Force redraw after both deployments
setTimeout(() => {
drawMap();
updateStats();
}, 100);
}
function analyzeCoverage(){
// 먼저 ė§µģ„ ķ“ė¦¬ģ–“ķ•˜ź³  źø°ė³ø ė ˆģ“ģ–“ ė‹¤ģ‹œ 그림
drawMap();
let covered=0, total=0;
for(let x=0;x<mapWidth;x+=20){
for(let y=battlefieldDivider;y<mapHeight;y+=20){
total++;
let ok=false;
for (const u of units){
if(u.side!=='blue') continue;
const spec = countryUnits[u.country][u.type];
const d = Math.hypot(u.x-x, u.y-y);
if (d <= spec.range / METERS_PER_PIXEL) {
ok=true;
break;
}
}
if(ok) covered++;
}
}
const pct = total>0? Math.round((covered/total)*100):0;
document.getElementById('coverage').textContent = pct+'%';
ctx.save();
ctx.globalAlpha=0.15;
for (const u of units){
if(u.side==='blue'){
const spec = countryUnits[u.country][u.type];
const r = spec.range/METERS_PER_PIXEL;
const g = ctx.createRadialGradient(u.x,u.y,0,u.x,u.y,r);
g.addColorStop(0,'rgba(33,150,243,0.5)');
g.addColorStop(1,'rgba(33,150,243,0)');
ctx.fillStyle=g;
ctx.beginPath();
ctx.arc(u.x,u.y,r,0,Math.PI*2);
ctx.fill();
}
}
ctx.restore();
}
function analyzeTerrain(){
const avgHeight = heightMap.flat().reduce((a,b)=>a+b,0) / (heightMap.length * heightMap[0].length);
const maxHeight = Math.max(...heightMap.flat());
const minHeight = Math.min(...heightMap.flat());
alert(`Terrain Analysis\n\nTerrain Type: ${selectedTerrain}\nAverage Elevation: ${Math.round(avgHeight*1000)}m\nMax Elevation: ${Math.round(maxHeight*1000)}m\nMin Elevation: ${Math.round(minHeight*1000)}m\n\nTactical Considerations:\n• High ground provides range advantage\n• Rivers create natural barriers\n• Roads enable rapid movement\n• Urban areas provide cover`);
}
function runSimulation(){
const blueForces = units.filter(u=>u.side==='blue');
const redForces = units.filter(u=>u.side==='red');
if(blueForces.length===0 || redForces.length===0){ alert('Both sides must have units deployed for simulation!'); return; }
let blueStrength=0, redStrength=0, blueFire=0, redFire=0;
blueForces.forEach(u=>{
const s = countryUnits[u.country][u.type];
const height = getHeightAt(u.x, u.y);
const heightBonus = height > 0.6 ? 1.2 : 1.0;
blueStrength += s.strength;
blueFire += s.firepower * ((u.health??100)/100) * heightBonus;
});
redForces.forEach(u=>{
const s = countryUnits[u.country][u.type];
const height = getHeightAt(u.x, u.y);
const heightBonus = height > 0.6 ? 1.2 : 1.0;
redStrength += s.strength;
redFire += s.firepower * ((u.health??100)/100) * heightBonus;
});
if (blueMission === 'defend' && redMission === 'attack') {
blueFire *= 3.0;
} else if (redMission === 'defend' && blueMission === 'attack') {
redFire *= 3.0;
}
if ((blueMission === 'defend' || redMission === 'defend') &&
(selectedTerrain === 'urban' || selectedTerrain === 'mountain')) {
if (blueMission === 'defend') blueFire *= 1.2;
if (redMission === 'defend') redFire *= 1.2;
}
const totalFp = blueFire + redFire;
const winProb = totalFp>0? (blueFire/totalFp*100):50;
let advice='';
if(winProb>70) advice='āœ… Overwhelming Superiority - Full Attack Viable';
else if(winProb>60) advice='āœ… Offensive Operations Favorable';
else if(winProb>40) advice='āš ļø Cautious Approach Required';
else advice='🚨 Critical Situation - Consider withdrawal';
const missionStatus = `\n[Mission Type]\n• Blue Team: ${blueMission.toUpperCase()}\n• Red Team: ${redMission.toUpperCase()}\n`;
const lanchesterNote = blueMission === 'defend' || redMission === 'defend' ?
'\n[Lanchester\'s Law Applied]\n• Defender has 3:1 combat advantage\n' : '';
alert(`šŸŽÆ Combat Simulation Results\n\n[Force Analysis]\n• Blue Firepower: ${Math.round(blueFire)}\n• Red Firepower: ${Math.round(redFire)}\n• Terrain: ${selectedTerrain}${missionStatus}${lanchesterNote}\n[Battle Prediction]\n• Blue Victory Probability: ${winProb.toFixed(1)}%\n\n[Tactical Recommendation]\n${advice}`);
}
function generateReport(){
const blueForces = units.filter(u=>u.side==='blue');
const redForces = units.filter(u=>u.side==='red');
let report = '═══════════════════════════════\n';
report += ' TACTICAL OPERATION REPORT\n';
report += '═══════════════════════════════\n\n';
report += `Report Time: ${new Date().toLocaleString('en-US')}\n`;
report += `Terrain Type: ${selectedTerrain}\n`;
report += `Map Scale: 1:${SCALE}\n`;
report += `Operation Area: 20km Ɨ 15km\n\n`;
report += `ā–¶ Blue Forces: ${blueForces.length} units\n`;
report += `ā–¶ Red Forces: ${redForces.length} units\n\n`;
if(blueCasualties>0 || redCasualties>0){
report += `[Combat Losses]\n`;
report += `• Blue Team: ${blueCasualties} casualties\n`;
report += `• Red Team: ${redCasualties} casualties\n\n`;
}
if(battleTimeElapsed > 0){
report += `[Battle Duration]\n`;
report += `• Battle Time: ${formatTime(battleTimeElapsed)}\n`;
report += `• Real Time: ${formatTime(realTimeElapsed)}\n`;
}
alert(report);
}
function clearAll(){
if(!confirm('Clear all units and reset?')) return;
units=[];
battleActive=false;
// Stop battle sound
battleSound.pause();
battleSound.currentTime = 0;
if(battleInterval) clearInterval(battleInterval);
if(battleAnimationFrame) cancelAnimationFrame(battleAnimationFrame);
blueCasualties=0;
redCasualties=0;
explosions=[];
bulletTrails=[];
battleTimeElapsed=0;
realTimeElapsed=0;
battleStartTime = 0;
document.getElementById('battle-status').textContent='Standby';
document.getElementById('blue-casualties').textContent='0';
document.getElementById('red-casualties').textContent='0';
document.getElementById('battle-time').textContent='00:00:00';
document.getElementById('real-time').textContent='00:00:00';
document.getElementById('start-battle').style.display='block';
document.getElementById('start-battle').textContent='āš”ļø START ENGAGEMENT';
document.getElementById('pause-battle').style.display='none';
drawMap();
updateStats();
document.getElementById('coverage').textContent='0%';
}
// ===== Battle System with Realistic Movement and Sound =====
const battleSound = new Audio('war.mp3');
battleSound.loop = true;
battleSound.volume = 0.7;
function startBattle(){
const blueForces = units.filter(u=>u.side==='blue' && u.health>0);
const redForces = units.filter(u=>u.side==='red' && u.health>0);
if(blueForces.length===0 || redForces.length===0){
alert('Both sides must have units to start battle!');
return;
}
battleActive = true;
battleStartTime = Date.now();
battleTimeElapsed = 0;
realTimeElapsed = 0;
// Start battle sound
battleSound.currentTime = 0;
battleSound.play().catch(e => console.log('Audio play failed:', e));
console.log('Battle started at:', battleStartTime);
document.getElementById('battle-status').textContent = 'šŸ”„ ENGAGED';
document.getElementById('start-battle').style.display = 'none';
document.getElementById('pause-battle').style.display = 'block';
// Clear any existing intervals
if(battleInterval) {
clearInterval(battleInterval);
battleInterval = null;
}
if(battleAnimationFrame) {
cancelAnimationFrame(battleAnimationFrame);
battleAnimationFrame = null;
}
// Start the battle loop
startBattleLoop();
}
function startBattleLoop() {
// Battle logic update every 100ms (10 times per second)
battleInterval = setInterval(() => {
if(battleActive) {
battleTick();
updateTimeDisplay();
}
}, TICK_INTERVAL);
// Start continuous animation
function animate() {
if(battleActive) {
// Clear canvas
ctx.clearRect(0, 0, mapWidth, mapHeight);
// Apply transform
ctx.save();
ctx.translate(mapWidth/2, mapHeight/2);
ctx.scale(zoomLevel, zoomLevel);
ctx.translate(-mapWidth/2 + offsetX, -mapHeight/2 + offsetY);
// Draw battlefield divider
ctx.strokeStyle = 'rgba(255,255,0,0.5)';
ctx.lineWidth = 2;
ctx.setLineDash([15, 5]);
ctx.beginPath();
ctx.moveTo(0, battlefieldDivider);
ctx.lineTo(mapWidth, battlefieldDivider);
ctx.stroke();
ctx.setLineDash([]);
// Draw all units
units.forEach(u => {
if(u.health > 0) {
drawUnit(u);
if(u.selected) drawUnitRange(u);
}
});
// Draw battle effects
explosions.forEach(exp => {
ctx.save();
ctx.globalAlpha = exp.opacity;
ctx.fillStyle = exp.color;
ctx.beginPath();
ctx.arc(exp.x, exp.y, exp.radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
});
bulletTrails.forEach(b => {
ctx.save();
ctx.strokeStyle = b.color;
ctx.lineWidth = 2;
ctx.globalAlpha = 1 - b.progress;
const cx = b.from.x + (b.to.x - b.from.x) * b.progress;
const cy = b.from.y + (b.to.y - b.from.y) * b.progress;
ctx.beginPath();
ctx.moveTo(b.from.x, b.from.y);
ctx.lineTo(cx, cy);
ctx.stroke();
ctx.restore();
});
ctx.restore();
battleAnimationFrame = requestAnimationFrame(animate);
}
}
animate();
}
function pauseBattle(){
battleActive = false;
// Pause battle sound
battleSound.pause();
if(battleInterval) {
clearInterval(battleInterval);
battleInterval = null;
}
if(battleAnimationFrame) {
cancelAnimationFrame(battleAnimationFrame);
battleAnimationFrame = null;
}
document.getElementById('battle-status').textContent = 'āøļø PAUSED';
document.getElementById('start-battle').style.display = 'block';
document.getElementById('start-battle').textContent = 'ā–¶ļø RESUME BATTLE';
document.getElementById('pause-battle').style.display = 'none';
// Redraw static map
drawMap();
}
function battleTick(){
if(!battleActive) return;
const res = checkVictoryConditions();
if(res){
endBattle(res);
return;
}
// Move and update all units
units.forEach(u => {
if(u.health <= 0) return;
// AI decision making
makeAIDecision(u);
// Move the unit with realistic speed
moveUnitRealistic(u);
// Combat
if(u.inCombat && !u.isRetreating) {
engageEnemy(u);
}
// Recover morale
if(!u.inCombat && u.morale < 100) {
u.morale = Math.min(100, u.morale + 0.5);
}
});
// Update visual effects
explosions = explosions.filter(e => {
e.radius += 2;
e.opacity -= 0.05;
return e.opacity > 0;
});
bulletTrails = bulletTrails.filter(b => {
b.progress += 0.1;
return b.progress < 1;
});
}
function makeAIDecision(unit){
const spec = countryUnits[unit.country][unit.type];
const info = findNearestEnemy(unit);
if(!info) return;
const {target, dist} = info;
const rangePx = spec.range / METERS_PER_PIXEL;
const unitHeight = getHeightAt(unit.x, unit.y);
const targetHeight = getHeightAt(target.x, target.y);
const heightAdvantage = unitHeight > targetHeight;
// HQ units stay back
if (unit.type === 'hq') {
if (unit.side === 'blue' && unit.y < battlefieldDivider + 100) {
unit.targetY = Math.max(battlefieldDivider + 100, unit.y);
} else if (unit.side === 'red' && unit.y > battlefieldDivider - 100) {
unit.targetY = Math.min(battlefieldDivider - 100, unit.y);
}
}
// Retreat if low health
if(unit.health < 30 || unit.morale < 20){
unit.isRetreating = true;
unit.inCombat = false;
unit.currentTarget = null;
unit.targetX = unit.x + (Math.random() - 0.5) * 50;
unit.targetY = (unit.side === 'blue') ? (mapHeight - 50) : 50;
return;
} else {
unit.isRetreating = false;
}
const effectiveRange = rangePx * (heightAdvantage ? 1.2 : 1.0);
// In range - engage
if(dist <= effectiveRange){
unit.inCombat = true;
unit.currentTarget = target;
if (unit.mission === 'defend') {
// Defenders hold position
unit.targetX = unit.x;
unit.targetY = unit.y;
}
else if (unit.mission === 'attack') {
// Attackers try to maintain optimal range but move more cautiously
const angle = Math.atan2(target.y - unit.y, target.x - unit.x);
const optimal = effectiveRange * 0.8;
if (dist > optimal) {
// Move forward cautiously - reduced from 20 to 5 pixels
unit.targetX = unit.x + Math.cos(angle) * 5;
unit.targetY = unit.y + Math.sin(angle) * 5;
} else {
// Small tactical movements
unit.targetX = unit.x + (Math.random() - 0.5) * 2;
unit.targetY = unit.y + (Math.random() - 0.5) * 2;
}
}
return;
}
// Out of range - move toward enemy
unit.inCombat = false;
unit.currentTarget = null;
if (unit.mission === 'attack') {
const angle = Math.atan2(target.y - unit.y, target.x - unit.x);
// Reduced advance distance from 100 to 20 pixels
const advanceDistance = Math.min(20, dist - effectiveRange * 0.8);
unit.targetX = unit.x + Math.cos(angle) * advanceDistance;
unit.targetY = unit.y + Math.sin(angle) * advanceDistance;
// Don't let HQ cross divider
if (unit.type === 'hq') {
if (unit.side === 'blue') {
unit.targetY = Math.min(battlefieldDivider - 50, unit.targetY);
} else {
unit.targetY = Math.max(battlefieldDivider + 50, unit.targetY);
}
}
} else if (unit.mission === 'defend') {
// Defenders make very small adjustments
if (dist < effectiveRange * 1.5) {
unit.targetX = unit.x + (Math.random() - 0.5) * 5;
unit.targetY = unit.y + (Math.random() - 0.5) * 5;
}
}
}
// New realistic movement function with road and village bonuses
function moveUnitRealistic(unit) {
const dx = unit.targetX - unit.x;
const dy = unit.targetY - unit.y;
const dist = Math.hypot(dx, dy);
if(dist < 0.5) {
unit.x = unit.targetX;
unit.y = unit.targetY;
return;
}
// Get unit specification for speed
const spec = countryUnits[unit.country][unit.type];
// Base speed in meters per minute (realistic military speeds)
let speedMPM = spec.speed || 100; // Default 100m/min for infantry
// Convert to pixels per tick
const gameMinutesPerTick = 0.1 * speedMultipliers[battleSpeed] / 60;
const metersPerTick = speedMPM * gameMinutesPerTick;
let pixelsPerTick = metersPerTick / METERS_PER_PIXEL;
// Check if unit is on or near a road (massive speed boost)
const gridSize = heightMap.length;
const cellWidth = mapWidth / gridSize;
const cellHeight = mapHeight / gridSize;
const gridX = Math.floor(unit.x / cellWidth);
const gridY = Math.floor(unit.y / cellHeight);
let onRoad = false;
for (let road of roads) {
for (let point of road) {
const roadDist = Math.sqrt(Math.pow(gridX - point.x, 2) + Math.pow(gridY - point.y, 2));
if (roadDist < 2) { // Within 2 grid cells of road
onRoad = true;
break;
}
}
if (onRoad) break;
}
if (onRoad) {
// Roads provide 2.5x speed for vehicles, 1.5x for infantry
if (spec.symbol === 'ā– ' || spec.symbol === 'ā–£' || spec.symbol === 'ā–¢' ||
spec.symbol === '⬢' || spec.symbol === 'ā—‰') {
pixelsPerTick *= 2.5; // Vehicles move much faster on roads
} else {
pixelsPerTick *= 1.5; // Infantry also benefits from roads
}
}
// Terrain effects
const height = getHeightAt(unit.x, unit.y);
const slope = getSlope(unit.x, unit.y);
let terrainMultiplier = 1.0;
// Slope penalties (reduced when on road)
if (!onRoad) {
if (slope > 30) {
terrainMultiplier *= 0.3;
} else if (slope > 15) {
terrainMultiplier *= 0.5;
} else if (slope > 5) {
terrainMultiplier *= 0.8;
}
} else {
// Roads reduce slope penalties
if (slope > 30) {
terrainMultiplier *= 0.6;
} else if (slope > 15) {
terrainMultiplier *= 0.8;
}
}
// Check if in a settlement (slows movement for attackers)
let inSettlement = false;
for (let settlement of settlements) {
const settlementX = settlement.x * cellWidth;
const settlementY = settlement.y * cellHeight;
const settlementDist = Math.hypot(unit.x - settlementX, unit.y - settlementY);
if (settlementDist < settlement.size * 2) {
inSettlement = true;
// Defenders move normally in settlements, attackers are slowed
const unitMission = unit.side === 'blue' ? blueMission : redMission;
if (unitMission === 'attack') {
terrainMultiplier *= 0.5; // Attackers move slowly through urban areas
}
break;
}
}
// Terrain type penalties (modified)
if (!onRoad && !inSettlement) {
if (selectedTerrain === 'urban') {
terrainMultiplier *= 0.7;
} else if (selectedTerrain === 'mountain') {
terrainMultiplier *= 0.5;
}
}
// Water is very slow for ground units
if (height < 0.2) {
// Air units not affected by water
if (!spec.symbol.includes('✈') && !unit.type.includes('heli') && !unit.type.includes('drone')) {
terrainMultiplier *= 0.1;
}
}
// Apply status modifiers
if (unit.isRetreating) {
terrainMultiplier *= 1.3;
}
if (unit.health < 50) {
terrainMultiplier *= 0.8;
}
// Calculate final speed
let finalSpeed = pixelsPerTick * terrainMultiplier;
// Ensure minimum movement
finalSpeed = Math.max(0.1, finalSpeed);
// Move the unit
if (dist > finalSpeed) {
unit.x += (dx / dist) * finalSpeed;
unit.y += (dy / dist) * finalSpeed;
} else {
unit.x = unit.targetX;
unit.y = unit.targetY;
}
}
function findNearestEnemy(unit){
const enemies = units.filter(u=>u.side!==unit.side && u.health>0);
if(enemies.length===0) return null;
let best=null, bestD=Infinity;
enemies.forEach(e=>{
const d = Math.hypot(unit.x-e.x, unit.y-e.y);
if(d<bestD){ bestD=d; best=e; }
});
return { target:best, dist:bestD };
}
function engageEnemy(unit){
if(!unit.currentTarget || unit.currentTarget.health<=0){
const info = findNearestEnemy(unit);
if(!info) { unit.inCombat=false; return; }
unit.currentTarget = info.target;
}
const spec = countryUnits[unit.country][unit.type];
const targetDist = Math.hypot(unit.x-unit.currentTarget.x, unit.y-unit.currentTarget.y);
const maxRange = spec.range / METERS_PER_PIXEL;
if (targetDist > maxRange) {
unit.inCombat = false;
unit.currentTarget = null;
return;
}
const now = Date.now();
// ė°œģ‚¬ 간격(화렄에 ė”°ė¼)
let fireRate = 3000 / (spec.firepower / 50);
if(now - unit.lastFired < fireRate) return;
unit.lastFired = now;
const rangePercentage = targetDist / maxRange;
// 명중넠 계산
let accuracy = 0.9 - (rangePercentage * 0.4);
const unitHeight = getHeightAt(unit.x, unit.y);
const targetHeight = getHeightAt(unit.currentTarget.x, unit.currentTarget.y);
if (unitHeight > targetHeight) {
accuracy *= 1.2;
}
const unitMission = unit.side === 'blue' ? blueMission : redMission;
const targetMission = unit.currentTarget.side === 'blue' ? blueMission : redMission;
let combatMultiplier = 1.0;
if (unitMission === 'defend' && targetMission === 'attack') {
combatMultiplier = 3.0; // ė°©ģ–“ģž ģš°ģœ„ (3:1)
accuracy *= 1.3;
} else if (unitMission === 'attack' && targetMission === 'defend') {
combatMultiplier = 0.33;
accuracy *= 0.7;
}
// ģ •ģ°©ģ§€(ė„ģ‹œ/ė§ˆģ„) ė°©ģ–“ ė³“ė„ˆģŠ¤
const gridSize = heightMap.length;
const cellWidth = mapWidth / gridSize;
const cellHeight = mapHeight / gridSize;
let targetInSettlement = false;
for (let settlement of settlements) {
const settlementX = settlement.x * cellWidth;
const settlementY = settlement.y * cellHeight;
const settlementDist = Math.hypot(unit.currentTarget.x - settlementX, unit.currentTarget.y - settlementY);
if (settlementDist < settlement.size * 2) {
targetInSettlement = true;
if (targetMission === 'defend') {
combatMultiplier *= 0.3; // 큰 피핓 경감
accuracy *= 0.6; // 명중넠 ķ•˜ė½
if (settlement.type === 'city') {
combatMultiplier *= 0.5;
accuracy *= 0.7;
} else if (settlement.type === 'town') {
combatMultiplier *= 0.7;
accuracy *= 0.8;
}
}
break;
}
}
if (targetMission === 'defend' && !targetInSettlement) {
if (selectedTerrain === 'urban' || selectedTerrain === 'mountain') {
combatMultiplier *= 0.8;
if (unitMission === 'defend') {
accuracy *= 1.1;
}
}
}
accuracy = Math.max(0.1, Math.min(0.95, accuracy));
// ģ‹œź° 효과(ķƒ„ė„ 궤적)
let trailColor = (unit.side==='blue'?'#2196F3':'#f44336');
bulletTrails.push({
from:{x:unit.x, y:unit.y},
to:{x:unit.currentTarget.x, y:unit.currentTarget.y},
color:trailColor,
progress:0
});
if(Math.random() < accuracy){
let damage = (spec.firepower/10) * (Math.random()*0.5 + 0.75);
damage *= combatMultiplier;
damage *= (1 - rangePercentage * 0.3);
unit.currentTarget.health -= damage;
unit.currentTarget.morale -= damage/2;
explosions.push({
x:unit.currentTarget.x,
y:unit.currentTarget.y,
radius:10,
color:(unit.side==='blue'?'#2196F3':'#f44336'),
opacity:0.8
});
if(unit.currentTarget.health<=0){
const targetSpec = countryUnits[unit.currentTarget.country][unit.currentTarget.type];
const casualties = targetSpec.strength;
if(unit.currentTarget.side==='blue'){
blueCasualties += casualties;
document.getElementById('blue-casualties').textContent = blueCasualties;
}else{
redCasualties += casualties;
document.getElementById('red-casualties').textContent = redCasualties;
}
unit.kills++;
unit.currentTarget.inCombat=false;
// HQ 격파 ģ‹œ ģ•„źµ° 사기 ģ €ķ•˜
if (unit.currentTarget.type === 'hq') {
units.filter(u => u.side === unit.currentTarget.side && u.health > 0)
.forEach(u => u.morale -= 30);
}
}
}
}
function checkVictoryConditions(){
const blueAlive = units.filter(u=>u.side==='blue' && u.health>0);
const redAlive = units.filter(u=>u.side==='red' && u.health>0);
if(blueAlive.length===0 && redAlive.length===0) return 'draw';
const blueHQ = units.find(u => u.side === 'blue' && u.type === 'hq');
const redHQ = units.find(u => u.side === 'red' && u.type === 'hq');
if (blueHQ && blueHQ.health <= 0) {
return 'red_victory_hq';
}
if (redHQ && redHQ.health <= 0) {
return 'blue_victory_hq';
}
if (blueHQ && blueHQ.health > 0) {
const enemiesNearby = units.filter(u =>
u.side === 'red' &&
u.health > 0 &&
Math.hypot(u.x - blueHQ.x, u.y - blueHQ.y) < 150
);
if (enemiesNearby.length >= 4) {
const angles = enemiesNearby.map(e =>
Math.atan2(e.y - blueHQ.y, e.x - blueHQ.x)
);
const quadrants = new Set(angles.map(a => Math.floor((a + Math.PI) / (Math.PI/2))));
if (quadrants.size >= 3) {
return 'red_victory_encircle';
}
}
}
if (redHQ && redHQ.health > 0) {
const enemiesNearby = units.filter(u =>
u.side === 'blue' &&
u.health > 0 &&
Math.hypot(u.x - redHQ.x, u.y - redHQ.y) < 150
);
if (enemiesNearby.length >= 4) {
const angles = enemiesNearby.map(e =>
Math.atan2(e.y - redHQ.y, e.x - redHQ.x)
);
const quadrants = new Set(angles.map(a => Math.floor((a + Math.PI) / (Math.PI/2))));
if (quadrants.size >= 3) {
return 'blue_victory_encircle';
}
}
}
const blueAll = units.filter(u=>u.side==='blue');
const redAll = units.filter(u=>u.side==='red');
if(blueAlive.length < Math.max(1, Math.floor(blueAll.length*0.1))) return 'red_victory_attrition';
if(redAlive.length < Math.max(1, Math.floor(redAll.length*0.1))) return 'blue_victory_attrition';
return '';
}
function endBattle(result){
battleActive=false;
// Stop battle sound
battleSound.pause();
battleSound.currentTime = 0;
if(battleInterval) clearInterval(battleInterval);
if(battleAnimationFrame) cancelAnimationFrame(battleAnimationFrame);
let message='';
let details='';
switch(result){
case 'blue_victory_hq':
message='šŸ”µ BLUE TEAM VICTORY!';
details='Enemy command destroyed - Red forces in disarray';
break;
case 'red_victory_hq':
message='šŸ”“ RED TEAM VICTORY!';
details='Enemy command destroyed - Blue forces in disarray';
break;
case 'blue_victory_encircle':
message='šŸ”µ BLUE TEAM VICTORY!';
details='Red HQ encircled and forced to surrender';
break;
case 'red_victory_encircle':
message='šŸ”“ RED TEAM VICTORY!';
details='Blue HQ encircled and forced to surrender';
break;
case 'blue_victory_attrition':
message='šŸ”µ BLUE TEAM VICTORY!';
details='Red forces decimated - 90% casualties';
break;
case 'red_victory_attrition':
message='šŸ”“ RED TEAM VICTORY!';
details='Blue forces decimated - 90% casualties';
break;
case 'draw':
message='āš–ļø MUTUAL DESTRUCTION';
details='Both forces annihilated';
break;
default:
message='Battle Ended';
break;
}
document.getElementById('battle-status').textContent='Battle Ended';
document.getElementById('start-battle').style.display='block';
document.getElementById('start-battle').textContent='āš”ļø START ENGAGEMENT';
document.getElementById('pause-battle').style.display='none';
alert(`Battle Complete!\n\n${message}\n${details}\n\n[Battle Results]\nBlue Casualties: ${blueCasualties}\nRed Casualties: ${redCasualties}\n\n[Battle Duration]\nBattle Time: ${formatTime(battleTimeElapsed)}\nReal Time: ${formatTime(realTimeElapsed)}\n\n[Victory Conditions]\n• HQ Destruction\n• Complete Encirclement\n• 90% Force Attrition`);
}
</script>
</body>
</html>