Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions map.html
Original file line number Diff line number Diff line change
Expand Up @@ -1401,6 +1401,10 @@ <h3>Layers</h3>
<input type="checkbox" id="dev-mob-spawn-labels">
Technical spawn labels
</label>
<label class="layer-toggle" title="When hovering one enemy, open tooltips for every enemy in the same spawn set (SG). Off = only the hovered enemy.">
<input type="checkbox" id="dev-sg-open-all-tooltips">
Open all spawn-set tooltips on hover
</label>
<label class="layer-toggle">
<input type="checkbox" id="layer-radii">
Aggro / link radius
Expand All @@ -1416,6 +1420,7 @@ <h3>Layers</h3>
<button type="button" id="btn-expand-collapse" class="dev-action-btn" style="width:100%;margin-top:6px">Expand all groups</button>
<div class="dev-help">
Technical labels: click chip to expand · middle-click hull to collapse<br>
Spawn-set tooltips: hover one enemy to open every tooltip in that SG<br>
Aggro / link radius: click a spawn on the map<br>
Alt+click map to log world coordinates
</div>
Expand Down
133 changes: 96 additions & 37 deletions map.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ L.Control.SpawnTimeFilter = L.Control.extend({
saveSpawnTimeFilter();
this._updateActive();
applySubGroupFilter();
refreshSpotSearchFromMobTypes();
});
this._buttons.push({ btn, filter: mode.filter });
}
Expand Down Expand Up @@ -553,6 +554,7 @@ const DEV_PREFS_KEY = 'ddon-dev-prefs';
const DEV_PANEL_KEY = 'ddon-dev-panel-open';

let _devMobSpawnLabels = false;
let _devSgOpenAllTooltips = false;
const DYNAMIC_ENEMY_LABEL = 'Dynamic Enemy';
let _enemySpawnDataLoaded = false;

Expand All @@ -573,7 +575,10 @@ const readMobSpawnLabelsPref = (devPrefs) => {
};

const saveDevPrefs = () => {
const prefs = { mobSpawnLabels: _devMobSpawnLabels };
const prefs = {
mobSpawnLabels: _devMobSpawnLabels,
sgOpenAllTooltips: _devSgOpenAllTooltips,
};
try { localStorage.setItem(DEV_PREFS_KEY, JSON.stringify(prefs)); } catch { /* ignore */ }
};

Expand Down Expand Up @@ -1429,7 +1434,7 @@ function _doSpread(markers, overlayLayer) {
.bindTooltip(`<b>${N} stacked here:</b><br>${stackLines.join('<br>')}`,
{ direction: 'top', offset: [0, -8] })
.addTo(overlayLayer);
anchor.on('mouseover', function() { _applyHighlight(group); });
anchor.on('mouseover', function() { _applyHighlight(group, { openTooltips: false }); });
anchor.on('mouseout', _unhighlightSG);

group.forEach((m, i) => {
Expand Down Expand Up @@ -3066,7 +3071,7 @@ function buildGroupDetails(g) {
else _enemySpawnPromise.then(applyBossStyle).catch(() => {});

marker
.on('mouseover', function() { _highlightSG(this._sgKey); })
.on('mouseover', function() { _highlightSG(this._sgKey, this); })
.on('mouseout', _unhighlightSG)
.on('click', function() { _radiiClickConsumed = true; showSpawnRadii(this); });
layer.addLayer(marker);
Expand Down Expand Up @@ -3298,25 +3303,38 @@ function _clearHighlight() {
_highlightedSet.clear();
}

function _applyHighlight(markers) {
function _applyHighlight(markers, { openTooltips = true, tooltipMarker = null } = {}) {
clearTimeout(_unhighlightTimer);
_clearHighlight(); // synchronously reset any previously lit markers
for (const m of markers) {
if (m._hidden) continue;
m.setStyle({ weight: 4, fillOpacity: 1.0, color: '#ffffff' });
m.setRadius(9);
m.openTooltip();
_highlightedSet.add(m);
if (m._spreadAnchor) {
m._spreadAnchor.setRadius(8);
m._spreadAnchor.setStyle({ weight: 2.5, fillOpacity: 1.0 });
}
if (m._spokeLine) m._spokeLine.setStyle({ weight: 2.5, opacity: 1.0, dashArray: null });
}
// Only open the hovered marker's tooltip — opening every spawn-set tooltip at once
// stacks dialogs over the map when a set has many enemies.
if (tooltipMarker && !tooltipMarker._hidden) tooltipMarker.openTooltip();
else if (openTooltips) {
for (const m of markers) {
if (!m._hidden) m.openTooltip();
}
}
}

function _highlightSG(sgKey) {
_applyHighlight((_sgMarkers[sgKey] || []).filter(m => !m._hidden));
function _highlightSG(sgKey, hoveredMarker = null) {
const openAll = _devSgOpenAllTooltips;
_applyHighlight(
(_sgMarkers[sgKey] || []).filter(m => !m._hidden),
openAll
? { openTooltips: true }
: { openTooltips: false, tooltipMarker: hoveredMarker },
);
}

function _unhighlightSG() {
Expand Down Expand Up @@ -5124,7 +5142,7 @@ const detectActivePreset = () => {
const _PRESET_SHORT_LABEL = {
revival: 'Revival',
rising: 'Rising',
'rising-endgame': 'Rising',
'rising-endgame': 'Rising Endgame',
custom: 'Custom',
};

Expand Down Expand Up @@ -7988,7 +8006,11 @@ function _navigateToSpot(target) {
if (targetFloor !== null && targetFloor !== currentLayer) _switchToFloor(targetFloor);
}
if (target.type === 'enemy' && target.groupId) {
if (_spotOpenedGroup && _spotOpenedGroup !== target.groupId) collapseGroup(_spotOpenedGroup);
// Legacy chip mode expands one group at a time. Player mode keeps all groups
// expanded — collapsing the previous spot would hide its enemies (empty yellow
// highlight rings remain on hover until Technical Spawn Labels re-expands).
if (useLegacyGroupChips() && _spotOpenedGroup && _spotOpenedGroup !== target.groupId)
collapseGroup(_spotOpenedGroup);
_spotOpenedGroup = target.groupId;
expandGroup(target.groupId);
const g = _groupStore.get(target.groupId);
Expand Down Expand Up @@ -8021,12 +8043,13 @@ function _navigateToSpot(target) {
// Fly to and highlight the specific marker position, not just the group centroid
const focusLatLng = targetMarker?.getLatLng() ?? target.latlng;
leafletMap.flyTo(focusLatLng, Math.max(leafletMap.getZoom(), 2), { duration: 0.4 });
_clearSpotHighlights();
_clearAllSpotHoverEffects();
_addSpotHighlight(focusLatLng);
if (targetMarker) setTimeout(() => targetMarker.openPopup(), 450);
} else if (target.type === 'item' && target.source === 'enemy' && target.groupId) {
// Item — enemy drop: reuse full enemy navigation
if (_spotOpenedGroup && _spotOpenedGroup !== target.groupId) collapseGroup(_spotOpenedGroup);
if (useLegacyGroupChips() && _spotOpenedGroup && _spotOpenedGroup !== target.groupId)
collapseGroup(_spotOpenedGroup);
_spotOpenedGroup = target.groupId;
expandGroup(target.groupId);
const g = _groupStore.get(target.groupId);
Expand All @@ -8053,13 +8076,13 @@ function _navigateToSpot(target) {
}
const focusLatLng = targetMarker?.getLatLng() ?? target.latlng;
leafletMap.flyTo(focusLatLng, Math.max(leafletMap.getZoom(), 2), { duration: 0.4 });
_clearSpotHighlights();
_clearAllSpotHoverEffects();
_addSpotHighlight(focusLatLng);
if (targetMarker) setTimeout(() => targetMarker.openPopup(), 450);
} else {
// Gather, shop item, or generic fallback
leafletMap.flyTo(target.latlng, Math.max(leafletMap.getZoom(), 2), { duration: 0.4 });
_clearSpotHighlights();
_clearAllSpotHoverEffects();
_addSpotHighlight(target.latlng);
if ((target.type === 'gather' || target.source === 'gather') && target.nodeKey) {
const m = _gatherMarkerByKey.get(target.nodeKey);
Expand Down Expand Up @@ -8939,10 +8962,23 @@ const meetsSpotMobTypeFilter = (entry) => {
return [...tags].some(t => _mobTypeFilters[t]);
};

/** Hide spot rows whose enemy isn't visible under the current Day/Night filter. */
const meetsSpotSpawnTimeFilter = (entry) => {
if (!_activeSpawnTimeFilter) return true;
if (entry.type !== 'enemy' && entry.source !== 'enemy') return true;
if (!_enemySpawnDataLoaded || !entry.spawnKey || !_enemySpawnCache) return true;
const all = _enemySpawnCache.get(entry.spawnKey) ?? [];
if (!all.length) return true; // dynamic / unknown slot
const visible = filterEntriesBySpawnTime(all);
if (entry.emCode) return visible.some((e) => e.emCode === entry.emCode && e.lv);
return visible.some((e) => !!e.lv);
};

const matchesSpotCriteria = (entry, criteria) =>
meetsSpotType(entry, criteria.filter)
&& meetsSpotLevelFilter(entry, criteria.filter, criteria)
&& meetsSpotMobTypeFilter(entry)
&& meetsSpotSpawnTimeFilter(entry)
&& (criteria.multiTerm || _spotEntryMatches(entry, criteria.term, criteria.exact));

const spotEnemyBaseName = (entry) => {
Expand Down Expand Up @@ -9035,6 +9071,7 @@ function collectMultiMobStageMatches(criteria, scope) {

if (!meetsSpotLevelFilter(entry, 'enemy', criteria)) continue;
if (!meetsSpotMobTypeFilter(entry)) continue;
if (!meetsSpotSpawnTimeFilter(entry)) continue;

let termHit = false;
for (const term of terms) {
Expand Down Expand Up @@ -9094,7 +9131,9 @@ function findRosterEnemySpots(row) {
if (e.type !== 'enemy') return false;
if (spotEnemyBaseName(e).toLowerCase() !== want) return false;
if (spotEnemyLevelKey(e) !== row.levelKey) return false;
return meetsSpotMobTypeFilter(e);
if (!meetsSpotMobTypeFilter(e)) return false;
if (!meetsSpotSpawnTimeFilter(e)) return false;
return true;
});
}

Expand Down Expand Up @@ -9170,7 +9209,8 @@ function buildStageGatherRoster() {

function findRosterItemSpots(row) {
return _spotIndex.filter((e) =>
e.type === 'item' && e.source === row.source && e.name === row.name);
e.type === 'item' && e.source === row.source && e.name === row.name
&& meetsSpotSpawnTimeFilter(e));
}

function rosterItemLocationKey(entry) {
Expand Down Expand Up @@ -9439,32 +9479,50 @@ function wireSpotRowNavigation(row, items, navigate) {
});
}

const SPOT_HOVER_HL_CAP = 80; // max yellow rings lit on a single row hover

function _clearAllSpotHoverEffects() {
_clearSpotHighlights();
_clearHighlight();
clearTimeout(_gatherHighlightTimer);
_clearGatherHighlight();
}

function _highlightSpotItems(items) {
// Yellow pulsing rings (same effect Gathering always used on list hover).
const drawnGroups = new Set();
let ringCount = 0;

for (const item of items) {
if (ringCount >= SPOT_HOVER_HL_CAP) break;
if (_currentFloorObbs && item.worldPos) {
const f = getEnemyFloor(item.worldPos.x, item.worldPos.y, item.worldPos.z, _currentFloorObbs);
if (f !== null && f !== currentLayer) continue;
}
const grp = item.groupId ? _groupStore.get(item.groupId) : null;
if (item.type === 'enemy' && grp && !grp.isExpanded) {
if (!drawnGroups.has(item.groupId)) {
drawnGroups.add(item.groupId);
_addChipHighlight(grp);
}
} else {
_addSpotHighlight(_resolveSpotLatLng(item));

const grp = item.groupId != null ? _groupStore.get(String(item.groupId)) : null;

// Always ring the spawn position. Prefer the live marker when expanded (spread);
// otherwise the indexed latlng — never the collapsed chip centroid.
const latlng = (grp?.isExpanded ? _resolveSpotLatLng(item) : null) ?? item.latlng;
if (!latlng) continue;
_addSpotHighlight(latlng);
ringCount++;

if (useLegacyGroupChips() && grp && !grp.isExpanded && !drawnGroups.has(item.groupId)) {
drawnGroups.add(item.groupId);
_addChipHighlight(grp);
}
}
}

function wireSpotRowHoverHighlight(row, items) {
if (!items.length) return;
row.addEventListener('mouseenter', () => {
_clearSpotHighlights();
_clearAllSpotHoverEffects();
_highlightSpotItems(items);
});
row.addEventListener('mouseleave', _clearSpotHighlights);
row.addEventListener('mouseleave', _clearAllSpotHoverEffects);
}

function _renderStageRoster(resultsEl, {
Expand Down Expand Up @@ -9497,8 +9555,6 @@ function _renderStageRoster(resultsEl, {
+ `${summaryNoun}${uniqueCount !== 1 ? 's' : ''} on this stage · click to go`;
frag.appendChild(summary);

const enableHoverHl = uniqueCount <= 50;

for (const { row, items } of rows) {
const el = document.createElement('div');
el.className = 'spot-result-row spot-roster-row';
Expand All @@ -9508,7 +9564,7 @@ function _renderStageRoster(resultsEl, {
iconForRow(row, items)
+ `<span class="spot-result-name">${row.displayName}</span>`;
wireSpotRowNavigation(el, items, _navigateToSpot);
if (enableHoverHl) wireSpotRowHoverHighlight(el, items);
wireSpotRowHoverHighlight(el, items);
frag.appendChild(el);
}

Expand Down Expand Up @@ -9612,7 +9668,7 @@ function _runSpotSearch() {
if (!resultsEl) return;

const criteria = readSpotSearchCriteria();
_clearSpotHighlights();
_clearAllSpotHoverEffects();

if (_spotGlobal) {
if (_globalSpotIndexPromise && !_globalSpotIndexReady) {
Expand Down Expand Up @@ -9696,8 +9752,6 @@ function _runSpotSearch() {
summary.textContent = summaryText;
frag.appendChild(summary);

const enableHoverHl = totalGroups <= 50;

for (const rawItems of cappedGroups) {
const items = orderSpotGroupItems(rawItems, 'local', criteria);
const first = items[0];
Expand All @@ -9722,10 +9776,7 @@ function _runSpotSearch() {
row.appendChild(preview);
}
wireSpotRowNavigation(row, items, _navigateToSpot);

if (enableHoverHl) {
wireSpotRowHoverHighlight(row, items);
}
wireSpotRowHoverHighlight(row, items);

frag.appendChild(row);
}
Expand Down Expand Up @@ -9813,7 +9864,7 @@ function _renderGlobalResults(matches, resultsEl, criteria = null) {
panel.classList.remove('open');
toggle.style.display = '';
cancelSpotSetOriginMode();
_clearSpotHighlights();
_clearAllSpotHoverEffects();
};

toggle.addEventListener('click', openPanel);
Expand Down Expand Up @@ -9892,7 +9943,7 @@ function _renderGlobalResults(matches, resultsEl, criteria = null) {
document.querySelectorAll('.spot-scope').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
_spotGlobal = btn.dataset.scope === 'global';
_clearSpotHighlights();
_clearAllSpotHoverEffects();
if (_spotGlobal && !_globalSpotIndexReady) {
_buildGlobalSpotIndex().then(() => _runSpotSearch());
} else {
Expand Down Expand Up @@ -11538,9 +11589,12 @@ function initDevPanel() {

const devPrefs = loadDevPrefs();
_devMobSpawnLabels = readMobSpawnLabelsPref(devPrefs);
_devSgOpenAllTooltips = !!devPrefs.sgOpenAllTooltips;

const mobLabelsEl = document.getElementById('dev-mob-spawn-labels');
if (mobLabelsEl) mobLabelsEl.checked = _devMobSpawnLabels;
const sgTooltipsEl = document.getElementById('dev-sg-open-all-tooltips');
if (sgTooltipsEl) sgTooltipsEl.checked = _devSgOpenAllTooltips;
applyDevDisplayPrefs();

mobLabelsEl?.addEventListener('change', () => {
Expand All @@ -11555,6 +11609,11 @@ function initDevPanel() {
saveLayerPrefs();
});

sgTooltipsEl?.addEventListener('change', () => {
_devSgOpenAllTooltips = sgTooltipsEl.checked;
saveDevPrefs();
});

const setPanelOpen = (open) => {
devPanel.classList.toggle('expanded', open);
panelToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
Expand Down