/* 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 "" + r.level + "" + fmt.format(r.xp_to_next) + "" + fmt.format(r.total_xp) + "" + fmt.format(Math.round(r.xp_per_hour)) + "" + r.hours.toFixed(2) + "" + r.total_hours.toFixed(1) + ""; }).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(); })();