feat: scroll-wheel zoom and click-to-select in radar

- Replace range dropdown with smooth scroll-wheel zoom on canvas
- Click dot on canvas to select it (white ring + name label)
- Click row in entity list to select on canvas
- Click again to deselect
- Selected row highlighted with blue accent in list
- Auto-scrolls list to keep selected row visible

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-08 12:28:09 +02:00
parent 359d255730
commit 60eab15fff
2 changed files with 93 additions and 38 deletions

View file

@ -3624,26 +3624,15 @@ function showRadarWindow(name) {
const controls = document.createElement('div');
controls.className = 'radar-controls';
const rangeLabel = document.createElement('label');
rangeLabel.textContent = 'Range: ';
const rangeSelect = document.createElement('select');
rangeSelect.className = 'radar-range-select';
[
{ value: '0.2', label: 'Close (~50m)' },
{ value: '0.5', label: 'Medium (~120m)' },
{ value: '1.0', label: 'Far (~240m)' },
{ value: '2.0', label: 'Very Far (~480m)' },
].forEach(opt => {
const o = document.createElement('option');
o.value = opt.value;
o.textContent = opt.label;
if (opt.value === '0.5') o.selected = true;
rangeSelect.appendChild(o);
});
rangeLabel.appendChild(rangeSelect);
controls.appendChild(rangeLabel);
const rangeDisplay = document.createElement('span');
rangeDisplay.className = 'radar-range-display';
rangeDisplay.textContent = 'Range: ~120m';
controls.appendChild(rangeDisplay);
// Compass is always heading-up (player facing direction = up)
const zoomHint = document.createElement('span');
zoomHint.className = 'radar-zoom-hint';
zoomHint.textContent = 'Scroll to zoom';
controls.appendChild(zoomHint);
content.appendChild(controls);
@ -3671,7 +3660,35 @@ function showRadarWindow(name) {
// Store refs on the window
win._radarCanvas = canvas;
win._radarListBody = listBody;
win._radarRangeSelect = rangeSelect;
win._radarRange = RADAR_DEFAULT_RANGE;
win._radarRangeDisplay = rangeDisplay;
win._radarSelectedId = null;
win._radarLastObjects = []; // cache for click hit-testing
// Scroll-wheel zoom on canvas
canvas.addEventListener('wheel', (e) => {
e.preventDefault();
const factor = e.deltaY > 0 ? 1.25 : 0.8;
win._radarRange = Math.max(0.02, Math.min(5.0, win._radarRange * factor));
const meters = Math.round(win._radarRange * 240);
win._radarRangeDisplay.textContent = `Range: ~${meters}m`;
});
// Click canvas to select nearest object
canvas.addEventListener('click', (e) => {
const rect = canvas.getBoundingClientRect();
const mx = (e.clientX - rect.left) * (canvas.width / rect.width);
const my = (e.clientY - rect.top) * (canvas.height / rect.height);
// Find closest rendered dot
let closest = null;
let closestDist = 20; // max pixel distance to select
(win._radarLastObjects || []).forEach(obj => {
if (obj._px === undefined) return;
const d = Math.sqrt((mx - obj._px) ** 2 + (my - obj._py) ** 2);
if (d < closestDist) { closestDist = d; closest = obj; }
});
win._radarSelectedId = closest ? closest.id : null;
});
// Send start_radar command
if (socket && socket.readyState === WebSocket.OPEN) {
@ -3686,7 +3703,8 @@ function updateRadarWindow(msg) {
const canvas = win._radarCanvas;
const listBody = win._radarListBody;
const range = parseFloat(win._radarRangeSelect.value) || RADAR_DEFAULT_RANGE;
const range = win._radarRange || RADAR_DEFAULT_RANGE;
const selectedId = win._radarSelectedId;
const objects = msg.objects || [];
const playerEW = msg.player_ew;
@ -3757,40 +3775,54 @@ function updateRadarWindow(msg) {
// Rotate world by heading so player facing = up on canvas
const rotAngle = headingRad;
// Draw objects
// Draw objects and cache positions for click hit-testing
const cosA = Math.cos(rotAngle);
const sinA = Math.sin(rotAngle);
objects.forEach(obj => {
const dEW = obj.ew - playerEW;
const dNS = obj.ns - playerNS;
// Rotate world by negative heading so player facing = up
const cosA = Math.cos(rotAngle);
const sinA = Math.sin(rotAngle);
const dx = dEW * cosA - dNS * sinA;
const dy = -(dEW * sinA + dNS * cosA);
const px = cx + dx * scale;
const py = cy + dy * scale;
// Store pixel position for click hit-testing
obj._px = px;
obj._py = py;
// Clip to circle
const distFromCenter = Math.sqrt((px - cx) ** 2 + (py - cy) ** 2);
if (distFromCenter > cx - 4) return;
const color = RADAR_COLORS[obj.object_class] || '#999';
const dotSize = (obj.object_class === 'Monster' || obj.object_class === 'Player') ? 4 : 3;
const isSelected = obj.id === selectedId;
const dotSize = isSelected ? 6 : ((obj.object_class === 'Monster' || obj.object_class === 'Player') ? 4 : 3);
// Selection ring
if (isSelected) {
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(px, py, dotSize + 3, 0, Math.PI * 2);
ctx.stroke();
}
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(px, py, dotSize, 0, Math.PI * 2);
ctx.fill();
// Label for players and portals
if (obj.object_class === 'Player' || obj.object_class === 'Portal') {
ctx.fillStyle = color;
// Label for players, portals, and selected object
if (obj.object_class === 'Player' || obj.object_class === 'Portal' || isSelected) {
ctx.fillStyle = isSelected ? '#fff' : color;
ctx.font = '9px monospace';
ctx.textAlign = 'left';
ctx.fillText(obj.name, px + 6, py + 3);
}
});
win._radarLastObjects = objects;
// Player dot (center)
ctx.fillStyle = '#ffcc00';
@ -3818,11 +3850,21 @@ function updateRadarWindow(msg) {
});
withDist.sort((a, b) => a.distMeters - b.distMeters);
// Rebuild list (simple approach — runs 1/sec so fine)
// Rebuild list with selection support
listBody.innerHTML = '';
let selectedRow = null;
withDist.forEach(obj => {
const row = document.createElement('div');
row.className = 'radar-entity-row';
if (obj.id === selectedId) {
row.classList.add('radar-entity-selected');
selectedRow = row;
}
// Click row to select on canvas
row.addEventListener('click', () => {
win._radarSelectedId = (win._radarSelectedId === obj.id) ? null : obj.id;
});
const colorDot = document.createElement('span');
colorDot.className = 're-color';
@ -3832,9 +3874,6 @@ function updateRadarWindow(msg) {
const nameSpan = document.createElement('span');
nameSpan.className = 're-name';
nameSpan.textContent = obj.name;
if (obj.health_pct !== null && obj.health_pct !== undefined) {
nameSpan.textContent += ` (${obj.health_pct}%)`;
}
row.appendChild(nameSpan);
const typeSpan = document.createElement('span');
@ -3856,6 +3895,9 @@ function updateRadarWindow(msg) {
listBody.appendChild(row);
});
// Scroll selected row into view
if (selectedRow) selectedRow.scrollIntoView({ block: 'nearest' });
}
function compassDir(angleDeg) {

View file

@ -2533,14 +2533,17 @@ table.ts-allegiance td:first-child {
color: #ccc;
}
.radar-range-select {
background: #222;
color: #ccc;
border: 1px solid #444;
padding: 2px 4px;
.radar-range-display {
color: #aaa;
font-size: 0.75rem;
}
.radar-zoom-hint {
color: #555;
font-size: 0.7rem;
font-style: italic;
}
.radar-canvas {
display: block;
margin: 0 auto;
@ -2578,6 +2581,16 @@ table.ts-allegiance td:first-child {
.radar-entity-row:hover {
background: #1a1a2a;
cursor: pointer;
}
.radar-entity-selected {
background: #1a2a3a;
border-left: 2px solid #4488ff;
}
.radar-entity-selected:hover {
background: #1a2a3a;
}
.re-color {