Identify ores with the cursor: pinged veins, ores-only label, vein outline (t77)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 17:43:49 -04:00
co-authored by Claude Opus 5.5
parent f4ca1cae57
commit f193b1c76c
11 changed files with 214 additions and 23 deletions
+47
View File
@@ -0,0 +1,47 @@
// Hover work is shared by the cursor label and the vein outline.
const CURSOR_ORES = new Uint8Array(NMAT);
for (let m = 0; m < NMAT; m++) if (PING_VEINS[m]) { CURSOR_ORES[m] = 1; CURSOR_ORES[YLD[m]] = 1; }
function oreLike(m, bar) { return !!(bar || CURSOR_ORES[m] || SOLIDMETAL[m]); }
function bitNeeded(m) {
if (HARD[m] > 6) return 'cannot be drilled';
return 'needs ' + BIT_TIERS.find(b => b.hard >= HARD[m]).name.toLowerCase();
}
function pingVisible(x, y) {
const p = GAME.ping;
if (!p || x < 0 || y < 0 || x >= W || y >= H) return false;
if (!p.cellSet) p.cellSet = new Set(p.cells);
if (!p.cellSet.has(y * W + x)) return false;
const start = Math.hypot(x - p.x, y - p.y) / PING_SPEED * 1000, ms = performance.now() - p.t0;
return ms >= start && ms < start + PING_HOLD + PING_FADE;
}
function cursorVein(i) {
const m = mat[i], cells = new Set([i]), queue = [i], edges = [];
const same = q => q >= 0 && q < N && mat[q] === m && (flags[q] & F_NAT);
const add = n => { if (cells.size < 6000 && same(n) && !cells.has(n)) { cells.add(n); queue.push(n); } };
for (let k = 0; k < queue.length; k++) {
const q = queue[k], x = q % W;
if (x) add(q - 1);
if (x < W - 1) add(q + 1);
add(q - W); add(q + W);
}
for (const q of cells) {
const x = q % W, y = (q / W) | 0;
if (!x || !same(q - 1)) edges.push([x, y, x, y + 1]);
if (x === W - 1 || !same(q + 1)) edges.push([x + 1, y, x + 1, y + 1]);
if (!same(q - W)) edges.push([x, y, x + 1, y]);
if (!same(q + W)) edges.push([x, y + 1, x + 1, y + 1]);
}
return { cells, edges, m, world: mat };
}
let cursorVeinCache = null, cursorLabelKey = null, cursorLabelSince = 0;
function hoveredVein(x, y) {
if (x < 0 || y < 0 || x >= W || y >= H) return null;
const i = y * W + x, m = mat[i];
if (!PING_VEINS[m] || !(flags[i] & F_NAT) || (SET.fog && fogSeen(x, y) < 64 && !pingVisible(x, y))) return null;
if (!cursorVeinCache || cursorVeinCache.world !== mat || cursorVeinCache.m !== m || !cursorVeinCache.cells.has(i)) cursorVeinCache = cursorVein(i);
return cursorVeinCache;
}
function cursorLabelAlpha(key, now) {
if (key !== cursorLabelKey) { cursorLabelKey = key; cursorLabelSince = now; }
return Math.max(0, Math.min(1, (2400 - (now - cursorLabelSince)) / 400));
}
+15 -8
View File
@@ -77,7 +77,8 @@ function reactionsOf(m, forNotebook) {
// What is under the cursor: t1 is the name, t2 the details, warn a short problem
// (too hard, out of reach) for the label next to the cursor.
function hoverInfo(ax, ay) {
if (SET.fog && fogSeen(ax, ay) < 64) return { t1: 'Unexplored', t2: [], t3: [] };
const pinged = pingVisible(ax, ay);
if (SET.fog && fogSeen(ax, ay) < 64 && !pinged) return { t1: 'Unexplored', t2: [], t3: [] };
if (ax < 0 || ay < 0 || ax >= W || ay >= H) return { t1: '', t2: ['Outside the world'], warn: '', air: true };
const i = ay * W + ax, m = mat[i];
let warn = '';
@@ -143,8 +144,8 @@ function hoverInfo(ax, ay) {
if (player.lens && !bar) t2.push(fmtT(temp[i]));
if (!bar && m !== EMPTY && PH[m] === PH_SOLID && !PARTMAT[m] && (flags[i] & F_NAT)) {
const ok = HARD[m] <= BIT_TIERS[player.bitTier].hard;
t2.push(HARD[m] > 6 ? 'cannot be drilled' : ok ? `drillable (hardness ${HARD[m]})` : `too hard (${HARD[m]})`);
if (!ok && toolId() === 'drill') warn = HARD[m] > 6 ? 'cannot be drilled' : 'too hard';
t2.push(HARD[m] > 6 ? 'cannot be drilled' : ok ? `drillable (hardness ${HARD[m]})` : bitNeeded(m));
if (!ok && toolId() === 'drill') warn = bitNeeded(m);
}
if (!bar && PH[m] === PH_POWDER) t2.push('loose');
if (!id && !bar && m === EMPTY && bgm[i]) { t1 = bgm[i] === WOOD ? 'Tree trunk' : 'Leaves'; t2.unshift(bgm[i] === WOOD ? 'drill it for wood chunks' : 'water and sand pass through'); }
@@ -163,7 +164,8 @@ function hoverInfo(ax, ay) {
// what it does, unless the cursor is on a machine or mold, which have their own details
const what = bar ? bar.m : !mo && !id && !(p && p !== P_GRATE && p !== P_PIPE) && m !== BELTL && m !== BELTR && m !== PIPE ? (m === EMPTY && bgm[i] ? bgm[i] : m) : 0;
const t3 = what ? reactionsOf(what) : [];
return { t1, t2, t3, warn, contents, air: m === EMPTY && !bgm[i] && !bar && !mo && !id && !p && !b };
if (pinged) t2.push('found by the ping');
return { t1, t2, t3, warn, contents, ore: oreLike(m, bar), m: bar ? bar.m : m, air: m === EMPTY && !bgm[i] && !bar && !mo && !id && !p && !b };
}
function renderInfo() {
const r = hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y));
@@ -173,15 +175,19 @@ function renderInfo() {
}
// Name of what is under the cursor, drawn right next to it
function drawCursorLabel(px, py, r) {
if (!SET.cursorLabel || (!GAME.mouse.in && !GAME.usingPad)) return;
if (SET.cursorLabel === 'off' || (!GAME.mouse.in && !GAME.usingPad)) { cursorLabelKey = null; return; }
const info = hoverInfo(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y));
if (!info.t1 || info.air) return;
if (!info.t1 || (info.air && !info.warn) || (SET.cursorLabel === 'ores' && !info.ore && !info.warn)) { cursorLabelKey = null; return; }
const vein = hoveredVein(Math.floor(GAME.aim.x), Math.floor(GAME.aim.y));
const alpha = cursorLabelAlpha(vein || info.m || info.t1, performance.now());
if (!alpha && !info.warn) return;
const ctx = R.ctx, dpr = R.canvas.width / window.innerWidth;
const fs = Math.round(12 * dpr * SET.uiScale);
if (!R.uiFont) R.uiFont = getComputedStyle(document.documentElement).getPropertyValue('--font-ui').trim() || 'sans-serif';
const main = info.t1 + (info.contents && info.warn ? ` ${info.contents}` : '');
const main = alpha ? info.t1 + (info.t2.includes('found by the ping') ? ' · found by the ping' : '') + (info.contents && info.warn ? ` ${info.contents}` : '') : '';
const sub = info.warn || info.contents || (R.lens ? info.t2.find(t => /°C$/.test(t)) || '' : '');
ctx.save();
ctx.globalAlpha = info.warn ? 1 : alpha;
ctx.font = `600 ${fs}px ${R.uiFont}`;
const w1 = ctx.measureText(main).width;
ctx.font = `500 ${Math.round(fs * 0.9)}px ${R.uiFont}`;
@@ -193,7 +199,8 @@ function drawCursorLabel(px, py, r) {
ctx.fillStyle = 'rgba(20,17,15,0.82)';
ctx.beginPath(); ctx.roundRect(x, y, w1 + w2 + padX * 2, h, 4 * dpr); ctx.fill();
ctx.textBaseline = 'middle';
ctx.font = `600 ${fs}px ${R.uiFont}`; ctx.fillStyle = '#efe6d8'; ctx.fillText(main, x + padX, y + h / 2);
ctx.font = `600 ${fs}px ${R.uiFont}`; ctx.fillStyle = '#efe6d8'; ctx.globalAlpha = alpha; ctx.fillText(main, x + padX, y + h / 2);
ctx.globalAlpha = info.warn ? 1 : alpha;
if (sub) { ctx.font = `500 ${Math.round(fs * 0.9)}px ${R.uiFont}`; ctx.fillStyle = info.warn ? '#ef6a55' : info.contents ? '#efe6d8' : '#ffd27a'; ctx.fillText(sub, x + padX + w1 + fs * 0.6, y + h / 2); }
ctx.restore();
}
+1
View File
@@ -74,6 +74,7 @@ function frame(now) {
if (GAME.infoOverlay) { drawFlowArrows(); drawFlowRates(); }
drawJoinMarks();
drawPing();
drawHoveredVein();
drawPingRecharge();
drawFilterTags(!!GAME.infoOverlay, GAME.editRun ? GAME.editRun[0] : hb);
drawFloaters();
+1 -1
View File
@@ -8,7 +8,7 @@ const SET_TABS = {
{ k: 'uiScale', label: 'Interface size', type: 'range', min: 0.8, max: 1.4, step: 0.05, fmt: v => Math.round(v * 100) + '%' },
{ k: 'fpsCap', label: 'Frame cap', d: 'Uncapped follows your display refresh rate.', type: 'select', opts: [[30, '30 fps'], [60, '60 fps'], [120, '120 fps'], [0, 'Uncapped']] },
{ k: 'fullscreen', label: 'Fullscreen', type: 'button', text: 'Toggle fullscreen', fn: toggleFullscreen },
{ k: 'cursorLabel', label: 'Name next to the cursor', d: 'Shows what you are pointing at right beside the cursor.', type: 'toggle' },
{ k: 'cursorLabel', label: 'Name next to the cursor', d: 'Briefly names ores and metals, or everything. Warnings stay visible.', type: 'select', opts: [['off', 'Off'], ['ores', 'Ores only'], ['all', 'Everything']] },
{ k: 'minimap', label: 'Show minimap', type: 'toggle' },
{ k: 'showPerf', label: 'Performance stats', d: 'Frame time, simulation time and active areas.', type: 'toggle' },
{ k: 'cameraSmooth', label: 'Camera smoothing', type: 'range', min: 0, max: 0.95, step: 0.05, fmt: v => Math.round(v * 100) + '%' },
+2 -1
View File
@@ -7,13 +7,14 @@ const SET_DEFAULT = {
look: 'smooth', lighting: 'full', lightEvery: 1, ambient: 0.12, daylight: 1, glow: 'low', grain: 1, incandescence: true, particles: true,
minimap: true, minimapZoom: 4, fog: true, thermalPalette: 'inferno', simSpeed: 1, maxSteps: 3,
volMaster: 0.8, volMusic: 0.5, volSfx: 0.8, muted: false, muteUnfocused: true,
deadzone: 0.18, aimSpeed: 1, autosave: 3, cardSeconds: 9, cursorLabel: true, sv: 2,
deadzone: 0.18, aimSpeed: 1, autosave: 3, cardSeconds: 9, cursorLabel: 'ores', sv: 2,
};
const SET = Object.assign({}, SET_DEFAULT);
// sv: settings version. 2: the default zoom became fully zoomed in, so older saved settings move there once.
function loadSettings() {
try {
const s = JSON.parse(localStorage.getItem('deepgrain.settings') || '{}'); Object.assign(SET, s);
if (typeof SET.cursorLabel === 'boolean') SET.cursorLabel = SET.cursorLabel ? 'ores' : 'off';
if (!(s.sv >= 2)) { SET.zoom = SET_DEFAULT.zoom; SET.sv = 2; if (Object.keys(s).length) saveSettings(); }
} catch (e) { /* storage unavailable */ }
}