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');
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue