1742 lines
54 KiB
JavaScript
1742 lines
54 KiB
JavaScript
// Colors
|
|
const localColor = "#109856"
|
|
const teamColor = "#68a3e5"
|
|
const enemyColor = "#ec040b"
|
|
const bombColor = "#eda338"
|
|
const textColor = "#d1d1d1"
|
|
|
|
const DEFAULT_TEXT_SIZE = 1.2;
|
|
const DEFAULT_ENTITY_SIZE = 1.5;
|
|
const DEFAULT_ZOOM_LEVEL = 1.3;
|
|
const DEFAULT_HEALTH_BAR_SIZE = 1.0;
|
|
|
|
// Settings
|
|
let shouldZoom = false;
|
|
let rotateMap = true;
|
|
let playerCentered = true;
|
|
let showOffscreenIndicators = true;
|
|
|
|
let drawHealth = true;
|
|
let drawStats = true;
|
|
let drawNames = true;
|
|
let drawGuns = true;
|
|
let drawMoney = true;
|
|
|
|
let canvasScale = 1;
|
|
let minTextSize = 16;
|
|
let minEntitySize = 10;
|
|
let textSizeMultiplier = 1.0;
|
|
let entitySizeMultiplier = 1.0;
|
|
let healthBarSizeMultiplier = 1.0;
|
|
let playerCenteredZoom = 1.0;
|
|
|
|
const NETWORK_SETTINGS = {
|
|
useInterpolation: true,
|
|
interpolationAmount: 0.6,
|
|
pingInterval: 3000,
|
|
maxRetries: 5,
|
|
requestTimeout: 5000,
|
|
reconnectDelay: 1000
|
|
};
|
|
|
|
let connectionHealthy = true;
|
|
let lastResponseTime = 0;
|
|
let requestTimeoutTimer = null;
|
|
let reconnecting = false;
|
|
let retryCount = 0;
|
|
|
|
let isRequestPending = false;
|
|
let frameCounter = 0;
|
|
let fpsStartTime = 0;
|
|
let currentFps = 0;
|
|
|
|
let temporarilyDisableRotation = false;
|
|
let rotationDisabledUntilRespawn = false;
|
|
let lastKnownPositions = {};
|
|
let deadPlayers = {};
|
|
let entityInterpolationData = {};
|
|
let lastUpdateTime = 0;
|
|
let networkLatencyHistory = [];
|
|
let lastPingSent = 0;
|
|
|
|
// Special values for spectator mode
|
|
const FULL_MAP_VIEW = "FULL_MAP";
|
|
const LOCAL_PLAYER_VIEW = "YOU";
|
|
|
|
let focusedPlayerYaw = 0;
|
|
let focusedPlayerName = localStorage.getItem('selectedPlayer') || LOCAL_PLAYER_VIEW;
|
|
let focusedPlayerPos = null;
|
|
let playerList = {};
|
|
let lastFocusedPlayer = localStorage.getItem('lastFocusedPlayer') || null;
|
|
|
|
// Common
|
|
let canvas = null;
|
|
let ctx = null;
|
|
|
|
// radarflow specific
|
|
let radarData = null;
|
|
let freq = 0;
|
|
let image = null;
|
|
let map = null;
|
|
let mapName = null;
|
|
let loaded = false;
|
|
let entityData = null;
|
|
let update = false;
|
|
let localYaw = 0;
|
|
let localPlayerPos = null;
|
|
|
|
/// Radarflow zoom in
|
|
let zoomSet = false;
|
|
let safetyBound = 50;
|
|
let boundingRect = null;
|
|
|
|
// Weapon IDs
|
|
const weaponIdMap = {
|
|
1: "DEAGLE", 2: "DUALIES", 3: "FIVE-SEVEN", 4: "GLOCK", 7: "AK-47",
|
|
8: "AUG", 9: "AWP", 10: "FAMAS", 11: "G3SG1", 13: "GALIL", 14: "M249",
|
|
16: "M4A4", 17: "MAC-10", 19: "P90", 23: "MP5", 24: "UMP", 25: "XM1014",
|
|
26: "BIZON", 27: "MAG-7", 28: "NEGEV", 29: "SAWED-OFF", 30: "TEC-9",
|
|
31: "ZEUS", 32: "P2000", 33: "MP7", 34: "MP9", 35: "NOVA", 36: "P250",
|
|
38: "SCAR-20", 39: "SG 553", 40: "SCOUT", 60: "M4A1-S", 61: "USP-S",
|
|
63: "CZ75", 64: "REVOLVER", 43: "FLASH", 44: "HE", 45: "SMOKE", 46: "MOLOTOV",
|
|
47: "DECOY", 48: "INCENDIARY", 49: "C4", 0: "KNIFE"
|
|
};
|
|
|
|
// Networking
|
|
let websocket = null;
|
|
const websocketAddr = location.protocol === 'https:'
|
|
? `wss://${window.location.host}/ws`
|
|
: `ws://${window.location.host}/ws`;
|
|
|
|
// Util functions
|
|
const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
|
|
const degreesToRadians = (degrees) => degrees * (Math.PI / 180);
|
|
const lerp = (start, end, t) => start * (1 - t) + end * t;
|
|
|
|
function lerpPosition(pos1, pos2, t) {
|
|
if (!pos1 || !pos2) return pos2 || pos1 || null;
|
|
return {
|
|
x: lerp(pos1.x, pos2.x, t),
|
|
y: lerp(pos1.y, pos2.y, t),
|
|
z: lerp(pos1.z, pos2.z, t)
|
|
};
|
|
}
|
|
|
|
function lerpAngle(a, b, t) {
|
|
while (a > 360) a -= 360;
|
|
while (a < 0) a += 360;
|
|
while (b > 360) b -= 360;
|
|
while (b < 0) b += 360;
|
|
|
|
let diff = b - a;
|
|
if (diff > 180) diff -= 360;
|
|
if (diff < -180) diff += 360;
|
|
|
|
return a + diff * t;
|
|
}
|
|
|
|
const pingTracker = {
|
|
history: [],
|
|
lastRequestTime: 0,
|
|
maxSamples: 10,
|
|
|
|
startRequest: function () {
|
|
this.lastRequestTime = performance.now();
|
|
},
|
|
|
|
endRequest: function () {
|
|
if (this.lastRequestTime === 0) return;
|
|
|
|
const ping = performance.now() - this.lastRequestTime;
|
|
this.history.push(ping);
|
|
|
|
if (this.history.length > this.maxSamples) {
|
|
this.history.shift();
|
|
}
|
|
|
|
this.lastRequestTime = 0;
|
|
},
|
|
|
|
getAveragePing: function () {
|
|
if (this.history.length === 0) return 0;
|
|
const sum = this.history.reduce((a, b) => a + b, 0);
|
|
return sum / this.history.length;
|
|
}
|
|
};
|
|
|
|
function updateEntityInterpolation(entityId, newData) {
|
|
const now = performance.now();
|
|
|
|
if (!entityInterpolationData[entityId]) {
|
|
entityInterpolationData[entityId] = {
|
|
current: JSON.parse(JSON.stringify(newData)),
|
|
target: JSON.parse(JSON.stringify(newData)),
|
|
lastUpdateTime: now
|
|
};
|
|
return entityInterpolationData[entityId].current;
|
|
}
|
|
|
|
entityInterpolationData[entityId].current = JSON.parse(JSON.stringify(entityInterpolationData[entityId].target));
|
|
entityInterpolationData[entityId].target = JSON.parse(JSON.stringify(newData));
|
|
entityInterpolationData[entityId].lastUpdateTime = now;
|
|
|
|
return entityInterpolationData[entityId].current;
|
|
}
|
|
|
|
function getInterpolatedEntityData(entityId) {
|
|
if (!NETWORK_SETTINGS.useInterpolation || !entityInterpolationData[entityId]) {
|
|
return null;
|
|
}
|
|
|
|
const data = entityInterpolationData[entityId];
|
|
const now = performance.now();
|
|
const elapsed = now - data.lastUpdateTime;
|
|
|
|
const pingTime = pingTracker.getAveragePing();
|
|
const targetDuration = Math.min(200, Math.max(50, pingTime * 0.8));
|
|
const t = Math.min(1, elapsed / targetDuration);
|
|
const easedT = t * (2 - t);
|
|
|
|
const result = JSON.parse(JSON.stringify(data.current));
|
|
|
|
if (result.Player) {
|
|
if (data.current.Player && data.target.Player) {
|
|
if (data.current.Player.pos && data.target.Player.pos) {
|
|
result.Player.pos = lerpPosition(
|
|
data.current.Player.pos,
|
|
data.target.Player.pos,
|
|
easedT * NETWORK_SETTINGS.interpolationAmount
|
|
);
|
|
}
|
|
|
|
if (data.current.Player.yaw !== undefined && data.target.Player.yaw !== undefined) {
|
|
result.Player.yaw = lerpAngle(
|
|
data.current.Player.yaw,
|
|
data.target.Player.yaw,
|
|
easedT * NETWORK_SETTINGS.interpolationAmount
|
|
);
|
|
}
|
|
}
|
|
} else if (result.Bomb) {
|
|
if (data.current.Bomb && data.target.Bomb) {
|
|
if (data.current.Bomb.pos && data.target.Bomb.pos) {
|
|
result.Bomb.pos = lerpPosition(
|
|
data.current.Bomb.pos,
|
|
data.target.Bomb.pos,
|
|
easedT * NETWORK_SETTINGS.interpolationAmount
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function render() {
|
|
requestAnimationFrame(render);
|
|
|
|
const now = performance.now();
|
|
if (!fpsStartTime) fpsStartTime = now;
|
|
frameCounter++;
|
|
|
|
if (now - fpsStartTime > 1000) {
|
|
currentFps = Math.round(frameCounter * 1000 / (now - fpsStartTime));
|
|
frameCounter = 0;
|
|
fpsStartTime = now;
|
|
}
|
|
|
|
if (!isRequestPending && websocket && websocket.readyState === WebSocket.OPEN) {
|
|
isRequestPending = true;
|
|
pingTracker.startRequest();
|
|
websocket.send("requestInfo");
|
|
}
|
|
|
|
renderFrame();
|
|
}
|
|
|
|
function sendRequest() {
|
|
isRequestPending = true;
|
|
pingTracker.startRequest();
|
|
|
|
clearTimeout(requestTimeoutTimer);
|
|
requestTimeoutTimer = setTimeout(() => {
|
|
if (isRequestPending) {
|
|
console.warn("[radarflow] Request timeout, retrying...");
|
|
isRequestPending = false;
|
|
|
|
if (retryCount < NETWORK_SETTINGS.maxRetries) {
|
|
retryCount++;
|
|
sendRequest();
|
|
} else {
|
|
retryCount = 0;
|
|
console.error("[radarflow] Maximum retries reached, reconnecting...");
|
|
reconnecting = true;
|
|
if (websocket) {
|
|
try {
|
|
websocket.close();
|
|
} catch (e) {
|
|
}
|
|
websocket = null;
|
|
}
|
|
setTimeout(connect, NETWORK_SETTINGS.reconnectDelay);
|
|
}
|
|
}
|
|
}, NETWORK_SETTINGS.requestTimeout);
|
|
|
|
websocket.send("requestInfo");
|
|
}
|
|
|
|
function renderFrame() {
|
|
fillCanvas();
|
|
|
|
if (entityData && loaded && map && image) {
|
|
processPlayerPositions();
|
|
|
|
if (update) {
|
|
updatePlayerDropdown();
|
|
update = false;
|
|
}
|
|
|
|
drawImage();
|
|
|
|
drawEntities();
|
|
|
|
drawBombTimer();
|
|
} else if (!loaded) {
|
|
const fontSize = Math.max(40 * canvasScale, 16);
|
|
ctx.font = `${fontSize}px Arial`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillStyle = textColor;
|
|
ctx.fillText(websocket ? "Not on server" : "Disconnected", canvas.width / 2, canvas.height / 2);
|
|
}
|
|
|
|
if (drawStats) {
|
|
const fontSize = Math.max(16 * canvasScale, 12);
|
|
ctx.font = `${fontSize}px Arial`;
|
|
ctx.textAlign = "left";
|
|
ctx.fillStyle = "#00FF00";
|
|
let rotationStatus = "Active";
|
|
if (temporarilyDisableRotation) rotationStatus = "Manually Disabled";
|
|
else if (rotationDisabledUntilRespawn) rotationStatus = "Disabled (Death)";
|
|
|
|
ctx.fillText(`${currentFps} FPS | ${freq} Hz | Ping: ${Math.round(pingTracker.getAveragePing())}ms | Rotation: ${rotationStatus}`, 10, fontSize + 4);
|
|
}
|
|
}
|
|
|
|
function processPlayerPositions() {
|
|
if (!entityData) return;
|
|
|
|
localPlayerPos = null;
|
|
focusedPlayerPos = null;
|
|
focusedPlayerYaw = 0;
|
|
let oldPlayerList = { ...playerList };
|
|
let newPlayerList = {};
|
|
let wasPlayerAlive = oldPlayerList[focusedPlayerName] && !oldPlayerList[focusedPlayerName].isDead;
|
|
|
|
entityData.forEach((data, index) => {
|
|
const entityId = `entity_${index}`;
|
|
|
|
if (data.Player) {
|
|
const player = data.Player;
|
|
if (NETWORK_SETTINGS.useInterpolation) {
|
|
updateEntityInterpolation(entityId, data);
|
|
}
|
|
|
|
if (player.playerType === "Local") {
|
|
localYaw = player.yaw;
|
|
localPlayerPos = player.pos;
|
|
newPlayerList[LOCAL_PLAYER_VIEW] = {
|
|
pos: player.pos,
|
|
yaw: player.yaw
|
|
};
|
|
|
|
lastKnownPositions[LOCAL_PLAYER_VIEW] = player.pos;
|
|
if (deadPlayers[LOCAL_PLAYER_VIEW]) delete deadPlayers[LOCAL_PLAYER_VIEW];
|
|
} else {
|
|
newPlayerList[player.playerName] = {
|
|
pos: player.pos,
|
|
yaw: player.yaw
|
|
};
|
|
|
|
lastKnownPositions[player.playerName] = player.pos;
|
|
if (deadPlayers[player.playerName]) delete deadPlayers[player.playerName];
|
|
}
|
|
|
|
if (player.playerName === focusedPlayerName ||
|
|
(focusedPlayerName === LOCAL_PLAYER_VIEW && player.playerType === "Local")) {
|
|
focusedPlayerPos = player.pos;
|
|
focusedPlayerYaw = player.yaw;
|
|
|
|
if (rotationDisabledUntilRespawn) {
|
|
console.log("[radarflow] Player respawned, re-enabling rotation");
|
|
rotationDisabledUntilRespawn = false;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
for (const playerName in oldPlayerList) {
|
|
if (!newPlayerList[playerName] && playerName !== FULL_MAP_VIEW) {
|
|
deadPlayers[playerName] = {
|
|
pos: lastKnownPositions[playerName] || null,
|
|
yaw: oldPlayerList[playerName].yaw || 0,
|
|
lastSeen: Date.now()
|
|
};
|
|
|
|
if (playerName === focusedPlayerName && wasPlayerAlive) {
|
|
console.log(`[radarflow] Focused player ${playerName} died, switching to full map view`);
|
|
localStorage.setItem('lastFocusedPlayer', playerName);
|
|
lastFocusedPlayer = playerName;
|
|
focusedPlayerName = FULL_MAP_VIEW;
|
|
localStorage.setItem('selectedPlayer', FULL_MAP_VIEW);
|
|
playerCentered = false;
|
|
update = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const playerName in deadPlayers) {
|
|
if (!newPlayerList[playerName] && playerName !== FULL_MAP_VIEW) {
|
|
if (Date.now() - deadPlayers[playerName].lastSeen < 60000) {
|
|
newPlayerList[playerName] = {
|
|
pos: deadPlayers[playerName].pos,
|
|
yaw: deadPlayers[playerName].yaw,
|
|
isDead: true
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
newPlayerList[FULL_MAP_VIEW] = {
|
|
isFullMap: true
|
|
};
|
|
|
|
playerList = newPlayerList;
|
|
|
|
if (focusedPlayerName === FULL_MAP_VIEW && lastFocusedPlayer) {
|
|
const playerIsBackAlive = newPlayerList[lastFocusedPlayer] && !newPlayerList[lastFocusedPlayer].isDead;
|
|
|
|
if (playerIsBackAlive) {
|
|
console.log(`[radarflow] Last focused player ${lastFocusedPlayer} is back alive, reselecting them`);
|
|
focusedPlayerName = lastFocusedPlayer;
|
|
localStorage.setItem('selectedPlayer', lastFocusedPlayer);
|
|
|
|
if (document.getElementById('centerCheck').checked) {
|
|
playerCentered = true;
|
|
}
|
|
|
|
focusedPlayerPos = newPlayerList[lastFocusedPlayer].pos;
|
|
focusedPlayerYaw = newPlayerList[lastFocusedPlayer].yaw;
|
|
|
|
rotationDisabledUntilRespawn = false;
|
|
|
|
const dropdown = document.getElementById('playerSelect');
|
|
if (dropdown) {
|
|
dropdown.value = lastFocusedPlayer;
|
|
}
|
|
|
|
update = true;
|
|
}
|
|
}
|
|
|
|
// Handle focused player state
|
|
if (focusedPlayerName === FULL_MAP_VIEW) {
|
|
playerCentered = false;
|
|
} else if (focusedPlayerPos === null) {
|
|
if (playerList[focusedPlayerName]?.isDead) {
|
|
console.log(`[radarflow] Focused player ${focusedPlayerName} is dead, switching to full map view`);
|
|
localStorage.setItem('lastFocusedPlayer', focusedPlayerName);
|
|
lastFocusedPlayer = focusedPlayerName;
|
|
focusedPlayerName = FULL_MAP_VIEW;
|
|
localStorage.setItem('selectedPlayer', FULL_MAP_VIEW);
|
|
playerCentered = false;
|
|
update = true;
|
|
}
|
|
else if (lastKnownPositions[focusedPlayerName]) {
|
|
console.log("[radarflow] Focused player disappeared, switching to full map view");
|
|
localStorage.setItem('lastFocusedPlayer', focusedPlayerName);
|
|
lastFocusedPlayer = focusedPlayerName;
|
|
focusedPlayerName = FULL_MAP_VIEW;
|
|
localStorage.setItem('selectedPlayer', FULL_MAP_VIEW);
|
|
playerCentered = false;
|
|
update = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
function drawImage() {
|
|
if (!image || !canvas || !map) return;
|
|
|
|
ctx.save();
|
|
|
|
if (focusedPlayerName === FULL_MAP_VIEW || !playerCentered) {
|
|
if (rotateMap && !temporarilyDisableRotation) {
|
|
ctx.translate(canvas.width / 2, canvas.height / 2);
|
|
ctx.translate(-canvas.width / 2, -canvas.height / 2);
|
|
}
|
|
} else if (playerCentered && focusedPlayerPos) {
|
|
if (playerCenteredZoom !== 1.0) {
|
|
ctx.translate(canvas.width / 2, canvas.height / 2);
|
|
ctx.scale(playerCenteredZoom, playerCenteredZoom);
|
|
ctx.translate(-canvas.width / 2, -canvas.height / 2);
|
|
}
|
|
|
|
if (rotateMap &&
|
|
focusedPlayerPos &&
|
|
!temporarilyDisableRotation &&
|
|
!rotationDisabledUntilRespawn) {
|
|
|
|
ctx.translate(canvas.width / 2, canvas.height / 2);
|
|
ctx.rotate(degreesToRadians(focusedPlayerYaw + 270));
|
|
ctx.translate(-canvas.width / 2, -canvas.height / 2);
|
|
}
|
|
|
|
const playerX = (focusedPlayerPos.x - map.pos_x) / map.scale;
|
|
const playerY = (focusedPlayerPos.y - map.pos_y) / -map.scale;
|
|
|
|
const playerCanvasX = (playerX / image.width) * canvas.width;
|
|
const playerCanvasY = (playerY / image.height) * canvas.height;
|
|
|
|
const translateX = (canvas.width / 2) - playerCanvasX;
|
|
const translateY = (canvas.height / 2) - playerCanvasY;
|
|
|
|
ctx.translate(translateX, translateY);
|
|
} else if (rotateMap &&
|
|
focusedPlayerPos &&
|
|
!temporarilyDisableRotation &&
|
|
!rotationDisabledUntilRespawn) {
|
|
ctx.translate(canvas.width / 2, canvas.height / 2);
|
|
ctx.rotate(degreesToRadians(focusedPlayerYaw + 270));
|
|
ctx.translate(-canvas.width / 2, -canvas.height / 2);
|
|
}
|
|
|
|
ctx.drawImage(
|
|
image,
|
|
0, 0, image.width, image.height,
|
|
0, 0, canvas.width, canvas.height
|
|
);
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
function toggleHealth() {
|
|
drawHealth = !drawHealth;
|
|
update = true;
|
|
localStorage.setItem('drawHealth', drawHealth ? 'true' : 'false');
|
|
}
|
|
|
|
function mapAndTransformCoordinates(pos) {
|
|
const canvasWidth = canvas.width;
|
|
const canvasHeight = canvas.height;
|
|
const imageWidth = image ? image.width : 1;
|
|
const imageHeight = image ? image.height : 1;
|
|
|
|
if (!map || !pos) return {
|
|
pos: { x: 0, y: 0 },
|
|
textSize: minTextSize * textSizeMultiplier
|
|
};
|
|
|
|
const posX = (pos.x - map.pos_x) / map.scale;
|
|
const posY = (pos.y - map.pos_y) / -map.scale;
|
|
|
|
let screenX = (posX / imageWidth) * canvasWidth;
|
|
let screenY = (posY / imageHeight) * canvasHeight;
|
|
|
|
if (playerCentered && focusedPlayerPos) {
|
|
const playerX = (focusedPlayerPos.x - map.pos_x) / map.scale;
|
|
const playerY = (focusedPlayerPos.y - map.pos_y) / -map.scale;
|
|
|
|
const playerRelX = playerX / imageWidth;
|
|
const playerRelY = playerY / imageHeight;
|
|
|
|
const centerX = canvasWidth / 2;
|
|
const centerY = canvasHeight / 2;
|
|
|
|
const playerScreenX = (playerX / imageWidth) * canvasWidth;
|
|
const playerScreenY = (playerY / imageHeight) * canvasHeight;
|
|
|
|
const deltaX = screenX - playerScreenX;
|
|
const deltaY = screenY - playerScreenY;
|
|
|
|
const zoomedDeltaX = deltaX * playerCenteredZoom;
|
|
const zoomedDeltaY = deltaY * playerCenteredZoom;
|
|
|
|
screenX = centerX + zoomedDeltaX;
|
|
screenY = centerY + zoomedDeltaY;
|
|
|
|
if (rotateMap &&
|
|
!temporarilyDisableRotation &&
|
|
!rotationDisabledUntilRespawn) {
|
|
|
|
const relX = screenX - centerX;
|
|
const relY = screenY - centerY;
|
|
|
|
const angle = degreesToRadians(focusedPlayerYaw + 270);
|
|
const cos = Math.cos(angle);
|
|
const sin = Math.sin(angle);
|
|
|
|
const rotX = relX * cos - relY * sin;
|
|
const rotY = relX * sin + relY * cos;
|
|
|
|
screenX = rotX + centerX;
|
|
screenY = rotY + centerY;
|
|
}
|
|
} else if (rotateMap &&
|
|
focusedPlayerPos &&
|
|
!temporarilyDisableRotation &&
|
|
!rotationDisabledUntilRespawn) {
|
|
|
|
const centerX = canvasWidth / 2;
|
|
const centerY = canvasHeight / 2;
|
|
|
|
const relX = screenX - centerX;
|
|
const relY = screenY - centerY;
|
|
|
|
const angle = degreesToRadians(focusedPlayerYaw + 270);
|
|
const cos = Math.cos(angle);
|
|
const sin = Math.sin(angle);
|
|
|
|
const rotX = relX * cos - relY * sin;
|
|
const rotY = relX * sin + relY * cos;
|
|
|
|
screenX = rotX + centerX;
|
|
screenY = rotY + centerY;
|
|
}
|
|
|
|
const finalTextSize = playerCentered ?
|
|
minTextSize * textSizeMultiplier * playerCenteredZoom :
|
|
minTextSize * textSizeMultiplier;
|
|
|
|
return {
|
|
pos: { x: screenX, y: screenY },
|
|
textSize: finalTextSize
|
|
};
|
|
}
|
|
|
|
function updateZoomLevel(value) {
|
|
playerCenteredZoom = parseFloat(value);
|
|
const valueDisplay = document.getElementById('zoomLevelValue');
|
|
if (valueDisplay) valueDisplay.textContent = value;
|
|
localStorage.setItem('playerCenteredZoom', value);
|
|
}
|
|
|
|
function toggleCentered() {
|
|
playerCentered = !playerCentered;
|
|
updateZoomSliderVisibility();
|
|
if (focusedPlayerName === FULL_MAP_VIEW) {
|
|
playerCentered = false;
|
|
document.getElementById('centerCheck').checked = false;
|
|
}
|
|
}
|
|
|
|
function updateZoomSliderVisibility() {
|
|
const zoomSliderContainer = document.getElementById('zoomLevelContainer');
|
|
if (zoomSliderContainer) {
|
|
zoomSliderContainer.style.display = playerCentered ? 'block' : 'none';
|
|
}
|
|
}
|
|
|
|
function toggleOffscreenIndicators() {
|
|
showOffscreenIndicators = !showOffscreenIndicators;
|
|
localStorage.setItem('showOffscreenIndicators', showOffscreenIndicators ? 'true' : 'false');
|
|
}
|
|
|
|
function drawPlayerHealth(pos, playerType, health, hasBomb) {
|
|
if (!map) return;
|
|
|
|
const transformed = mapAndTransformCoordinates(pos);
|
|
const mapPos = transformed.pos;
|
|
const textSize = transformed.textSize;
|
|
|
|
let extraOffset = 0;
|
|
if (drawNames) extraOffset += 15;
|
|
if (drawGuns) extraOffset += 15;
|
|
if (hasBomb) extraOffset += 15;
|
|
if (drawMoney) extraOffset += 15;
|
|
|
|
let textY = mapPos.y + 20 + extraOffset;
|
|
|
|
let healthColor;
|
|
if (health > 70) {
|
|
healthColor = "#32CD32";
|
|
} else if (health > 30) {
|
|
healthColor = "#FFFF00";
|
|
} else {
|
|
healthColor = "#FF0000";
|
|
}
|
|
|
|
const barWidth = Math.max(60, 40 * textSizeMultiplier) * healthBarSizeMultiplier;
|
|
const barHeight = Math.max(8, 5 * textSizeMultiplier) * healthBarSizeMultiplier;
|
|
|
|
ctx.fillStyle = "#444444";
|
|
ctx.fillRect(mapPos.x - barWidth / 2, textY, barWidth, barHeight);
|
|
|
|
ctx.fillStyle = healthColor;
|
|
const healthWidth = (health / 100) * barWidth;
|
|
ctx.fillRect(mapPos.x - barWidth / 2, textY, healthWidth, barHeight);
|
|
|
|
ctx.font = `bold ${textSize}px Arial`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "top";
|
|
|
|
ctx.lineWidth = 3;
|
|
ctx.strokeStyle = "black";
|
|
ctx.strokeText(`${health}HP`, mapPos.x, textY + barHeight + 2);
|
|
ctx.fillStyle = healthColor;
|
|
ctx.fillText(`${health}HP`, mapPos.x, textY + barHeight + 2);
|
|
}
|
|
|
|
function drawEntities() {
|
|
if (!entityData) return;
|
|
|
|
const clipRect = {
|
|
x: -50,
|
|
y: -50,
|
|
width: canvas.width + 100,
|
|
height: canvas.height + 100
|
|
};
|
|
|
|
const offscreenEnemies = [];
|
|
|
|
entityData.forEach((entity, index) => {
|
|
const entityId = `entity_${index}`;
|
|
let interpolatedEntity = null;
|
|
|
|
if (NETWORK_SETTINGS.useInterpolation) {
|
|
interpolatedEntity = getInterpolatedEntityData(entityId);
|
|
}
|
|
|
|
const renderEntity = interpolatedEntity || entity;
|
|
|
|
if (!renderEntity) return;
|
|
|
|
let pos;
|
|
if (renderEntity.Bomb) {
|
|
pos = renderEntity.Bomb.pos;
|
|
} else if (renderEntity.Player) {
|
|
pos = renderEntity.Player.pos;
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
if (!pos) return;
|
|
|
|
const transformed = mapAndTransformCoordinates(pos);
|
|
const mapPos = transformed.pos;
|
|
|
|
const isVisible = mapPos.x >= clipRect.x &&
|
|
mapPos.x <= clipRect.x + clipRect.width &&
|
|
mapPos.y >= clipRect.y &&
|
|
mapPos.y <= clipRect.y + clipRect.height;
|
|
|
|
if (!isVisible && renderEntity.Player &&
|
|
renderEntity.Player.playerType === "Enemy" &&
|
|
playerCentered && showOffscreenIndicators) {
|
|
|
|
offscreenEnemies.push({
|
|
pos: mapPos,
|
|
originalPos: pos,
|
|
player: renderEntity.Player
|
|
});
|
|
}
|
|
|
|
if (isVisible) {
|
|
if (renderEntity.Bomb) {
|
|
drawBomb(renderEntity.Bomb.pos, renderEntity.Bomb.isPlanted);
|
|
} else if (renderEntity.Player) {
|
|
const player = renderEntity.Player;
|
|
let fillStyle = localColor;
|
|
|
|
switch (player.playerType) {
|
|
case "Team": fillStyle = teamColor; break;
|
|
case "Enemy": fillStyle = enemyColor; break;
|
|
}
|
|
|
|
drawEntity(
|
|
player.pos,
|
|
fillStyle,
|
|
player.isDormant,
|
|
player.hasBomb,
|
|
player.yaw,
|
|
player.hasAwp,
|
|
player.playerType,
|
|
player.isScoped,
|
|
player.playerName,
|
|
false,
|
|
player.weaponId
|
|
);
|
|
|
|
if (!player.isDormant) {
|
|
if (drawNames) {
|
|
drawPlayerName(
|
|
player.pos,
|
|
player.playerName,
|
|
player.playerType,
|
|
player.hasAwp,
|
|
player.hasBomb,
|
|
player.isScoped
|
|
);
|
|
}
|
|
|
|
if (drawGuns) {
|
|
drawPlayerWeapon(
|
|
player.pos,
|
|
player.playerType,
|
|
player.weaponId
|
|
);
|
|
}
|
|
|
|
if (player.hasBomb) {
|
|
drawPlayerBomb(
|
|
player.pos,
|
|
player.playerType
|
|
);
|
|
}
|
|
|
|
if (drawMoney && typeof player.money === 'number') {
|
|
drawPlayerMoney(
|
|
player.pos,
|
|
player.playerType,
|
|
player.money,
|
|
player.hasBomb
|
|
);
|
|
}
|
|
|
|
if (drawHealth && typeof player.health === 'number') {
|
|
drawPlayerHealth(
|
|
player.pos,
|
|
player.playerType,
|
|
player.health,
|
|
player.hasBomb
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
if (playerCentered && showOffscreenIndicators) {
|
|
drawOffscreenIndicators(offscreenEnemies);
|
|
}
|
|
}
|
|
|
|
function drawOffscreenIndicators(offscreenEnemies) {
|
|
if (!offscreenEnemies.length) return;
|
|
|
|
const centerX = canvas.width / 2;
|
|
const centerY = canvas.height / 2;
|
|
const padding = 40;
|
|
|
|
offscreenEnemies.forEach(enemy => {
|
|
const dx = enemy.pos.x - centerX;
|
|
const dy = enemy.pos.y - centerY;
|
|
let angle = Math.atan2(dy, dx);
|
|
|
|
const indicatorRadius = Math.min(canvas.width, canvas.height) / 2 - padding;
|
|
let indicatorX = centerX + Math.cos(angle) * indicatorRadius;
|
|
let indicatorY = centerY + Math.sin(angle) * indicatorRadius;
|
|
|
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
const maxDistance = Math.sqrt(canvas.width * canvas.width + canvas.height * canvas.height);
|
|
const opacity = 0.4 + (1 - Math.min(distance / maxDistance, 1)) * 0.6;
|
|
|
|
drawOffscreenIndicator(
|
|
indicatorX,
|
|
indicatorY,
|
|
angle,
|
|
enemyColor,
|
|
opacity,
|
|
enemy.player
|
|
);
|
|
});
|
|
}
|
|
|
|
function drawOffscreenIndicator(x, y, angle, color, opacity, player) {
|
|
const size = 14 * entitySizeMultiplier;
|
|
|
|
ctx.save();
|
|
ctx.translate(x, y);
|
|
ctx.rotate(angle);
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(size, 0);
|
|
ctx.lineTo(-size / 2, -size / 2);
|
|
ctx.lineTo(-size / 2, size / 2);
|
|
ctx.closePath();
|
|
|
|
const r = parseInt(color.slice(1, 3), 16);
|
|
const g = parseInt(color.slice(3, 5), 16);
|
|
const b = parseInt(color.slice(5, 7), 16);
|
|
|
|
ctx.fillStyle = `rgba(${r}, ${g}, ${b}, ${opacity})`;
|
|
ctx.fill();
|
|
|
|
ctx.strokeStyle = 'black';
|
|
ctx.lineWidth = 1;
|
|
ctx.stroke();
|
|
|
|
if (player.hasAwp) {
|
|
ctx.beginPath();
|
|
ctx.arc(-size / 2, 0, size / 4, 0, Math.PI * 2);
|
|
ctx.fillStyle = 'orange';
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
}
|
|
|
|
if (drawHealth && typeof player.health === 'number') {
|
|
let healthColor;
|
|
if (player.health > 70) {
|
|
healthColor = "#32CD32";
|
|
} else if (player.health > 30) {
|
|
healthColor = "#FFFF00";
|
|
} else {
|
|
healthColor = "#FF0000";
|
|
}
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(-size / 2, -size / 2, size / 4, 0, Math.PI * 2);
|
|
ctx.fillStyle = healthColor;
|
|
ctx.fill();
|
|
ctx.stroke();
|
|
}
|
|
|
|
ctx.restore();
|
|
}
|
|
|
|
function drawBombTimer() {
|
|
if (!radarData || !radarData.bombPlanted || radarData.bombExploded || radarData.bombDefuseTimeleft < 0) {
|
|
return;
|
|
}
|
|
|
|
const maxWidth = 1024 - 128 - 128;
|
|
const timeleft = radarData.bombDefuseTimeleft;
|
|
|
|
const timerHeight = Math.max(16, 10 * canvasScale);
|
|
const timerY = Math.max(16, 10 * canvasScale);
|
|
const fontSize = Math.max(24, 18 * canvasScale);
|
|
|
|
ctx.fillStyle = "black";
|
|
ctx.fillRect(128, timerY, maxWidth, timerHeight);
|
|
|
|
if (radarData.bombBeingDefused) {
|
|
ctx.fillStyle = radarData.bombCanDefuse ? teamColor : enemyColor;
|
|
} else {
|
|
ctx.fillStyle = bombColor;
|
|
}
|
|
|
|
ctx.fillRect(130, timerY + 2, (maxWidth - 2) * (timeleft / 40), timerHeight - 4);
|
|
|
|
ctx.font = `bold ${fontSize}px Arial`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillStyle = textColor;
|
|
ctx.fillText(`${timeleft.toFixed(1)}s`, 1024 / 2, timerY + timerHeight + fontSize / 2 + 4);
|
|
|
|
ctx.strokeStyle = "black";
|
|
ctx.lineWidth = 2;
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(128 + (maxWidth * (5 / 40)), timerY);
|
|
ctx.lineTo(128 + (maxWidth * (5 / 40)), timerY + timerHeight);
|
|
ctx.stroke();
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(130 + (maxWidth - 2) * (10 / 40), timerY);
|
|
ctx.lineTo(130 + (maxWidth - 2) * (10 / 40), timerY + timerHeight);
|
|
ctx.stroke();
|
|
|
|
if (radarData.bombCanDefuse) {
|
|
ctx.strokeStyle = "green";
|
|
ctx.beginPath();
|
|
ctx.moveTo(130 + (maxWidth - 2) * (radarData.bombDefuseEnd / 40), timerY);
|
|
ctx.lineTo(130 + (maxWidth - 2) * (radarData.bombDefuseEnd / 40), timerY + timerHeight);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
function fillCanvas() {
|
|
ctx.fillStyle = "#0f0f0f";
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
}
|
|
|
|
function updatePlayerDropdown() {
|
|
const dropdown = document.getElementById('playerSelect');
|
|
if (!dropdown) return;
|
|
|
|
const currentValue = dropdown.value;
|
|
const savedPlayer = localStorage.getItem('selectedPlayer');
|
|
|
|
while (dropdown.options.length > 0) {
|
|
dropdown.remove(0);
|
|
}
|
|
|
|
const fullMapOption = document.createElement('option');
|
|
fullMapOption.value = FULL_MAP_VIEW;
|
|
fullMapOption.textContent = "FULL MAP";
|
|
dropdown.appendChild(fullMapOption);
|
|
|
|
const localOption = document.createElement('option');
|
|
localOption.value = "local";
|
|
localOption.textContent = "YOU";
|
|
dropdown.appendChild(localOption);
|
|
|
|
for (const playerName in playerList) {
|
|
if (playerName !== LOCAL_PLAYER_VIEW && playerName !== FULL_MAP_VIEW) {
|
|
const option = document.createElement('option');
|
|
option.value = playerName;
|
|
|
|
if (playerList[playerName].isDead) {
|
|
option.textContent = `${playerName} (DEAD)`;
|
|
option.style.color = "#777777";
|
|
} else {
|
|
option.textContent = playerName;
|
|
}
|
|
|
|
dropdown.appendChild(option);
|
|
}
|
|
}
|
|
|
|
if (Object.keys(playerList).includes(currentValue) || currentValue === "local" || currentValue === FULL_MAP_VIEW) {
|
|
dropdown.value = currentValue;
|
|
}
|
|
else if (savedPlayer === FULL_MAP_VIEW) {
|
|
dropdown.value = FULL_MAP_VIEW;
|
|
focusedPlayerName = FULL_MAP_VIEW;
|
|
playerCentered = false;
|
|
}
|
|
else if (savedPlayer && Object.keys(playerList).includes(savedPlayer)) {
|
|
dropdown.value = savedPlayer;
|
|
focusedPlayerName = savedPlayer;
|
|
|
|
if (playerList[savedPlayer]?.isDead) {
|
|
dropdown.value = FULL_MAP_VIEW;
|
|
focusedPlayerName = FULL_MAP_VIEW;
|
|
localStorage.setItem('selectedPlayer', FULL_MAP_VIEW);
|
|
playerCentered = false;
|
|
}
|
|
}
|
|
else {
|
|
dropdown.value = FULL_MAP_VIEW;
|
|
focusedPlayerName = FULL_MAP_VIEW;
|
|
localStorage.setItem('selectedPlayer', FULL_MAP_VIEW);
|
|
playerCentered = false;
|
|
}
|
|
}
|
|
|
|
function changePlayerFocus() {
|
|
const dropdown = document.getElementById('playerSelect');
|
|
|
|
if (dropdown.value !== "local" &&
|
|
dropdown.value !== FULL_MAP_VIEW &&
|
|
playerList[dropdown.value]?.isDead) {
|
|
|
|
console.log(`[radarflow] Attempted to focus on dead player ${dropdown.value}, switching to full map view`);
|
|
localStorage.setItem('lastFocusedPlayer', dropdown.value);
|
|
lastFocusedPlayer = dropdown.value;
|
|
dropdown.value = FULL_MAP_VIEW;
|
|
focusedPlayerName = FULL_MAP_VIEW;
|
|
playerCentered = false;
|
|
}
|
|
else if (dropdown.value === FULL_MAP_VIEW) {
|
|
focusedPlayerName = FULL_MAP_VIEW;
|
|
playerCentered = false;
|
|
}
|
|
else {
|
|
localStorage.removeItem('lastFocusedPlayer');
|
|
lastFocusedPlayer = null;
|
|
focusedPlayerName = dropdown.value === "local" ? LOCAL_PLAYER_VIEW : dropdown.value;
|
|
|
|
playerCentered = document.getElementById('centerCheck').checked;
|
|
|
|
if (playerList[focusedPlayerName]) {
|
|
focusedPlayerPos = playerList[focusedPlayerName].pos;
|
|
focusedPlayerYaw = playerList[focusedPlayerName].yaw;
|
|
}
|
|
|
|
rotationDisabledUntilRespawn = false;
|
|
}
|
|
|
|
localStorage.setItem('selectedPlayer', focusedPlayerName);
|
|
update = true;
|
|
}
|
|
|
|
function mapCoordinates(coordinates) {
|
|
if (!map || !coordinates) {
|
|
return { x: 0, y: 0 };
|
|
}
|
|
|
|
const offset_x = (coordinates.x - map.pos_x) / map.scale;
|
|
const offset_y = (coordinates.y - map.pos_y) / -map.scale;
|
|
|
|
return { x: offset_x, y: offset_y };
|
|
}
|
|
|
|
function drawPlayerName(pos, playerName, playerType, hasAwp, hasBomb, isScoped) {
|
|
if (!map) return;
|
|
|
|
const transformed = mapAndTransformCoordinates(pos);
|
|
const mapPos = transformed.pos;
|
|
const textSize = transformed.textSize;
|
|
|
|
const textY = mapPos.y + 20;
|
|
|
|
let displayName = playerName;
|
|
if (playerType === "Local") {
|
|
displayName = "YOU";
|
|
ctx.fillStyle = localColor;
|
|
} else if (playerType === "Team") {
|
|
ctx.fillStyle = teamColor;
|
|
} else if (playerType === "Enemy") {
|
|
ctx.fillStyle = enemyColor;
|
|
}
|
|
|
|
if (isScoped) {
|
|
displayName += " [SCOPED]";
|
|
}
|
|
|
|
ctx.font = `bold ${textSize}px Arial`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "top";
|
|
|
|
ctx.lineWidth = 3;
|
|
ctx.strokeStyle = "black";
|
|
ctx.strokeText(displayName, mapPos.x, textY);
|
|
ctx.fillText(displayName, mapPos.x, textY);
|
|
}
|
|
|
|
function drawPlayerMoney(pos, playerType, money, hasBomb) {
|
|
if (!map) return;
|
|
|
|
const transformed = mapAndTransformCoordinates(pos);
|
|
const mapPos = transformed.pos;
|
|
const textSize = transformed.textSize * 0.8;
|
|
|
|
let extraOffset = 0;
|
|
if (drawNames) extraOffset += 15;
|
|
if (drawGuns) extraOffset += 15;
|
|
if (hasBomb) extraOffset += 15;
|
|
|
|
let textY = mapPos.y + 20 + extraOffset;
|
|
|
|
const formattedMoney = '$' + (money || 0).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
|
|
|
if (money >= 10000) {
|
|
ctx.fillStyle = "#32CD32";
|
|
} else if (money >= 4500) {
|
|
ctx.fillStyle = "#FFFF00";
|
|
} else {
|
|
ctx.fillStyle = "#FF4500";
|
|
}
|
|
|
|
ctx.font = `bold ${textSize}px Arial`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "top";
|
|
|
|
ctx.lineWidth = 3;
|
|
ctx.strokeStyle = "black";
|
|
ctx.strokeText(formattedMoney, mapPos.x, textY);
|
|
ctx.fillText(formattedMoney, mapPos.x, textY);
|
|
}
|
|
|
|
function drawPlayerWeapon(pos, playerType, weaponId) {
|
|
if (!map) return;
|
|
|
|
const transformed = mapAndTransformCoordinates(pos);
|
|
const mapPos = transformed.pos;
|
|
const textSize = transformed.textSize * 0.8;
|
|
|
|
const textY = mapPos.y + (drawNames ? 35 : 20);
|
|
|
|
let weaponName = getWeaponName(weaponId);
|
|
|
|
if (weaponId === 9) {
|
|
ctx.fillStyle = "orange";
|
|
} else {
|
|
ctx.fillStyle = textColor;
|
|
}
|
|
|
|
ctx.font = `bold ${textSize}px Arial`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "top";
|
|
|
|
ctx.lineWidth = 3;
|
|
ctx.strokeStyle = "black";
|
|
ctx.strokeText(`[${weaponName}]`, mapPos.x, textY);
|
|
ctx.fillText(`[${weaponName}]`, mapPos.x, textY);
|
|
}
|
|
|
|
function drawPlayerBomb(pos, playerType) {
|
|
if (!map) return;
|
|
|
|
const transformed = mapAndTransformCoordinates(pos);
|
|
const mapPos = transformed.pos;
|
|
const textSize = transformed.textSize * 0.8;
|
|
|
|
const textY = mapPos.y + (drawNames ? (drawGuns ? 50 : 35) : (drawGuns ? 35 : 20));
|
|
|
|
ctx.fillStyle = bombColor;
|
|
ctx.font = `bold ${textSize}px Arial`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "top";
|
|
|
|
ctx.lineWidth = 3;
|
|
ctx.strokeStyle = "black";
|
|
ctx.strokeText("[C4]", mapPos.x, textY);
|
|
ctx.fillText("[C4]", mapPos.x, textY);
|
|
}
|
|
|
|
function drawBomb(pos, planted) {
|
|
if (!map) return;
|
|
|
|
const transformed = mapAndTransformCoordinates(pos);
|
|
const mapPos = transformed.pos;
|
|
const size = minEntitySize * entitySizeMultiplier;
|
|
|
|
ctx.beginPath();
|
|
ctx.arc(mapPos.x, mapPos.y, size, 0, 2 * Math.PI);
|
|
ctx.fillStyle = bombColor;
|
|
ctx.fill();
|
|
|
|
ctx.lineWidth = 3;
|
|
ctx.strokeStyle = "black";
|
|
ctx.stroke();
|
|
|
|
ctx.font = `bold ${Math.max(size * 1.2, minTextSize)}px Arial`;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "middle";
|
|
ctx.fillStyle = "white";
|
|
ctx.fillText("C4", mapPos.x, mapPos.y);
|
|
|
|
ctx.closePath();
|
|
|
|
if (planted && ((new Date().getTime() / 1000) % 1) > 0.5) {
|
|
ctx.strokeStyle = enemyColor;
|
|
ctx.lineWidth = 3;
|
|
ctx.beginPath();
|
|
ctx.arc(mapPos.x, mapPos.y, size + 4, 0, 2 * Math.PI);
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
function drawEntity(pos, fillStyle, dormant, hasBomb, yaw, hasAwp, playerType, isScoped, playerName, isPlanted, weaponId) {
|
|
if (!map) return;
|
|
|
|
const transformed = mapAndTransformCoordinates(pos);
|
|
const mapPos = transformed.pos;
|
|
|
|
let circleRadius = minEntitySize * entitySizeMultiplier;
|
|
const distance = circleRadius + 2;
|
|
const radius = distance + 5;
|
|
const arrowWidth = 35;
|
|
|
|
const isFocusedPlayer = playerName === focusedPlayerName ||
|
|
(focusedPlayerName === LOCAL_PLAYER_VIEW && playerType === "Local");
|
|
|
|
const isDeadPlayer = playerList[playerName]?.isDead || false;
|
|
|
|
let adjustedYaw = yaw;
|
|
|
|
const shouldAdjustRotation = rotateMap &&
|
|
!temporarilyDisableRotation &&
|
|
!rotationDisabledUntilRespawn;
|
|
|
|
if (shouldAdjustRotation) {
|
|
if (isFocusedPlayer) {
|
|
adjustedYaw = 90;
|
|
} else {
|
|
adjustedYaw = (yaw + 180) - focusedPlayerYaw + 270;
|
|
}
|
|
}
|
|
|
|
if (dormant || isDeadPlayer) {
|
|
if (isDeadPlayer) {
|
|
ctx.beginPath();
|
|
ctx.arc(mapPos.x, mapPos.y, circleRadius, 0, 2 * Math.PI);
|
|
ctx.fillStyle = "#777777";
|
|
ctx.fill();
|
|
ctx.closePath();
|
|
|
|
const xSize = circleRadius * 0.7;
|
|
ctx.beginPath();
|
|
ctx.moveTo(mapPos.x - xSize, mapPos.y - xSize);
|
|
ctx.lineTo(mapPos.x + xSize, mapPos.y + xSize);
|
|
ctx.moveTo(mapPos.x + xSize, mapPos.y - xSize);
|
|
ctx.lineTo(mapPos.x - xSize, mapPos.y + xSize);
|
|
ctx.strokeStyle = "#000000";
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
ctx.closePath();
|
|
} else {
|
|
ctx.font = `bold ${transformed.textSize}px Arial`;
|
|
ctx.textAlign = "center";
|
|
ctx.fillStyle = fillStyle;
|
|
ctx.fillText("?", mapPos.x, mapPos.y);
|
|
}
|
|
} else {
|
|
if (isFocusedPlayer) {
|
|
ctx.beginPath();
|
|
ctx.arc(mapPos.x, mapPos.y, circleRadius + 4, 0, 2 * Math.PI);
|
|
ctx.fillStyle = "#FFFFFF";
|
|
ctx.fill();
|
|
ctx.closePath();
|
|
}
|
|
|
|
if (hasAwp) {
|
|
ctx.beginPath();
|
|
ctx.arc(mapPos.x, mapPos.y, circleRadius, 0, 2 * Math.PI);
|
|
ctx.fillStyle = "orange";
|
|
ctx.fill();
|
|
circleRadius -= 2;
|
|
}
|
|
|
|
// Draw circle
|
|
ctx.beginPath();
|
|
ctx.arc(mapPos.x, mapPos.y, circleRadius, 0, 2 * Math.PI);
|
|
ctx.fillStyle = fillStyle;
|
|
ctx.fill();
|
|
ctx.closePath();
|
|
|
|
const arrowHeadX = mapPos.x + radius * Math.cos(adjustedYaw * (Math.PI / 180));
|
|
const arrowHeadY = mapPos.y - radius * Math.sin(adjustedYaw * (Math.PI / 180));
|
|
|
|
const arrowCornerX1 = mapPos.x + distance * Math.cos((adjustedYaw - arrowWidth) * (Math.PI / 180));
|
|
const arrowCornerY1 = mapPos.y - distance * Math.sin((adjustedYaw - arrowWidth) * (Math.PI / 180));
|
|
|
|
const arrowCornerX2 = mapPos.x + distance * Math.cos((adjustedYaw + arrowWidth) * (Math.PI / 180));
|
|
const arrowCornerY2 = mapPos.y - distance * Math.sin((adjustedYaw + arrowWidth) * (Math.PI / 180));
|
|
|
|
const cicleYaw = 90 - adjustedYaw;
|
|
const startAngle = degreesToRadians(cicleYaw - arrowWidth) - Math.PI / 2;
|
|
const endAngle = degreesToRadians(cicleYaw + arrowWidth) - Math.PI / 2;
|
|
|
|
// Draw arrow
|
|
ctx.beginPath();
|
|
ctx.arc(mapPos.x, mapPos.y, distance, startAngle, endAngle);
|
|
ctx.lineTo(arrowCornerX1, arrowCornerY1);
|
|
ctx.lineTo(arrowHeadX, arrowHeadY);
|
|
ctx.lineTo(arrowCornerX2, arrowCornerY2);
|
|
ctx.closePath();
|
|
ctx.fillStyle = 'white';
|
|
ctx.fill();
|
|
|
|
if (isScoped) {
|
|
const lineOfSightX = arrowHeadX + 1024 * Math.cos(adjustedYaw * (Math.PI / 180));
|
|
const lineOfSightY = arrowHeadY - 1024 * Math.sin(adjustedYaw * (Math.PI / 180));
|
|
ctx.beginPath();
|
|
ctx.moveTo(arrowHeadX, arrowHeadY);
|
|
ctx.lineTo(lineOfSightX, lineOfSightY);
|
|
|
|
ctx.strokeStyle = playerType == "Enemy" ? enemyColor : teamColor;
|
|
ctx.lineWidth = 2;
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
}
|
|
|
|
function getWeaponName(weaponId) {
|
|
if (weaponIdMap[weaponId]) {
|
|
return weaponIdMap[weaponId];
|
|
}
|
|
|
|
if (weaponId >= 500) {
|
|
return "KNIFE";
|
|
}
|
|
|
|
return "KNIFE";
|
|
}
|
|
|
|
function loadMap(mapName) {
|
|
if (!mapName) return;
|
|
|
|
console.log(`[radarflow] Loading map "${mapName}"`);
|
|
loaded = true;
|
|
|
|
// Load JSON data
|
|
const jsonPath = `assets/json/${mapName}.json`;
|
|
fetch(jsonPath)
|
|
.then(response => {
|
|
if (!response.ok) throw new Error(`JSON not found: ${response.status}`);
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log("[radarflow] Map data loaded");
|
|
map = data;
|
|
update = true;
|
|
})
|
|
.catch(error => {
|
|
console.error(`[radarflow] Error loading JSON: ${error}`);
|
|
});
|
|
|
|
const imagePath = `assets/image/${mapName}_radar_psd.png`;
|
|
const map_img = new Image();
|
|
|
|
map_img.onload = () => {
|
|
console.log("[radarflow] Map image loaded");
|
|
image = map_img;
|
|
update = true;
|
|
};
|
|
|
|
map_img.onerror = (e) => {
|
|
console.error(`[radarflow] Error loading image: ${e}`);
|
|
};
|
|
|
|
map_img.src = imagePath;
|
|
}
|
|
|
|
function unloadMap() {
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
map = null;
|
|
mapName = null;
|
|
loaded = false;
|
|
update = true;
|
|
}
|
|
|
|
function processData(data) {
|
|
if (!data) return;
|
|
|
|
const now = performance.now();
|
|
lastUpdateTime = now;
|
|
lastResponseTime = now;
|
|
connectionHealthy = true;
|
|
isRequestPending = false;
|
|
|
|
radarData = data;
|
|
freq = data.freq;
|
|
entityData = data.entityData;
|
|
|
|
if (data.money_reveal_enabled !== undefined) {
|
|
const checkbox = document.getElementById("moneyReveal");
|
|
if (checkbox) checkbox.checked = data.money_reveal_enabled;
|
|
}
|
|
|
|
if (data.ingame === false) {
|
|
if (loaded) unloadMap();
|
|
} else {
|
|
if (!loaded && data.mapName) {
|
|
mapName = data.mapName;
|
|
loadMap(mapName);
|
|
}
|
|
}
|
|
|
|
update = true;
|
|
}
|
|
|
|
function decompressData(data) {
|
|
try {
|
|
pingTracker.endRequest();
|
|
|
|
clearTimeout(requestTimeoutTimer);
|
|
lastResponseTime = performance.now();
|
|
connectionHealthy = true;
|
|
retryCount = 0;
|
|
|
|
const rtt = pingTracker.getAveragePing();
|
|
networkLatencyHistory.push(rtt);
|
|
if (networkLatencyHistory.length > 10) {
|
|
networkLatencyHistory.shift();
|
|
}
|
|
|
|
if (data[0] === 0x01) {
|
|
try {
|
|
if (typeof pako === 'undefined') {
|
|
console.error("[radarflow] Pako library not available");
|
|
return null;
|
|
}
|
|
|
|
const decompressed = pako.inflate(data.slice(1));
|
|
const text = new TextDecoder().decode(decompressed);
|
|
return JSON.parse(text);
|
|
} catch (e) {
|
|
console.error("[radarflow] Decompression error:", e);
|
|
return null;
|
|
}
|
|
} else if (data[0] === 0x00) {
|
|
try {
|
|
const text = new TextDecoder().decode(data.slice(1));
|
|
return JSON.parse(text);
|
|
} catch (e) {
|
|
console.error("[radarflow] Parse error:", e);
|
|
return null;
|
|
}
|
|
} else {
|
|
console.error("[radarflow] Unknown data format");
|
|
return null;
|
|
}
|
|
} catch (e) {
|
|
console.error("[radarflow] Data processing error:", e);
|
|
isRequestPending = false;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function connect() {
|
|
reconnecting = true;
|
|
|
|
if (websocket == null) {
|
|
console.log(`[radarflow] Connecting to ${websocketAddr}`);
|
|
|
|
let socket = new WebSocket(websocketAddr);
|
|
socket.binaryType = "arraybuffer";
|
|
|
|
socket.onopen = () => {
|
|
console.log("[radarflow] Connection established");
|
|
lastResponseTime = performance.now();
|
|
connectionHealthy = true;
|
|
reconnecting = false;
|
|
isRequestPending = false;
|
|
retryCount = 0;
|
|
|
|
setTimeout(() => {
|
|
socket.send(`ping:0`);
|
|
}, 500);
|
|
|
|
if (!fpsStartTime) {
|
|
requestAnimationFrame(render);
|
|
}
|
|
};
|
|
|
|
socket.onmessage = (event) => {
|
|
if (event.data === "pong") {
|
|
lastResponseTime = performance.now();
|
|
return;
|
|
}
|
|
|
|
if (event.data === "error") {
|
|
console.error("[radarflow] Server error");
|
|
isRequestPending = false;
|
|
return;
|
|
}
|
|
|
|
if (event.data instanceof ArrayBuffer) {
|
|
const data = new Uint8Array(event.data);
|
|
const jsonData = decompressData(data);
|
|
if (jsonData) processData(jsonData);
|
|
} else if (typeof event.data === 'string') {
|
|
try {
|
|
const jsonData = JSON.parse(event.data);
|
|
if (jsonData.action === "toggleMoneyReveal") {
|
|
document.getElementById("moneyReveal").checked = jsonData.enabled;
|
|
} else {
|
|
processData(jsonData);
|
|
}
|
|
|
|
lastResponseTime = performance.now();
|
|
} catch (e) {
|
|
console.error("[radarflow] JSON parse error:", e);
|
|
}
|
|
}
|
|
};
|
|
|
|
socket.onclose = (event) => {
|
|
console.log("[radarflow] Connection closed");
|
|
websocket = null;
|
|
|
|
if (!reconnecting) {
|
|
unloadMap();
|
|
}
|
|
|
|
setTimeout(connect, NETWORK_SETTINGS.reconnectDelay);
|
|
};
|
|
|
|
socket.onerror = (error) => {
|
|
console.error("[radarflow] WebSocket error:", error);
|
|
};
|
|
|
|
websocket = socket;
|
|
} else {
|
|
reconnecting = false;
|
|
}
|
|
}
|
|
|
|
function updateTextSize(value) {
|
|
textSizeMultiplier = parseFloat(value);
|
|
const valueDisplay = document.getElementById('textSizeValue');
|
|
if (valueDisplay) valueDisplay.textContent = value;
|
|
localStorage.setItem('textSizeMultiplier', value);
|
|
}
|
|
|
|
function updateEntitySize(value) {
|
|
entitySizeMultiplier = parseFloat(value);
|
|
const valueDisplay = document.getElementById('entitySizeValue');
|
|
if (valueDisplay) valueDisplay.textContent = value;
|
|
localStorage.setItem('entitySizeMultiplier', value);
|
|
}
|
|
|
|
function updateHealthBarSize(value) {
|
|
healthBarSizeMultiplier = parseFloat(value);
|
|
const valueDisplay = document.getElementById('healthBarSizeValue');
|
|
if (valueDisplay) valueDisplay.textContent = value;
|
|
localStorage.setItem('healthBarSizeMultiplier', value);
|
|
}
|
|
|
|
function resetSizes() {
|
|
const textSlider = document.getElementById('textSizeSlider');
|
|
const entitySlider = document.getElementById('entitySizeSlider');
|
|
const zoomSlider = document.getElementById('zoomLevelSlider');
|
|
const healthBarSlider = document.getElementById('healthBarSizeSlider');
|
|
|
|
if (textSlider) textSlider.value = DEFAULT_TEXT_SIZE.toString();
|
|
if (entitySlider) entitySlider.value = DEFAULT_ENTITY_SIZE.toString();
|
|
if (zoomSlider) zoomSlider.value = DEFAULT_ZOOM_LEVEL.toString();
|
|
if (healthBarSlider) healthBarSlider.value = DEFAULT_HEALTH_BAR_SIZE.toString();
|
|
|
|
updateTextSize(DEFAULT_TEXT_SIZE.toString());
|
|
updateEntitySize(DEFAULT_ENTITY_SIZE.toString());
|
|
updateZoomLevel(DEFAULT_ZOOM_LEVEL.toString());
|
|
updateHealthBarSize(DEFAULT_HEALTH_BAR_SIZE.toString());
|
|
}
|
|
|
|
function toggleZoom() {
|
|
shouldZoom = !shouldZoom;
|
|
}
|
|
|
|
function toggleStats() {
|
|
drawStats = !drawStats;
|
|
}
|
|
|
|
function toggleNames() {
|
|
drawNames = !drawNames;
|
|
}
|
|
|
|
function toggleGuns() {
|
|
drawGuns = !drawGuns;
|
|
}
|
|
|
|
function toggleRotate() {
|
|
rotateMap = !rotateMap;
|
|
}
|
|
|
|
function toggleMoneyReveal() {
|
|
if (websocket && websocket.readyState === WebSocket.OPEN) {
|
|
console.log("[radarflow] Sending toggleMoneyReveal command");
|
|
websocket.send("toggleMoneyReveal");
|
|
}
|
|
}
|
|
|
|
function toggleDisplayMoney() {
|
|
drawMoney = !drawMoney;
|
|
update = true;
|
|
localStorage.setItem('drawMoney', drawMoney ? 'true' : 'false');
|
|
}
|
|
|
|
function togglePerformanceMode() {
|
|
const performanceMode = document.getElementById('performanceMode').checked;
|
|
|
|
if (performanceMode) {
|
|
drawNames = false;
|
|
drawGuns = false;
|
|
drawMoney = false;
|
|
drawHealth = false;
|
|
|
|
NETWORK_SETTINGS.interpolationAmount = 0.85;
|
|
|
|
document.getElementById("namesCheck").checked = false;
|
|
document.getElementById("gunsCheck").checked = false;
|
|
document.getElementById("moneyDisplay").checked = false;
|
|
document.getElementById("healthCheck").checked = false;
|
|
|
|
console.log("[radarflow] Performance mode enabled with enhanced smoothing");
|
|
} else {
|
|
drawNames = document.getElementById("namesCheck").checked = true;
|
|
drawGuns = document.getElementById("gunsCheck").checked = true;
|
|
drawMoney = document.getElementById("moneyDisplay").checked = true;
|
|
drawHealth = document.getElementById("healthCheck").checked = true;
|
|
|
|
NETWORK_SETTINGS.interpolationAmount = 0.7;
|
|
|
|
console.log("[radarflow] Performance mode disabled");
|
|
}
|
|
}
|
|
|
|
window.addEventListener('resize', () => {
|
|
if (canvas) {
|
|
const canvasRect = canvas.getBoundingClientRect();
|
|
canvasScale = Math.min(canvasRect.width, canvasRect.height) / 1024;
|
|
}
|
|
});
|
|
|
|
addEventListener("DOMContentLoaded", () => {
|
|
const savedDrawHealth = localStorage.getItem('drawHealth');
|
|
drawHealth = savedDrawHealth !== null ? savedDrawHealth === 'true' : true;
|
|
|
|
const savedDrawMoney = localStorage.getItem('drawMoney');
|
|
drawMoney = savedDrawMoney !== null ? savedDrawMoney === 'true' : true;
|
|
|
|
const savedTextSize = localStorage.getItem('textSizeMultiplier');
|
|
textSizeMultiplier = savedTextSize !== null ? parseFloat(savedTextSize) : DEFAULT_TEXT_SIZE;
|
|
|
|
const savedEntitySize = localStorage.getItem('entitySizeMultiplier');
|
|
entitySizeMultiplier = savedEntitySize !== null ? parseFloat(savedEntitySize) : DEFAULT_ENTITY_SIZE;
|
|
|
|
const savedHealthBarSize = localStorage.getItem('healthBarSizeMultiplier');
|
|
healthBarSizeMultiplier = savedHealthBarSize !== null ? parseFloat(savedHealthBarSize) : DEFAULT_HEALTH_BAR_SIZE;
|
|
|
|
const savedOffscreenIndicators = localStorage.getItem('showOffscreenIndicators');
|
|
showOffscreenIndicators = savedOffscreenIndicators !== null ? savedOffscreenIndicators === 'true' : true;
|
|
|
|
const checkboxes = {
|
|
"zoomCheck": false,
|
|
"statsCheck": true,
|
|
"namesCheck": true,
|
|
"gunsCheck": true,
|
|
"moneyDisplay": drawMoney,
|
|
"moneyReveal": false,
|
|
"rotateCheck": true,
|
|
"centerCheck": true,
|
|
"healthCheck": drawHealth,
|
|
"offscreenCheck": showOffscreenIndicators
|
|
};
|
|
|
|
Object.entries(checkboxes).forEach(([id, state]) => {
|
|
const checkbox = document.getElementById(id);
|
|
if (checkbox) checkbox.checked = state;
|
|
});
|
|
|
|
const textSizeSlider = document.getElementById('textSizeSlider');
|
|
if (textSizeSlider) {
|
|
textSizeSlider.value = textSizeMultiplier;
|
|
const textSizeValue = document.getElementById('textSizeValue');
|
|
if (textSizeValue) textSizeValue.textContent = textSizeMultiplier;
|
|
}
|
|
|
|
const entitySizeSlider = document.getElementById('entitySizeSlider');
|
|
if (entitySizeSlider) {
|
|
entitySizeSlider.value = entitySizeMultiplier;
|
|
const entitySizeValue = document.getElementById('entitySizeValue');
|
|
if (entitySizeValue) entitySizeValue.textContent = entitySizeMultiplier;
|
|
}
|
|
|
|
const healthBarSizeSlider = document.getElementById('healthBarSizeSlider');
|
|
if (healthBarSizeSlider) {
|
|
healthBarSizeSlider.value = healthBarSizeMultiplier;
|
|
const healthBarSizeValue = document.getElementById('healthBarSizeValue');
|
|
if (healthBarSizeValue) healthBarSizeValue.textContent = healthBarSizeMultiplier;
|
|
}
|
|
|
|
const savedZoom = localStorage.getItem('playerCenteredZoom');
|
|
playerCenteredZoom = savedZoom !== null ? parseFloat(savedZoom) : DEFAULT_ZOOM_LEVEL;
|
|
|
|
const zoomSlider = document.getElementById('zoomLevelSlider');
|
|
if (zoomSlider) {
|
|
zoomSlider.value = playerCenteredZoom;
|
|
const zoomValue = document.getElementById('zoomLevelValue');
|
|
if (zoomValue) zoomValue.textContent = playerCenteredZoom;
|
|
}
|
|
|
|
updateZoomSliderVisibility();
|
|
|
|
canvas = document.getElementById('canvas');
|
|
if (canvas) {
|
|
canvas.width = 1024;
|
|
canvas.height = 1024;
|
|
ctx = canvas.getContext('2d');
|
|
|
|
const canvasRect = canvas.getBoundingClientRect();
|
|
canvasScale = Math.min(canvasRect.width, canvasRect.height) / 1024;
|
|
|
|
connect();
|
|
} else {
|
|
console.error("[radarflow] Canvas element not found");
|
|
}
|
|
}); |