Spaces:
Running
Running
Download grindscale/app.js from CyberMax-tools/cybermax: direct link, hf CLI and curl.
- Browser
- Download file 12.3 kB
-
https://huggingface.co/spaces/CyberMax-tools/cybermax/resolve/main/grindscale/app.js
- Command line
-
hf download hf://spaces/CyberMax-tools/cybermax/grindscale/app.js
-
curl -L -o app.js https://huggingface.co/spaces/CyberMax-tools/cybermax/resolve/main/grindscale/app.js
12.3 kB
| /* Grindscale web designer. © 2026 CyberMax. All rights reserved. */ | |
| (function () { | |
| "use strict"; | |
| var CHECKOUT = "https://buy.polar.sh/polar_cl_Tp16Od5fWQJCW5rH0vizEbmv7rJYVD8swONwY0oJrLJ"; // Polar checkout link (revenue/packs/polar/grindscale/polar.json) | |
| var BUNDLE = "https://buy.polar.sh/polar_cl_0VZrsUmgkJkVTDpll5E0TTHLI9eloGE5pmDyS4D9P8L"; // Grindscale + Tintmoor + Nature Pack + Blipwren | |
| var CLASSROOM = "https://buy.polar.sh/polar_cl_PXmHmjrUz9SvLbg64KuHujuaLY00P3E87dP6b2dkMul"; | |
| var B = window.Grindscale; | |
| var $ = function (s) { return document.querySelector(s); }; | |
| var $$ = function (s) { return Array.prototype.slice.call(document.querySelectorAll(s)); }; | |
| var KEYS = Object.keys(B.DEFAULTS); | |
| var CUSTOM = "Custom"; | |
| var state = Object.assign({}, B.DEFAULTS); | |
| var chartKind = "xp"; | |
| var fmt = new Intl.NumberFormat("en-US"); | |
| function n1(v) { return v >= 100 ? fmt.format(Math.round(v)) : v.toFixed(1); } | |
| function fill() { | |
| var ps = $("#preset"); | |
| [CUSTOM].concat(Object.keys(B.PRESETS)).forEach(function (p) { ps.add(new Option(p === CUSTOM ? "Custom (my values)" : p, p)); }); | |
| B.CURVES.forEach(function (c) { $("#curve").add(new Option(c, c)); }); | |
| } | |
| function readHash() { | |
| var h = location.hash.replace(/^#/, ""); | |
| if (!/=/.test(h)) return false; | |
| var q = new URLSearchParams(h), any = false; | |
| KEYS.forEach(function (k) { | |
| if (!q.has(k)) return; | |
| var v = q.get(k); | |
| if (k === "curve") { if (B.CURVES.indexOf(v) >= 0) { state.curve = v; any = true; } } | |
| else if (isFinite(+v) && v !== "") { state[k] = +v; any = true; } | |
| }); | |
| return any; | |
| } | |
| function toForm() { | |
| KEYS.forEach(function (k) { $("#" + k).value = state[k]; }); | |
| $$("[data-for]").forEach(function (el) { el.hidden = el.getAttribute("data-for").split(" ").indexOf(state.curve) < 0; }); | |
| } | |
| function clamp(v, lo, hi) { return Math.min(hi, Math.max(lo, v)); } | |
| function fromForm() { | |
| KEYS.forEach(function (k) { | |
| var v = $("#" + k).value; | |
| if (k === "curve") state.curve = v; | |
| else if (v !== "" && isFinite(+v)) state[k] = +v; | |
| }); | |
| state.max_level = clamp(Math.round(state.max_level), 10, 100); | |
| state.round_to = Math.max(1, state.round_to); | |
| state.base_xp = Math.max(1, state.base_xp); | |
| state.xph_l1 = Math.max(1, state.xph_l1); | |
| state.target_hours = Math.max(0.1, state.target_hours); | |
| state.sessions_per_day = Math.max(0.1, state.sessions_per_day); | |
| state.minutes_per_session = Math.max(1, state.minutes_per_session); | |
| } | |
| function formulaText() { | |
| var s = state; | |
| return { | |
| Linear: "XP(L) = " + s.base_xp + " + " + s.linear_step + " × (L − 1)", | |
| Power: "XP(L) = " + s.base_xp + " × L^" + s.power_exp, | |
| Exponential: "XP(L) = " + s.base_xp + " × " + s.exp_growth + "^(L − 1)", | |
| Blend: "XP(L) = " + (1 - s.blend_weight).toFixed(2) + " × " + s.base_xp + " × L^" + s.power_exp + " + " + s.blend_weight + " × " + s.base_xp + " × " + s.exp_growth + "^(L − 1)" | |
| }[s.curve] + ", rounded to " + s.round_to + "."; | |
| } | |
| var rows = []; | |
| function render() { | |
| rows = B.xpTable(state); | |
| var sm = B.summary(state); | |
| $("#t-xp").textContent = fmt.format(sm.total_xp); | |
| $("#t-hours").textContent = n1(sm.total_hours); | |
| $("#t-ratio").textContent = Math.round(sm.ratio * 100) + "%"; | |
| $("#t-days").textContent = n1(sm.days_to_max); | |
| var v = $("#verdict"); | |
| v.textContent = sm.verdict + ". Level 10 after " + n1(sm.hours_to_10) + " h; halfway after " + n1(sm.hours_to_half) + " h."; | |
| v.className = "verdict " + (sm.ratio > 1.15 || sm.ratio < 0.85 ? "off" : "ok"); | |
| $("#formula").textContent = formulaText(); | |
| var tb = $("#levels tbody"); | |
| tb.innerHTML = rows.map(function (r) { | |
| return "<tr><td>" + r.level + "</td><td>" + fmt.format(r.xp_to_next) + "</td><td>" + fmt.format(r.total_xp) + "</td><td>" + | |
| fmt.format(Math.round(r.xp_per_hour)) + "</td><td>" + r.hours.toFixed(2) + "</td><td>" + r.total_hours.toFixed(1) + "</td></tr>"; | |
| }).join(""); | |
| draw(); | |
| } | |
| function css(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); } | |
| function draw() { | |
| var c = $("#chart"), dpr = window.devicePixelRatio || 1; | |
| var w = c.clientWidth, h = c.clientHeight; | |
| c.width = Math.round(w * dpr); c.height = Math.round(h * dpr); | |
| var g = c.getContext("2d"); | |
| g.setTransform(dpr, 0, 0, dpr, 0, 0); | |
| g.clearRect(0, 0, w, h); | |
| var data = rows.map(function (r) { return chartKind === "xp" ? r.xp_to_next : chartKind === "hours" ? r.total_hours : r.hours; }); | |
| if (chartKind !== "hours") data = data.slice(0, -1); | |
| var maxV = Math.max.apply(null, data.concat([1e-9])); | |
| var L = 52, R = 10, T = 12, Bm = 26, pw = w - L - R, ph = h - T - Bm; | |
| g.font = "11px system-ui, sans-serif"; | |
| g.fillStyle = css("--muted"); g.strokeStyle = css("--line"); g.lineWidth = 1; | |
| for (var i = 0; i <= 4; i++) { | |
| var y = T + ph - ph * i / 4; | |
| g.beginPath(); g.moveTo(L, y); g.lineTo(w - R, y); g.stroke(); | |
| var val = maxV * i / 4; | |
| g.textAlign = "right"; g.fillText(val >= 1000 ? fmt.format(Math.round(val / 100) / 10) + "k" : (val >= 10 ? Math.round(val) : val.toFixed(1)), L - 6, y + 4); | |
| } | |
| var nPts = data.length, step = Math.max(1, Math.ceil(state.max_level / 10)); | |
| g.textAlign = "center"; | |
| for (var lv = 1; lv <= nPts; lv += step) g.fillText(lv, L + (nPts > 1 ? pw * (lv - 1) / (nPts - 1) : 0), h - 8); | |
| if (target() && chartKind === "hours") { | |
| var ty = T + ph - ph * Math.min(1, state.target_hours / maxV); | |
| g.setLineDash([4, 4]); g.strokeStyle = css("--warn"); g.beginPath(); g.moveTo(L, ty); g.lineTo(w - R, ty); g.stroke(); g.setLineDash([]); | |
| g.fillStyle = css("--warn"); g.textAlign = "left"; g.fillText("target " + state.target_hours + " h", L + 4, ty - 4); | |
| } | |
| g.strokeStyle = css("--accent"); g.lineWidth = 2.5; g.beginPath(); | |
| data.forEach(function (v, i) { | |
| var x = L + (nPts > 1 ? pw * i / (nPts - 1) : 0), y = T + ph - ph * v / maxV; | |
| if (i) g.lineTo(x, y); else g.moveTo(x, y); | |
| }); | |
| g.stroke(); | |
| } | |
| function target() { return state.target_hours > 0; } | |
| function update() { fromForm(); $("#preset").value = matchPreset(); toForm(); render(); } | |
| function matchPreset() { | |
| var names = Object.keys(B.PRESETS); | |
| for (var i = 0; i < names.length; i++) { | |
| var p = B.PRESETS[names[i]]; | |
| if (KEYS.every(function (k) { return p[k] === state[k]; })) return names[i]; | |
| } | |
| return CUSTOM; | |
| } | |
| function download(name, text, type) { | |
| var a = document.createElement("a"); | |
| a.href = URL.createObjectURL(new Blob([text], { type: type })); | |
| a.download = name; | |
| document.body.appendChild(a); a.click(); a.remove(); | |
| setTimeout(function () { URL.revokeObjectURL(a.href); }, 2000); | |
| } | |
| function copy(text, what) { | |
| var done = function () { toast(what + " copied"); }; | |
| if (navigator.clipboard && window.isSecureContext) navigator.clipboard.writeText(text).then(done, function () { fallback(text); done(); }); | |
| else { fallback(text); done(); } | |
| } | |
| function fallback(text) { | |
| var t = document.createElement("textarea"); t.value = text; t.style.position = "fixed"; t.style.opacity = "0"; | |
| document.body.appendChild(t); t.select(); try { document.execCommand("copy"); } catch (e) { /* ignore */ } t.remove(); | |
| } | |
| var toastTimer; | |
| function toast(msg) { | |
| var t = $("#toast"); t.textContent = msg; t.classList.add("show"); | |
| clearTimeout(toastTimer); toastTimer = setTimeout(function () { t.classList.remove("show"); }, 2400); | |
| } | |
| function arr() { return rows.map(function (r) { return r.xp_to_next; }).join(", "); } | |
| function json() { | |
| return JSON.stringify({ generator: "Grindscale by CyberMax", settings: state, levels: rows.map(function (r) { | |
| return { level: r.level, xp_to_next: r.xp_to_next, total_xp: r.total_xp, xp_per_hour: B.xround(r.xp_per_hour, 2) }; | |
| }) }, null, 1); | |
| } | |
| function gd() { | |
| return "# XP needed to go from level i+1 to i+2 (index 0 = level 1). Made with Grindscale by CyberMax.\n" + | |
| "const XP_TO_NEXT: Array[int] = [" + arr() + "]\n\n" + | |
| "static func level_for_xp(total_xp: int) -> int:\n\tvar lv := 1\n\tfor need in XP_TO_NEXT:\n\t\tif need <= 0 or total_xp < need:\n\t\t\tbreak\n\t\ttotal_xp -= need\n\t\tlv += 1\n\treturn lv\n"; | |
| } | |
| function cs() { | |
| return "// XP needed to go from level i+1 to i+2 (index 0 = level 1). Made with Grindscale by CyberMax.\n" + | |
| "public static class XpTable\n{\n public static readonly long[] XpToNext = { " + arr() + " };\n\n" + | |
| " public static int LevelForXp(long totalXp)\n {\n int level = 1;\n foreach (var need in XpToNext)\n {\n if (need <= 0 || totalXp < need) break;\n totalXp -= need;\n level++;\n }\n return level;\n }\n}\n"; | |
| } | |
| function link() { | |
| var q = new URLSearchParams(); | |
| KEYS.forEach(function (k) { q.set(k, state[k]); }); | |
| return location.origin + location.pathname + "#" + q.toString(); | |
| } | |
| // ----- drops ----- | |
| var syncing = false; | |
| function drops(src) { | |
| if (syncing) return; | |
| syncing = true; | |
| var pct = +$("#d-chance").value, onein = +$("#d-onein").value; | |
| if (src === "onein" && onein >= 1) { pct = 100 / onein; $("#d-chance").value = +pct.toPrecision(6); } | |
| else if (pct > 0) $("#d-onein").value = +(100 / pct).toPrecision(6); | |
| syncing = false; | |
| var p = Math.min(1, Math.max(1e-7, pct / 100)); | |
| var pity = Math.round(+$("#d-pity").value) || 0, rate = +$("#d-rate").value || 0, n = Math.max(1, Math.round(+$("#d-n").value || 1)); | |
| var d = B.drop(p, pity > 0 ? pity : 0, rate); | |
| function cap(t) { return pity > 0 ? Math.min(t, pity) : t; } | |
| $("#d-50").textContent = fmt.format(cap(d.tries50)); | |
| $("#d-90").textContent = fmt.format(cap(d.tries90)); | |
| $("#d-99").textContent = fmt.format(cap(d.tries99)); | |
| $("#d-exp").textContent = d.expected >= 100 ? fmt.format(Math.round(d.expected)) : d.expected.toFixed(1); | |
| $("#d-h90").textContent = rate > 0 ? n1(cap(d.tries90) / rate) : "–"; | |
| $("#d-within").textContent = (d.within(n) * 100).toFixed(d.within(n) > 0.999 && d.within(n) < 1 ? 2 : 1) + "%"; | |
| } | |
| function init() { | |
| fill(); | |
| if (!readHash()) state = Object.assign({}, B.DEFAULTS); | |
| $("#preset").value = matchPreset(); | |
| toForm(); | |
| render(); | |
| $("#preset").addEventListener("change", function () { | |
| var p = B.PRESETS[this.value]; | |
| if (p) { state = Object.assign({}, p); toForm(); render(); } | |
| }); | |
| KEYS.forEach(function (k) { $("#" + k).addEventListener(k === "curve" ? "change" : "input", update); }); | |
| $$("[data-chart]").forEach(function (b) { | |
| b.addEventListener("click", function () { | |
| chartKind = b.getAttribute("data-chart"); | |
| $$("[data-chart]").forEach(function (x) { x.classList.toggle("on", x === b); }); | |
| draw(); | |
| }); | |
| }); | |
| window.addEventListener("resize", draw); | |
| $("#dl-csv").addEventListener("click", function () { download("grindscale-xp-curve.csv", B.csv(state), "text/csv"); }); | |
| $("#cp-json").addEventListener("click", function () { copy(json(), "JSON"); }); | |
| $("#cp-gd").addEventListener("click", function () { copy(gd(), "GDScript"); }); | |
| $("#cp-cs").addEventListener("click", function () { copy(cs(), "C#"); }); | |
| $("#cp-link").addEventListener("click", function () { history.replaceState(null, "", link()); copy(link(), "Link"); }); | |
| ["d-chance", "d-onein", "d-pity", "d-rate", "d-n"].forEach(function (id) { | |
| $("#" + id).addEventListener("input", function () { drops(id === "d-onein" ? "onein" : "chance"); }); | |
| }); | |
| drops("chance"); | |
| var buy = $("#buy"); | |
| $("#buy-bundle").href = BUNDLE; $("#buy-class").href = CLASSROOM; | |
| if (/^https:\/\//.test(CHECKOUT)) buy.href = CHECKOUT; | |
| else { buy.textContent = "Coming soon"; buy.removeAttribute("href"); buy.classList.add("ghost"); } | |
| if (/[?&]thanks=1/.test(location.search)) setTimeout(function () { toast("Thanks for your order! Your download link is in the Polar receipt email."); }, 800); | |
| window.__bb = { state: function () { return state; }, rows: function () { return rows; }, gd: gd, cs: cs, json: json }; | |
| } | |
| init(); | |
| })(); | |