Spaces:
Running
Running
Manual changes saved
Browse files- index.html +167 -99
index.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
-
<title>BattleZone Royale -
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
<script src="https://unpkg.com/feather-icons"></script>
|
| 9 |
<style>
|
|
@@ -137,6 +137,13 @@
|
|
| 137 |
-webkit-user-select:none;
|
| 138 |
user-select:none;
|
| 139 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
.mobile-joystick .thumb {
|
| 141 |
width:58px; height:58px; border-radius:50%;
|
| 142 |
background: rgba(255,255,255,0.08);
|
|
@@ -144,6 +151,8 @@
|
|
| 144 |
transform: translate(0,0);
|
| 145 |
transition: transform 0.06s linear;
|
| 146 |
}
|
|
|
|
|
|
|
| 147 |
/* Buttons now reside left of pickaxe slot in HUD; this container is shown only on mobile */
|
| 148 |
.mobile-buttons-hud {
|
| 149 |
display:flex;
|
|
@@ -173,6 +182,11 @@
|
|
| 173 |
.mobile-controls { display:none !important; }
|
| 174 |
.mobile-buttons-hud { display:none !important; } /* hide HUD buttons on desktop too */
|
| 175 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
</style>
|
| 177 |
</head>
|
| 178 |
<body>
|
|
@@ -212,7 +226,7 @@
|
|
| 212 |
<li><strong>WASD</strong> — Move</li>
|
| 213 |
<li><strong>Mouse</strong> — Aim</li>
|
| 214 |
<li><strong>Left Click</strong> — Shoot (if weapon equipped or in selected slot) or pickaxe melee</li>
|
| 215 |
-
<li><strong>E</strong> — Use medkit in selected slot OR Interact / Loot</li>
|
| 216 |
<li><strong>Q</strong> — Build (costs 10 materials)</li>
|
| 217 |
<li><strong>R</strong> — Reload selected weapon</li>
|
| 218 |
<li><strong>1-5</strong> — Select inventory slot (also equips it)</li>
|
|
@@ -322,11 +336,14 @@
|
|
| 322 |
<div id="hudGear" aria-label="gear"></div>
|
| 323 |
</div>
|
| 324 |
|
| 325 |
-
<!-- Mobile Controls (joystick
|
| 326 |
<div id="mobileControls" class="mobile-controls hidden" aria-hidden="true">
|
| 327 |
<div id="mobileJoystick" class="mobile-joystick" role="application" aria-label="Movement joystick">
|
| 328 |
<div id="joystickThumb" class="thumb"></div>
|
| 329 |
</div>
|
|
|
|
|
|
|
|
|
|
| 330 |
</div>
|
| 331 |
|
| 332 |
</div>
|
|
@@ -334,6 +351,9 @@
|
|
| 334 |
</div>
|
| 335 |
|
| 336 |
<script>
|
|
|
|
|
|
|
|
|
|
| 337 |
// DOM references
|
| 338 |
const landingScreen = document.getElementById('landingScreen');
|
| 339 |
const gameScreen = document.getElementById('gameScreen');
|
|
@@ -349,11 +369,9 @@
|
|
| 349 |
const victoryScreen = document.getElementById('victoryScreen');
|
| 350 |
const goHomeBtn = document.getElementById('goHomeBtn');
|
| 351 |
const continueBtn = document.getElementById('continueBtn');
|
| 352 |
-
const biomeGrid = document.getElementById('biomeGrid');
|
| 353 |
|
| 354 |
// Mobile button references (in HUD)
|
| 355 |
const mobileButtonsHud = document.getElementById('mobileButtonsHud');
|
| 356 |
-
// Joystick container
|
| 357 |
const mobileControls = document.getElementById('mobileControls');
|
| 358 |
|
| 359 |
// Loading screen elements
|
|
@@ -375,9 +393,14 @@
|
|
| 375 |
const ctn = document.getElementById('canvasContainer');
|
| 376 |
canvas.width = Math.max(900, Math.floor(ctn.clientWidth));
|
| 377 |
canvas.height = Math.max(560, Math.floor(ctn.clientHeight));
|
| 378 |
-
//
|
| 379 |
-
|
| 380 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
cameraUpdate();
|
| 382 |
miniTerrainCache = null; // rebuild on resize
|
| 383 |
}
|
|
@@ -391,8 +414,7 @@
|
|
| 391 |
selectedSlot:0,
|
| 392 |
equippedIndex: -1,
|
| 393 |
lastShot:0, lastMelee:0,
|
| 394 |
-
// medkit
|
| 395 |
-
usingMedkit: false, usingMedkitUntil: 0, usingMedkitStart: 0, usingMedkitTimeout: null
|
| 396 |
};
|
| 397 |
|
| 398 |
// Input
|
|
@@ -428,7 +450,10 @@
|
|
| 428 |
window.addEventListener('keyup',(e)=>{
|
| 429 |
const k = e.key.toLowerCase();
|
| 430 |
if (k in keys) keys[k] = false;
|
| 431 |
-
|
|
|
|
|
|
|
|
|
|
| 432 |
if (k === 'q') keys.q = true;
|
| 433 |
});
|
| 434 |
|
|
@@ -451,12 +476,10 @@
|
|
| 451 |
});
|
| 452 |
window.addEventListener('mouseup', ()=> mouse.down = false);
|
| 453 |
|
| 454 |
-
// Touch support for
|
| 455 |
-
// Also, we wire separate on-screen shoot button to control mouse.down.
|
| 456 |
(function addCanvasTouchHandlers(){
|
| 457 |
let canvasTouchId = null;
|
| 458 |
canvas.addEventListener('touchstart', (ev) => {
|
| 459 |
-
// find first touch that's inside canvas and not used by joystick/buttons
|
| 460 |
for (const t of ev.changedTouches){
|
| 461 |
const rect = canvas.getBoundingClientRect();
|
| 462 |
const x = t.clientX - rect.left, y = t.clientY - rect.top;
|
|
@@ -667,10 +690,6 @@
|
|
| 667 |
function updateHUD(){
|
| 668 |
const now = performance.now();
|
| 669 |
let healthText = `${Math.max(0,Math.floor(player.health))}%`;
|
| 670 |
-
if (player.usingMedkit){
|
| 671 |
-
const remaining = Math.max(0, Math.ceil((player.usingMedkitUntil - now)/1000));
|
| 672 |
-
healthText += ` (Healing ${remaining}s)`;
|
| 673 |
-
}
|
| 674 |
hudHealthText.textContent = healthText;
|
| 675 |
const slots = hudGear.querySelectorAll('.gear-slot');
|
| 676 |
slots.forEach(s => {
|
|
@@ -751,63 +770,33 @@
|
|
| 751 |
}
|
| 752 |
}
|
| 753 |
|
| 754 |
-
// Player interact & medkit usage
|
| 755 |
function attemptUseOrInteract(){
|
| 756 |
const sel = player.selectedSlot;
|
| 757 |
const selItem = player.inventory[sel];
|
| 758 |
if (selItem && selItem.type === 'medkit'){
|
| 759 |
-
|
| 760 |
-
if (player.usingMedkit){
|
| 761 |
-
cancelPlayerMedkitUse();
|
| 762 |
-
} else {
|
| 763 |
-
startPlayerMedkitUse(sel);
|
| 764 |
-
}
|
| 765 |
} else {
|
| 766 |
interactNearby();
|
| 767 |
}
|
| 768 |
}
|
| 769 |
|
| 770 |
-
function
|
| 771 |
const it = player.inventory[slot];
|
| 772 |
if (!it || it.type !== 'medkit' || it.amount <= 0) return;
|
| 773 |
-
if (player.usingMedkit) return;
|
| 774 |
const now = performance.now();
|
| 775 |
-
|
| 776 |
-
player.
|
| 777 |
-
player.
|
| 778 |
-
|
| 779 |
-
player.
|
| 780 |
-
|
| 781 |
-
if (!player.usingMedkit) return;
|
| 782 |
-
const cur = player.inventory[slot];
|
| 783 |
-
if (cur && cur.type === 'medkit'){
|
| 784 |
-
cur.amount -= 1;
|
| 785 |
-
player.health = Math.min(100, player.health + 50);
|
| 786 |
-
if (cur.amount <= 0) player.inventory[slot] = null;
|
| 787 |
-
}
|
| 788 |
-
player.usingMedkit = false;
|
| 789 |
-
player.usingMedkitTimeout = null;
|
| 790 |
-
updateHUD();
|
| 791 |
-
}, 3000);
|
| 792 |
-
updateHUD();
|
| 793 |
-
}
|
| 794 |
-
|
| 795 |
-
function cancelPlayerMedkitUse(){
|
| 796 |
-
if (!player.usingMedkit) return;
|
| 797 |
-
player.usingMedkit = false;
|
| 798 |
-
player.usingMedkitStart = 0;
|
| 799 |
-
player.usingMedkitUntil = 0;
|
| 800 |
-
if (player.usingMedkitTimeout){
|
| 801 |
-
clearTimeout(player.usingMedkitTimeout);
|
| 802 |
-
player.usingMedkitTimeout = null;
|
| 803 |
-
}
|
| 804 |
updateHUD();
|
| 805 |
}
|
| 806 |
|
| 807 |
function interactNearby(){
|
| 808 |
const sel = player.selectedSlot;
|
| 809 |
const selItem = player.inventory[sel];
|
| 810 |
-
// medkit immediate use removed — now 3s usage handled above
|
| 811 |
const range = 56;
|
| 812 |
for (const chest of chests){
|
| 813 |
if (chest.opened) continue;
|
|
@@ -865,7 +854,7 @@
|
|
| 865 |
}
|
| 866 |
}
|
| 867 |
|
| 868 |
-
// Enemy
|
| 869 |
function enemyEquipBestWeapon(e){
|
| 870 |
let bestIdx = -1;
|
| 871 |
let bestScore = -Infinity;
|
|
@@ -889,7 +878,6 @@
|
|
| 889 |
e.usingMedkitStart = now;
|
| 890 |
e.usingMedkitUntil = now + 3000;
|
| 891 |
e.usingMedkitSlot = s;
|
| 892 |
-
// set nextHealTime to prevent immediate re-trigger
|
| 893 |
e.nextHealTime = now + 5000;
|
| 894 |
return true;
|
| 895 |
}
|
|
@@ -963,7 +951,6 @@
|
|
| 963 |
const it = e.inventory[s];
|
| 964 |
if (it && it.type==='medkit'){ it.amount += p.amount;
|
| 965 |
if (e.health < 60) {
|
| 966 |
-
// start medkit use if not already using
|
| 967 |
if (!e.usingMedkit) enemyStartMedkitUse(e, performance.now());
|
| 968 |
}
|
| 969 |
return;
|
|
@@ -1059,7 +1046,7 @@
|
|
| 1059 |
return chosen;
|
| 1060 |
}
|
| 1061 |
|
| 1062 |
-
// Bullets update (
|
| 1063 |
function bulletsUpdate(dt){
|
| 1064 |
for (let i=bullets.length-1;i>=0;i--){
|
| 1065 |
const b = bullets[i];
|
|
@@ -1074,8 +1061,6 @@
|
|
| 1074 |
if (dPlayer < hitRadiusPlayer){
|
| 1075 |
// apply damage
|
| 1076 |
player.health -= b.dmg;
|
| 1077 |
-
// cancel medkit usage if player is healing
|
| 1078 |
-
if (player.usingMedkit) cancelPlayerMedkitUse();
|
| 1079 |
if (player.health <= 0){ player.health = 0; playerDeath(); }
|
| 1080 |
bullets.splice(i,1);
|
| 1081 |
continue;
|
|
@@ -1093,7 +1078,6 @@
|
|
| 1093 |
e.lastAttackedTime = performance.now();
|
| 1094 |
// cancel enemy medkit if they are healing
|
| 1095 |
if (e.usingMedkit){
|
| 1096 |
-
// if they were using medkit, cancel it when hit
|
| 1097 |
e.usingMedkit = false; e.usingMedkitSlot = -1; e.usingMedkitStart = 0; e.usingMedkitUntil = 0;
|
| 1098 |
}
|
| 1099 |
if (e.health <= 0){
|
|
@@ -1115,12 +1099,10 @@
|
|
| 1115 |
if (obj.dead) continue;
|
| 1116 |
const rr = getObjectRadius(obj);
|
| 1117 |
const d = Math.hypot(obj.x - b.x, obj.y - b.y);
|
| 1118 |
-
// use a small tolerance so bullets clearly collide
|
| 1119 |
if (d < rr + 2){
|
| 1120 |
obj.hp -= b.dmg;
|
| 1121 |
if (obj.hp <= 0){
|
| 1122 |
obj.dead = true;
|
| 1123 |
-
// reward materials if player shot the object
|
| 1124 |
if (b.shooter === 'player') player.materials += (obj.type === 'wood' ? 3 : 6);
|
| 1125 |
}
|
| 1126 |
hitObj = obj;
|
|
@@ -1132,7 +1114,7 @@
|
|
| 1132 |
continue;
|
| 1133 |
}
|
| 1134 |
|
| 1135 |
-
// 4) Hit chests
|
| 1136 |
for (const chest of chests){
|
| 1137 |
if (chest.opened) continue;
|
| 1138 |
const d = Math.hypot(chest.x - b.x, chest.y - b.y);
|
|
@@ -1154,7 +1136,7 @@
|
|
| 1154 |
}
|
| 1155 |
}
|
| 1156 |
|
| 1157 |
-
// Helpers to find nearest
|
| 1158 |
function findNearestChest(e, maxDist = 1200){
|
| 1159 |
let best = null; let bd = Infinity;
|
| 1160 |
for (const c of chests){ if (c.opened) continue; const d = Math.hypot(c.x - e.x, c.y - e.y); if (d < bd && d <= maxDist){ bd = d; best = c; } }
|
|
@@ -1171,7 +1153,7 @@
|
|
| 1171 |
return best;
|
| 1172 |
}
|
| 1173 |
|
| 1174 |
-
// Enemy AI (
|
| 1175 |
function updateEnemies(dt, now){
|
| 1176 |
const minSeparation = 20;
|
| 1177 |
for (const e of enemies){
|
|
@@ -1199,18 +1181,16 @@
|
|
| 1199 |
|
| 1200 |
// Handle enemy medkit usage finishing/cancelling
|
| 1201 |
if (e.usingMedkit){
|
| 1202 |
-
// if they were attacked while using, cancel
|
| 1203 |
if (e.lastAttackedTime > e.usingMedkitStart){
|
| 1204 |
e.usingMedkit = false; e.usingMedkitSlot = -1; e.usingMedkitStart = 0; e.usingMedkitUntil = 0;
|
| 1205 |
} else if (now >= e.usingMedkitUntil){
|
| 1206 |
applyEnemyMedkitUseNow(e);
|
| 1207 |
} else {
|
| 1208 |
-
// still using medkit; don't do other heavy actions
|
| 1209 |
continue;
|
| 1210 |
}
|
| 1211 |
}
|
| 1212 |
|
| 1213 |
-
// STORM behavior
|
| 1214 |
if (storm.active){
|
| 1215 |
const distToSafeCenter = Math.hypot(e.x - storm.centerX, e.y - storm.centerY);
|
| 1216 |
if (distToSafeCenter > storm.radius){
|
|
@@ -1280,7 +1260,6 @@
|
|
| 1280 |
if (e.state === 'gather') e.gatherTimeLeft -= dt;
|
| 1281 |
|
| 1282 |
if (e.health < 60 && now >= (e.nextHealTime || 0) && !e.usingMedkit){
|
| 1283 |
-
// start medkit use (3s) if possible
|
| 1284 |
if (enemyStartMedkitUse(e, now)){
|
| 1285 |
continue;
|
| 1286 |
}
|
|
@@ -1365,7 +1344,7 @@
|
|
| 1365 |
continue;
|
| 1366 |
}
|
| 1367 |
|
| 1368 |
-
// Combat logic
|
| 1369 |
if (e.equippedIndex === -1) enemyEquipBestWeapon(e);
|
| 1370 |
if (e.reloadPending){
|
| 1371 |
if (now >= e.reloadingUntil){
|
|
@@ -1538,7 +1517,7 @@
|
|
| 1538 |
}
|
| 1539 |
}
|
| 1540 |
|
| 1541 |
-
// Separation
|
| 1542 |
for (let i = 0; i < enemies.length; i++){
|
| 1543 |
const a = enemies[i];
|
| 1544 |
if (!a || a.health <= 0) continue;
|
|
@@ -1586,7 +1565,6 @@
|
|
| 1586 |
while (stormDamageAccumulator >= 1){
|
| 1587 |
stormDamageAccumulator -=1;
|
| 1588 |
player.health -= 1;
|
| 1589 |
-
if (player.usingMedkit) cancelPlayerMedkitUse();
|
| 1590 |
if (player.health <= 0){ player.health = 0; playerDeath(); }
|
| 1591 |
}
|
| 1592 |
} else stormDamageAccumulator = 0;
|
|
@@ -1611,8 +1589,7 @@
|
|
| 1611 |
}
|
| 1612 |
}
|
| 1613 |
|
| 1614 |
-
// Drawing functions (
|
| 1615 |
-
// (All draw functions from the previous code remain unchanged and are present below.)
|
| 1616 |
function drawWorld(){
|
| 1617 |
const TILE = 600;
|
| 1618 |
const cols = Math.ceil(WORLD.width / TILE);
|
|
@@ -1820,7 +1797,7 @@
|
|
| 1820 |
|
| 1821 |
function drawCrosshair(){}
|
| 1822 |
|
| 1823 |
-
// Minimap functions
|
| 1824 |
function buildMiniTerrainCache(){
|
| 1825 |
const mw = minimapCanvas.width;
|
| 1826 |
const mh = minimapCanvas.height;
|
|
@@ -1921,6 +1898,7 @@
|
|
| 1921 |
const dt = Math.min(0.05, (ts - lastTime)/1000);
|
| 1922 |
lastTime = ts;
|
| 1923 |
|
|
|
|
| 1924 |
let dx=0, dy=0;
|
| 1925 |
if (keys.w) dy -= 1; if (keys.s) dy += 1; if (keys.a) dx -= 1; if (keys.d) dx += 1;
|
| 1926 |
if (dx !== 0 || dy !== 0){
|
|
@@ -1940,8 +1918,7 @@
|
|
| 1940 |
if (isCollidingSolid(player.x, player.y, player.radius)){
|
| 1941 |
player.y = oldY;
|
| 1942 |
}
|
| 1943 |
-
//
|
| 1944 |
-
if (player.usingMedkit && (dx !== 0 || dy !== 0)) cancelPlayerMedkitUse();
|
| 1945 |
}
|
| 1946 |
player.x = Math.max(16, Math.min(WORLD.width-16, player.x));
|
| 1947 |
player.y = Math.max(16, Math.min(WORLD.height-16, player.y));
|
|
@@ -1973,7 +1950,6 @@
|
|
| 1973 |
}
|
| 1974 |
|
| 1975 |
if (keys.r) { reloadEquipped(); keys.r = false; }
|
| 1976 |
-
if (keys.e){ attemptUseOrInteract(); keys.e = false; }
|
| 1977 |
if (keys.q){ tryBuild(); keys.q = false; }
|
| 1978 |
|
| 1979 |
updateEnemies(dt, performance.now());
|
|
@@ -2034,7 +2010,7 @@
|
|
| 2034 |
player.health = 100; player.armor = 0; player.kills = 0; player.materials = 0;
|
| 2035 |
player.inventory = [null,null,null,null,null];
|
| 2036 |
player.selectedSlot = 0; player.equippedIndex = -1; player.lastShot = 0; player.lastMelee = 0;
|
| 2037 |
-
player.
|
| 2038 |
|
| 2039 |
populateWorld();
|
| 2040 |
initHUD();
|
|
@@ -2082,8 +2058,6 @@
|
|
| 2082 |
function endGame(){ gameActive = false; alert('Match over!'); }
|
| 2083 |
function playerDeath(){
|
| 2084 |
gameActive = false;
|
| 2085 |
-
// cancel medkit use on death
|
| 2086 |
-
cancelPlayerMedkitUse();
|
| 2087 |
deathScreen.classList.remove('hidden');
|
| 2088 |
}
|
| 2089 |
|
|
@@ -2157,7 +2131,7 @@
|
|
| 2157 |
});
|
| 2158 |
});
|
| 2159 |
|
| 2160 |
-
// Mobile controls wiring
|
| 2161 |
(function setupMobileControls(){
|
| 2162 |
const joystick = document.getElementById('mobileJoystick');
|
| 2163 |
const thumb = document.getElementById('joystickThumb');
|
|
@@ -2166,8 +2140,10 @@
|
|
| 2166 |
const buildBtn = document.getElementById('buildBtn');
|
| 2167 |
const reloadBtn = document.getElementById('reloadBtn');
|
| 2168 |
|
| 2169 |
-
const
|
| 2170 |
-
|
|
|
|
|
|
|
| 2171 |
mobileControls.classList.add('hidden');
|
| 2172 |
if (mobileButtonsHud) mobileButtonsHud.classList.add('hidden');
|
| 2173 |
return;
|
|
@@ -2176,7 +2152,7 @@
|
|
| 2176 |
mobileControls.setAttribute('aria-hidden','false');
|
| 2177 |
if (mobileButtonsHud) { mobileButtonsHud.classList.remove('hidden'); mobileButtonsHud.setAttribute('aria-hidden','false'); }
|
| 2178 |
|
| 2179 |
-
//
|
| 2180 |
let joyTouchId = null;
|
| 2181 |
let joyCenter = null;
|
| 2182 |
const maxRadius = 52; // thumb allowed radius
|
|
@@ -2205,12 +2181,10 @@
|
|
| 2205 |
ny = dy / dist * maxRadius;
|
| 2206 |
}
|
| 2207 |
thumb.style.transform = `translate(${nx}px, ${ny}px)`;
|
| 2208 |
-
// map to directional keys
|
| 2209 |
if (dist < deadzone){
|
| 2210 |
keys.w = keys.a = keys.s = keys.d = false;
|
| 2211 |
return;
|
| 2212 |
}
|
| 2213 |
-
// use axis threshold approach
|
| 2214 |
keys.w = (dy < -10);
|
| 2215 |
keys.s = (dy > 10);
|
| 2216 |
keys.a = (dx < -10);
|
|
@@ -2223,10 +2197,8 @@
|
|
| 2223 |
resetJoystick();
|
| 2224 |
}
|
| 2225 |
|
| 2226 |
-
// Touch handlers for joystick
|
| 2227 |
joystick.addEventListener('touchstart', (ev) => {
|
| 2228 |
for (const t of ev.changedTouches){
|
| 2229 |
-
// only accept first active joystick touch
|
| 2230 |
if (joyTouchId === null){
|
| 2231 |
handleJoyStart(t);
|
| 2232 |
ev.preventDefault();
|
|
@@ -2273,12 +2245,14 @@
|
|
| 2273 |
if (shootTouchId === null){
|
| 2274 |
shootTouchId = t.identifier;
|
| 2275 |
mouse.down = true;
|
| 2276 |
-
//
|
| 2277 |
-
|
| 2278 |
-
|
| 2279 |
-
|
| 2280 |
-
|
| 2281 |
-
|
|
|
|
|
|
|
| 2282 |
e.preventDefault();
|
| 2283 |
break;
|
| 2284 |
}
|
|
@@ -2335,6 +2309,100 @@
|
|
| 2335 |
[shootBtn, interactBtn, buildBtn, reloadBtn].forEach(b => {
|
| 2336 |
b.addEventListener('touchmove', (ev)=> ev.preventDefault(), { passive:false });
|
| 2337 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2338 |
})();
|
| 2339 |
|
| 2340 |
// Initialize UI state
|
|
|
|
| 3 |
<head>
|
| 4 |
<meta charset="utf-8" />
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 6 |
+
<title>BattleZone Royale - Mobile Improvements</title>
|
| 7 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
<script src="https://unpkg.com/feather-icons"></script>
|
| 9 |
<style>
|
|
|
|
| 137 |
-webkit-user-select:none;
|
| 138 |
user-select:none;
|
| 139 |
}
|
| 140 |
+
.mobile-joystick.aim {
|
| 141 |
+
right: 12px;
|
| 142 |
+
left: auto;
|
| 143 |
+
}
|
| 144 |
+
.mobile-joystick.small {
|
| 145 |
+
width:110px; height:110px;
|
| 146 |
+
}
|
| 147 |
.mobile-joystick .thumb {
|
| 148 |
width:58px; height:58px; border-radius:50%;
|
| 149 |
background: rgba(255,255,255,0.08);
|
|
|
|
| 151 |
transform: translate(0,0);
|
| 152 |
transition: transform 0.06s linear;
|
| 153 |
}
|
| 154 |
+
.mobile-joystick.small .thumb { width:44px; height:44px; }
|
| 155 |
+
|
| 156 |
/* Buttons now reside left of pickaxe slot in HUD; this container is shown only on mobile */
|
| 157 |
.mobile-buttons-hud {
|
| 158 |
display:flex;
|
|
|
|
| 182 |
.mobile-controls { display:none !important; }
|
| 183 |
.mobile-buttons-hud { display:none !important; } /* hide HUD buttons on desktop too */
|
| 184 |
}
|
| 185 |
+
|
| 186 |
+
/* Make minimap smaller on narrow/mobile */
|
| 187 |
+
@media (max-width: 900px){
|
| 188 |
+
#minimap { width:120px; height:78px; right:8px; top:8px; padding:5px; }
|
| 189 |
+
}
|
| 190 |
</style>
|
| 191 |
</head>
|
| 192 |
<body>
|
|
|
|
| 226 |
<li><strong>WASD</strong> — Move</li>
|
| 227 |
<li><strong>Mouse</strong> — Aim</li>
|
| 228 |
<li><strong>Left Click</strong> — Shoot (if weapon equipped or in selected slot) or pickaxe melee</li>
|
| 229 |
+
<li><strong>E</strong> — Use medkit in selected slot OR Interact / Loot (press once)</li>
|
| 230 |
<li><strong>Q</strong> — Build (costs 10 materials)</li>
|
| 231 |
<li><strong>R</strong> — Reload selected weapon</li>
|
| 232 |
<li><strong>1-5</strong> — Select inventory slot (also equips it)</li>
|
|
|
|
| 336 |
<div id="hudGear" aria-label="gear"></div>
|
| 337 |
</div>
|
| 338 |
|
| 339 |
+
<!-- Mobile Controls: left joystick (movement) and right joystick (aim) -->
|
| 340 |
<div id="mobileControls" class="mobile-controls hidden" aria-hidden="true">
|
| 341 |
<div id="mobileJoystick" class="mobile-joystick" role="application" aria-label="Movement joystick">
|
| 342 |
<div id="joystickThumb" class="thumb"></div>
|
| 343 |
</div>
|
| 344 |
+
<div id="mobileJoystickAim" class="mobile-joystick aim small" role="application" aria-label="Aiming joystick">
|
| 345 |
+
<div id="joystickThumbAim" class="thumb"></div>
|
| 346 |
+
</div>
|
| 347 |
</div>
|
| 348 |
|
| 349 |
</div>
|
|
|
|
| 351 |
</div>
|
| 352 |
|
| 353 |
<script>
|
| 354 |
+
// Global mobile detection
|
| 355 |
+
const IS_MOBILE = ('ontouchstart' in window) || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
|
| 356 |
+
|
| 357 |
// DOM references
|
| 358 |
const landingScreen = document.getElementById('landingScreen');
|
| 359 |
const gameScreen = document.getElementById('gameScreen');
|
|
|
|
| 369 |
const victoryScreen = document.getElementById('victoryScreen');
|
| 370 |
const goHomeBtn = document.getElementById('goHomeBtn');
|
| 371 |
const continueBtn = document.getElementById('continueBtn');
|
|
|
|
| 372 |
|
| 373 |
// Mobile button references (in HUD)
|
| 374 |
const mobileButtonsHud = document.getElementById('mobileButtonsHud');
|
|
|
|
| 375 |
const mobileControls = document.getElementById('mobileControls');
|
| 376 |
|
| 377 |
// Loading screen elements
|
|
|
|
| 393 |
const ctn = document.getElementById('canvasContainer');
|
| 394 |
canvas.width = Math.max(900, Math.floor(ctn.clientWidth));
|
| 395 |
canvas.height = Math.max(560, Math.floor(ctn.clientHeight));
|
| 396 |
+
// make minimap smaller on mobile
|
| 397 |
+
if (IS_MOBILE){
|
| 398 |
+
minimapCanvas.width = 120;
|
| 399 |
+
minimapCanvas.height = 78;
|
| 400 |
+
} else {
|
| 401 |
+
minimapCanvas.width = 220;
|
| 402 |
+
minimapCanvas.height = 140;
|
| 403 |
+
}
|
| 404 |
cameraUpdate();
|
| 405 |
miniTerrainCache = null; // rebuild on resize
|
| 406 |
}
|
|
|
|
| 414 |
selectedSlot:0,
|
| 415 |
equippedIndex: -1,
|
| 416 |
lastShot:0, lastMelee:0,
|
| 417 |
+
lastMedkitUsed: 0 // cooldown tracker for instant medkit presses
|
|
|
|
| 418 |
};
|
| 419 |
|
| 420 |
// Input
|
|
|
|
| 450 |
window.addEventListener('keyup',(e)=>{
|
| 451 |
const k = e.key.toLowerCase();
|
| 452 |
if (k in keys) keys[k] = false;
|
| 453 |
+
// Press E once to use medkit or interact (no holding)
|
| 454 |
+
if (k === 'e') {
|
| 455 |
+
attemptUseOrInteract();
|
| 456 |
+
}
|
| 457 |
if (k === 'q') keys.q = true;
|
| 458 |
});
|
| 459 |
|
|
|
|
| 476 |
});
|
| 477 |
window.addEventListener('mouseup', ()=> mouse.down = false);
|
| 478 |
|
| 479 |
+
// Touch support for canvas aiming (still supported)
|
|
|
|
| 480 |
(function addCanvasTouchHandlers(){
|
| 481 |
let canvasTouchId = null;
|
| 482 |
canvas.addEventListener('touchstart', (ev) => {
|
|
|
|
| 483 |
for (const t of ev.changedTouches){
|
| 484 |
const rect = canvas.getBoundingClientRect();
|
| 485 |
const x = t.clientX - rect.left, y = t.clientY - rect.top;
|
|
|
|
| 690 |
function updateHUD(){
|
| 691 |
const now = performance.now();
|
| 692 |
let healthText = `${Math.max(0,Math.floor(player.health))}%`;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 693 |
hudHealthText.textContent = healthText;
|
| 694 |
const slots = hudGear.querySelectorAll('.gear-slot');
|
| 695 |
slots.forEach(s => {
|
|
|
|
| 770 |
}
|
| 771 |
}
|
| 772 |
|
| 773 |
+
// Player interact & medkit usage - instantaneous on press, usable while moving/taking damage
|
| 774 |
function attemptUseOrInteract(){
|
| 775 |
const sel = player.selectedSlot;
|
| 776 |
const selItem = player.inventory[sel];
|
| 777 |
if (selItem && selItem.type === 'medkit'){
|
| 778 |
+
useMedkitInstant(sel);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 779 |
} else {
|
| 780 |
interactNearby();
|
| 781 |
}
|
| 782 |
}
|
| 783 |
|
| 784 |
+
function useMedkitInstant(slot){
|
| 785 |
const it = player.inventory[slot];
|
| 786 |
if (!it || it.type !== 'medkit' || it.amount <= 0) return;
|
|
|
|
| 787 |
const now = performance.now();
|
| 788 |
+
// small cooldown to avoid spamming
|
| 789 |
+
if (player.lastMedkitUsed && now - player.lastMedkitUsed < 700) return;
|
| 790 |
+
player.lastMedkitUsed = now;
|
| 791 |
+
it.amount -= 1;
|
| 792 |
+
player.health = Math.min(100, player.health + 50);
|
| 793 |
+
if (it.amount <= 0) player.inventory[slot] = null;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 794 |
updateHUD();
|
| 795 |
}
|
| 796 |
|
| 797 |
function interactNearby(){
|
| 798 |
const sel = player.selectedSlot;
|
| 799 |
const selItem = player.inventory[sel];
|
|
|
|
| 800 |
const range = 56;
|
| 801 |
for (const chest of chests){
|
| 802 |
if (chest.opened) continue;
|
|
|
|
| 854 |
}
|
| 855 |
}
|
| 856 |
|
| 857 |
+
// Enemy medkit logic kept unchanged (they still take 3s)
|
| 858 |
function enemyEquipBestWeapon(e){
|
| 859 |
let bestIdx = -1;
|
| 860 |
let bestScore = -Infinity;
|
|
|
|
| 878 |
e.usingMedkitStart = now;
|
| 879 |
e.usingMedkitUntil = now + 3000;
|
| 880 |
e.usingMedkitSlot = s;
|
|
|
|
| 881 |
e.nextHealTime = now + 5000;
|
| 882 |
return true;
|
| 883 |
}
|
|
|
|
| 951 |
const it = e.inventory[s];
|
| 952 |
if (it && it.type==='medkit'){ it.amount += p.amount;
|
| 953 |
if (e.health < 60) {
|
|
|
|
| 954 |
if (!e.usingMedkit) enemyStartMedkitUse(e, performance.now());
|
| 955 |
}
|
| 956 |
return;
|
|
|
|
| 1046 |
return chosen;
|
| 1047 |
}
|
| 1048 |
|
| 1049 |
+
// Bullets update (keep same but removed medkit cancellation when taking damage)
|
| 1050 |
function bulletsUpdate(dt){
|
| 1051 |
for (let i=bullets.length-1;i>=0;i--){
|
| 1052 |
const b = bullets[i];
|
|
|
|
| 1061 |
if (dPlayer < hitRadiusPlayer){
|
| 1062 |
// apply damage
|
| 1063 |
player.health -= b.dmg;
|
|
|
|
|
|
|
| 1064 |
if (player.health <= 0){ player.health = 0; playerDeath(); }
|
| 1065 |
bullets.splice(i,1);
|
| 1066 |
continue;
|
|
|
|
| 1078 |
e.lastAttackedTime = performance.now();
|
| 1079 |
// cancel enemy medkit if they are healing
|
| 1080 |
if (e.usingMedkit){
|
|
|
|
| 1081 |
e.usingMedkit = false; e.usingMedkitSlot = -1; e.usingMedkitStart = 0; e.usingMedkitUntil = 0;
|
| 1082 |
}
|
| 1083 |
if (e.health <= 0){
|
|
|
|
| 1099 |
if (obj.dead) continue;
|
| 1100 |
const rr = getObjectRadius(obj);
|
| 1101 |
const d = Math.hypot(obj.x - b.x, obj.y - b.y);
|
|
|
|
| 1102 |
if (d < rr + 2){
|
| 1103 |
obj.hp -= b.dmg;
|
| 1104 |
if (obj.hp <= 0){
|
| 1105 |
obj.dead = true;
|
|
|
|
| 1106 |
if (b.shooter === 'player') player.materials += (obj.type === 'wood' ? 3 : 6);
|
| 1107 |
}
|
| 1108 |
hitObj = obj;
|
|
|
|
| 1114 |
continue;
|
| 1115 |
}
|
| 1116 |
|
| 1117 |
+
// 4) Hit chests
|
| 1118 |
for (const chest of chests){
|
| 1119 |
if (chest.opened) continue;
|
| 1120 |
const d = Math.hypot(chest.x - b.x, chest.y - b.y);
|
|
|
|
| 1136 |
}
|
| 1137 |
}
|
| 1138 |
|
| 1139 |
+
// Helpers to find nearest (unchanged)
|
| 1140 |
function findNearestChest(e, maxDist = 1200){
|
| 1141 |
let best = null; let bd = Infinity;
|
| 1142 |
for (const c of chests){ if (c.opened) continue; const d = Math.hypot(c.x - e.x, c.y - e.y); if (d < bd && d <= maxDist){ bd = d; best = c; } }
|
|
|
|
| 1153 |
return best;
|
| 1154 |
}
|
| 1155 |
|
| 1156 |
+
// Enemy AI (keeps behavior; medkit usage for enemies remains as 3s)
|
| 1157 |
function updateEnemies(dt, now){
|
| 1158 |
const minSeparation = 20;
|
| 1159 |
for (const e of enemies){
|
|
|
|
| 1181 |
|
| 1182 |
// Handle enemy medkit usage finishing/cancelling
|
| 1183 |
if (e.usingMedkit){
|
|
|
|
| 1184 |
if (e.lastAttackedTime > e.usingMedkitStart){
|
| 1185 |
e.usingMedkit = false; e.usingMedkitSlot = -1; e.usingMedkitStart = 0; e.usingMedkitUntil = 0;
|
| 1186 |
} else if (now >= e.usingMedkitUntil){
|
| 1187 |
applyEnemyMedkitUseNow(e);
|
| 1188 |
} else {
|
|
|
|
| 1189 |
continue;
|
| 1190 |
}
|
| 1191 |
}
|
| 1192 |
|
| 1193 |
+
// STORM behavior
|
| 1194 |
if (storm.active){
|
| 1195 |
const distToSafeCenter = Math.hypot(e.x - storm.centerX, e.y - storm.centerY);
|
| 1196 |
if (distToSafeCenter > storm.radius){
|
|
|
|
| 1260 |
if (e.state === 'gather') e.gatherTimeLeft -= dt;
|
| 1261 |
|
| 1262 |
if (e.health < 60 && now >= (e.nextHealTime || 0) && !e.usingMedkit){
|
|
|
|
| 1263 |
if (enemyStartMedkitUse(e, now)){
|
| 1264 |
continue;
|
| 1265 |
}
|
|
|
|
| 1344 |
continue;
|
| 1345 |
}
|
| 1346 |
|
| 1347 |
+
// Combat logic ...
|
| 1348 |
if (e.equippedIndex === -1) enemyEquipBestWeapon(e);
|
| 1349 |
if (e.reloadPending){
|
| 1350 |
if (now >= e.reloadingUntil){
|
|
|
|
| 1517 |
}
|
| 1518 |
}
|
| 1519 |
|
| 1520 |
+
// Separation and clipping
|
| 1521 |
for (let i = 0; i < enemies.length; i++){
|
| 1522 |
const a = enemies[i];
|
| 1523 |
if (!a || a.health <= 0) continue;
|
|
|
|
| 1565 |
while (stormDamageAccumulator >= 1){
|
| 1566 |
stormDamageAccumulator -=1;
|
| 1567 |
player.health -= 1;
|
|
|
|
| 1568 |
if (player.health <= 0){ player.health = 0; playerDeath(); }
|
| 1569 |
}
|
| 1570 |
} else stormDamageAccumulator = 0;
|
|
|
|
| 1589 |
}
|
| 1590 |
}
|
| 1591 |
|
| 1592 |
+
// Drawing functions (kept)
|
|
|
|
| 1593 |
function drawWorld(){
|
| 1594 |
const TILE = 600;
|
| 1595 |
const cols = Math.ceil(WORLD.width / TILE);
|
|
|
|
| 1797 |
|
| 1798 |
function drawCrosshair(){}
|
| 1799 |
|
| 1800 |
+
// Minimap functions
|
| 1801 |
function buildMiniTerrainCache(){
|
| 1802 |
const mw = minimapCanvas.width;
|
| 1803 |
const mh = minimapCanvas.height;
|
|
|
|
| 1898 |
const dt = Math.min(0.05, (ts - lastTime)/1000);
|
| 1899 |
lastTime = ts;
|
| 1900 |
|
| 1901 |
+
// Movement from keys/joystick
|
| 1902 |
let dx=0, dy=0;
|
| 1903 |
if (keys.w) dy -= 1; if (keys.s) dy += 1; if (keys.a) dx -= 1; if (keys.d) dx += 1;
|
| 1904 |
if (dx !== 0 || dy !== 0){
|
|
|
|
| 1918 |
if (isCollidingSolid(player.x, player.y, player.radius)){
|
| 1919 |
player.y = oldY;
|
| 1920 |
}
|
| 1921 |
+
// Note: medkit use is instant now; moving does not cancel medkit
|
|
|
|
| 1922 |
}
|
| 1923 |
player.x = Math.max(16, Math.min(WORLD.width-16, player.x));
|
| 1924 |
player.y = Math.max(16, Math.min(WORLD.height-16, player.y));
|
|
|
|
| 1950 |
}
|
| 1951 |
|
| 1952 |
if (keys.r) { reloadEquipped(); keys.r = false; }
|
|
|
|
| 1953 |
if (keys.q){ tryBuild(); keys.q = false; }
|
| 1954 |
|
| 1955 |
updateEnemies(dt, performance.now());
|
|
|
|
| 2010 |
player.health = 100; player.armor = 0; player.kills = 0; player.materials = 0;
|
| 2011 |
player.inventory = [null,null,null,null,null];
|
| 2012 |
player.selectedSlot = 0; player.equippedIndex = -1; player.lastShot = 0; player.lastMelee = 0;
|
| 2013 |
+
player.lastMedkitUsed = 0;
|
| 2014 |
|
| 2015 |
populateWorld();
|
| 2016 |
initHUD();
|
|
|
|
| 2058 |
function endGame(){ gameActive = false; alert('Match over!'); }
|
| 2059 |
function playerDeath(){
|
| 2060 |
gameActive = false;
|
|
|
|
|
|
|
| 2061 |
deathScreen.classList.remove('hidden');
|
| 2062 |
}
|
| 2063 |
|
|
|
|
| 2131 |
});
|
| 2132 |
});
|
| 2133 |
|
| 2134 |
+
// Mobile controls wiring: movement joystick + aim joystick + HUD buttons
|
| 2135 |
(function setupMobileControls(){
|
| 2136 |
const joystick = document.getElementById('mobileJoystick');
|
| 2137 |
const thumb = document.getElementById('joystickThumb');
|
|
|
|
| 2140 |
const buildBtn = document.getElementById('buildBtn');
|
| 2141 |
const reloadBtn = document.getElementById('reloadBtn');
|
| 2142 |
|
| 2143 |
+
const aimJoystick = document.getElementById('mobileJoystickAim');
|
| 2144 |
+
const aimThumb = document.getElementById('joystickThumbAim');
|
| 2145 |
+
|
| 2146 |
+
if (!IS_MOBILE) {
|
| 2147 |
mobileControls.classList.add('hidden');
|
| 2148 |
if (mobileButtonsHud) mobileButtonsHud.classList.add('hidden');
|
| 2149 |
return;
|
|
|
|
| 2152 |
mobileControls.setAttribute('aria-hidden','false');
|
| 2153 |
if (mobileButtonsHud) { mobileButtonsHud.classList.remove('hidden'); mobileButtonsHud.setAttribute('aria-hidden','false'); }
|
| 2154 |
|
| 2155 |
+
// Movement joystick
|
| 2156 |
let joyTouchId = null;
|
| 2157 |
let joyCenter = null;
|
| 2158 |
const maxRadius = 52; // thumb allowed radius
|
|
|
|
| 2181 |
ny = dy / dist * maxRadius;
|
| 2182 |
}
|
| 2183 |
thumb.style.transform = `translate(${nx}px, ${ny}px)`;
|
|
|
|
| 2184 |
if (dist < deadzone){
|
| 2185 |
keys.w = keys.a = keys.s = keys.d = false;
|
| 2186 |
return;
|
| 2187 |
}
|
|
|
|
| 2188 |
keys.w = (dy < -10);
|
| 2189 |
keys.s = (dy > 10);
|
| 2190 |
keys.a = (dx < -10);
|
|
|
|
| 2197 |
resetJoystick();
|
| 2198 |
}
|
| 2199 |
|
|
|
|
| 2200 |
joystick.addEventListener('touchstart', (ev) => {
|
| 2201 |
for (const t of ev.changedTouches){
|
|
|
|
| 2202 |
if (joyTouchId === null){
|
| 2203 |
handleJoyStart(t);
|
| 2204 |
ev.preventDefault();
|
|
|
|
| 2245 |
if (shootTouchId === null){
|
| 2246 |
shootTouchId = t.identifier;
|
| 2247 |
mouse.down = true;
|
| 2248 |
+
// if aim joystick not used, aim forward in facing direction (so shooting has a direction)
|
| 2249 |
+
if (!aimActive) {
|
| 2250 |
+
const rect = canvas.getBoundingClientRect();
|
| 2251 |
+
mouse.canvasX = rect.width * 0.6;
|
| 2252 |
+
mouse.canvasY = rect.height * 0.5;
|
| 2253 |
+
mouse.worldX = mouse.canvasX + camera.x;
|
| 2254 |
+
mouse.worldY = mouse.canvasY + camera.y;
|
| 2255 |
+
}
|
| 2256 |
e.preventDefault();
|
| 2257 |
break;
|
| 2258 |
}
|
|
|
|
| 2309 |
[shootBtn, interactBtn, buildBtn, reloadBtn].forEach(b => {
|
| 2310 |
b.addEventListener('touchmove', (ev)=> ev.preventDefault(), { passive:false });
|
| 2311 |
});
|
| 2312 |
+
|
| 2313 |
+
// Aim joystick (right)
|
| 2314 |
+
let aimTouchId = null;
|
| 2315 |
+
let aimCenter = null;
|
| 2316 |
+
const aimMaxRadius = 44; // smaller control
|
| 2317 |
+
let aimActive = false;
|
| 2318 |
+
|
| 2319 |
+
function resetAim(){
|
| 2320 |
+
aimThumb.style.transform = `translate(0px,0px)`;
|
| 2321 |
+
aimActive = false;
|
| 2322 |
+
}
|
| 2323 |
+
|
| 2324 |
+
function aimStart(t){
|
| 2325 |
+
const rect = aimJoystick.getBoundingClientRect();
|
| 2326 |
+
aimCenter = { x: rect.left + rect.width/2, y: rect.top + rect.height/2 };
|
| 2327 |
+
aimTouchId = t.identifier;
|
| 2328 |
+
aimActive = true;
|
| 2329 |
+
updateAim(t.clientX, t.clientY);
|
| 2330 |
+
}
|
| 2331 |
+
|
| 2332 |
+
function updateAim(clientX, clientY){
|
| 2333 |
+
if (!aimCenter) return;
|
| 2334 |
+
let dx = clientX - aimCenter.x;
|
| 2335 |
+
let dy = clientY - aimCenter.y;
|
| 2336 |
+
const dist = Math.hypot(dx, dy);
|
| 2337 |
+
let nx = dx, ny = dy;
|
| 2338 |
+
if (dist > aimMaxRadius){
|
| 2339 |
+
nx = dx / dist * aimMaxRadius;
|
| 2340 |
+
ny = dy / dist * aimMaxRadius;
|
| 2341 |
+
}
|
| 2342 |
+
aimThumb.style.transform = `translate(${nx}px, ${ny}px)`;
|
| 2343 |
+
|
| 2344 |
+
// compute angle and set player's aim
|
| 2345 |
+
if (Math.hypot(nx, ny) < 6) return;
|
| 2346 |
+
const angle = Math.atan2(ny, nx); // aim direction relative to screen; convert to world
|
| 2347 |
+
// Map aim direction to a point in world in front of player (distance scaled)
|
| 2348 |
+
const aimDistance = 600; // how far the aim projects
|
| 2349 |
+
const targetWorldX = player.x + Math.cos(angle) * aimDistance;
|
| 2350 |
+
const targetWorldY = player.y + Math.sin(angle) * aimDistance;
|
| 2351 |
+
|
| 2352 |
+
mouse.worldX = targetWorldX;
|
| 2353 |
+
mouse.worldY = targetWorldY;
|
| 2354 |
+
// update canvas coords based on camera
|
| 2355 |
+
mouse.canvasX = mouse.worldX - camera.x;
|
| 2356 |
+
mouse.canvasY = mouse.worldY - camera.y;
|
| 2357 |
+
player.angle = Math.atan2(mouse.worldY - player.y, mouse.worldX - player.x);
|
| 2358 |
+
}
|
| 2359 |
+
|
| 2360 |
+
function aimEnd(){
|
| 2361 |
+
aimTouchId = null;
|
| 2362 |
+
aimCenter = null;
|
| 2363 |
+
resetAim();
|
| 2364 |
+
aimActive = false;
|
| 2365 |
+
}
|
| 2366 |
+
|
| 2367 |
+
aimJoystick.addEventListener('touchstart', (ev) => {
|
| 2368 |
+
for (const t of ev.changedTouches){
|
| 2369 |
+
if (aimTouchId === null){
|
| 2370 |
+
aimStart(t);
|
| 2371 |
+
ev.preventDefault();
|
| 2372 |
+
break;
|
| 2373 |
+
}
|
| 2374 |
+
}
|
| 2375 |
+
}, { passive:false });
|
| 2376 |
+
|
| 2377 |
+
window.addEventListener('touchmove', (ev) => {
|
| 2378 |
+
for (const t of ev.changedTouches){
|
| 2379 |
+
if (t.identifier === aimTouchId){
|
| 2380 |
+
updateAim(t.clientX, t.clientY);
|
| 2381 |
+
ev.preventDefault();
|
| 2382 |
+
break;
|
| 2383 |
+
}
|
| 2384 |
+
}
|
| 2385 |
+
}, { passive:false });
|
| 2386 |
+
|
| 2387 |
+
window.addEventListener('touchend', (ev) => {
|
| 2388 |
+
for (const t of ev.changedTouches){
|
| 2389 |
+
if (t.identifier === aimTouchId){
|
| 2390 |
+
aimEnd();
|
| 2391 |
+
ev.preventDefault();
|
| 2392 |
+
break;
|
| 2393 |
+
}
|
| 2394 |
+
}
|
| 2395 |
+
});
|
| 2396 |
+
|
| 2397 |
+
window.addEventListener('touchcancel', (ev) => {
|
| 2398 |
+
for (const t of ev.changedTouches){
|
| 2399 |
+
if (t.identifier === aimTouchId){
|
| 2400 |
+
aimEnd();
|
| 2401 |
+
ev.preventDefault();
|
| 2402 |
+
break;
|
| 2403 |
+
}
|
| 2404 |
+
}
|
| 2405 |
+
});
|
| 2406 |
})();
|
| 2407 |
|
| 2408 |
// Initialize UI state
|