Release 0.9.0

This commit is contained in:
2025-11-21 07:26:02 +01:00
committed by ecv
commit 472f0812e7
240 changed files with 20033 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
{%- macro construct_toggle_link(bool, active_text, deactivate_event, inactive_text, activate_event) -%}
{%- set bool = bool|default(false) -%}
{%- set active_text = active_text|default(none) -%}
{%- set deactivate_event = deactivate_event|default(none) -%}
{%- set inactive_text = inactive_text|default(none) -%}
{%- set activate_event = activate_event|default(none) -%}
{%- if bool == true -%}
{%- if deactivate_event != none and activate_event != none -%}
<span class="active"><a href="#" onclick="window.socket.emit('{{ deactivate_event[0] }}', {{ deactivate_event[1] }}); return false;">{{ active_text }}</a></span>
{%- elif deactivate_event != none and activate_event == none -%}
<span class="active"><a href="#" onclick="window.socket.emit('{{ deactivate_event[0] }}', {{ deactivate_event[1] }}); return false;">{{ active_text }}</a></span>
{%- endif -%}
{%- else -%}
{%- if deactivate_event != none and activate_event != none -%}
<span class="inactive"><a href="#" onclick="window.socket.emit('{{ activate_event[0] }}', {{ activate_event[1] }}); return false;">{{ inactive_text }}</a></span>
{%- elif deactivate_event != none and activate_event == none -%}
<span class="inactive"><a href="#" onclick="window.socket.emit('{{ deactivate_event[0] }}', {{ deactivate_event[1] }}); return false;">{{ active_text }}</a></span>
{%- endif -%}
{%- endif -%}
{%- endmacro -%}

View File

@@ -0,0 +1,10 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_control_info_link" class="info">
{{- construct_toggle_link(
True,
"i", ['widget_event', ['players', ['toggle_players_widget_view', {
'steamid': player.steamid,
'action': 'show_info_view'
}]]]
)-}}
</span>

View File

@@ -0,0 +1,13 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
{%- if player.is_initialized == true -%}
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_control_kick_link" class="info">
{{- construct_toggle_link(
player.is_initialized,
"kick", ['widget_event', ['players', ['kick_player', {
'action': 'kick_player',
'steamid': player.steamid,
'confirmed': 'False'
}]]]
)-}}
</span>
{%- endif -%}

View File

@@ -0,0 +1,9 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
<div>
{{ construct_toggle_link(
options_view_toggle,
"options", ['widget_event', ['players', ['toggle_players_widget_view', {'steamid': steamid, "action": "show_options"}]]],
"back", ['widget_event', ['players', ['toggle_players_widget_view', {'steamid': steamid, "action": "show_frontend"}]]]
)}}
</div>

View File

@@ -0,0 +1,3 @@
<div id="player_table_widget_options_toggle" class="pull_out right">
{{ control_switch_options_view }}
</div>

View File

@@ -0,0 +1,43 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
<table class="delete_modal">
<tr>
<td colspan="2">
<p>Are you sure you want to kick player {{ steamid }}?</p>
</td>
</tr>
<tr>
<td>
<p>By clicking [confirm] you will continue to proceed kicking the dude.</p>
</td>
<td>
<p>Clicking [cancel] will abort the kicking.</p>
</td>
</tr>
<tr>
<td colspan="2">
<div id="kick_player_reason">
<input name="kick_player_reason" value="{{ reason }}" />
</div>
</td>
</tr>
<tr>
<td>
<div id="kick_player_confirm" class="modal_delete_button">
<span class="active">
{% include "manage_players_widget/modal_confirm_kick_send_data.html" %}
</span>
</div>
</td>
<td>
<div id="kick_player_cancel" class="modal_cancel_button">
{{ construct_toggle_link(
True,
"cancel", ['widget_event', ['players', ['kick_player', {
'action': "cancel_kick_player",
'steamid': 'steamid'
}]]]
) }}
</div>
</td>
</tr>
</table>

View File

@@ -0,0 +1,15 @@
<script>
function send_kick_data_to_bot() {
let kick_reason = $("input[name='kick_player_reason']").val();
window.socket.emit(
'widget_event',
['players', ['kick_player', {
'action': 'kick_player',
'steamid': '{{ steamid }}',
'reason': kick_reason,
'confirmed': 'True'
}]]
);
}
</script>
<a href="#" onclick="send_kick_data_to_bot(); return false;">confirm</a>

View File

@@ -0,0 +1,7 @@
<tr>
<td colspan="11">
<div>
{{ action_delete_button }}
</div>
</td>
</tr>

View File

@@ -0,0 +1,13 @@
<tr>
<th>*</th>
<th onclick="window.sorting(this, player_table, 1)">actions</th>
<th onclick="window.sorting(this, player_table, 2)">level</th>
<th onclick="window.sorting(this, player_table, 3)">name</th>
<th onclick="window.sorting(this, player_table, 4)">health</th>
<th onclick="window.sorting(this, player_table, 5)">id</th>
<th onclick="window.sorting(this, player_table, 6)">steamid</th>
<th onclick="window.sorting(this, player_table, 7)">pos</th>
<th onclick="window.sorting(this, player_table, 8)">zombies</th>
<th onclick="window.sorting(this, player_table, 9)" class="right">last seen</th>
<th onclick="window.sorting(this, player_table, 10)" class="right">gametime</th>
</tr>

View File

@@ -0,0 +1,30 @@
{%- from 'jinja2_macros.html' import construct_toggle_link with context -%}
<tr id="player_table_row_{{ player.dataset }}_{{ player.steamid }}"{%- if css_class %} class="{{ css_class }}"{%- endif -%}>
<td>
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_control_select_link" class="select_button">{{ control_select_link }}</span>
</td>
<td class="nobr" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_actions">{{ control_info_link }}{{ control_kick_link }}</td>
<td class="center" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_level">{{ player.level }}</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_name" onclick="$(this).selectText();">{{ player.name }}</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_health">{{ player.health }}</td>
<td class="right" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_id" onclick="$(this).selectText();">{{ player.id }}</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_steamid" onclick="$(this).selectText();">{{ player.steamid }}</td>
<td class="position right" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_pos" onclick="$(this).selectText();">
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_pos_x">
{{ ((player | default({})).pos | default({}) ).x | default('0') }}
</span>
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_pos_y">
{{ ((player | default({})).pos | default({}) ).y | default('0') }}
</span>
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_pos_z">
{{ ((player | default({})).pos | default({}) ).z | default('0') }}
</span>
</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_zombies">{{ player.zombies }}</td>
<td class="nobr right" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_last_updated_servertime">
{{ player.last_updated_servertime }}
</td>
<td class="nobr right" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_last_seen_gametime">
{{ player.last_seen_gametime }}
</td>
</tr>

View File

@@ -0,0 +1,33 @@
<header>
<div>
<span>Players</span>
</div>
</header>
<aside>
{{ options_toggle }}
</aside>
<main>
<table class="data_table">
<caption>
<span>offline</span>
<span class="in_limbo">offline and dead</span>
<span class="is_online in_limbo">logging in</span>
<span class="is_online is_initialized">online</span>
<span class="in_limbo is_online is_initialized">online and dead</span>
</caption>
<thead>
{{ table_header }}
</thead>
<tbody id="player_table">
{{ table_rows }}
</tbody>
<tfoot>
{{ table_footer }}
</tfoot>
</table>
<div class="dialog">
<div id="manage_players_widget_modal" class="modal-content">
<p>this is the text inside the modal</p>
</div>
</div>
</main>

View File

@@ -0,0 +1,155 @@
<header>
<div>
<span>Players</span>
</div>
</header>
<aside>
{{ options_toggle }}
</aside>
<main>
<table class="info_sheet">
<thead>
<tr>
<th colspan="3">player-info ({{ player.steamid }})</th>
</tr>
</thead>
<tbody id="player_table_row_{{ player.dataset }}_{{ player.steamid }}">
<tr>
<td>Name</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_name" onclick="$(this).selectText();">{{ player.name }}</td>
<td>the players steam-name</td>
</tr>
<tr>
<td>ID</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_id" onclick="$(this).selectText();">{{ player.id }}</td>
<td></td>
</tr>
<tr>
<td>SteamID</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_steamid" onclick="$(this).selectText();">{{ player.steamid }}</td>
<td></td>
</tr>
<tr>
<td>Health</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_health">{{ player.health }}</td>
<td></td>
</tr>
<tr>
<td>Position</td>
<td class="position" onclick="$(this).selectText();">
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_pos_x">{{ ((player | default({})).pos | default({}) ).x | default('0') }}</span>
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_pos_y">{{ ((player | default({})).pos | default({}) ).y | default('0') }}</span>
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_pos_z">{{ ((player | default({})).pos | default({}) ).z | default('0') }}</span>
</td>
<td></td>
</tr>
<tr>
<td>Rotation</td>
<td class="position">
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_rot_x">{{ ((player | default({})).rot | default({}) ).x | default('0') }}</span>
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_rot_y">{{ ((player | default({})).rot | default({}) ).y | default('0') }}</span>
<span id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_rot_z">{{ ((player | default({})).rot | default({}) ).z | default('0') }}</span>
</td>
<td></td>
</tr>
<tr>
<td>Level</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_level">{{ player.level }}</td>
<td></td>
</tr>
<tr>
<td>IP-Address</td>
<td onclick="$(this).selectText();" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_ip">{{ player.ip }}</td>
<td></td>
</tr>
<tr>
<td>Ping</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_ping">{{ player.ping }}</td>
<td></td>
</tr>
<tr>
<td>Deaths</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_deaths">{{ player.deaths }}</td>
<td></td>
</tr>
<tr>
<td>Zombie-Kills</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_zombies">{{ player.zombies }}</td>
<td></td>
</tr>
<tr>
<td>Player-Kills</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_players">{{ player.players }}</td>
<td></td>
</tr>
<tr>
<td>Score</td>
<td id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_score">{{ player.score }}</td>
<td></td>
</tr>
<tr>
<td>Last seen</td>
<td class="nobr" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_last_updated_servertime">
{{ player.last_updated_servertime }}
</td>
<td></td>
</tr>
<tr>
<td>First seen (gametime)</td>
<td class="nobr" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_first_seen_gametime">
{{ player.first_seen_gametime }}
</td>
<td></td>
</tr>
<tr>
<td>Last seen (gametime)</td>
<td class="nobr" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_last_seen_gametime">
{{ player.last_seen_gametime }}
</td>
<td></td>
</tr>
<tr>
<td>dataset</td>
<td class="nobr" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_origin">
{{ player.dataset }}
</td>
<td>the server-instance this entry is from</td>
</tr>
<tr>
<td>is_authenticated</td>
<td class="nobr" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_is_authenticated">
{{ player.is_authenticated | default("False") }}
</td>
<td>has authenticated with the bot</td>
</tr>
<tr>
<td>in_limbo</td>
<td class="nobr" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_in_limbo">
{{ player.in_limbo | default("False") }}
</td>
<td>hasn't got any health, is dead(ish)!</td>
</tr>
<tr>
<td>is_initialized</td>
<td class="nobr" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_is_initialized">
{{ player.is_initialized | default("False") }}
</td>
<td>player is online, has health, is ready to go!</td>
</tr>
<tr>
<td>is_online</td>
<td class="nobr" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_is_online">
{{ player.is_online | default("False") }}
</td>
<td>we can see you!</td>
</tr>
<tr>
<td>is_muted</td>
<td class="nobr" id="player_table_row_{{ player.dataset }}_{{ player.steamid }}_is_muted">
{{ player.is_muted | default("False") }}
</td>
<td>come again?</td>
</tr>
</tbody>
</table>
</main>

View File

@@ -0,0 +1,27 @@
<header>
<div>
<span>Players</span>
</div>
</header>
<aside>
{{ options_toggle }}
</aside>
<main>
<table class="options_table">
<thead>
<tr>
<th colspan="2">player table widget options</th>
</tr>
</thead>
<tbody>
<tr>
<th colspan="2"><span>widget-options</span></th>
</tr>
{% for key, value in widget_options.items() %}
<tr>
<td><span>{{key}}</span></td><td>{{value}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</main>

View File

@@ -0,0 +1,168 @@
// ========================================
// Player Popup Actions
// ========================================
// Kick player from map popup
window.kickPlayerFromMap = function(steamid, playerName) {
const reason = prompt('Kick reason for ' + playerName + ':', 'Admin action');
if (reason === null) {
return; // User cancelled
}
window.socket.emit(
'widget_event',
['players',
['kick_player', {
'action': 'kick_player',
'steamid': steamid,
'reason': reason || 'No reason provided',
'confirmed': 'True'
}]]
);
console.log('[MAP] Kicking player:', steamid, 'Reason:', reason);
};
// Send message to player from map popup
window.messagePlayerFromMap = function(steamid, playerName) {
const message = prompt('Message to ' + playerName + ':', '');
if (!message) {
return; // User cancelled or empty message
}
window.socket.emit(
'widget_event',
['players',
['say_to_player', {
'steamid': steamid,
'message': message
}]]
);
console.log('[MAP] Sending message to player:', steamid, 'Message:', message);
};
// Toggle player mute status from map popup
window.togglePlayerMuteFromMap = function(steamid, dataset, isMuted) {
window.socket.emit(
'widget_event',
['players',
['toggle_player_mute', {
'steamid': steamid,
'dataset': dataset,
'mute_status': isMuted
}]]
);
console.log('[MAP] Toggling player mute status:', steamid, 'to', isMuted);
};
// Toggle player authentication status from map popup
window.togglePlayerAuthFromMap = function(steamid, dataset, isAuth) {
window.socket.emit(
'widget_event',
['players',
['toggle_player_authentication', {
'steamid': steamid,
'dataset': dataset,
'auth_status': isAuth
}]]
);
console.log('[MAP] Toggling player auth status:', steamid, 'to', isAuth);
};
// Teleport player from map popup - click to select destination
window.teleportPlayerFromMap = function(steamid, playerName) {
// Close any open popups
map.closePopup();
// Create info message
const infoDiv = L.DomUtil.create('div', 'coordinates-display');
infoDiv.style.bottom = '50px';
infoDiv.style.background = 'rgba(255, 204, 0, 0.95)';
infoDiv.style.borderColor = 'var(--lcars-golden-tanoi)';
infoDiv.style.color = '#000';
infoDiv.style.fontWeight = 'bold';
infoDiv.innerHTML = '🎯 Click destination for ' + playerName + ' (Locations will use their TP entry)';
document.getElementById('map').appendChild(infoDiv);
// Change cursor
map.getContainer().style.cursor = 'crosshair';
// Wait for click
map.once('click', function(e) {
const clickCoords = e.latlng;
let targetX = Math.round(clickCoords.lat);
let targetY = 0; // Default Y, will be adjusted
let targetZ = Math.round(clickCoords.lng);
// Check if click is inside any location with teleport_entry
let foundLocationTeleport = false;
for (const locationId in locations) {
const loc = locations[locationId];
const teleportEntry = loc.teleport_entry || {};
const hasTeleport = teleportEntry.x !== undefined &&
teleportEntry.y !== undefined &&
teleportEntry.z !== undefined;
if (hasTeleport) {
// Check if click is inside this location's bounds
if (isInsideLocation(clickCoords.lat, clickCoords.lng, loc)) {
// Use location's teleport entry
targetX = Math.round(parseFloat(teleportEntry.x));
targetY = Math.round(parseFloat(teleportEntry.y));
targetZ = Math.round(parseFloat(teleportEntry.z));
foundLocationTeleport = true;
console.log('[MAP] Using location teleport entry:', loc.name, 'at', targetX, targetY, targetZ);
break;
}
}
}
// If no location teleport found, use default Y (ground level)
if (!foundLocationTeleport) {
targetY = -1; // Standard ground level
}
// Call teleport_player action
window.socket.emit(
'widget_event',
['players',
['teleport_player', {
'steamid': steamid,
'coordinates': {
'x': targetX.toString(),
'y': targetY.toString(),
'z': targetZ.toString()
}
}]]
);
console.log('[MAP] Teleporting player:', steamid, 'to', targetX, targetY, targetZ);
// Cleanup
map.getContainer().style.cursor = '';
document.getElementById('map').removeChild(infoDiv);
});
};
// Helper function to check if coordinates are inside a location
function isInsideLocation(x, z, loc) {
const coords = loc.coordinates;
const dims = loc.dimensions;
const shape = loc.shape;
if (shape === 'circle' || shape === 'spherical') {
const radius = parseFloat(dims.radius || 0);
const distance = Math.sqrt(
Math.pow(x - coords.x, 2) +
Math.pow(z - coords.z, 2)
);
return distance <= radius;
} else if (shape === 'rectangle' || shape === 'box') {
const width = parseFloat(dims.width || 0);
const length = parseFloat(dims.length || 0);
return (
x >= coords.x - width && x <= coords.x + width &&
z >= coords.z - length && z <= coords.z + length
);
}
return false;
}

View File

@@ -0,0 +1,3 @@
// Player markers are now loaded dynamically via Socket.IO
// Initial loading is handled by player_position_update events
// See player_update_handler.html for marker creation logic

View File

@@ -0,0 +1,64 @@
// Helper function to create player popup content
function createPlayerPopup(steamid, player) {
const healthMax = 150;
const healthPercent = Math.round((player.health / healthMax) * 100);
// Status badge
let statusBadge = '🟢 Online';
let statusColor = '#66ff66';
if (player.in_limbo) {
statusBadge = '💀 Dead';
statusColor = '#ff6666';
} else if (!player.is_initialized) {
statusBadge = '🔄 Spawning';
statusColor = '#ffaa66';
}
// Permission badge
let permissionBadge = '';
if (player.permission_level === 0) {
permissionBadge = '<span style="background: #ff0000; color: white; padding: 2px 6px; border-radius: 3px; font-size: 0.85em; font-weight: bold;">🛡️ ADMIN</span>';
}
// Use template literal for clean HTML
return `
<div style="min-width: 280px; font-family: monospace;">
<b style="font-size: 1.1em;">${player.name}</b>
${permissionBadge ? '<br>' + permissionBadge : ''}
<br><span style="font-size: 0.9em; color: ${statusColor};">${statusBadge}</span>
<br><hr style="margin: 5px 0; border-color: #333;">
<b>❤️ Health:</b> ${player.health}/${healthMax} (${healthPercent}%)
<br><b>⭐ Level:</b> ${player.level} | <b>🎯 Score:</b> ${player.score}
<br><b>🧟 Zombies:</b> ${player.zombies} | <b>💀 Deaths:</b> ${player.deaths}
<br><b>👥 Players:</b> ${player.players} | <b>📡 Ping:</b> ${player.ping}ms
<br><hr style="margin: 8px 0; border-color: #333;">
<div style="display: flex; gap: 5px; margin-bottom: 5px;">
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer; flex: 1;">
<input type="checkbox"
${player.is_muted ? 'checked' : ''}
onchange="togglePlayerMuteFromMap('${steamid}', this.checked)"
style="cursor: pointer; width: 16px; height: 16px;" />
<span style="font-weight: bold; font-size: 0.9em;">🔇 Muted</span>
</label>
<label style="display: flex; align-items: center; gap: 5px; cursor: pointer; flex: 1;">
<input type="checkbox"
${player.is_authenticated ? 'checked' : ''}
onchange="togglePlayerAuthFromMap('${steamid}', this.checked)"
style="cursor: pointer; width: 16px; height: 16px;" />
<span style="font-weight: bold; font-size: 0.9em;">✅ Auth</span>
</label>
</div>
<div style="display: flex; gap: 5px; margin-bottom: 5px;">
<button onclick="messagePlayerFromMap('${steamid}', '${player.name.replace(/'/g, "\\'")}', event)"
style="flex: 1; padding: 6px 10px; background: var(--lcars-anakiwa); color: #000; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 0.9em;">
💬 Message</button>
<button onclick="kickPlayerFromMap('${steamid}', '${player.name.replace(/'/g, "\\'")}', event)"
style="flex: 1; padding: 6px 10px; background: var(--lcars-hopbush); color: white; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 0.9em;">
👢 Kick</button>
</div>
<button onclick="teleportPlayerFromMap('${steamid}', '${player.name.replace(/'/g, "\\'")}', event)"
style="width: 100%; padding: 8px 10px; background: var(--lcars-golden-tanoi); color: #000; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; font-size: 0.95em;">
🎯 Teleport Player - Click Map</button>
</div>
`;
}

View File

@@ -0,0 +1,58 @@
// Handle player position updates
if (data.data_type === 'player_position_update' && data.payload) {
const playerData = data.payload;
const steamid = playerData.steamid;
const pos = playerData.position;
console.log('[MAP] Processing player update for:', steamid, pos);
if (!steamid || !pos) {
console.warn('[MAP] Invalid player update data:', playerData);
return;
}
// Update or create player data in players object
if (!players[steamid]) {
players[steamid] = {};
}
// Update all player data
players[steamid].pos = pos;
players[steamid].name = playerData.name || players[steamid].name || 'Player';
players[steamid].level = playerData.level !== undefined ? playerData.level : (players[steamid].level || 0);
players[steamid].health = playerData.health !== undefined ? playerData.health : (players[steamid].health || 0);
players[steamid].zombies = playerData.zombies !== undefined ? playerData.zombies : (players[steamid].zombies || 0);
players[steamid].deaths = playerData.deaths !== undefined ? playerData.deaths : (players[steamid].deaths || 0);
players[steamid].players = playerData.players !== undefined ? playerData.players : (players[steamid].players || 0);
players[steamid].score = playerData.score !== undefined ? playerData.score : (players[steamid].score || 0);
players[steamid].ping = playerData.ping !== undefined ? playerData.ping : (players[steamid].ping || 0);
players[steamid].is_muted = playerData.is_muted !== undefined ? playerData.is_muted : (players[steamid].is_muted || false);
players[steamid].is_authenticated = playerData.is_authenticated !== undefined ? playerData.is_authenticated : (players[steamid].is_authenticated || false);
players[steamid].in_limbo = playerData.in_limbo !== undefined ? playerData.in_limbo : (players[steamid].in_limbo || false);
players[steamid].is_initialized = playerData.is_initialized !== undefined ? playerData.is_initialized : (players[steamid].is_initialized || false);
players[steamid].permission_level = playerData.permission_level || players[steamid].permission_level || null;
players[steamid].dataset = playerData.dataset || players[steamid].dataset || '';
// Update or create marker
if (playerMarkers[steamid]) {
// Update existing marker position and popup
playerMarkers[steamid].setLatLng([pos.x, pos.z]);
playerMarkers[steamid].setPopupContent(createPlayerPopup(steamid, players[steamid]));
console.log('[MAP] Updated player marker:', steamid);
} else {
// Create new marker
const marker = L.circleMarker([pos.x, pos.z], {
radius: 5,
fillColor: '#66ccff',
color: '#0099ff',
weight: 2,
opacity: 1,
fillOpacity: 0.9,
className: 'player-marker'
}).addTo(map);
marker.bindPopup(createPlayerPopup(steamid, players[steamid]));
playerMarkers[steamid] = marker;
console.log('[MAP] Created new player marker:', steamid);
}
}