/* 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();
})();