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:
parent
359d255730
commit
60eab15fff
2 changed files with 93 additions and 38 deletions
108
static/script.js
108
static/script.js
|
|
@ -3624,26 +3624,15 @@ function showRadarWindow(name) {
|
||||||
const controls = document.createElement('div');
|
const controls = document.createElement('div');
|
||||||
controls.className = 'radar-controls';
|
controls.className = 'radar-controls';
|
||||||
|
|
||||||
const rangeLabel = document.createElement('label');
|
const rangeDisplay = document.createElement('span');
|
||||||
rangeLabel.textContent = 'Range: ';
|
rangeDisplay.className = 'radar-range-display';
|
||||||
const rangeSelect = document.createElement('select');
|
rangeDisplay.textContent = 'Range: ~120m';
|
||||||
rangeSelect.className = 'radar-range-select';
|
controls.appendChild(rangeDisplay);
|
||||||
[
|
|
||||||
{ 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);
|
|
||||||
|
|
||||||
// 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);
|
content.appendChild(controls);
|
||||||
|
|
||||||
|
|
@ -3671,7 +3660,35 @@ function showRadarWindow(name) {
|
||||||
// Store refs on the window
|
// Store refs on the window
|
||||||
win._radarCanvas = canvas;
|
win._radarCanvas = canvas;
|
||||||
win._radarListBody = listBody;
|
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
|
// Send start_radar command
|
||||||
if (socket && socket.readyState === WebSocket.OPEN) {
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
||||||
|
|
@ -3686,7 +3703,8 @@ function updateRadarWindow(msg) {
|
||||||
|
|
||||||
const canvas = win._radarCanvas;
|
const canvas = win._radarCanvas;
|
||||||
const listBody = win._radarListBody;
|
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 objects = msg.objects || [];
|
||||||
|
|
||||||
const playerEW = msg.player_ew;
|
const playerEW = msg.player_ew;
|
||||||
|
|
@ -3757,40 +3775,54 @@ function updateRadarWindow(msg) {
|
||||||
// Rotate world by heading so player facing = up on canvas
|
// Rotate world by heading so player facing = up on canvas
|
||||||
const rotAngle = headingRad;
|
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 => {
|
objects.forEach(obj => {
|
||||||
const dEW = obj.ew - playerEW;
|
const dEW = obj.ew - playerEW;
|
||||||
const dNS = obj.ns - playerNS;
|
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 dx = dEW * cosA - dNS * sinA;
|
||||||
const dy = -(dEW * sinA + dNS * cosA);
|
const dy = -(dEW * sinA + dNS * cosA);
|
||||||
|
|
||||||
const px = cx + dx * scale;
|
const px = cx + dx * scale;
|
||||||
const py = cy + dy * scale;
|
const py = cy + dy * scale;
|
||||||
|
|
||||||
|
// Store pixel position for click hit-testing
|
||||||
|
obj._px = px;
|
||||||
|
obj._py = py;
|
||||||
|
|
||||||
// Clip to circle
|
// Clip to circle
|
||||||
const distFromCenter = Math.sqrt((px - cx) ** 2 + (py - cy) ** 2);
|
const distFromCenter = Math.sqrt((px - cx) ** 2 + (py - cy) ** 2);
|
||||||
if (distFromCenter > cx - 4) return;
|
if (distFromCenter > cx - 4) return;
|
||||||
|
|
||||||
const color = RADAR_COLORS[obj.object_class] || '#999';
|
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.fillStyle = color;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(px, py, dotSize, 0, Math.PI * 2);
|
ctx.arc(px, py, dotSize, 0, Math.PI * 2);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
// Label for players and portals
|
// Label for players, portals, and selected object
|
||||||
if (obj.object_class === 'Player' || obj.object_class === 'Portal') {
|
if (obj.object_class === 'Player' || obj.object_class === 'Portal' || isSelected) {
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = isSelected ? '#fff' : color;
|
||||||
ctx.font = '9px monospace';
|
ctx.font = '9px monospace';
|
||||||
ctx.textAlign = 'left';
|
ctx.textAlign = 'left';
|
||||||
ctx.fillText(obj.name, px + 6, py + 3);
|
ctx.fillText(obj.name, px + 6, py + 3);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
win._radarLastObjects = objects;
|
||||||
|
|
||||||
// Player dot (center)
|
// Player dot (center)
|
||||||
ctx.fillStyle = '#ffcc00';
|
ctx.fillStyle = '#ffcc00';
|
||||||
|
|
@ -3818,11 +3850,21 @@ function updateRadarWindow(msg) {
|
||||||
});
|
});
|
||||||
withDist.sort((a, b) => a.distMeters - b.distMeters);
|
withDist.sort((a, b) => a.distMeters - b.distMeters);
|
||||||
|
|
||||||
// Rebuild list (simple approach — runs 1/sec so fine)
|
// Rebuild list with selection support
|
||||||
listBody.innerHTML = '';
|
listBody.innerHTML = '';
|
||||||
|
let selectedRow = null;
|
||||||
withDist.forEach(obj => {
|
withDist.forEach(obj => {
|
||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'radar-entity-row';
|
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');
|
const colorDot = document.createElement('span');
|
||||||
colorDot.className = 're-color';
|
colorDot.className = 're-color';
|
||||||
|
|
@ -3832,9 +3874,6 @@ function updateRadarWindow(msg) {
|
||||||
const nameSpan = document.createElement('span');
|
const nameSpan = document.createElement('span');
|
||||||
nameSpan.className = 're-name';
|
nameSpan.className = 're-name';
|
||||||
nameSpan.textContent = obj.name;
|
nameSpan.textContent = obj.name;
|
||||||
if (obj.health_pct !== null && obj.health_pct !== undefined) {
|
|
||||||
nameSpan.textContent += ` (${obj.health_pct}%)`;
|
|
||||||
}
|
|
||||||
row.appendChild(nameSpan);
|
row.appendChild(nameSpan);
|
||||||
|
|
||||||
const typeSpan = document.createElement('span');
|
const typeSpan = document.createElement('span');
|
||||||
|
|
@ -3856,6 +3895,9 @@ function updateRadarWindow(msg) {
|
||||||
|
|
||||||
listBody.appendChild(row);
|
listBody.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Scroll selected row into view
|
||||||
|
if (selectedRow) selectedRow.scrollIntoView({ block: 'nearest' });
|
||||||
}
|
}
|
||||||
|
|
||||||
function compassDir(angleDeg) {
|
function compassDir(angleDeg) {
|
||||||
|
|
|
||||||
|
|
@ -2533,14 +2533,17 @@ table.ts-allegiance td:first-child {
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.radar-range-select {
|
.radar-range-display {
|
||||||
background: #222;
|
color: #aaa;
|
||||||
color: #ccc;
|
|
||||||
border: 1px solid #444;
|
|
||||||
padding: 2px 4px;
|
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.radar-zoom-hint {
|
||||||
|
color: #555;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
.radar-canvas {
|
.radar-canvas {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
@ -2578,6 +2581,16 @@ table.ts-allegiance td:first-child {
|
||||||
|
|
||||||
.radar-entity-row:hover {
|
.radar-entity-row:hover {
|
||||||
background: #1a1a2a;
|
background: #1a1a2a;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radar-entity-selected {
|
||||||
|
background: #1a2a3a;
|
||||||
|
border-left: 2px solid #4488ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.radar-entity-selected:hover {
|
||||||
|
background: #1a2a3a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.re-color {
|
.re-color {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue