Visual ore highlighting menu for MineFun.io
// ==UserScript==
// @name MineFun Ore Highlight
// @namespace https://greasyfork.org/
// @version 1.0.0
// @description Visual ore highlighting menu for MineFun.io
// @match *://minefun.io/*
// @match *://www.minefun.io/*
// @grant none
// @run-at document-start
// ==/UserScript==
(() => {
"use strict";
const CONFIG = {
toggleKey: ",",
ores: {
diamond: {
names: [
"diamond_ore",
"diamondore",
"diamond"
],
color: "#00aaff"
},
gold: {
names: [
"gold_ore",
"goldore",
"gold"
],
color: "#ffd21f"
},
iron: {
names: [
"iron_ore",
"ironore",
"iron"
],
color: "#ffffff"
},
coal: {
names: [
"coal_ore",
"coalore",
"coal"
],
color: "#111111"
}
}
};
const state = {
menu: false,
enabled: {
diamond: true,
gold: true,
iron: true,
coal: true
},
distance: 999,
opacity: 0.95,
markers: new Map()
};
let panel = null;
let markerLayer = null;
function cssEscapeSafe(value) {
return String(value).replace(/[^a-zA-Z0-9_-]/g, "_");
}
function createUI() {
if (document.getElementById("mf-ore-highlight-ui")) return;
markerLayer = document.createElement("div");
markerLayer.id = "mf-ore-highlight-layer";
Object.assign(markerLayer.style, {
position: "fixed",
inset: "0",
pointerEvents: "none",
zIndex: "2147483645",
overflow: "hidden"
});
document.documentElement.appendChild(markerLayer);
panel = document.createElement("div");
panel.id = "mf-ore-highlight-ui";
Object.assign(panel.style, {
position: "fixed",
top: "80px",
left: "20px",
width: "245px",
padding: "14px",
background: "rgba(12,12,18,.96)",
border: "1px solid rgba(255,255,255,.18)",
borderRadius: "12px",
color: "#fff",
fontFamily: "Arial, sans-serif",
fontSize: "13px",
zIndex: "2147483647",
boxShadow: "0 10px 35px rgba(0,0,0,.45)",
display: "none",
userSelect: "none"
});
panel.innerHTML = `
<div style="
font-size:16px;
font-weight:800;
margin-bottom:10px;
">
⛏ MineFun Ore Highlight
</div>
<div id="mf-ore-status" style="
opacity:.7;
margin-bottom:10px;
">
Detector: waiting...
</div>
${makeToggle("diamond", "💎 Diamond", CONFIG.ores.diamond.color)}
${makeToggle("gold", "🟨 Gold", CONFIG.ores.gold.color)}
${makeToggle("iron", "⬜ Iron", CONFIG.ores.iron.color)}
${makeToggle("coal", "⬛ Coal", CONFIG.ores.coal.color)}
<div style="
margin-top:12px;
padding-top:10px;
border-top:1px solid rgba(255,255,255,.1);
">
<div style="margin-bottom:5px;">
Range: <b id="mf-range-value">999</b>
</div>
<input
id="mf-range"
type="range"
min="32"
max="999"
value="999"
style="width:100%;"
>
</div>
<div style="
margin-top:10px;
font-size:11px;
opacity:.55;
line-height:1.45;
">
Press <b>,</b> to open/close.<br>
Visual only — does not mine or place blocks.
</div>
`;
document.documentElement.appendChild(panel);
for (const ore of Object.keys(CONFIG.ores)) {
const cb = panel.querySelector(`#mf-check-${ore}`);
cb.addEventListener("change", () => {
state.enabled[ore] = cb.checked;
updateMarkers();
});
}
const range = panel.querySelector("#mf-range");
range.addEventListener("input", () => {
state.distance = Number(range.value);
panel.querySelector("#mf-range-value").textContent =
String(state.distance);
updateMarkers();
});
}
function makeToggle(id, label, color) {
return `
<label style="
display:flex;
align-items:center;
gap:8px;
margin:7px 0;
cursor:pointer;
">
<input
type="checkbox"
id="mf-check-${id}"
checked
>
<span style="
width:12px;
height:12px;
display:inline-block;
background:${color};
border-radius:3px;
box-shadow:0 0 7px ${color};
"></span>
<span>${label}</span>
</label>
`;
}
function toggleMenu() {
state.menu = !state.menu;
if (panel) {
panel.style.display = state.menu ? "block" : "none";
}
}
function installKeys() {
window.addEventListener("keydown", e => {
if (e.key === "," && !e.repeat) {
e.preventDefault();
e.stopPropagation();
toggleMenu();
}
}, true);
}
function objectToName(obj) {
if (!obj) return "";
if (typeof obj === "string") {
return obj.toLowerCase();
}
const keys = [
"id",
"name",
"type",
"block",
"blockId",
"blockName",
"item",
"key"
];
for (const key of keys) {
if (obj[key] != null) {
const value = String(obj[key]).toLowerCase();
if (value.length <= 150) {
return value;
}
}
}
return "";
}
function detectOre(name) {
if (!name) return null;
for (const [ore, cfg] of Object.entries(CONFIG.ores)) {
if (!state.enabled[ore]) continue;
for (const candidate of cfg.names) {
if (name.includes(candidate)) {
return ore;
}
}
}
return null;
}
function getDistance(a, b) {
if (!a || !b) return Infinity;
const dx = Number(a.x ?? 0) - Number(b.x ?? 0);
const dy = Number(a.y ?? 0) - Number(b.y ?? 0);
const dz = Number(a.z ?? 0) - Number(b.z ?? 0);
return Math.sqrt(dx * dx + dy * dy + dz * dz);
}
function getPlayerPosition() {
const candidates = [
window.player,
window.localPlayer,
window.game?.localPlayer,
window.game?.player,
window.world?.localPlayer,
window.client?.player
];
for (const p of candidates) {
if (!p) continue;
const pos =
p.position ||
p.pos ||
p.coordinates;
if (
pos &&
Number.isFinite(Number(pos.x)) &&
Number.isFinite(Number(pos.y)) &&
Number.isFinite(Number(pos.z))
) {
return {
x: Number(pos.x),
y: Number(pos.y),
z: Number(pos.z)
};
}
if (
Number.isFinite(Number(p.x)) &&
Number.isFinite(Number(p.y)) &&
Number.isFinite(Number(p.z))
) {
return {
x: Number(p.x),
y: Number(p.y),
z: Number(p.z)
};
}
}
return null;
}
function getPossibleBlockSources() {
return [
window.world?.blocks,
window.game?.world?.blocks,
window.world?.chunks,
window.game?.world?.chunks,
window.blocks,
window.blockMap
].filter(Boolean);
}
function collectBlocks() {
const result = [];
const sources = getPossibleBlockSources();
for (const source of sources) {
try {
if (source instanceof Map) {
for (const [key, value] of source.entries()) {
const name = objectToName(value) || String(key).toLowerCase();
const pos =
value?.position ||
value?.pos ||
(
Number.isFinite(value?.x) &&
Number.isFinite(value?.y) &&
Number.isFinite(value?.z)
)
? value
: null;
if (!pos) continue;
result.push({
x: Number(pos.x),
y: Number(pos.y),
z: Number(pos.z),
name
});
}
}
else if (Array.isArray(source)) {
for (const value of source) {
const name = objectToName(value);
const pos =
value?.position ||
value?.pos ||
value;
if (
!pos ||
!Number.isFinite(Number(pos.x)) ||
!Number.isFinite(Number(pos.y)) ||
!Number.isFinite(Number(pos.z))
) continue;
result.push({
x: Number(pos.x),
y: Number(pos.y),
z: Number(pos.z),
name
});
}
}
} catch (_) {}
}
return result;
}
function worldToScreen(pos) {
/*
* Không giả định một API camera cụ thể.
* Thử các camera mà client có thể expose.
*/
const cameras = [
window.camera,
window.game?.camera,
window.world?.camera,
window.renderer?.camera
];
for (const camera of cameras) {
if (!camera) continue;
try {
if (typeof camera.worldToScreen === "function") {
const p = camera.worldToScreen(pos);
if (
p &&
Number.isFinite(p.x) &&
Number.isFinite(p.y)
) {
return p;
}
}
if (typeof camera.project === "function") {
const p = camera.project(pos);
if (
p &&
Number.isFinite(p.x) &&
Number.isFinite(p.y)
) {
return {
x: (p.x + 1) * window.innerWidth / 2,
y: (1 - p.y) * window.innerHeight / 2
};
}
}
} catch (_) {}
}
return null;
}
function makeMarker(ore) {
const el = document.createElement("div");
el.className = `mf-ore-marker mf-${cssEscapeSafe(ore)}`;
Object.assign(el.style, {
position: "absolute",
width: "20px",
height: "20px",
transform: "translate(-50%, -50%)",
border: `3px solid ${CONFIG.ores[ore].color}`,
boxShadow:
`0 0 7px ${CONFIG.ores[ore].color},
0 0 14px ${CONFIG.ores[ore].color}`,
background:
`${CONFIG.ores[ore].color}22`,
borderRadius: "5px"
});
return el;
}
function clearMarkers() {
for (const marker of state.markers.values()) {
marker.remove();
}
state.markers.clear();
}
function updateMarkers() {
const player = getPlayerPosition();
const blocks = collectBlocks();
if (!player) {
setStatus("Detector: no player data");
clearMarkers();
return;
}
clearMarkers();
let shown = 0;
for (const block of blocks) {
const ore = detectOre(block.name);
if (!ore) continue;
const dist = getDistance(player, block);
if (dist > state.distance) continue;
const screen = worldToScreen({
x: block.x + 0.5,
y: block.y + 0.5,
z: block.z + 0.5
});
if (!screen) continue;
if (
screen.x < -50 ||
screen.x > window.innerWidth + 50 ||
screen.y < -50 ||
screen.y > window.innerHeight + 50
) {
continue;
}
const key =
`${ore}:${block.x}:${block.y}:${block.z}`;
const marker = makeMarker(ore);
marker.style.left = `${screen.x}px`;
marker.style.top = `${screen.y}px`;
marker.title =
`${ore.toUpperCase()} @ ` +
`${block.x}, ${block.y}, ${block.z}`;
markerLayer.appendChild(marker);
state.markers.set(key, marker);
shown++;
}
setStatus(`Detector: ${shown} marker(s)`);
}
function setStatus(text) {
const el = panel?.querySelector("#mf-ore-status");
if (el) {
el.textContent = text;
}
}
function startLoop() {
let last = 0;
function loop(now) {
if (now - last >= 150) {
last = now;
try {
updateMarkers();
} catch (err) {
console.debug("[MF Ore Highlight]", err);
}
}
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
}
function boot() {
createUI();
installKeys();
startLoop();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", boot, {
once: true
});
} else {
boot();
}
})();