MZ Tactics Manager

Userscript to manage tactics in ManagerZone

安装此脚本
作者推荐脚本

您可能也喜欢ylOppTacticsPreview (Modified)

安装此脚本
  1. // ==UserScript==
  2. // @name MZ Tactics Manager
  3. // @namespace douglaskampl
  4. // @version 13.3.7
  5. // @description Userscript to manage tactics in ManagerZone
  6. // @author Douglas Vieira
  7. // @match https://www.managerzone.com/?p=tactics
  8. // @match https://www.managerzone.com/?p=national_teams&sub=tactics&type=*
  9. // @icon https://yt3.googleusercontent.com/ytc/AIdro_mDHaJkwjCgyINFM7cdUV2dWPPnL9Q58vUsrhOmRqkatg=s160-c-k-c0x00ffffff-no-rj
  10. // @grant GM_getValue
  11. // @grant GM_setValue
  12. // @grant GM_deleteValue
  13. // @grant GM_addStyle
  14. // @run-at document-idle
  15. // @license MIT
  16. // ==/UserScript==
  17.  
  18. (function () {
  19. 'use strict';
  20.  
  21. !function (r, t) { "object" == typeof exports && "undefined" != typeof module ? module.exports = t() : "function" == typeof define && define.amd ? define(t) : (r = "undefined" != typeof globalThis ? globalThis : r || self).jsSHA = t() }(this, (function () { "use strict"; var r = function (t, n) { return r = Object.setPrototypeOf || { __proto__: [] } instanceof Array && function (r, t) { r.__proto__ = t } || function (r, t) { for (var n in t) Object.prototype.hasOwnProperty.call(t, n) && (r[n] = t[n]) }, r(t, n) }; "function" == typeof SuppressedError && SuppressedError; var t = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", n = "ARRAYBUFFER not supported by this environment", i = "UINT8ARRAY not supported by this environment"; function e(r, t, n, i) { var e, o, u, s = t || [0], f = (n = n || 0) >>> 3, h = -1 === i ? 3 : 0; for (e = 0; e < r.length; e += 1)o = (u = e + f) >>> 2, s.length <= o && s.push(0), s[o] |= r[e] << 8 * (h + i * (u % 4)); return { value: s, binLen: 8 * r.length + n } } function o(r, o, u) { switch (o) { case "UTF8": case "UTF16BE": case "UTF16LE": break; default: throw new Error("encoding must be UTF8, UTF16BE, or UTF16LE") }switch (r) { case "HEX": return function (r, t, n) { return function (r, t, n, i) { var e, o, u, s; if (0 != r.length % 2) throw new Error("String of HEX type must be in byte increments"); var f = t || [0], h = (n = n || 0) >>> 3, a = -1 === i ? 3 : 0; for (e = 0; e < r.length; e += 2) { if (o = parseInt(r.substr(e, 2), 16), isNaN(o)) throw new Error("String of HEX type contains invalid characters"); for (u = (s = (e >>> 1) + h) >>> 2; f.length <= u;)f.push(0); f[u] |= o << 8 * (a + i * (s % 4)) } return { value: f, binLen: 4 * r.length + n } }(r, t, n, u) }; case "TEXT": return function (r, t, n) { return function (r, t, n, i, e) { var o, u, s, f, h, a, c, w, E = 0, v = n || [0], l = (i = i || 0) >>> 3; if ("UTF8" === t) for (c = -1 === e ? 3 : 0, s = 0; s < r.length; s += 1)for (u = [], 128 > (o = r.charCodeAt(s)) ? u.push(o) : 2048 > o ? (u.push(192 | o >>> 6), u.push(128 | 63 & o)) : 55296 > o || 57344 <= o ? u.push(224 | o >>> 12, 128 | o >>> 6 & 63, 128 | 63 & o) : (s += 1, o = 65536 + ((1023 & o) << 10 | 1023 & r.charCodeAt(s)), u.push(240 | o >>> 18, 128 | o >>> 12 & 63, 128 | o >>> 6 & 63, 128 | 63 & o)), f = 0; f < u.length; f += 1) { for (h = (a = E + l) >>> 2; v.length <= h;)v.push(0); v[h] |= u[f] << 8 * (c + e * (a % 4)), E += 1 } else for (c = -1 === e ? 2 : 0, w = "UTF16LE" === t && 1 !== e || "UTF16LE" !== t && 1 === e, s = 0; s < r.length; s += 1) { for (o = r.charCodeAt(s), !0 === w && (o = (f = 255 & o) << 8 | o >>> 8), h = (a = E + l) >>> 2; v.length <= h;)v.push(0); v[h] |= o << 8 * (c + e * (a % 4)), E += 2 } return { value: v, binLen: 8 * E + i } }(r, o, t, n, u) }; case "B64": return function (r, n, i) { return function (r, n, i, e) { var o, u, s, f, h, a, c = 0, w = n || [0], E = (i = i || 0) >>> 3, v = -1 === e ? 3 : 0, l = r.indexOf("="); if (-1 === r.search(/^[a-zA-Z0-9=+/]+$/)) throw new Error("Invalid character in base-64 string"); if (r = r.replace(/=/g, ""), -1 !== l && l < r.length) throw new Error("Invalid '=' found in base-64 string"); for (o = 0; o < r.length; o += 4) { for (f = r.substr(o, 4), s = 0, u = 0; u < f.length; u += 1)s |= t.indexOf(f.charAt(u)) << 18 - 6 * u; for (u = 0; u < f.length - 1; u += 1) { for (h = (a = c + E) >>> 2; w.length <= h;)w.push(0); w[h] |= (s >>> 16 - 8 * u & 255) << 8 * (v + e * (a % 4)), c += 1 } } return { value: w, binLen: 8 * c + i } }(r, n, i, u) }; case "BYTES": return function (r, t, n) { return function (r, t, n, i) { var e, o, u, s, f = t || [0], h = (n = n || 0) >>> 3, a = -1 === i ? 3 : 0; for (o = 0; o < r.length; o += 1)e = r.charCodeAt(o), u = (s = o + h) >>> 2, f.length <= u && f.push(0), f[u] |= e << 8 * (a + i * (s % 4)); return { value: f, binLen: 8 * r.length + n } }(r, t, n, u) }; case "ARRAYBUFFER": try { new ArrayBuffer(0) } catch (r) { throw new Error(n) } return function (r, t, n) { return function (r, t, n, i) { return e(new Uint8Array(r), t, n, i) }(r, t, n, u) }; case "UINT8ARRAY": try { new Uint8Array(0) } catch (r) { throw new Error(i) } return function (r, t, n) { return e(r, t, n, u) }; default: throw new Error("format must be HEX, TEXT, B64, BYTES, ARRAYBUFFER, or UINT8ARRAY") } } function u(r, e, o, u) { switch (r) { case "HEX": return function (r) { return function (r, t, n, i) { var e, o, u = "0123456789abcdef", s = "", f = t / 8, h = -1 === n ? 3 : 0; for (e = 0; e < f; e += 1)o = r[e >>> 2] >>> 8 * (h + n * (e % 4)), s += u.charAt(o >>> 4 & 15) + u.charAt(15 & o); return i.outputUpper ? s.toUpperCase() : s }(r, e, o, u) }; case "B64": return function (r) { return function (r, n, i, e) { var o, u, s, f, h, a = "", c = n / 8, w = -1 === i ? 3 : 0; for (o = 0; o < c; o += 3)for (f = o + 1 < c ? r[o + 1 >>> 2] : 0, h = o + 2 < c ? r[o + 2 >>> 2] : 0, s = (r[o >>> 2] >>> 8 * (w + i * (o % 4)) & 255) << 16 | (f >>> 8 * (w + i * ((o + 1) % 4)) & 255) << 8 | h >>> 8 * (w + i * ((o + 2) % 4)) & 255, u = 0; u < 4; u += 1)a += 8 * o + 6 * u <= n ? t.charAt(s >>> 6 * (3 - u) & 63) : e.b64Pad; return a }(r, e, o, u) }; case "BYTES": return function (r) { return function (r, t, n) { var i, e, o = "", u = t / 8, s = -1 === n ? 3 : 0; for (i = 0; i < u; i += 1)e = r[i >>> 2] >>> 8 * (s + n * (i % 4)) & 255, o += String.fromCharCode(e); return o }(r, e, o) }; case "ARRAYBUFFER": try { new ArrayBuffer(0) } catch (r) { throw new Error(n) } return function (r) { return function (r, t, n) { var i, e = t / 8, o = new ArrayBuffer(e), u = new Uint8Array(o), s = -1 === n ? 3 : 0; for (i = 0; i < e; i += 1)u[i] = r[i >>> 2] >>> 8 * (s + n * (i % 4)) & 255; return o }(r, e, o) }; case "UINT8ARRAY": try { new Uint8Array(0) } catch (r) { throw new Error(i) } return function (r) { return function (r, t, n) { var i, e = t / 8, o = -1 === n ? 3 : 0, u = new Uint8Array(e); for (i = 0; i < e; i += 1)u[i] = r[i >>> 2] >>> 8 * (o + n * (i % 4)) & 255; return u }(r, e, o) }; default: throw new Error("format must be HEX, B64, BYTES, ARRAYBUFFER, or UINT8ARRAY") } } var s = [1116352408, 1899447441, 3049323471, 3921009573, 961987163, 1508970993, 2453635748, 2870763221, 3624381080, 310598401, 607225278, 1426881987, 1925078388, 2162078206, 2614888103, 3248222580, 3835390401, 4022224774, 264347078, 604807628, 770255983, 1249150122, 1555081692, 1996064986, 2554220882, 2821834349, 2952996808, 3210313671, 3336571891, 3584528711, 113926993, 338241895, 666307205, 773529912, 1294757372, 1396182291, 1695183700, 1986661051, 2177026350, 2456956037, 2730485921, 2820302411, 3259730800, 3345764771, 3516065817, 3600352804, 4094571909, 275423344, 430227734, 506948616, 659060556, 883997877, 958139571, 1322822218, 1537002063, 1747873779, 1955562222, 2024104815, 2227730452, 2361852424, 2428436474, 2756734187, 3204031479, 3329325298], f = [3238371032, 914150663, 812702999, 4144912697, 4290775857, 1750603025, 1694076839, 3204075428], h = [1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225]; function a(r) { var t = { outputUpper: !1, b64Pad: "=", outputLen: -1 }, n = r || {}, i = "Output length must be a multiple of 8"; if (t.outputUpper = n.outputUpper || !1, n.b64Pad && (t.b64Pad = n.b64Pad), n.outputLen) { if (n.outputLen % 8 != 0) throw new Error(i); t.outputLen = n.outputLen } else if (n.shakeLen) { if (n.shakeLen % 8 != 0) throw new Error(i); t.outputLen = n.shakeLen } if ("boolean" != typeof t.outputUpper) throw new Error("Invalid outputUpper formatting option"); if ("string" != typeof t.b64Pad) throw new Error("Invalid b64Pad formatting option"); return t } function c(r, t) { return r >>> t | r << 32 - t } function w(r, t) { return r >>> t } function E(r, t, n) { return r & t ^ ~r & n } function v(r, t, n) { return r & t ^ r & n ^ t & n } function l(r) { return c(r, 2) ^ c(r, 13) ^ c(r, 22) } function p(r, t) { var n = (65535 & r) + (65535 & t); return (65535 & (r >>> 16) + (t >>> 16) + (n >>> 16)) << 16 | 65535 & n } function A(r, t, n, i) { var e = (65535 & r) + (65535 & t) + (65535 & n) + (65535 & i); return (65535 & (r >>> 16) + (t >>> 16) + (n >>> 16) + (i >>> 16) + (e >>> 16)) << 16 | 65535 & e } function d(r, t, n, i, e) { var o = (65535 & r) + (65535 & t) + (65535 & n) + (65535 & i) + (65535 & e); return (65535 & (r >>> 16) + (t >>> 16) + (n >>> 16) + (i >>> 16) + (e >>> 16) + (o >>> 16)) << 16 | 65535 & o } function y(r) { return c(r, 7) ^ c(r, 18) ^ w(r, 3) } function U(r) { return c(r, 6) ^ c(r, 11) ^ c(r, 25) } function T(r) { return "SHA-224" == r ? f.slice() : h.slice() } function b(r, t) { var n, i, e, o, u, f, h, a, T, b, R, m, F = []; for (n = t[0], i = t[1], e = t[2], o = t[3], u = t[4], f = t[5], h = t[6], a = t[7], R = 0; R < 64; R += 1)F[R] = R < 16 ? r[R] : A(c(m = F[R - 2], 17) ^ c(m, 19) ^ w(m, 10), F[R - 7], y(F[R - 15]), F[R - 16]), T = d(a, U(u), E(u, f, h), s[R], F[R]), b = p(l(n), v(n, i, e)), a = h, h = f, f = u, u = p(o, T), o = e, e = i, i = n, n = p(T, b); return t[0] = p(n, t[0]), t[1] = p(i, t[1]), t[2] = p(e, t[2]), t[3] = p(o, t[3]), t[4] = p(u, t[4]), t[5] = p(f, t[5]), t[6] = p(h, t[6]), t[7] = p(a, t[7]), t } return function (t) { function n(r, n, i) { var e = this; if ("SHA-224" !== r && "SHA-256" !== r) throw new Error("Chosen SHA variant is not supported"); var u = i || {}; return (e = t.call(this, r, n, i) || this).t = e.i, e.o = !0, e.u = -1, e.h = o(e.v, e.l, e.u), e.p = b, e.A = function (r) { return r.slice() }, e.U = T, e.T = function (t, n, i, e) { return function (r, t, n, i, e) { for (var o, u = 15 + (t + 65 >>> 9 << 4), s = t + n; r.length <= u;)r.push(0); for (r[t >>> 5] |= 128 << 24 - t % 32, r[u] = 4294967295 & s, r[u - 1] = s / 4294967296 | 0, o = 0; o < r.length; o += 16)i = b(r.slice(o, o + 16), i); return "SHA-224" === e ? [i[0], i[1], i[2], i[3], i[4], i[5], i[6]] : i }(t, n, i, e, r) }, e.R = T(r), e.m = 512, e.F = "SHA-224" === r ? 224 : 256, e.g = !1, u.hmacKey && e.B(function (r, t, n, i) { var e = r + " must include a value and format"; if (!t) { if (!i) throw new Error(e); return i } if (void 0 === t.value || !t.format) throw new Error(e); return o(t.format, t.encoding || "UTF8", n)(t.value) }("hmacKey", u.hmacKey, e.u)), e } return function (t, n) { if ("function" != typeof n && null !== n) throw new TypeError("Class extends value " + String(n) + " is not a constructor or null"); function i() { this.constructor = t } r(t, n), t.prototype = null === n ? Object.create(n) : (i.prototype = n.prototype, new i) }(n, t), n }(function () { function r(r, t, n) { var i = n || {}; if (this.v = t, this.l = i.encoding || "UTF8", this.numRounds = i.numRounds || 1, isNaN(this.numRounds) || this.numRounds !== parseInt(this.numRounds, 10) || 1 > this.numRounds) throw new Error("numRounds must a integer >= 1"); this.S = r, this.H = [], this.Y = 0, this.C = !1, this.I = 0, this.L = !1, this.N = [], this.X = [] } return r.prototype.update = function (r) { var t, n = 0, i = this.m >>> 5, e = this.h(r, this.H, this.Y), o = e.binLen, u = e.value, s = o >>> 5; for (t = 0; t < s; t += i)n + this.m <= o && (this.R = this.p(u.slice(t, t + i), this.R), n += this.m); return this.I += n, this.H = u.slice(n >>> 5), this.Y = o % this.m, this.C = !0, this }, r.prototype.getHash = function (r, t) { var n, i, e = this.F, o = a(t); if (this.g) { if (-1 === o.outputLen) throw new Error("Output length must be specified in options"); e = o.outputLen } var s = u(r, e, this.u, o); if (this.L && this.t) return s(this.t(o)); for (i = this.T(this.H.slice(), this.Y, this.I, this.A(this.R), e), n = 1; n < this.numRounds; n += 1)this.g && e % 32 != 0 && (i[i.length - 1] &= 16777215 >>> 24 - e % 32), i = this.T(i, e, 0, this.U(this.S), e); return s(i) }, r.prototype.setHMACKey = function (r, t, n) { if (!this.o) throw new Error("Variant does not support HMAC"); if (this.C) throw new Error("Cannot set MAC key after calling update"); var i = o(t, (n || {}).encoding || "UTF8", this.u); this.B(i(r)) }, r.prototype.B = function (r) { var t, n = this.m >>> 3, i = n / 4 - 1; if (1 !== this.numRounds) throw new Error("Cannot set numRounds with MAC"); if (this.L) throw new Error("MAC key already set"); for (n < r.binLen / 8 && (r.value = this.T(r.value, r.binLen, 0, this.U(this.S), this.F)); r.value.length <= i;)r.value.push(0); for (t = 0; t <= i; t += 1)this.N[t] = 909522486 ^ r.value[t], this.X[t] = 1549556828 ^ r.value[t]; this.R = this.p(this.N, this.R), this.I = this.m, this.L = !0 }, r.prototype.getHMAC = function (r, t) { var n = a(t); return u(r, this.F, this.u, n)(this.i()) }, r.prototype.i = function () { var r; if (!this.L) throw new Error("Cannot call getHMAC without first setting MAC key"); var t = this.T(this.H.slice(), this.Y, this.I, this.A(this.R), this.F); return r = this.p(this.X, this.U(this.S)), r = this.T(t, this.F, this.m, r, this.F) }, r }()) }));
  22.  
  23. const wakkanai = `@import url('https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700&f[]=general-sans@400,500&display=swap');:root{--bg-color:#111827;--text-color:#F9FAFB;--primary-accent-color:#634999;--secondary-text-color:#94A3B8;--shadow-color-dark:rgba(0,0,0,0.25);--shadow-color-light:rgba(255,255,255,0.03);--style-accent-teal:#14B8A6;--style-accent-green:#22C55E;--style-accent-orange:#F97316;--style-accent-gray:#6B7280;--success-color:#22C55E;--error-color:#EF4444;--info-color:#3B82F6;--warning-color:#F59E0B;--border-color:rgba(148,163,184,0.1);--border-color-hover:rgba(148,163,184,0.2);--border-color-focus:var(--primary-accent-color);--input-bg-color:rgba(255,255,255,0.04);--input-bg-color-hover:rgba(255,255,255,0.06);--input-bg-color-focus:rgba(255,255,255,0.07);--button-secondary-bg:rgba(255,255,255,0.05);--button-secondary-bg-hover:rgba(255,255,255,0.08);--button-secondary-bg-active:rgba(0,0,0,0.05);--border-radius:8px;--shadow-base:3px 3px 8px var(--shadow-color-dark),-2px -2px 4px var(--shadow-color-light);--shadow-inset:inset 2px 2px 4px var(--shadow-color-dark),inset -2px -2px 4px var(--shadow-color-light);--shadow-concave:4px 4px 8px var(--shadow-color-dark),-4px -4px 8px var(--shadow-color-light),inset 1px 1px 2px var(--shadow-color-light),inset -1px -1px 2px var(--shadow-color-dark);}#mz_tactics_panel{font-family:"Satoshi",-apple-system,sans-serif;background-color:var(--bg-color);border-radius:12px;padding:16px 16px 4px 16px;margin:8px;box-shadow:var(--shadow-base);border:1px solid var(--border-color);transition:max-height 0.4s ease-out,padding 0.4s ease-out,margin 0.4s ease-out,opacity 0.3s ease-out;max-height:1000px;opacity:1;color:var(--text-color);overflow:visible;}#mz_tactics_panel.collapsed{max-height:0 !important;padding-top:0 !important;padding-bottom:0 !important;margin-top:0 !important;margin-bottom:0 !important;opacity:0 !important;border:none !important;overflow:hidden !important;}.mz-group{background-color:rgba(0,0,0,0.1);border-radius:var(--border-radius);padding:12px;margin:8px 0;box-shadow:none;border:1px solid var(--border-color);position:relative;transition:max-height 0.3s ease-out,padding 0.3s ease-out,margin 0.3s ease-out;overflow:visible;}.mz-group-main-title{display:flex;justify-content:space-between;align-items:center;color:var(--text-color);font-size:18px;font-weight:500;margin:-4px 0 12px 0;padding-bottom:8px;border-bottom:1px solid var(--border-color);}.mz-title-container{display:flex;align-items:center;gap:8px;flex-grow:1;flex-wrap:nowrap;min-width:0;}.mz-main-title{font-family:"Satoshi",sans-serif;font-size:20px;font-weight:700;margin:0;padding:0;letter-spacing:0.2px;white-space:nowrap;background:linear-gradient(to right,var(--primary-accent-color),#A78BFA);-webkit-background-clip:text;background-clip:text;color:transparent;}.mz-version-text{color:#2A8C5E;opacity:0.7;font-size:0.9em;font-weight:400;margin-right:10px;white-space:nowrap;}.mz-divider{width:50px;height:1px;background:var(--border-color);margin:10px auto 0;opacity:0.5;}#toggle_panel_btn{background:transparent;border:none;color:var(--secondary-text-color);cursor:pointer;padding:8px;width:32px;height:32px;border-radius:50%;margin-left:auto;font-size:18px;transition:all 0.3s ease;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;}#toggle_panel_btn:hover{background:rgba(255,255,255,0.05);color:var(--text-color);}#collapsed_icon{display:none;width:20px;height:18px;border-radius:50%;background:#1F2937;color:var(--secondary-text-color);font-size:9px;font-weight:bold;text-align:center;line-height:18px;cursor:pointer;transition:transform 0.3s ease,box-shadow 0.3s ease,opacity 0.3s ease;box-shadow:var(--shadow-base);border:1px solid var(--border-color);margin-left:8px;vertical-align:middle;flex-shrink:0;opacity:0;transform:scale(0.8);}#collapsed_icon.visible{display:inline-block;opacity:1;transform:scale(1);}#collapsed_icon:hover{transform:scale(1.1);box-shadow:0 0 10px rgba(139,92,246,0.3);color:var(--text-color);}#mz_tactics_panel .mzbtn{display:inline-flex;align-items:center;justify-content:center;padding:8px 14px;margin:4px;font-family:"Satoshi",sans-serif;font-size:13px;font-weight:500;color:var(--text-color);background:var(--button-secondary-bg);border:1px solid var(--border-color);border-radius:var(--border-radius);cursor:pointer;transition:all 0.2s ease;min-height:36px;box-shadow:0 1px 2px rgba(0,0,0,0.1);}#mz_tactics_panel .mzbtn:hover:not(:disabled){background:var(--button-secondary-bg-hover);border-color:var(--border-color-hover);transform:translateY(-1px);box-shadow:0 3px 6px rgba(0,0,0,0.15);}#mz_tactics_panel .mzbtn:active:not(:disabled){background:var(--button-secondary-bg-active);transform:translateY(0);box-shadow:inset 0 1px 2px rgba(0,0,0,0.2);}#mz_tactics_panel .mzbtn:disabled{opacity:0.5;cursor:not-allowed;}#mz_tactics_panel select{font-family:"Satoshi",sans-serif;font-size:14px;color:var(--text-color);padding:7px 13px;border:1px solid var(--border-color);border-radius:var(--border-radius);background-color:var(--input-bg-color);cursor:pointer;margin:0;transition:all 0.2s ease;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-image:url("data:image/svg+xml;utf8,<svg fill='%2394A3B8' height='24' viewBox='0 0 24 24' width='24' xmlns='http://www.w3.org/2000/svg'><path d='M7 10l5 5 5-5z'/></svg>");background-repeat:no-repeat;background-position:right 10px top 50%;padding-right:30px;height:36px;box-sizing:border-box;}#mz_tactics_panel select:hover:not(:disabled){background:var(--input-bg-color-hover);border-color:var(--border-color-hover);}#mz_tactics_panel select:focus{outline:none;border-color:var(--border-color-focus);box-shadow:0 0 0 2px rgba(139,92,246,0.3);background:var(--input-bg-color-focus);}#mz_tactics_panel select option{background-color:#1F2937;color:var(--text-color);padding:5px 10px;}#mz_tactics_panel select:disabled{opacity:0.5;cursor:not-allowed;}.tactics-selector-section{margin-bottom:12px;}.tactics-selector-label{display:block;margin-bottom:5px;color:var(--secondary-text-color);font-size:12px;}.modal-info-wrapper{width:100%;max-width:550px;}.modal-tabs{display:flex;border-bottom:1px solid var(--border-color);margin-bottom:15px;}.modal-tab{padding:10px 15px;cursor:pointer;border:none;background:none;color:var(--secondary-text-color);opacity:0.8;border-bottom:2px solid transparent;transition:all 0.3s ease;font-family:"Satoshi",sans-serif;font-size:14px;font-weight:500;}.modal-tab:hover{opacity:1;color:var(--text-color);background-color:rgba(255,255,255,0.03);}.modal-tab.active{opacity:1;color:var(--primary-accent-color);border-bottom-color:var(--primary-accent-color);font-weight:600;}.modal-tab-content{display:none;padding:5px;max-height:40vh;overflow-y:auto;overflow-x:hidden;scrollbar-width:thin;scrollbar-color:var(--primary-accent-color) rgba(255,255,255,0.05);}#mz_tactics_panel .mztm-custom-select-list-container{background-color:#1F2937;}#mz_tactics_panel #category-selector option{background-color:#1F2937;}.modal-tab-content::-webkit-scrollbar{width:6px;}.modal-tab-content::-webkit-scrollbar-track{background:rgba(255,255,255,0.05);border-radius:3px;}.modal-tab-content::-webkit-scrollbar-thumb{background-color:var(--primary-accent-color);border-radius:3px;}.modal-tab-content.active{display:block;animation:fadeIn 0.5s ease;}.modal-tab-content[data-tab-id="links"] a{color:var(--info-color);}.faq-section h3{color:var(--primary-accent-color);margin-top:20px;margin-bottom:15px;font-size:16px;border-bottom:1px solid var(--border-color);padding-bottom:5px;}.faq-item{margin-bottom:15px;}.faq-item h4{font-weight:600;margin-bottom:5px;color:var(--text-color);font-size:14px;}.faq-item p{font-size:13px;line-height:1.5;color:var(--secondary-text-color);margin:0;}.faq-item code{background-color:var(--input-bg-color);padding:2px 5px;border-radius:4px;font-size:0.9em;color:var(--primary-accent-color);border:1px solid var(--border-color);}.info-modal-content a{color:var(--primary-accent-color);text-decoration:none;transition:color 0.3s ease;}.info-modal-content a:hover{color:var(--primary-accent-color);opacity:0.8;text-decoration:underline;}.info-modal-content ul{list-style:none;padding:0;}.info-modal-content ul li{margin:12px 0;padding:8px 12px;border-radius:var(--border-radius);background:var(--input-bg-color);border:1px solid var(--border-color);transition:all 0.3s ease;}.info-modal-content ul li:hover{background:var(--input-bg-color-hover);}#mz-modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background-color:rgba(17,24,39,0.8);backdrop-filter:blur(4px);display:flex;align-items:center;justify-content:center;z-index:10000;opacity:0;transition:opacity 0.3s ease;}#mz-modal-container{background:var(--bg-color);border-radius:12px;padding:24px;box-shadow:0 10px 25px rgba(0,0,0,0.3);border:1px solid var(--border-color);max-width:550px;width:90%;transform:scale(0.9);transition:transform 0.3s ease;color:var(--text-color);font-family:"Satoshi",-apple-system,sans-serif;}#mz-modal-container.management-modal{max-width:700px;width:95%;}#mz-modal-overlay.active{opacity:1;}#mz-modal-overlay.active #mz-modal-container{transform:scale(1);}#mz-modal-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;border-bottom:none;padding-bottom:0;}#mz-modal-title{font-size:20px;font-weight:500;margin:0;}#mz-modal-close{background:transparent;border:none;color:var(--secondary-text-color);font-size:22px;cursor:pointer;transition:all 0.3s ease;padding:0;width:36px;height:36px;display:flex;align-items:center;justify-content:center;border-radius:50%;}#mz-modal-close:hover{background:rgba(255,255,255,0.05);color:var(--error-color);}#mz-modal-content{margin-bottom:24px;white-space:pre-line;line-height:1.5;color:var(--secondary-text-color);}#mz-modal-input{width:calc(100% - 32px);background:var(--input-bg-color);border:1px solid var(--border-color);color:var(--text-color);padding:14px 16px;border-radius:var(--border-radius);font-family:"Satoshi",sans-serif;font-size:15px;margin-bottom:20px;transition:all 0.3s ease;box-sizing:border-box;}#mz-modal-input:focus{outline:none;border-color:var(--border-color-focus);box-shadow:0 0 0 2px rgba(139,92,246,0.3);background:var(--input-bg-color-focus);}.mz-modal-label{display:block;margin-bottom:8px;font-size:14px;color:var(--text-color);opacity:0.9;margin-top:10px;}#mz-modal-description{width:calc(100% - 0);background:var(--input-bg-color);border:1px solid var(--border-color);color:var(--text-color);padding:12px 16px;border-radius:var(--border-radius);font-family:"Satoshi",sans-serif;font-size:14px;margin-bottom:20px;transition:all 0.3s ease;box-sizing:border-box;resize:vertical;min-height:60px;}#mz-modal-description:focus{outline:none;border-color:var(--border-color-focus);box-shadow:0 0 0 2px rgba(139,92,246,0.3);background:var(--input-bg-color-focus);}#mz-modal-buttons{display:flex;justify-content:flex-start;gap:12px;margin-top:15px;}.mz-modal-btn{display:inline-flex;align-items:center;justify-content:center;padding:10px 18px;font-family:"Satoshi",sans-serif;font-size:15px;font-weight:500;color:var(--text-color);background:var(--button-secondary-bg);border:1px solid var(--border-color);border-radius:var(--border-radius);cursor:pointer;transition:all 0.2s ease;min-width:90px;box-shadow:0 1px 2px rgba(0,0,0,0.1);}.mz-modal-btn:hover{background:var(--button-secondary-bg-hover);border-color:var(--border-color-hover);transform:translateY(-1px);box-shadow:0 3px 6px rgba(0,0,0,0.15);}.mz-modal-btn:active{background:var(--button-secondary-bg-active);transform:translateY(0);box-shadow:inset 0 1px 2px rgba(0,0,0,0.2);}.mz-modal-btn.primary{background:var(--primary-accent-color);color:#ffffff;font-weight:500;border:none;}.mz-modal-btn.primary:hover{background-color:#7C3AED;}.mz-modal-btn.cancel{background:transparent;color:var(--secondary-text-color);border:1px solid var(--border-color);}.mz-modal-btn.cancel:hover{background:rgba(255,255,255,0.03);color:var(--text-color);border-color:var(--border-color-hover);}.mz-modal-icon{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border-radius:50%;margin-right:14px;font-weight:bold;font-size:1.2em;}.mz-modal-icon.success{color:var(--success-color);background:rgba(34,197,94,0.1);}.mz-modal-icon.error{color:var(--error-color);background:rgba(239,68,68,0.1);}.mz-modal-icon.info{color:var(--info-color);background:rgba(59,130,246,0.1);}.mz-modal-title-with-icon{display:flex;align-items:center;}.formations-controls-container{display:flex;flex-wrap:nowrap;gap:8px;margin-top:0;align-items:center;}.tactics-search-box{width:140px !important;padding:8px 12px;margin-bottom:0 !important;border:1px solid var(--border-color);border-radius:var(--border-radius);background-color:var(--input-bg-color);color:var(--text-color);font-family:"Satoshi",sans-serif;font-size:14px;box-sizing:border-box;height:36px;transition:all 0.2s ease;position:relative;flex-shrink:0;}.tactics-search-box:focus{outline:none;border-color:var(--border-color-focus);box-shadow:0 0 0 2px rgba(139,92,246,0.3);background:var(--input-bg-color-focus);}.tactics-search-box.filtering{border-bottom:2px solid var(--primary-accent-color);animation:pulse-border 1.5s infinite;}@keyframes pulse-border{0%{border-color:var(--primary-accent-color);}50%{border-color:transparent;}100%{border-color:var(--primary-accent-color);}}.category-filter-wrapper{display:flex;align-items:center;gap:4px;flex-shrink:0;}#category_filter_selector{min-width:140px;flex-grow:0;}#manage_items_btn{padding:4px 8px;min-width:auto;height:36px;font-size:14px;line-height:1;flex-shrink:0;color:var(--secondary-text-color);}#manage_items_btn:hover{color:var(--text-color);}.mztm-custom-select-wrapper{position:relative;flex:1;min-width:180px;flex-grow:1;flex-shrink:1;}.mztm-custom-select-trigger{display:flex;align-items:center;justify-content:space-between;font-family:"Satoshi",sans-serif;font-size:14px;color:var(--text-color);padding:8px 14px;border:1px solid var(--border-color);border-radius:var(--border-radius);background-color:var(--input-bg-color);cursor:pointer;margin:0;transition:all 0.2s ease;height:36px;box-sizing:border-box;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;}.mztm-custom-select-trigger:hover:not(.disabled){background:var(--input-bg-color-hover);border-color:var(--border-color-hover);}.mztm-custom-select-trigger.open{border-color:var(--border-color-focus);box-shadow:0 0 0 2px rgba(139,92,246,0.3);border-bottom-left-radius:0;border-bottom-right-radius:0;background:var(--input-bg-color-focus);}.mztm-custom-select-trigger.disabled{opacity:0.5;cursor:not-allowed;}.mztm-custom-select-trigger::after{content:'';border:solid var(--secondary-text-color);border-width:0 2px 2px 0;display:inline-block;padding:3px;transform:rotate(45deg);-webkit-transform:rotate(45deg);margin-left:10px;flex-shrink:0;transition:border-color 0.2s ease;}.mztm-custom-select-trigger:hover:not(.disabled)::after{border-color:var(--text-color);}.mztm-custom-select-trigger.open::after{transform:rotate(-135deg);-webkit-transform:rotate(-135deg);margin-top:3px;}.mztm-custom-select-placeholder{color:var(--secondary-text-color);font-style:italic;}.mztm-custom-select-list-container{position:absolute;top:100%;left:0;right:0;background-color:#1F2937;border:1px solid var(--border-color-focus);border-top:none;border-radius:0 0 8px 8px;z-index:1001;max-height:250px;overflow-y:auto;box-shadow:0 5px 10px rgba(0,0,0,0.2);display:none;scrollbar-width:thin;scrollbar-color:var(--primary-accent-color) rgba(255,255,255,0.05);}.mztm-custom-select-list-container.open{display:block;animation:fadeIn 0.2s ease-out;}.mztm-custom-select-list-container::-webkit-scrollbar{width:6px;}.mztm-custom-select-list-container::-webkit-scrollbar-track{background:rgba(255,255,255,0.05);border-radius:3px;}.mztm-custom-select-list-container::-webkit-scrollbar-thumb{background-color:var(--primary-accent-color);border-radius:3px;}.mztm-custom-select-list{list-style:none;padding:0;margin:0;}.mztm-custom-select-category{color:var(--primary-accent-color);font-size:11px;font-weight:600;padding:6px 12px;background:rgba(0,0,0,0.2);margin-top:2px;border-top:1px solid var(--border-color);border-bottom:1px solid var(--border-color);cursor:default;text-transform:uppercase;letter-spacing:0.5px;}.mztm-custom-select-item{padding:8px 12px;cursor:pointer;transition:background-color 0.15s ease,color 0.15s ease;font-size:14px;color:var(--text-color);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}.mztm-custom-select-item:hover,.mztm-custom-select-item.highlighted{background-color:var(--primary-accent-color);color:#ffffff;}.mztm-custom-select-item.disabled{opacity:0.5;cursor:default;background-color:transparent !important;color:var(--secondary-text-color) !important;}.mztm-custom-select-item.hidden{display:none;}.mztm-custom-select-no-results{padding:10px 12px;color:var(--secondary-text-color);font-style:italic;cursor:default;}.tactics-style-indicator{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px;}.tactics-style-indicator.accent-teal{background-color:var(--style-accent-teal);}.tactics-style-indicator.accent-green{background-color:var(--style-accent-green);}.tactics-style-indicator.accent-orange{background-color:var(--style-accent-orange);}.tactics-style-indicator.accent-gray{background-color:var(--style-accent-gray);}#category-selector{width:75%;padding:10px 12px;border:1px solid var(--border-color);border-radius:var(--border-radius);background-color:var(--input-bg-color);color:var(--text-color);font-family:"Satoshi",sans-serif;font-size:13px;box-sizing:border-box;}#category-selector option{padding:8px;background-color:#1F2937;}.category-selection-container{margin-bottom:10px;}.category-selection-label{display:block;margin-bottom:5px;font-size:14px;color:var(--text-color);opacity:0.9;}.new-category-input-container{margin-top:10px;display:none;}.new-category-input-container.visible{display:block;}#new-category-input{width:100%;padding:10px 12px;border:1px solid var(--border-color);border-radius:var(--border-radius);background-color:var(--input-bg-color);color:var(--text-color);font-family:"Satoshi",sans-serif;font-size:14px;box-sizing:border-box;}#new-category-input:focus{outline:none;border-color:var(--border-color-focus);box-shadow:0 0 0 2px rgba(139,92,246,0.3);background:var(--input-bg-color-focus);}@keyframes fadeIn{from{opacity:0;transform:translateY(-5px);}to{opacity:1;transform:translateY(0);}}@keyframes shake{0%,100%{transform:translateX(0);}25%{transform:translateX(-2px);}50%{transform:translateX(0);}75%{transform:translateX(2px);}}.mztm-custom-select-wrapper.filtering .mztm-custom-select-trigger{border-color:var(--primary-accent-color);animation:pulse-border 1.5s infinite;}.action-buttons-section{display:flex;flex-direction:column;flex-wrap:nowrap;margin-top:10px;gap:4px;}.action-buttons-row{display:flex;flex-wrap:wrap;gap:6px;width:100%;}.footer-actions{position:absolute;bottom:10px;right:16px;background:transparent !important;border:none !important;box-shadow:none !important;font-family:"General Sans",sans-serif !important;color:gold !important;opacity:0.8;transition:opacity 0.2s ease;}.footer-actions:hover{opacity:1;transform:none !important;}#manage_action_dropdown_menu{max-height:200px;overflow-y:auto;overflow-x:hidden;}#loading-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:rgba(17,24,39,0.7);backdrop-filter:blur(3px);display:flex;justify-content:center;align-items:center;z-index:10001;opacity:0;transition:opacity 0.3s ease;pointer-events:none;}#loading-overlay.visible{opacity:1;pointer-events:auto;}#loading-spinner{border:4px solid rgba(255,255,255,0.1);border-left-color:var(--primary-accent-color);border-radius:50%;width:40px;height:40px;animation:spin 1s linear infinite;}@keyframes spin{to{transform:rotate(360deg);}}.mode-toggle-container{display:flex;align-items:center;margin:0 10px 0 30px;white-space:nowrap;}.mode-toggle-switch{position:relative;display:inline-block;width:44px;height:22px;margin:0 10px;vertical-align:middle;}.mode-toggle-switch input{opacity:0;width:0;height:0;}.mode-toggle-slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background-color:#4B5563;transition:0.4s;border-radius:22px;border:1px solid var(--border-color);}.mode-toggle-slider:before{position:absolute;content:"";height:16px;width:16px;left:3px;bottom:2px;background-color:white;transition:0.4s;border-radius:50%;}.mode-toggle-switch input:checked + .mode-toggle-slider{background-color:var(--primary-accent-color);}.mode-toggle-switch input:checked + .mode-toggle-slider:before{transform:translateX(22px);}.mode-toggle-label{font-size:12px;vertical-align:middle;color:var(--secondary-text-color);opacity:0.8;transition:opacity 0.2s ease,color 0.2s ease;}.mode-toggle-label.active{opacity:1;color:var(--text-color);font-weight:500;}.section-content{position:relative;padding-bottom:2px;overflow:visible;}.management-modal-wrapper{width:100%;}.management-modal-content{display:none;padding:5px;max-height:50vh;overflow-y:auto;overflow-x:hidden;scrollbar-width:thin;scrollbar-color:var(--primary-accent-color) rgba(255,255,255,0.05);}.management-modal-content::-webkit-scrollbar{width:8px;}.management-modal-content::-webkit-scrollbar-track{background:rgba(255,255,255,0.05);border-radius:4px;}.management-modal-content::-webkit-scrollbar-thumb{background-color:var(--primary-accent-color);border-radius:4px;}.management-modal-content.active{display:block;animation:fadeIn 0.5s ease;}.formation-management-list,.category-management-list{list-style:none;padding:0;margin:10px 0 0 0;}.formation-management-list li,.category-management-list li{display:flex;justify-content:space-between;align-items:center;padding:10px 5px;border-bottom:1px solid var(--border-color);transition:background-color 0.2s ease;}.formation-management-list li:last-child,.category-management-list li:last-child{border-bottom:none;}.item-name-container{flex-grow:1;margin-right:10px;display:flex;align-items:center;min-width:0;}.item-name{flex-grow:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-color);}.item-name-input{flex-grow:1;background:var(--input-bg-color-hover);border:1px solid var(--border-color-hover);color:var(--text-color);padding:4px 8px;border-radius:4px;font-family:"Satoshi",sans-serif;font-size:14px;margin-right:5px;}.item-name-input:focus{outline:none;border-color:var(--border-color-focus);box-shadow:0 0 0 2px rgba(139,92,246,0.3);}.item-controls{display:flex;align-items:center;gap:8px;flex-shrink:0;}.item-category-select{padding:4px 8px !important;height:30px !important;font-size:12px !important;min-width:120px;background-position:right 5px top 50%;padding-right:25px !important;flex-shrink:0;background-color:var(--input-bg-color);border-color:var(--border-color);color:var(--secondary-text-color);}.item-category-select:hover{background-color:var(--input-bg-color-hover);border-color:var(--border-color-hover);color:var(--text-color);}.item-action-btn{background:transparent;border:none;color:var(--secondary-text-color);opacity:0.7;cursor:pointer;font-size:16px;padding:4px;transition:opacity 0.2s ease,color 0.2s ease;line-height:1;}.item-action-btn:hover{opacity:1;color:var(--text-color);}.save-name-btn{color:var(--success-color);}.cancel-name-btn{color:var(--error-color);}.delete-item-btn{color:var(--error-color);}.delete-item-btn:hover{color:var(--error-color);}.add-category-section{display:flex;gap:8px;padding:10px 5px;border-bottom:1px solid var(--border-color);margin-bottom:10px;}.add-category-input{flex-grow:1;background:var(--input-bg-color);border:1px solid var(--border-color);color:var(--text-color);padding:8px 12px;border-radius:var(--border-radius);font-family:"Satoshi",sans-serif;font-size:14px;}.add-category-input:focus{outline:none;border-color:var(--border-color-focus);box-shadow:0 0 0 2px rgba(139,92,246,0.3);background:var(--input-bg-color-focus);}.add-category-btn{padding:8px 15px !important;font-size:14px !important;min-width:auto !important;background:var(--primary-accent-color) !important;color:white !important;border:none !important;}.add-category-btn:hover{background:#7C3AED !important;transform:none !important;box-shadow:none !important;}.category-management-list li span{flex-grow:1;margin-right:10px;color:var(--text-color);}.category-remove-btn{background-color:rgba(239,68,68,0.1) !important;border:1px solid rgba(239,68,68,0.3) !important;color:var(--error-color) !important;padding:5px 10px !important;min-width:auto !important;font-size:13px !important;}.category-remove-btn:hover{background-color:rgba(239,68,68,0.2) !important;border-color:rgba(239,68,68,0.5) !important;color:#FECACA !important;transform:none !important;box-shadow:none !important;}.no-items-message,.no-custom-categories-message{padding:15px;text-align:center;color:var(--secondary-text-color);}#mztm-tactic-preview{position:fixed;z-index:10002;background-color:#1F2937;color:var(--text-color);border:1px solid var(--border-color-hover);border-radius:var(--border-radius);padding:10px 15px;font-family:"Satoshi",sans-serif;font-size:13px;box-shadow:0 4px 12px rgba(0,0,0,0.2);max-width:250px;pointer-events:none;line-height:1.5;opacity:0;transition:opacity 0.2s ease-in-out;white-space:normal;display:none;}.mztm-preview-formation{margin-bottom:8px;padding-bottom:6px;border-bottom:1px solid var(--border-color);font-size:14px;}.mztm-preview-formation strong{color:var(--primary-accent-color);font-weight:500;}.mztm-preview-desc{max-height:150px;overflow-y:auto;word-wrap:break-word;scrollbar-width:thin;color:var(--secondary-text-color);scrollbar-color:var(--primary-accent-color) rgba(255,255,255,0.05);}.mztm-preview-desc::-webkit-scrollbar{width:4px;}.mztm-preview-desc::-webkit-scrollbar-track{background:rgba(255,255,255,0.05);border-radius:2px;}.mztm-preview-desc::-webkit-scrollbar-thumb{background-color:var(--primary-accent-color);border-radius:2px;}.mztm-preview-no-desc{color:var(--secondary-text-color);opacity:0.7;font-style:italic;}`;
  24. GM_addStyle(wakkanai);
  25.  
  26. const OUTFIELD_PLAYERS_SELECTOR = '.fieldpos.fieldpos-ok.ui-draggable:not(.substitute):not(.goalkeeper):not(.substitute.goalkeeper), .fieldpos.fieldpos-collision.ui-draggable:not(.substitute):not(.goalkeeper):not(.substitute.goalkeeper)';
  27. const GOALKEEPER_SELECTOR = '.fieldpos.fieldpos-ok.goalkeeper.ui-draggable';
  28. const FORMATION_TEXT_SELECTOR = '#formation_text';
  29. const TACTIC_SLOT_SELECTOR = '.ui-state-default.ui-corner-top.ui-tabs-selected.ui-state-active.invalid';
  30. const MIN_PLAYERS_ON_PITCH = 11;
  31. const MAX_TACTIC_NAME_LENGTH = 50;
  32. const MAX_CATEGORY_NAME_LENGTH = 30;
  33. const MAX_DESCRIPTION_LENGTH = 250;
  34. const SCRIPT_VERSION = '13.3.7';
  35. const DISPLAY_VERSION = '13.3';
  36. const SCRIPT_NAME = 'MZ Tactics Manager';
  37. const VERSION_KEY = 'mz_tactics_version';
  38. const COLLAPSED_KEY = 'mz_tactics_collapsed';
  39. const VIEW_MODE_KEY = 'mztm_view_mode';
  40. const CATEGORIES_STORAGE_KEY = 'mz_tactics_categories';
  41. const FORMATIONS_STORAGE_KEY = 'mztm_formations';
  42. const OLD_FORMATIONS_STORAGE_KEY = 'ls_tactics';
  43. const COMPLETE_TACTICS_STORAGE_KEY = 'mztm_complete_tactics';
  44. const ROSTER_CACHE_KEY = 'mztm_roster_cache';
  45. const USER_INFO_CACHE_KEY = 'mztm_user_info_cache';
  46. const CATEGORY_FILTER_STORAGE_KEY = 'mztm_last_category_filter';
  47. const ROSTER_CACHE_DURATION_MS = 3600000;
  48. const USER_INFO_CACHE_DURATION_MS = 86400000;
  49. const INITIAL_FORMATIONS_DATA = {"tactics": [{"name": "SP_3d_1dm (1)","coordinates": [[96,54],[147,54],[120,94],[96,141],[139,169],[52,171],[96,188],[139,216],[54,217],[97,236]]},{"name": "SP_3d_1dm (2)","coordinates": [[101,54],[74,96],[120,97],[96,137],[55,163],[137,163],[96,184],[55,210],[137,210],[96,231]]},{"name": "SP_3d_1dm (3)","coordinates": [[97,54],[138,78],[97,101],[96,147],[56,170],[136,171],[96,194],[56,218],[136,218],[96,241]]},{"name": "SP_3d_1dm (4)","coordinates": [[118,54],[97,95],[144,95],[97,148],[57,172],[137,172],[97,195],[57,219],[137,219],[97,242]]},{"name": "SP_3d_1dm (5)","coordinates": [[125,54],[82,77],[122,101],[81,126],[56,167],[136,167],[96,190],[56,214],[136,214],[96,237]]},{"name": "SP_3d_mand (1)","coordinates": [[96,54],[136,77],[96,101],[136,124],[96,148],[72,188],[120,188],[49,228],[96,228],[143,228]]},{"name": "SP_3d_morph (1)","coordinates": [[86,54],[154,54],[120,85],[96,125],[143,125],[72,165],[119,165],[49,205],[96,205],[143,205]]},{"name": "SP_3d_morph (2)","coordinates": [[82,55],[161,56],[124,88],[157,121],[96,125],[72,165],[119,165],[49,205],[96,205],[143,205]]},{"name": "SP_4d_2dm_c (1)","coordinates": [[100,54],[73,96],[123,98],[97,136],[73,176],[120,176],[26,209],[167,209],[73,224],[120,224]]},{"name": "SP_4d_2dm_c (2)","coordinates": [[69,54],[129,54],[97,92],[97,139],[73,179],[120,179],[26,209],[167,209],[73,227],[120,227]]},{"name": "SP_4d_2dm_c (3)","coordinates": [[97,54],[145,54],[119,98],[97,146],[73,186],[120,186],[26,209],[167,209],[73,234],[120,234]]},{"name": "SP_4d_2dm_w (1)","coordinates": [[77,54],[144,54],[111,87],[96,134],[70,172],[122,172],[3,217],[191,217],[73,219],[120,220]]},{"name": "SP_4d_2dm_w (2)","coordinates": [[91,55],[130,83],[85,103],[98,149],[121,190],[75,191],[5,214],[189,214],[74,237],[121,237]]},{"name": "SP_4d_3dm (1)","coordinates": [[100,54],[74,99],[121,99],[50,174],[144,174],[96,176],[26,216],[73,216],[120,216],[167,216]]},{"name": "SP_4d_3dm (2)","coordinates": [[92,54],[143,54],[113,103],[97,160],[45,174],[151,176],[3,215],[187,215],[74,218],[128,218]]},{"name": "SP_5d_2dm (1)","coordinates": [[106,54],[81,93],[96,137],[73,177],[120,177],[2,205],[190,205],[49,217],[96,217],[143,217]]},{"name": "SP_5d_2dm (2)","coordinates": [[101,72],[75,118],[121,122],[73,177],[120,177],[2,205],[190,205],[49,217],[96,217],[143,217]]},{"name": "SP_5d_2dm (3)","coordinates": [[66,54],[126,54],[96,125],[73,165],[120,165],[2,205],[49,205],[96,205],[143,205],[190,205]]},{"name": "WP_2d_3dm_1w (1)","coordinates": [[125,56],[190,60],[78,71],[97,190],[49,191],[146,191],[191,215],[3,218],[74,230],[121,230]]},{"name": "WP_3d_1dm_1w (1)","coordinates": [[124,54],[191,60],[82,73],[97,145],[57,168],[137,170],[97,194],[57,217],[137,217],[97,241]]},{"name": "WP_3d_2dm_1w (1)","coordinates": [[128,54],[189,62],[78,69],[120,148],[72,149],[73,195],[120,195],[50,235],[97,235],[143,236]]},{"name": "WP_3d_3dm_1w (1)","coordinates": [[188,67],[79,72],[124,102],[98,142],[74,182],[121,182],[168,182],[51,222],[98,222],[145,222]]},{"name": "WP_4d_2dm_1w_c (1)","coordinates": [[125,54],[191,60],[82,71],[97,150],[74,192],[121,192],[34,216],[161,216],[74,240],[121,240]]},{"name": "WP_4d_2dm_1w_c (2)","coordinates": [[191,60],[82,71],[123,92],[97,150],[74,192],[121,192],[34,216],[161,216],[74,240],[121,240]]},{"name": "WP_4d_2dm_1w_w (1)","coordinates": [[122,54],[189,58],[79,72],[97,142],[73,182],[122,182],[7,209],[187,209],[72,228],[123,228]]},{"name": "WP_4d_2dm_2w_c (1)","coordinates": [[78,71],[191,72],[164,110],[96,153],[72,193],[120,193],[30,220],[162,220],[72,240],[120,240]]},{"name": "WP_4d_3dm_1w_w (1)","coordinates": [[126,54],[190,61],[80,72],[50,174],[97,174],[144,174],[30,217],[77,217],[124,217],[171,217]]},{"name": "WP_4d_2dm_1w_w (2)","coordinates": [[124,54],[189,54],[77,71],[97,128],[96,176],[144,176],[26,216],[73,216],[120,216],[167,216]]},{"name": "WP_5d_2dm_1w (1)","coordinates": [[190,57],[78,71],[96,123],[74,174],[121,174],[192,207],[4,208],[50,214],[97,214],[144,214]]},{"name": "WP_5d_2dm_2w (1)","coordinates": [[190,60],[81,71],[168,105],[122,177],[75,178],[5,207],[189,207],[50,217],[144,218],[97,219]]}]};
  50. const DEFAULT_CATEGORIES = {
  51. 'short_passing': {
  52. id: 'short_passing',
  53. name: 'Short Passing',
  54. color: '#54a0ff'
  55. },
  56. 'wing_play': {
  57. id: 'wing_play',
  58. name: 'Wing Play',
  59. color: '#5dd39e'
  60. }
  61. };
  62. const NEW_CATEGORY_ID = 'new_category';
  63. const OTHER_CATEGORY_ID = 'other';
  64. const USERSCRIPT_STRINGS = {
  65. addButton: 'Add',
  66. addCurrentTactic: 'Add Current',
  67. addWithXmlButton: 'Add via XML',
  68. manageButton: 'Manage',
  69. deleteButton: 'Delete',
  70. renameButton: 'Edit',
  71. updateButton: 'Update Coords',
  72. clearButton: 'Clear',
  73. resetButton: 'Reset',
  74. importButton: 'Import',
  75. exportButton: 'Export',
  76. infoButton: 'FAQ',
  77. saveButton: 'Save',
  78. tacticNamePrompt: 'Please enter a name and a category',
  79. addAlert: 'Formation {} added successfully.',
  80. deleteAlert: 'Item {} deleted successfully.',
  81. renameAlert: 'Item {} successfully edited.',
  82. updateAlert: 'Formation {} updated successfully.',
  83. clearAlert: 'Formations cleared successfully.',
  84. resetAlert: 'Formations were reset successfully.',
  85. importAlert: 'Formations imported successfully.',
  86. exportAlert: 'Formations JSON copied to clipboard.',
  87. deleteConfirmation: 'Do you really want to delete {}?',
  88. updateConfirmation: 'Do you really want to update {} coords?',
  89. clearConfirmation: 'Do you really want to clear all saved formations?',
  90. resetConfirmation: 'Reset to default formations? This will remove all your custom formations.',
  91. invalidTacticError: 'Invalid formation. Ensure 11 players are on the pitch.',
  92. noTacticNameProvidedError: 'No name provided.',
  93. alreadyExistingTacticNameError: 'Name already exists.',
  94. tacticNameMaxLengthError: `Name is too long (max ${MAX_TACTIC_NAME_LENGTH} chars).`,
  95. noTacticSelectedError: 'No item selected.',
  96. duplicateTacticError: 'This formation already exists.',
  97. duplicateTacticErrorWithName: 'This formation already exists (name: {}).',
  98. noChangesMadeError: 'No changes detected in player positions.',
  99. invalidImportError: 'Invalid import data. Please provide valid JSON.',
  100. modalContentInfoText: 'MZ Tactics Manager by douglaskampl.',
  101. modalContentFeedbackText: 'For feedback or suggestions, contact via GB/Chat.',
  102. usefulContent: '',
  103. tacticsDropdownMenuLabel: 'Select a Formation',
  104. completeTacticsDropdownMenuLabel: 'Select a Tactic',
  105. errorTitle: 'Error',
  106. doneTitle: 'Success',
  107. confirmationTitle: 'Confirmation',
  108. deleteTacticConfirmButton: 'Delete',
  109. cancelConfirmButton: 'Cancel',
  110. updateConfirmButton: 'Update',
  111. clearTacticsConfirmButton: 'Clear',
  112. resetTacticsConfirmButton: 'Reset',
  113. addConfirmButton: 'Add',
  114. xmlValidationError: 'Invalid XML format',
  115. xmlParsingError: 'Error parsing XML',
  116. xmlPlaceholder: 'Paste Formation XML here',
  117. tacticNamePlaceholder: 'Formation name',
  118. managerTitle: SCRIPT_NAME,
  119. searchPlaceholder: 'Search...',
  120. allTacticsFilter: 'All',
  121. noTacticsFound: 'No formations found',
  122. welcomeMessage: `Userscript updated to v${SCRIPT_VERSION}.\n\nChanges in this version:\n Fixed initial tactics (for people using the userscript for the first time or when resetting tactics).`,
  123. welcomeGotIt: 'Got it!',
  124. removeCategoryConfirmation: 'Remove category "{}"? (All formations in this category will be moved to "Other").',
  125. removeCategoryAlert: 'Category "{}" removed successfully.',
  126. removeCategoryButton: 'Remove',
  127. completeTacticsTitle: 'Tactics Management',
  128. saveCompleteTacticButton: 'Save Current',
  129. loadCompleteTacticButton: 'Load',
  130. deleteCompleteTacticButton: 'Delete',
  131. renameCompleteTacticButton: 'Rename',
  132. updateCompleteTacticButton: 'Update with Current',
  133. importCompleteTacticsButton: 'Import',
  134. exportCompleteTacticsButton: 'Export',
  135. completeTacticNamePrompt: 'Please enter a name for the tactic',
  136. renameCompleteTacticPrompt: 'Enter a new name for the tactic:',
  137. updateCompleteTacticConfirmation: 'Overwrite tactic "{}" with the current setup (positions, rules, settings) from the pitch?',
  138. completeTacticSaveSuccess: 'Tactic {} saved successfully.',
  139. completeTacticLoadSuccess: 'Tactic {} loaded successfully.',
  140. completeTacticDeleteSuccess: 'Tactic {} deleted successfully.',
  141. completeTacticRenameSuccess: 'Tactic renamed to {} successfully.',
  142. completeTacticUpdateSuccess: 'Tactic {} updated successfully.',
  143. importCompleteTacticsTitle: 'Import Tactics (JSON)',
  144. exportCompleteTacticsTitle: 'Export Tactics (JSON)',
  145. importCompleteTacticsPlaceholder: 'Paste Tactics JSON here',
  146. importCompleteTacticsAlert: 'Tactics imported successfully.',
  147. exportCompleteTacticsAlert: 'Tactics JSON copied to clipboard.',
  148. invalidCompleteImportError: 'Invalid import data. Please provide valid JSON (object map).',
  149. errorFetchingRoster: 'Error fetching team roster. Cannot load Tactic.',
  150. errorInsufficientPlayers: 'Not enough available players in roster to fill required positions.',
  151. errorXmlExportParse: 'Error parsing XML from native export.',
  152. errorXmlGenerate: 'Error generating XML for import.',
  153. errorImportFailed: 'Native import failed. Check XML validity or player availability.',
  154. warningPlayersSubstituted: 'Warning: roster mismatch. Some were players replaced at random. Tactic updated!',
  155. invalidXmlForImport: 'MZ rejected the generated XML. It might be invalid or player assignments failed.',
  156. completeTacticNamePlaceholder: 'Tactic name',
  157. normalModeLabel: 'Formations',
  158. completeModeLabel: 'Tactics',
  159. modeLabel: '',
  160. manageCategoriesTitle: 'Manage Categories',
  161. noCustomCategories: 'No custom categories to manage.',
  162. manageCategoriesDoneButton: 'Done',
  163. managementModalTitle: 'Manage Formations & Categories',
  164. formationsTabTitle: 'Formations',
  165. categoriesTabTitle: 'Categories',
  166. addCategoryPlaceholder: 'New category name...',
  167. addCategoryButton: '+ Add',
  168. categoryNameMaxLengthError: `Category name too long (max ${MAX_CATEGORY_NAME_LENGTH} chars).`,
  169. saveChangesButton: 'Save Changes',
  170. changesSavedSuccess: 'Changes saved successfully.',
  171. noChangesToSave: 'No changes to save.',
  172. descriptionLabel: 'Description (optional):',
  173. descriptionPlaceholder: `Enter a short description (max ${MAX_DESCRIPTION_LENGTH} chars)...`,
  174. descriptionMaxLengthError: `Description too long (max ${MAX_DESCRIPTION_LENGTH} chars).`,
  175. previewFormationLabel: 'Formation:',
  176. xmlRequiredError: 'Please paste the XML data first.',
  177. invalidXmlFormatError: 'The provided text does not appear to be valid XML.',
  178. noTacticsSaved: 'No formations saved',
  179. noCompleteTacticsSaved: 'No tactics saved'
  180. };
  181. const DEFAULT_MODAL_STRINGS = {
  182. ok: 'OK',
  183. cancel: 'Cancel',
  184. error: 'Error',
  185. close: '×'
  186. };
  187.  
  188. let tactics = [];
  189. let completeTactics = {};
  190. let currentFilter = 'all';
  191. let searchTerm = '';
  192. let categories = {};
  193. let rosterCache = { data: null, timestamp: 0, teamId: null };
  194. let userInfoCache = { teamId: null, username: null, timestamp: 0 };
  195. let teamId = null;
  196. let username = null;
  197. let loadingOverlay = null;
  198. let currentViewMode = 'normal';
  199. let collapsedIconElement = null;
  200. let previewElement = null;
  201. let previewHideTimeout = null;
  202. let currentOpenDropdown = null;
  203. let selectedFormationTacticId = null;
  204. let selectedCompleteTacticName = null;
  205.  
  206. function createModalIcon(type) {
  207. if (!type) return null;
  208. const i = document.createElement('div');
  209. i.classList.add('mz-modal-icon');
  210. if (type === 'success') {
  211. i.classList.add('success');
  212. i.innerHTML = '✓';
  213. } else if (type === 'error') {
  214. i.classList.add('error');
  215. i.innerHTML = '✗';
  216. } else if (type === 'info') {
  217. i.classList.add('info');
  218. i.innerHTML = 'ℹ';
  219. }
  220. return i;
  221. }
  222.  
  223. function validateModalInput(inputElement, validatorFn, errorElementId) {
  224. if (!validatorFn || !inputElement || !inputElement.parentNode) return null;
  225. const validationError = validatorFn(inputElement.value);
  226. const existingError = document.getElementById(errorElementId);
  227. if (existingError) existingError.remove();
  228. if (!validationError) return null;
  229. const errorContainer = document.createElement('div');
  230. errorContainer.id = errorElementId;
  231. errorContainer.style.color = '#ff6b6b';
  232. errorContainer.style.marginTop = inputElement.tagName === 'TEXTAREA' ? '5px' : '-10px';
  233. errorContainer.style.marginBottom = '10px';
  234. errorContainer.style.fontSize = '13px';
  235. errorContainer.textContent = validationError;
  236. inputElement.parentNode.insertBefore(errorContainer, inputElement.nextSibling);
  237. return validationError;
  238. }
  239.  
  240.  
  241. function closeModal(overlayElement, callback) {
  242. if (!overlayElement) return;
  243. overlayElement.classList.remove('active');
  244. setTimeout(() => {
  245. if (overlayElement && overlayElement.parentNode === document.body) document.body.removeChild(overlayElement);
  246. if (callback) callback();
  247. }, 300);
  248. }
  249.  
  250. function handleAlertConfirm(options, inputElement, descElement, categorySelect, newCategoryInput, overlayElement, resolve) {
  251. if (options.input === 'text' && options.inputValidator && inputElement) {
  252. const validationError = validateModalInput(inputElement, options.inputValidator, 'mz-modal-input-error');
  253. if (validationError) return;
  254. }
  255. if (options.descriptionInput === 'textarea' && options.descriptionValidator && descElement) {
  256. const descValidationError = validateModalInput(descElement, options.descriptionValidator, 'mz-modal-desc-error');
  257. if (descValidationError) return;
  258. }
  259.  
  260. let selectedCategoryId = null;
  261. let newCategoryName = null;
  262. if (categorySelect) {
  263. selectedCategoryId = categorySelect.value;
  264. if (selectedCategoryId === NEW_CATEGORY_ID && newCategoryInput) {
  265. newCategoryName = newCategoryInput.value.trim();
  266. const categoryErrorElement = document.getElementById('new-category-error');
  267. if (categoryErrorElement) categoryErrorElement.remove();
  268. if (!newCategoryName) {
  269. const errorText = document.createElement('div');
  270. errorText.style.color = '#ff6b6b';
  271. errorText.style.marginTop = '5px';
  272. errorText.style.fontSize = '13px';
  273. errorText.textContent = 'Category name cannot be empty.';
  274. errorText.id = 'new-category-error';
  275. newCategoryInput.parentNode.appendChild(errorText);
  276. return;
  277. }
  278. const existingCategory = Object.values(categories).find(cat => cat.name.toLowerCase() === newCategoryName.toLowerCase());
  279. if (existingCategory) {
  280. const errorText = document.createElement('div');
  281. errorText.style.color = '#ff6b6b';
  282. errorText.style.marginTop = '5px';
  283. errorText.style.fontSize = '13px';
  284. errorText.textContent = 'Category name already exists.';
  285. errorText.id = 'new-category-error';
  286. newCategoryInput.parentNode.appendChild(errorText);
  287. return;
  288. }
  289. }
  290. }
  291. closeModal(overlayElement, () => {
  292. let result = {
  293. isConfirmed: true
  294. };
  295. if (options.input === 'text') {
  296. result.value = inputElement ? inputElement.value : null;
  297. }
  298. if (options.descriptionInput === 'textarea') {
  299. result.description = descElement ? descElement.value : null;
  300. }
  301. if (categorySelect) {
  302. if (selectedCategoryId === NEW_CATEGORY_ID && newCategoryName) {
  303. const newCategoryId = generateCategoryId(newCategoryName);
  304. const newCategory = {
  305. id: newCategoryId,
  306. name: newCategoryName,
  307. color: generateCategoryColor(newCategoryName)
  308. };
  309. result.category = newCategory;
  310. addCategory(newCategory);
  311. } else {
  312. result.category = categories[selectedCategoryId] || categories[OTHER_CATEGORY_ID] || {
  313. id: OTHER_CATEGORY_ID,
  314. name: 'Other',
  315. color: '#8395a7'
  316. };
  317. }
  318. }
  319. resolve(result);
  320. });
  321. }
  322.  
  323. function handleAlertCancel(overlayElement, resolve) {
  324. closeModal(overlayElement, () => {
  325. resolve({
  326. isConfirmed: false,
  327. value: null,
  328. description: null
  329. });
  330. });
  331. }
  332.  
  333. function setUpKeyboardHandler(confirmHandler, cancelHandler, inputElement, descElement) {
  334. return function (event) {
  335. if (event.key === 'Escape') {
  336. cancelHandler();
  337. } else if (event.key === 'Enter') {
  338. const activeEl = document.activeElement;
  339. if (!(activeEl === descElement && descElement?.tagName === 'TEXTAREA') && !(activeEl === inputElement && inputElement?.tagName === 'TEXTAREA')) {
  340. confirmHandler();
  341. } else if (activeEl === inputElement && inputElement?.tagName === 'INPUT') {
  342. confirmHandler();
  343. }
  344. }
  345. };
  346. }
  347.  
  348. function showAlert(options) {
  349. return new Promise((resolve) => {
  350. const overlay = document.createElement('div');
  351. overlay.id = 'mz-modal-overlay';
  352. const container = document.createElement('div');
  353. container.id = 'mz-modal-container';
  354. if (options.modalClass) container.classList.add(options.modalClass);
  355. const header = document.createElement('div');
  356. header.id = 'mz-modal-header';
  357. const titleContainer = document.createElement('div');
  358. titleContainer.classList.add('mz-modal-title-with-icon');
  359. const icon = createModalIcon(options.type);
  360. if (icon) titleContainer.appendChild(icon);
  361. const title = document.createElement('h2');
  362. title.id = 'mz-modal-title';
  363. title.textContent = options.title || '';
  364. titleContainer.appendChild(title);
  365. header.appendChild(titleContainer);
  366. const closeButton = document.createElement('button');
  367. closeButton.id = 'mz-modal-close';
  368. closeButton.innerHTML = DEFAULT_MODAL_STRINGS.close;
  369. header.appendChild(closeButton);
  370. const content = document.createElement('div');
  371. content.id = 'mz-modal-content';
  372. if (options.htmlContent) {
  373. content.appendChild(options.htmlContent);
  374. } else if (options.text) {
  375. const textNode = document.createTextNode(options.text);
  376. content.appendChild(textNode);
  377. }
  378. let inputElem = null,
  379. descElem = null,
  380. descLabel = null,
  381. categorySelectElem = null,
  382. newCategoryInputElem = null,
  383. categoryContainer = null;
  384.  
  385. if (options.input === 'text') {
  386. inputElem = document.createElement('input');
  387. inputElem.id = 'mz-modal-input';
  388. inputElem.type = 'text';
  389. inputElem.value = options.inputValue || '';
  390. inputElem.placeholder = options.placeholder || '';
  391. }
  392.  
  393. if (options.descriptionInput === 'textarea') {
  394. descLabel = document.createElement('label');
  395. descLabel.className = 'mz-modal-label';
  396. descLabel.textContent = options.descriptionLabel || USERSCRIPT_STRINGS.descriptionLabel;
  397. descLabel.htmlFor = 'mz-modal-description';
  398. descElem = document.createElement('textarea');
  399. descElem.id = 'mz-modal-description';
  400. descElem.value = options.descriptionValue || '';
  401. descElem.placeholder = options.descriptionPlaceholder || USERSCRIPT_STRINGS.descriptionPlaceholder;
  402. descElem.rows = 3;
  403. }
  404.  
  405. if (options.showCategorySelector) {
  406. categoryContainer = document.createElement('div');
  407. categoryContainer.className = 'category-selection-container';
  408. const categoryLabel = document.createElement('label');
  409. categoryLabel.className = 'category-selection-label';
  410. categoryLabel.textContent = 'Category:';
  411. categoryContainer.appendChild(categoryLabel);
  412. categorySelectElem = document.createElement('select');
  413. categorySelectElem.id = 'category-selector';
  414. const usedCategoryIds = new Set(tactics.map(t => t.style).filter(Boolean));
  415. if (options.currentCategory) usedCategoryIds.add(options.currentCategory);
  416. const availableCategories = Object.values(categories).filter(cat => DEFAULT_CATEGORIES[cat.id] || cat.id === OTHER_CATEGORY_ID || usedCategoryIds.has(cat.id));
  417. availableCategories.sort((a, b) => {
  418. if (a.id === OTHER_CATEGORY_ID) return 1;
  419. if (b.id === OTHER_CATEGORY_ID) return -1;
  420. return a.name.localeCompare(b.name);
  421. });
  422. availableCategories.forEach(cat => {
  423. if (cat.id !== OTHER_CATEGORY_ID) {
  424. const opt = document.createElement('option');
  425. opt.value = cat.id;
  426. opt.textContent = cat.name;
  427. categorySelectElem.appendChild(opt);
  428. }
  429. });
  430. const otherOption = document.createElement('option');
  431. otherOption.value = OTHER_CATEGORY_ID;
  432. otherOption.textContent = getCategoryName(OTHER_CATEGORY_ID);
  433. categorySelectElem.appendChild(otherOption);
  434. const addNewOption = document.createElement('option');
  435. addNewOption.value = NEW_CATEGORY_ID;
  436. addNewOption.textContent = '+ New category';
  437. categorySelectElem.appendChild(addNewOption);
  438. categorySelectElem.value = (options.currentCategory && categories[options.currentCategory]) ? options.currentCategory : OTHER_CATEGORY_ID;
  439. const newCategoryContainer = document.createElement('div');
  440. newCategoryContainer.className = 'new-category-input-container';
  441. newCategoryInputElem = document.createElement('input');
  442. newCategoryInputElem.id = 'new-category-input';
  443. newCategoryInputElem.type = 'text';
  444. newCategoryInputElem.placeholder = 'New category name';
  445. newCategoryContainer.appendChild(newCategoryInputElem);
  446. categorySelectElem.addEventListener('change', function () {
  447. const isNew = this.value === NEW_CATEGORY_ID;
  448. newCategoryContainer.classList.toggle('visible', isNew);
  449. if (isNew) newCategoryInputElem.focus();
  450. const categoryError = document.getElementById('new-category-error');
  451. if (categoryError) categoryError.remove();
  452. });
  453. categoryContainer.appendChild(categorySelectElem);
  454. categoryContainer.appendChild(newCategoryContainer);
  455. }
  456. const buttons = document.createElement('div');
  457. buttons.id = 'mz-modal-buttons';
  458. const confirmHandler = () => handleAlertConfirm(options, inputElem, descElem, categorySelectElem, newCategoryInputElem, overlay, resolve);
  459. const cancelHandler = () => handleAlertCancel(overlay, resolve);
  460. const confirmButton = document.createElement('button');
  461. confirmButton.classList.add('mz-modal-btn', 'primary');
  462. confirmButton.textContent = options.confirmButtonText || DEFAULT_MODAL_STRINGS.ok;
  463. confirmButton.addEventListener('click', confirmHandler);
  464. buttons.appendChild(confirmButton);
  465. if (options.showCancelButton) {
  466. const cancelButton = document.createElement('button');
  467. cancelButton.classList.add('mz-modal-btn', 'cancel');
  468. cancelButton.textContent = options.cancelButtonText || DEFAULT_MODAL_STRINGS.cancel;
  469. cancelButton.addEventListener('click', cancelHandler);
  470. buttons.appendChild(cancelButton);
  471. }
  472. closeButton.addEventListener('click', cancelHandler);
  473. const keyboardHandler = setUpKeyboardHandler(confirmHandler, cancelHandler, inputElem, descElem);
  474. document.addEventListener('keydown', keyboardHandler);
  475.  
  476. container.appendChild(header);
  477. container.appendChild(content);
  478. if (inputElem) {
  479. container.appendChild(inputElem);
  480. }
  481. if (descLabel && descElem) {
  482. container.appendChild(descLabel);
  483. container.appendChild(descElem);
  484. }
  485. if (categoryContainer) {
  486. container.appendChild(categoryContainer);
  487. }
  488. container.appendChild(buttons);
  489.  
  490. overlay.appendChild(container);
  491. document.body.appendChild(overlay);
  492. setTimeout(() => {
  493. overlay.classList.add('active');
  494. if (inputElem) inputElem.focus();
  495. else if (descElem) descElem.focus();
  496. if (categorySelectElem && categorySelectElem.value === NEW_CATEGORY_ID) newCategoryInputElem.focus();
  497. }, 10);
  498. overlay.addEventListener('transitionend', () => {
  499. if (!overlay.classList.contains('active')) document.removeEventListener('keydown', keyboardHandler);
  500. });
  501. });
  502. }
  503.  
  504. function showSuccessMessage(title, text) {
  505. return showAlert({
  506. title: title || USERSCRIPT_STRINGS.doneTitle,
  507. text: text,
  508. type: 'success'
  509. });
  510. }
  511.  
  512. function showErrorMessage(title, text) {
  513. return showAlert({
  514. title: title || USERSCRIPT_STRINGS.errorTitle,
  515. text: text,
  516. type: 'error'
  517. });
  518. }
  519.  
  520. function showWelcomeMessage() {
  521. return showAlert({
  522. title: 'Hello',
  523. text: USERSCRIPT_STRINGS.welcomeMessage,
  524. confirmButtonText: USERSCRIPT_STRINGS.welcomeGotIt
  525. });
  526. }
  527.  
  528. function showLoadingOverlay() {
  529. if (!loadingOverlay) {
  530. loadingOverlay = document.createElement('div');
  531. loadingOverlay.id = 'loading-overlay';
  532. const spinner = document.createElement('div');
  533. spinner.id = 'loading-spinner';
  534. loadingOverlay.appendChild(spinner);
  535. document.body.appendChild(loadingOverlay);
  536. }
  537. setTimeout(() => loadingOverlay.classList.add('visible'), 10);
  538. }
  539.  
  540. function hideLoadingOverlay() {
  541. if (loadingOverlay) loadingOverlay.classList.remove('visible');
  542. }
  543.  
  544. function isFootball() {
  545. return !!document.querySelector('div#tactics_box.soccer.clearfix');
  546. }
  547.  
  548. function sha256Hash(s) {
  549. const shaObj = new jsSHA('SHA-256', 'TEXT');
  550. shaObj.update(s);
  551. return shaObj.getHash('HEX');
  552. }
  553.  
  554. function insertAfterElement(newNode, referenceNode) {
  555. if (referenceNode && referenceNode.parentNode) {
  556. referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
  557. } else {
  558. console.warn("MZTM: Reference node for insertion not found or has no parent.");
  559. }
  560. }
  561.  
  562. function appendChildren(parent, children) {
  563. children.forEach((child) => {
  564. if (child) parent.appendChild(child);
  565. });
  566. }
  567.  
  568. function getFormattedDate() {
  569. const now = new Date();
  570. const year = now.getFullYear();
  571. const month = (now.getMonth() + 1).toString().padStart(2, '0');
  572. const day = now.getDate().toString().padStart(2, '0');
  573. return `${year}-${month}-${day}`;
  574. }
  575.  
  576. async function fetchTacticsFromGMStorage() {
  577. return GM_getValue(FORMATIONS_STORAGE_KEY, {
  578. tactics: []
  579. });
  580. }
  581.  
  582. function storeTacticsInGMStorage(data) {
  583. GM_setValue(FORMATIONS_STORAGE_KEY, data);
  584. }
  585.  
  586. async function validateDuplicateTactic(id) {
  587. const data = await GM_getValue(FORMATIONS_STORAGE_KEY) || { tactics: [] };
  588. const duplicateTactic = data.tactics.find(t => t.id === id);
  589. return duplicateTactic ? duplicateTactic.name : null;
  590. }
  591.  
  592. async function saveTacticToStorage(tactic) {
  593. const data = await GM_getValue(FORMATIONS_STORAGE_KEY) || {
  594. tactics: []
  595. };
  596. data.tactics.push(tactic);
  597. await GM_setValue(FORMATIONS_STORAGE_KEY, data);
  598. }
  599.  
  600. async function validateDuplicateTacticWithUpdatedCoord(newId, currentTactic, data) {
  601. if (newId === currentTactic.id) return { status: 'unchanged', name: null };
  602. const duplicateTactic = data.tactics.find(t => t.id === newId);
  603. if (duplicateTactic) return { status: 'duplicate', name: duplicateTactic.name };
  604. return { status: 'unique', name: null };
  605. }
  606.  
  607. function loadCompleteTacticsData() {
  608. completeTactics = GM_getValue(COMPLETE_TACTICS_STORAGE_KEY, {});
  609. updateCompleteTacticsDropdown();
  610. }
  611.  
  612. function saveCompleteTacticsData() {
  613. GM_setValue(COMPLETE_TACTICS_STORAGE_KEY, completeTactics);
  614. }
  615.  
  616. async function fetchTeamIdAndUsername(forceRefresh = false) {
  617. const now = Date.now();
  618. const cachedInfo = GM_getValue(USER_INFO_CACHE_KEY);
  619. if (!forceRefresh && cachedInfo && cachedInfo.teamId && cachedInfo.username && (now - cachedInfo.timestamp < USER_INFO_CACHE_DURATION_MS)) {
  620. teamId = cachedInfo.teamId;
  621. username = cachedInfo.username;
  622. return {
  623. teamId,
  624. username
  625. };
  626. }
  627. try {
  628. const usernameElement = document.getElementById('header-username');
  629. if (!usernameElement) throw new Error('No username element found');
  630. const currentUsername = usernameElement.textContent.trim();
  631. const url = `/xml/manager_data.php?sport_id=1&username=${encodeURIComponent(currentUsername)}`;
  632. const response = await fetch(url);
  633. if (!response.ok) throw new Error(`HTTP error ${response.status}`);
  634. const xmlString = await response.text();
  635. const parser = new DOMParser();
  636. const xmlDoc = parser.parseFromString(xmlString, 'text/xml');
  637. const teamElement = xmlDoc.querySelector('Team[sport="soccer"]');
  638. if (!teamElement) throw new Error('No soccer team data found in XML');
  639. const currentTeamId = teamElement.getAttribute('teamId');
  640. if (!currentTeamId) throw new Error('No team ID found in XML');
  641. teamId = currentTeamId;
  642. username = currentUsername;
  643. const newUserInfo = {
  644. teamId: teamId,
  645. username: username,
  646. timestamp: now
  647. };
  648. GM_setValue(USER_INFO_CACHE_KEY, newUserInfo);
  649. return {
  650. teamId,
  651. username
  652. };
  653. } catch (error) {
  654. console.error('Error fetching Team ID and Username:', error);
  655. showErrorMessage(USERSCRIPT_STRINGS.errorTitle, 'Could not fetch team info. Some features might be limited.');
  656. return {
  657. teamId: null,
  658. username: null
  659. };
  660. }
  661. }
  662.  
  663. async function fetchTeamRoster(forceRefresh = false) {
  664. const now = Date.now();
  665. if (!teamId) {
  666. const ids = await fetchTeamIdAndUsername();
  667. if (!ids.teamId) {
  668. console.error("MZTM: Cannot fetch roster without Team ID.");
  669. return null;
  670. }
  671. }
  672. const cachedRoster = GM_getValue(ROSTER_CACHE_KEY);
  673. const isCacheValid = !forceRefresh && cachedRoster && cachedRoster.data && cachedRoster.teamId === teamId && (now - cachedRoster.timestamp < ROSTER_CACHE_DURATION_MS);
  674. if (isCacheValid) {
  675. return cachedRoster.data;
  676. }
  677. try {
  678. const url = `/xml/team_playerlist.php?sport_id=1&team_id=${teamId}`;
  679. const response = await fetch(url);
  680. if (!response.ok) throw new Error(`HTTP error ${response.status}`);
  681. const xmlString = await response.text();
  682. const parser = new DOMParser();
  683. const xmlDoc = parser.parseFromString(xmlString, 'text/xml');
  684. const playerElements = Array.from(xmlDoc.querySelectorAll('TeamPlayers Player'));
  685. const roster = playerElements.map(p => p.getAttribute('id')).filter(id => id);
  686. if (roster.length === 0) {
  687. console.warn("MZTM: Fetched roster is empty for team", teamId);
  688. }
  689. rosterCache = {
  690. data: roster,
  691. timestamp: now,
  692. teamId: teamId
  693. };
  694. GM_setValue(ROSTER_CACHE_KEY, rosterCache);
  695. return roster;
  696. } catch (error) {
  697. console.error('Error fetching team roster:', error);
  698. showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.errorFetchingRoster);
  699. return null;
  700. }
  701. }
  702.  
  703. function generateUniqueId(coordinates) {
  704. coordinates.sort((a, b) => {
  705. if (a[1] !== b[1]) return a[1] - b[1];
  706. else return a[0] - b[0];
  707. });
  708. const coordString = coordinates.map(coord => `${coord[0]},${coord[1]}`).join(';');
  709. return sha256Hash(coordString);
  710. }
  711.  
  712. function handleTacticSelection(tacticId) {
  713. selectedFormationTacticId = tacticId;
  714. if (!tacticId) return;
  715. const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR));
  716. const selectedTactic = tactics.find(td => td.id === tacticId);
  717. if (selectedTactic) {
  718. if (outfieldPlayers.length < MIN_PLAYERS_ON_PITCH - 1) {
  719. const hiddenTrigger = document.getElementById('hidden_trigger_button');
  720. if (hiddenTrigger) hiddenTrigger.click();
  721. setTimeout(() => rearrangePlayers(selectedTactic.coordinates), 100);
  722. } else {
  723. rearrangePlayers(selectedTactic.coordinates);
  724. }
  725. }
  726. }
  727.  
  728. function rearrangePlayers(coordinates) {
  729. const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR));
  730. findBestPositions(outfieldPlayers, coordinates);
  731. for (let i = 0; i < outfieldPlayers.length; ++i) {
  732. if (coordinates[i]) {
  733. outfieldPlayers[i].style.left = coordinates[i][0] + 'px';
  734. outfieldPlayers[i].style.top = coordinates[i][1] + 'px';
  735. removeCollision(outfieldPlayers[i]);
  736. }
  737. }
  738. removeTacticSlotInvalidStatus();
  739. updateFormationTextDisplay(getFormation(coordinates));
  740. }
  741.  
  742. function findBestPositions(players, coordinates) {
  743. players.sort((a, b) => parseInt(a.style.top) - parseInt(b.style.top));
  744. coordinates.sort((a, b) => a[1] - b[1]);
  745. }
  746.  
  747. function removeCollision(playerElement) {
  748. if (playerElement.classList.contains('fieldpos-collision')) {
  749. playerElement.classList.remove('fieldpos-collision');
  750. playerElement.classList.add('fieldpos-ok');
  751. }
  752. }
  753.  
  754. function removeTacticSlotInvalidStatus() {
  755. const slot = document.querySelector(TACTIC_SLOT_SELECTOR);
  756. if (slot) slot.classList.remove('invalid');
  757. }
  758.  
  759. function updateFormationTextDisplay(formation) {
  760. const formationTextElement = document.querySelector(FORMATION_TEXT_SELECTOR);
  761. if (formationTextElement) {
  762. const defs = formationTextElement.querySelector('.defs'),
  763. mids = formationTextElement.querySelector('.mids'),
  764. atts = formationTextElement.querySelector('.atts');
  765. if (defs) defs.textContent = formation.defenders;
  766. if (mids) mids.textContent = formation.midfielders;
  767. if (atts) atts.textContent = formation.strikers;
  768. }
  769. }
  770.  
  771. function getFormation(coordinates) {
  772. let strikers = 0,
  773. midfielders = 0,
  774. defenders = 0;
  775. for (const coord of coordinates) {
  776. const y = coord[1];
  777. if (y < 103) strikers++;
  778. else if (y <= 204) midfielders++;
  779. else defenders++;
  780. }
  781. return {
  782. strikers,
  783. midfielders,
  784. defenders
  785. };
  786. }
  787.  
  788. function getFormationFromCompleteTactic(tacticData) {
  789. let strikers = 0,
  790. midfielders = 0,
  791. defenders = 0;
  792. const outfieldCoords = tacticData.initialCoords.filter(p => p.pos === 'normal');
  793. for (const coord of outfieldCoords) {
  794. const y = coord.y;
  795. const effectiveY = y - 9;
  796. if (effectiveY < 103) strikers++;
  797. else if (effectiveY <= 204) midfielders++;
  798. else defenders++;
  799. }
  800. if (strikers + midfielders + defenders !== 10) {
  801. console.warn("MZTM: Calculated formation from complete tactic doesn't sum to 10 outfield players.");
  802. }
  803. return {
  804. strikers,
  805. midfielders,
  806. defenders
  807. };
  808. }
  809.  
  810. function formatFormationString(formationObj) {
  811. if (!formationObj || typeof formationObj.defenders === 'undefined') return 'N/A';
  812. return `${formationObj.defenders}-${formationObj.midfielders}-${formationObj.strikers}`;
  813. }
  814.  
  815. function validateTacticPlayerCount(outfieldPlayers) {
  816. const isGoalkeeperPresent = document.querySelector(GOALKEEPER_SELECTOR);
  817. outfieldPlayers = outfieldPlayers.filter(p => !p.classList.contains('fieldpos-collision'));
  818. if (outfieldPlayers.length < MIN_PLAYERS_ON_PITCH - 1 || !isGoalkeeperPresent) {
  819. showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidTacticError);
  820. return false;
  821. }
  822. return true;
  823. }
  824.  
  825. async function loadInitialTacticsAndCategories() {
  826. const initialTacticsToSave = [];
  827.  
  828. if (INITIAL_FORMATIONS_DATA && INITIAL_FORMATIONS_DATA.tactics) {
  829. INITIAL_FORMATIONS_DATA.tactics.forEach(tacticEntry => {
  830. if (tacticEntry.name && Array.isArray(tacticEntry.coordinates)) {
  831. const newId = generateUniqueId(tacticEntry.coordinates);
  832. let categoryId = OTHER_CATEGORY_ID;
  833.  
  834. if (DEFAULT_CATEGORIES.short_passing && tacticEntry.name.startsWith('SP_')) {
  835. categoryId = DEFAULT_CATEGORIES.short_passing.id;
  836. } else if (DEFAULT_CATEGORIES.wing_play && tacticEntry.name.startsWith('WP_')) {
  837. categoryId = DEFAULT_CATEGORIES.wing_play.id;
  838. }
  839.  
  840. initialTacticsToSave.push({
  841. name: tacticEntry.name,
  842. coordinates: tacticEntry.coordinates,
  843. id: newId,
  844. style: categoryId,
  845. description: ''
  846. });
  847. }
  848. });
  849. }
  850.  
  851. tactics = initialTacticsToSave;
  852. tactics.sort((a, b) => a.name.localeCompare(b.name));
  853. await GM_setValue(FORMATIONS_STORAGE_KEY, { tactics: tactics });
  854. }
  855.  
  856. function generateCategoryId(name) {
  857. return sha256Hash(name.toLowerCase()).substring(0, 10);
  858. }
  859.  
  860. function generateCategoryColor(name) {
  861. const hash = sha256Hash(name);
  862. const hue = parseInt(hash.substring(0, 6), 16) % 360;
  863. const saturation = 50 + (parseInt(hash.substring(6, 8), 16) % 30);
  864. const lightness = 55 + (parseInt(hash.substring(8, 10), 16) % 15);
  865. return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
  866. }
  867.  
  868. function addCategory(category) {
  869. categories[category.id] = category;
  870. saveCategories();
  871. }
  872.  
  873. function saveCategories() {
  874. GM_setValue(CATEGORIES_STORAGE_KEY, categories);
  875. }
  876.  
  877. function loadCategories() {
  878. const storedCategories = GM_getValue(CATEGORIES_STORAGE_KEY);
  879. if (storedCategories && typeof storedCategories === 'object') {
  880. categories = storedCategories;
  881. if (!categories.short_passing) {
  882. categories.short_passing = DEFAULT_CATEGORIES.short_passing;
  883. }
  884. if (!categories.wing_play) {
  885. categories.wing_play = DEFAULT_CATEGORIES.wing_play;
  886. }
  887. } else {
  888. categories = {
  889. ...DEFAULT_CATEGORIES
  890. };
  891. saveCategories();
  892. }
  893. if (!categories[OTHER_CATEGORY_ID]) {
  894. categories[OTHER_CATEGORY_ID] = {
  895. id: OTHER_CATEGORY_ID,
  896. name: 'Other',
  897. color: '#8395a7'
  898. };
  899. }
  900. }
  901.  
  902. function loadCategoryColor(categoryId) {
  903. if (categories[categoryId]) return categories[categoryId].color;
  904. else if (categoryId === 'short_passing') return DEFAULT_CATEGORIES.short_passing.color;
  905. else if (categoryId === 'wing_play') return DEFAULT_CATEGORIES.wing_play.color;
  906. else if (categoryId === OTHER_CATEGORY_ID || !categoryId) return '#8395a7';
  907. else return '#8395a7';
  908. }
  909.  
  910. function getCategoryName(categoryId) {
  911. if (categories[categoryId]) return categories[categoryId].name;
  912. else if (categoryId === 'short_passing') return 'Short Passing';
  913. else if (categoryId === 'wing_play') return 'Wing Play';
  914. else if (categoryId === OTHER_CATEGORY_ID || !categoryId) return 'Other';
  915. else return categoryId || 'Uncategorized';
  916. }
  917.  
  918. async function removeCategory(categoryId, sourceModalElement = null) {
  919. if (!categoryId || categoryId === 'all' || categoryId === OTHER_CATEGORY_ID || DEFAULT_CATEGORIES[categoryId]) {
  920. console.error("Cannot remove this category:", categoryId);
  921. return false;
  922. }
  923.  
  924. const categoryName = getCategoryName(categoryId);
  925. const confirmation = await showAlert({
  926. title: USERSCRIPT_STRINGS.confirmationTitle,
  927. text: USERSCRIPT_STRINGS.removeCategoryConfirmation.replace('{}', categoryName),
  928. showCancelButton: true,
  929. confirmButtonText: USERSCRIPT_STRINGS.removeCategoryButton,
  930. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton,
  931. type: 'error'
  932. });
  933.  
  934. if (!confirmation.isConfirmed) return false;
  935.  
  936. const data = await GM_getValue(FORMATIONS_STORAGE_KEY, { tactics: [] });
  937. let updated = false;
  938. data.tactics = data.tactics.map(t => {
  939. if (t.style === categoryId) {
  940. t.style = OTHER_CATEGORY_ID;
  941. updated = true;
  942. }
  943. return t;
  944. });
  945. tactics = tactics.map(t => {
  946. if (t.style === categoryId) t.style = OTHER_CATEGORY_ID;
  947. return t;
  948. });
  949.  
  950. if (updated) await GM_setValue(FORMATIONS_STORAGE_KEY, data);
  951.  
  952. delete categories[categoryId];
  953. saveCategories();
  954.  
  955. if (currentFilter === categoryId) {
  956. currentFilter = 'all';
  957. GM_setValue(CATEGORY_FILTER_STORAGE_KEY, currentFilter);
  958. }
  959.  
  960. updateTacticsDropdown();
  961. updateCategoryFilterDropdown();
  962.  
  963. if (sourceModalElement) {
  964. const categoryItem = sourceModalElement.querySelector(`li[data-category-id="${categoryId}"]`);
  965. if (categoryItem) {
  966. categoryItem.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
  967. categoryItem.style.opacity = '0';
  968. categoryItem.style.transform = 'translateX(-20px)';
  969. setTimeout(() => categoryItem.remove(), 300);
  970. }
  971. }
  972.  
  973. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.removeCategoryAlert.replace('{}', categoryName));
  974. return true;
  975. }
  976.  
  977. async function showManagementModal() {
  978. const modalContent = createManagementModalContent();
  979. await showAlert({
  980. title: USERSCRIPT_STRINGS.managementModalTitle,
  981. htmlContent: modalContent,
  982. confirmButtonText: USERSCRIPT_STRINGS.manageCategoriesDoneButton,
  983. showCancelButton: false,
  984. modalClass: 'management-modal'
  985. });
  986. updateCategoryFilterDropdown();
  987. updateTacticsDropdown();
  988. }
  989.  
  990. function createManagementModalContent() {
  991. const wrapper = document.createElement('div');
  992. wrapper.className = 'management-modal-wrapper';
  993.  
  994. const tabsConfig = [
  995. { id: 'formations', title: USERSCRIPT_STRINGS.formationsTabTitle, contentGenerator: createFormationsManagementTab },
  996. { id: 'categories', title: USERSCRIPT_STRINGS.categoriesTabTitle, contentGenerator: createCategoriesManagementTab }
  997. ];
  998.  
  999. const tabsContainer = createModalTabs(tabsConfig, wrapper);
  1000. wrapper.appendChild(tabsContainer);
  1001.  
  1002. tabsConfig.forEach((tab, index) => {
  1003. const contentDiv = document.createElement('div');
  1004. contentDiv.className = 'management-modal-content';
  1005. contentDiv.dataset.tabId = tab.id;
  1006. if (index === 0) contentDiv.classList.add('active');
  1007. tab.contentGenerator(contentDiv);
  1008. wrapper.appendChild(contentDiv);
  1009. });
  1010.  
  1011. wrapper.addEventListener('click', handleManagementModalClick);
  1012. wrapper.addEventListener('change', handleManagementModalChange);
  1013. wrapper.addEventListener('keydown', handleManagementModalKeydown);
  1014.  
  1015. return wrapper;
  1016. }
  1017.  
  1018. function createFormationsManagementTab(container) {
  1019. container.innerHTML = '';
  1020. const list = document.createElement('ul');
  1021. list.className = 'formation-management-list';
  1022.  
  1023. const sortedTactics = [...tactics].sort((a, b) => a.name.localeCompare(b.name));
  1024.  
  1025. if (sortedTactics.length === 0) {
  1026. const message = document.createElement('p');
  1027. message.textContent = 'No formations saved yet.';
  1028. message.className = 'no-items-message';
  1029. list.appendChild(message);
  1030. } else {
  1031. sortedTactics.forEach(tactic => {
  1032. list.appendChild(createFormationManagementItem(tactic));
  1033. });
  1034. }
  1035. container.appendChild(list);
  1036. }
  1037.  
  1038. function createFormationManagementItem(tactic) {
  1039. const listItem = document.createElement('li');
  1040. listItem.dataset.tacticId = tactic.id;
  1041.  
  1042. const nameContainer = document.createElement('div');
  1043. nameContainer.className = 'item-name-container';
  1044. const nameSpan = document.createElement('span');
  1045. nameSpan.className = 'item-name';
  1046. nameSpan.textContent = tactic.name;
  1047. nameContainer.appendChild(nameSpan);
  1048.  
  1049. const controlsContainer = document.createElement('div');
  1050. controlsContainer.className = 'item-controls';
  1051.  
  1052. const categorySelect = document.createElement('select');
  1053. categorySelect.className = 'item-category-select';
  1054. populateCategorySelect(categorySelect, tactic.style);
  1055.  
  1056. const editBtn = document.createElement('button');
  1057. editBtn.className = 'item-action-btn edit-name-btn';
  1058. editBtn.innerHTML = '✏️';
  1059. editBtn.title = 'Edit name & description';
  1060.  
  1061. const deleteBtn = document.createElement('button');
  1062. deleteBtn.className = 'item-action-btn delete-item-btn';
  1063. deleteBtn.innerHTML = '🗑️';
  1064. deleteBtn.title = 'Delete formation';
  1065.  
  1066. appendChildren(controlsContainer, [categorySelect, editBtn, deleteBtn]);
  1067. appendChildren(listItem, [nameContainer, controlsContainer]);
  1068.  
  1069. return listItem;
  1070. }
  1071.  
  1072. function populateCategorySelect(selectElement, currentCategoryId) {
  1073. selectElement.innerHTML = '';
  1074. const availableCategories = Object.values(categories)
  1075. .sort((a, b) => {
  1076. if (a.id === OTHER_CATEGORY_ID) return 1;
  1077. if (b.id === OTHER_CATEGORY_ID) return -1;
  1078. return a.name.localeCompare(b.name);
  1079. });
  1080.  
  1081. availableCategories.forEach(cat => {
  1082. const option = document.createElement('option');
  1083. option.value = cat.id;
  1084. option.textContent = cat.name;
  1085. if (cat.id === (currentCategoryId || OTHER_CATEGORY_ID)) {
  1086. option.selected = true;
  1087. }
  1088. selectElement.appendChild(option);
  1089. });
  1090. }
  1091.  
  1092. function createCategoriesManagementTab(container) {
  1093. container.innerHTML = '';
  1094.  
  1095. const addCategorySection = document.createElement('div');
  1096. addCategorySection.className = 'add-category-section';
  1097. const newCategoryInput = document.createElement('input');
  1098. newCategoryInput.type = 'text';
  1099. newCategoryInput.placeholder = USERSCRIPT_STRINGS.addCategoryPlaceholder;
  1100. newCategoryInput.className = 'add-category-input';
  1101. const addCategoryBtn = document.createElement('button');
  1102. addCategoryBtn.className = 'mz-modal-btn add-category-btn';
  1103. addCategoryBtn.textContent = USERSCRIPT_STRINGS.addCategoryButton;
  1104. appendChildren(addCategorySection, [newCategoryInput, addCategoryBtn]);
  1105. container.appendChild(addCategorySection);
  1106.  
  1107. const list = document.createElement('ul');
  1108. list.className = 'category-management-list';
  1109.  
  1110. const customCategories = Object.values(categories)
  1111. .filter(cat => cat.id !== OTHER_CATEGORY_ID && !DEFAULT_CATEGORIES[cat.id])
  1112. .sort((a, b) => a.name.localeCompare(b.name));
  1113.  
  1114. const noCatMsg = document.createElement('p');
  1115. noCatMsg.textContent = USERSCRIPT_STRINGS.noCustomCategories;
  1116. noCatMsg.className = 'no-custom-categories-message';
  1117. noCatMsg.style.display = customCategories.length === 0 ? 'block' : 'none';
  1118. list.appendChild(noCatMsg);
  1119.  
  1120. customCategories.forEach(cat => {
  1121. list.appendChild(createCategoryManagementItem(cat));
  1122. });
  1123.  
  1124. container.appendChild(list);
  1125. }
  1126.  
  1127. function createCategoryManagementItem(category) {
  1128. const listItem = document.createElement('li');
  1129. listItem.dataset.categoryId = category.id;
  1130.  
  1131. const nameSpan = document.createElement('span');
  1132. nameSpan.textContent = category.name;
  1133. nameSpan.style.flexGrow = '1';
  1134. nameSpan.style.marginRight = '10px';
  1135.  
  1136. const removeBtn = document.createElement('button');
  1137. removeBtn.textContent = 'Remove';
  1138. removeBtn.className = 'mz-modal-btn category-remove-btn';
  1139. removeBtn.title = `Remove category "${category.name}"`;
  1140.  
  1141. listItem.appendChild(nameSpan);
  1142. listItem.appendChild(removeBtn);
  1143. return listItem;
  1144. }
  1145.  
  1146. function handleManagementModalClick(event) {
  1147. const target = event.target;
  1148. const listItem = target.closest('li');
  1149.  
  1150. if (target.classList.contains('edit-name-btn') && listItem) {
  1151. handleEditFormationInModal(listItem);
  1152. } else if (target.classList.contains('delete-item-btn') && listItem) {
  1153. handleDeleteFormationInModal(listItem);
  1154. } else if (target.classList.contains('add-category-btn')) {
  1155. handleAddNewCategoryInModal(target.closest('.add-category-section'));
  1156. } else if (target.classList.contains('category-remove-btn') && listItem) {
  1157. handleDeleteCategoryInModal(listItem);
  1158. }
  1159. }
  1160.  
  1161. function handleManagementModalChange(event) {
  1162. const target = event.target;
  1163. if (target.classList.contains('item-category-select')) {
  1164. const listItem = target.closest('li');
  1165. const tacticId = listItem?.dataset.tacticId;
  1166. const newCategoryId = target.value;
  1167. if (tacticId && newCategoryId) {
  1168. updateFormationCategory(tacticId, newCategoryId);
  1169. }
  1170. }
  1171. }
  1172.  
  1173. function handleManagementModalKeydown(event) {
  1174. if (event.key === 'Enter' && !event.shiftKey) {
  1175. const target = event.target;
  1176. if (target.classList.contains('add-category-input')) {
  1177. handleAddNewCategoryInModal(target.closest('.add-category-section'));
  1178. event.preventDefault();
  1179. }
  1180. }
  1181. }
  1182.  
  1183. async function handleEditFormationInModal(listItem) {
  1184. const tacticId = listItem.dataset.tacticId;
  1185. const tactic = tactics.find(t => t.id === tacticId);
  1186. if (!tactic) return;
  1187.  
  1188. await editTactic(tactic.id, listItem);
  1189. }
  1190.  
  1191. async function handleDeleteFormationInModal(listItem) {
  1192. const tacticId = listItem.dataset.tacticId;
  1193. const tactic = tactics.find(t => t.id === tacticId);
  1194. if (!tactic) return;
  1195.  
  1196. const confirmation = await showAlert({
  1197. title: USERSCRIPT_STRINGS.confirmationTitle,
  1198. text: USERSCRIPT_STRINGS.deleteConfirmation.replace('{}', tactic.name),
  1199. showCancelButton: true,
  1200. confirmButtonText: USERSCRIPT_STRINGS.deleteTacticConfirmButton,
  1201. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton,
  1202. type: 'error'
  1203. });
  1204.  
  1205. if (!confirmation.isConfirmed) return;
  1206.  
  1207. const deletedCategoryId = tactic.style;
  1208. const data = await GM_getValue(FORMATIONS_STORAGE_KEY) || { tactics: [] };
  1209. data.tactics = data.tactics.filter(t => t.id !== tacticId);
  1210. await GM_setValue(FORMATIONS_STORAGE_KEY, data);
  1211. tactics = tactics.filter(t => t.id !== tacticId);
  1212.  
  1213. listItem.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
  1214. listItem.style.opacity = '0';
  1215. listItem.style.transform = 'translateX(-20px)';
  1216. setTimeout(() => {
  1217. listItem.remove();
  1218. const list = document.querySelector('.formation-management-list');
  1219. if (list && !list.querySelector('li')) {
  1220. const message = document.createElement('p');
  1221. message.textContent = 'No formations saved yet.';
  1222. message.className = 'no-items-message';
  1223. list.appendChild(message);
  1224. }
  1225. }, 300);
  1226.  
  1227. const categoryStillUsed = tactics.some(t => t.style === deletedCategoryId);
  1228. if (!categoryStillUsed && deletedCategoryId && !DEFAULT_CATEGORIES[deletedCategoryId] && deletedCategoryId !== OTHER_CATEGORY_ID) {
  1229. delete categories[deletedCategoryId];
  1230. saveCategories();
  1231. if (currentFilter === deletedCategoryId) {
  1232. currentFilter = 'all';
  1233. GM_setValue(CATEGORY_FILTER_STORAGE_KEY, currentFilter);
  1234. }
  1235. const catTab = document.querySelector('.management-modal-content[data-tab-id="categories"]');
  1236. if (catTab) createCategoriesManagementTab(catTab);
  1237. }
  1238.  
  1239. updateTacticsDropdown();
  1240. updateCategoryFilterDropdown();
  1241. }
  1242.  
  1243. async function updateFormationCategory(tacticId, newCategoryId) {
  1244. const tacticIndex = tactics.findIndex(t => t.id === tacticId);
  1245. if (tacticIndex === -1) return;
  1246.  
  1247. const originalCategoryId = tactics[tacticIndex].style;
  1248. if (originalCategoryId === newCategoryId) return;
  1249.  
  1250. tactics[tacticIndex].style = newCategoryId;
  1251. const data = await GM_getValue(FORMATIONS_STORAGE_KEY, { tactics: [] });
  1252. const dataIndex = data.tactics.findIndex(t => t.id === tacticId);
  1253. if (dataIndex !== -1) {
  1254. data.tactics[dataIndex].style = newCategoryId;
  1255. await GM_setValue(FORMATIONS_STORAGE_KEY, data);
  1256.  
  1257. const originalCategoryStillUsed = tactics.some(t => t.style === originalCategoryId);
  1258. if (!originalCategoryStillUsed && originalCategoryId && !DEFAULT_CATEGORIES[originalCategoryId] && originalCategoryId !== OTHER_CATEGORY_ID) {
  1259. delete categories[originalCategoryId];
  1260. saveCategories();
  1261. if (currentFilter === originalCategoryId) {
  1262. currentFilter = 'all';
  1263. GM_setValue(CATEGORY_FILTER_STORAGE_KEY, currentFilter);
  1264. }
  1265. const catTab = document.querySelector('.management-modal-content[data-tab-id="categories"]');
  1266. if (catTab) createCategoriesManagementTab(catTab);
  1267. }
  1268.  
  1269. updateTacticsDropdown();
  1270. updateCategoryFilterDropdown();
  1271. } else {
  1272. showErrorMessage(USERSCRIPT_STRINGS.errorTitle, "Failed to update category in storage.");
  1273. tactics[tacticIndex].style = originalCategoryId;
  1274. const selectElement = document.querySelector(`li[data-tactic-id="${tacticId}"] .item-category-select`);
  1275. if (selectElement) selectElement.value = originalCategoryId || OTHER_CATEGORY_ID;
  1276. }
  1277. }
  1278.  
  1279. async function handleAddNewCategoryInModal(addSection) {
  1280. const input = addSection.querySelector('.add-category-input');
  1281. const list = addSection.nextElementSibling;
  1282. if (!input || !list) return;
  1283.  
  1284. const name = input.value.trim();
  1285. if (!name) {
  1286. showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticNameProvidedError.replace("name", "category name"));
  1287. input.focus();
  1288. return;
  1289. }
  1290. if (name.length > MAX_CATEGORY_NAME_LENGTH) {
  1291. showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.categoryNameMaxLengthError);
  1292. input.focus();
  1293. return;
  1294. }
  1295. const existingCategory = Object.values(categories).find(cat => cat.name.toLowerCase() === name.toLowerCase());
  1296. if (existingCategory) {
  1297. showErrorMessage(USERSCRIPT_STRINGS.errorTitle, "Category name already exists.");
  1298. input.focus();
  1299. return;
  1300. }
  1301.  
  1302. const newCategoryId = generateCategoryId(name);
  1303. const newCategory = {
  1304. id: newCategoryId,
  1305. name: name,
  1306. color: generateCategoryColor(name)
  1307. };
  1308.  
  1309. addCategory(newCategory);
  1310. input.value = '';
  1311.  
  1312. const noCatMsg = list.querySelector('.no-custom-categories-message');
  1313. if (noCatMsg) noCatMsg.style.display = 'none';
  1314.  
  1315. const newItem = createCategoryManagementItem(newCategory);
  1316. list.appendChild(newItem);
  1317. newItem.style.opacity = '0';
  1318. newItem.style.transform = 'translateY(-10px)';
  1319. setTimeout(() => {
  1320. newItem.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
  1321. newItem.style.opacity = '1';
  1322. newItem.style.transform = 'translateY(0)';
  1323. }, 10);
  1324.  
  1325. updateCategoryFilterDropdown();
  1326. document.querySelectorAll('.item-category-select').forEach(select => {
  1327. const currentTacticId = select.closest('li')?.dataset.tacticId;
  1328. const currentTactic = tactics.find(t => t.id === currentTacticId);
  1329. populateCategorySelect(select, currentTactic?.style);
  1330. });
  1331. }
  1332.  
  1333. async function handleDeleteCategoryInModal(listItem) {
  1334. const categoryId = listItem.dataset.categoryId;
  1335. const categoryName = getCategoryName(categoryId);
  1336. if (!categoryId || !categoryName) return;
  1337.  
  1338. const success = await removeCategory(categoryId, listItem.closest('.management-modal-content'));
  1339. if (success) {
  1340. document.querySelectorAll('.item-category-select').forEach(select => {
  1341. const currentTacticId = select.closest('li')?.dataset.tacticId;
  1342. const currentTactic = tactics.find(t => t.id === currentTacticId);
  1343. populateCategorySelect(select, currentTactic?.style);
  1344. });
  1345. const formationsTab = document.querySelector('.management-modal-content[data-tab-id="formations"]');
  1346. if (formationsTab) createFormationsManagementTab(formationsTab);
  1347. }
  1348. }
  1349.  
  1350. async function addNewTactic() {
  1351. const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR));
  1352. const coordinates = outfieldPlayers.map(p => [parseInt(p.style.left), parseInt(p.style.top)]);
  1353. if (!validateTacticPlayerCount(outfieldPlayers)) return;
  1354. const id = generateUniqueId(coordinates);
  1355. const duplicateName = await validateDuplicateTactic(id);
  1356. if (duplicateName) {
  1357. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.duplicateTacticErrorWithName.replace('{}', duplicateName));
  1358. return;
  1359. }
  1360. const result = await showAlert({
  1361. title: USERSCRIPT_STRINGS.tacticNamePrompt,
  1362. input: 'text',
  1363. inputValue: '',
  1364. placeholder: USERSCRIPT_STRINGS.tacticNamePlaceholder,
  1365. inputValidator: (v) => {
  1366. if (!v) return USERSCRIPT_STRINGS.noTacticNameProvidedError;
  1367. if (v.length > MAX_TACTIC_NAME_LENGTH) return USERSCRIPT_STRINGS.tacticNameMaxLengthError;
  1368. if (tactics.some(t => t.name === v)) return USERSCRIPT_STRINGS.alreadyExistingTacticNameError;
  1369. return null;
  1370. },
  1371. descriptionInput: 'textarea',
  1372. descriptionValue: '',
  1373. descriptionPlaceholder: USERSCRIPT_STRINGS.descriptionPlaceholder,
  1374. descriptionValidator: (d) => {
  1375. if (d && d.length > MAX_DESCRIPTION_LENGTH) return USERSCRIPT_STRINGS.descriptionMaxLengthError;
  1376. return null;
  1377. },
  1378. showCategorySelector: true,
  1379. showCancelButton: true,
  1380. confirmButtonText: USERSCRIPT_STRINGS.saveButton,
  1381. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton
  1382. });
  1383. if (!result.isConfirmed || !result.value) return;
  1384. const name = result.value;
  1385. const description = result.description || '';
  1386. const categoryId = result.category.id;
  1387. const tactic = {
  1388. name: name,
  1389. description: description,
  1390. coordinates: coordinates,
  1391. id: id,
  1392. style: categoryId
  1393. };
  1394. await saveTacticToStorage(tactic);
  1395. tactics.push(tactic);
  1396. tactics.sort((a, b) => a.name.localeCompare(b.name));
  1397. updateTacticsDropdown(id);
  1398. updateCategoryFilterDropdown();
  1399. handleTacticSelection(tactic.id);
  1400. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.addAlert.replace('{}', tactic.name));
  1401. }
  1402.  
  1403. async function addNewTacticWithXml() {
  1404. const xmlResult = await showAlert({
  1405. title: USERSCRIPT_STRINGS.xmlPlaceholder,
  1406. input: 'text',
  1407. inputValue: '',
  1408. placeholder: USERSCRIPT_STRINGS.xmlPlaceholder,
  1409. showCancelButton: true,
  1410. confirmButtonText: USERSCRIPT_STRINGS.addConfirmButton,
  1411. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton
  1412. });
  1413.  
  1414. if (!xmlResult.isConfirmed) return;
  1415. const xml = xmlResult.value;
  1416.  
  1417. if (!xml) {
  1418. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.xmlRequiredError);
  1419. return;
  1420. }
  1421. if (!xml.trim().startsWith('<') || !xml.trim().endsWith('>')) {
  1422. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidXmlFormatError);
  1423. return;
  1424. }
  1425.  
  1426. const nameResult = await showAlert({
  1427. title: USERSCRIPT_STRINGS.tacticNamePrompt,
  1428. input: 'text',
  1429. inputValue: '',
  1430. placeholder: USERSCRIPT_STRINGS.tacticNamePlaceholder,
  1431. inputValidator: (v) => {
  1432. if (!v) return USERSCRIPT_STRINGS.noTacticNameProvidedError;
  1433. if (v.length > MAX_TACTIC_NAME_LENGTH) return USERSCRIPT_STRINGS.tacticNameMaxLengthError;
  1434. if (tactics.some(t => t.name === v)) return USERSCRIPT_STRINGS.alreadyExistingTacticNameError;
  1435. return null;
  1436. },
  1437. descriptionInput: 'textarea',
  1438. descriptionValue: '',
  1439. descriptionPlaceholder: USERSCRIPT_STRINGS.descriptionPlaceholder,
  1440. descriptionValidator: (d) => {
  1441. if (d && d.length > MAX_DESCRIPTION_LENGTH) return USERSCRIPT_STRINGS.descriptionMaxLengthError;
  1442. return null;
  1443. },
  1444. showCategorySelector: true,
  1445. showCancelButton: true,
  1446. confirmButtonText: USERSCRIPT_STRINGS.saveButton,
  1447. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton
  1448. });
  1449. if (!nameResult.isConfirmed || !nameResult.value) return;
  1450. const name = nameResult.value;
  1451. const description = nameResult.description || '';
  1452. const categoryId = nameResult.category.id;
  1453. try {
  1454. const newTactic = await convertXmlToSimpleFormationJson(xml, name);
  1455. const id = generateUniqueId(newTactic.coordinates);
  1456. const duplicateName = await validateDuplicateTactic(id);
  1457. if (duplicateName) {
  1458. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.duplicateTacticErrorWithName.replace('{}', duplicateName));
  1459. return;
  1460. }
  1461. newTactic.id = id;
  1462. newTactic.style = categoryId;
  1463. newTactic.description = description;
  1464. await saveTacticToStorage(newTactic);
  1465. tactics.push(newTactic);
  1466. tactics.sort((a, b) => a.name.localeCompare(b.name));
  1467. updateTacticsDropdown(id);
  1468. updateCategoryFilterDropdown();
  1469. handleTacticSelection(newTactic.id);
  1470. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.addAlert.replace('{}', newTactic.name));
  1471. } catch (error) {
  1472. console.error('XMLError:', error);
  1473. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.xmlParsingError + (error.message ? `: ${error.message}` : ''));
  1474. }
  1475. }
  1476.  
  1477. async function deleteTactic() {
  1478. const selectedTactic = tactics.find(t => t.id === selectedFormationTacticId);
  1479. if (!selectedTactic) {
  1480. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError);
  1481. return;
  1482. }
  1483. const confirmation = await showAlert({
  1484. title: USERSCRIPT_STRINGS.confirmationTitle,
  1485. text: USERSCRIPT_STRINGS.deleteConfirmation.replace('{}', selectedTactic.name),
  1486. showCancelButton: true,
  1487. confirmButtonText: USERSCRIPT_STRINGS.deleteTacticConfirmButton,
  1488. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton
  1489. });
  1490. if (!confirmation.isConfirmed) return;
  1491. const deletedCategoryId = selectedTactic.style;
  1492. const data = await GM_getValue(FORMATIONS_STORAGE_KEY) || {
  1493. tactics: []
  1494. };
  1495. data.tactics = data.tactics.filter(t => t.id !== selectedTactic.id);
  1496. await GM_setValue(FORMATIONS_STORAGE_KEY, data);
  1497. tactics = tactics.filter(t => t.id !== selectedTactic.id);
  1498. selectedFormationTacticId = null;
  1499. const categoryStillUsed = tactics.some(t => t.style === deletedCategoryId);
  1500. if (!categoryStillUsed && deletedCategoryId && !DEFAULT_CATEGORIES[deletedCategoryId] && deletedCategoryId !== OTHER_CATEGORY_ID) {
  1501. delete categories[deletedCategoryId];
  1502. saveCategories();
  1503. if (currentFilter === deletedCategoryId) {
  1504. currentFilter = 'all';
  1505. GM_setValue(CATEGORY_FILTER_STORAGE_KEY, currentFilter);
  1506. }
  1507. }
  1508. updateTacticsDropdown();
  1509. updateCategoryFilterDropdown();
  1510. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.deleteAlert.replace('{}', selectedTactic.name));
  1511. }
  1512.  
  1513. async function editTactic(tacticIdToEdit = null, sourceListItem = null) {
  1514. const idToUse = tacticIdToEdit || selectedFormationTacticId;
  1515. const selectedTactic = tactics.find(t => t.id === idToUse);
  1516.  
  1517. if (!selectedTactic) {
  1518. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError);
  1519. return;
  1520. }
  1521.  
  1522. const originalName = selectedTactic.name;
  1523. const originalCategory = selectedTactic.style;
  1524. const originalDescription = selectedTactic.description || '';
  1525.  
  1526. const result = await showAlert({
  1527. title: 'Edit Formation',
  1528. input: 'text',
  1529. inputValue: originalName,
  1530. placeholder: USERSCRIPT_STRINGS.tacticNamePlaceholder,
  1531. inputValidator: (v) => {
  1532. if (!v) return USERSCRIPT_STRINGS.noTacticNameProvidedError;
  1533. if (v.length > MAX_TACTIC_NAME_LENGTH) return USERSCRIPT_STRINGS.tacticNameMaxLengthError;
  1534. if (v !== originalName && tactics.some(t => t.name === v)) return USERSCRIPT_STRINGS.alreadyExistingTacticNameError;
  1535. return null;
  1536. },
  1537. descriptionInput: 'textarea',
  1538. descriptionValue: originalDescription,
  1539. descriptionPlaceholder: USERSCRIPT_STRINGS.descriptionPlaceholder,
  1540. descriptionValidator: (d) => {
  1541. if (d && d.length > MAX_DESCRIPTION_LENGTH) return USERSCRIPT_STRINGS.descriptionMaxLengthError;
  1542. return null;
  1543. },
  1544. showCategorySelector: true,
  1545. currentCategory: selectedTactic.style,
  1546. showCancelButton: true,
  1547. confirmButtonText: USERSCRIPT_STRINGS.saveButton,
  1548. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton
  1549. });
  1550.  
  1551. if (!result.isConfirmed) return;
  1552.  
  1553. const newName = result.value || originalName;
  1554. const newDescription = result.description || '';
  1555. const newCategory = result.category?.id || originalCategory;
  1556.  
  1557. if (newName === originalName && newCategory === originalCategory && newDescription === originalDescription) {
  1558. return;
  1559. }
  1560.  
  1561. const categoryChanged = originalCategory !== newCategory;
  1562. const data = await GM_getValue(FORMATIONS_STORAGE_KEY) || {
  1563. tactics: []
  1564. };
  1565.  
  1566. let updatedInStorage = false;
  1567. data.tactics = data.tactics.map(t => {
  1568. if (t.id === selectedTactic.id) {
  1569. t.name = newName;
  1570. t.style = newCategory;
  1571. t.description = newDescription;
  1572. updatedInStorage = true;
  1573. }
  1574. return t;
  1575. });
  1576.  
  1577. if (!updatedInStorage) {
  1578. console.error("MZTM: Failed to find tactic in storage for update.", selectedTactic.id);
  1579. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, "Failed to update tactic in storage.");
  1580. return;
  1581. }
  1582.  
  1583. await GM_setValue(FORMATIONS_STORAGE_KEY, data);
  1584.  
  1585. const tacticIndex = tactics.findIndex(t => t.id === selectedTactic.id);
  1586. if (tacticIndex !== -1) {
  1587. tactics[tacticIndex].name = newName;
  1588. tactics[tacticIndex].style = newCategory;
  1589. tactics[tacticIndex].description = newDescription;
  1590. } else {
  1591. console.error("MZTM: Failed to find tactic in memory for update.", selectedTactic.id);
  1592. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, "Internal error updating tactic data.");
  1593. await initializeUsData();
  1594. updateTacticsDropdown();
  1595. updateCategoryFilterDropdown();
  1596. return;
  1597. }
  1598.  
  1599. if (categoryChanged) {
  1600. const originalCategoryStillUsed = tactics.some(t => t.style === originalCategory);
  1601. if (!originalCategoryStillUsed && originalCategory && !DEFAULT_CATEGORIES[originalCategory] && originalCategory !== OTHER_CATEGORY_ID) {
  1602. delete categories[originalCategory];
  1603. saveCategories();
  1604. if (currentFilter === originalCategory) {
  1605. currentFilter = 'all';
  1606. GM_setValue(CATEGORY_FILTER_STORAGE_KEY, currentFilter);
  1607. }
  1608. }
  1609. }
  1610.  
  1611. tactics.sort((a, b) => a.name.localeCompare(b.name));
  1612. updateTacticsDropdown(selectedTactic.id);
  1613. updateCategoryFilterDropdown();
  1614.  
  1615. if (sourceListItem) {
  1616. const nameSpan = sourceListItem.querySelector('.item-name');
  1617. if (nameSpan) nameSpan.textContent = newName;
  1618. const categorySelect = sourceListItem.querySelector('.item-category-select');
  1619. if (categorySelect) categorySelect.value = newCategory;
  1620. }
  1621.  
  1622. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.renameAlert.replace('{}', newName));
  1623. }
  1624.  
  1625. async function updateTactic() {
  1626. const outfieldPlayers = Array.from(document.querySelectorAll(OUTFIELD_PLAYERS_SELECTOR));
  1627. const selectedTactic = tactics.find(t => t.id === selectedFormationTacticId);
  1628. if (!selectedTactic) {
  1629. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError);
  1630. return;
  1631. }
  1632. if (!validateTacticPlayerCount(outfieldPlayers)) return;
  1633. const updatedCoordinates = outfieldPlayers.map(p => [parseInt(p.style.left), parseInt(p.style.top)]);
  1634. const newId = generateUniqueId(updatedCoordinates);
  1635. const data = await GM_getValue(FORMATIONS_STORAGE_KEY) || {
  1636. tactics: []
  1637. };
  1638. const validationResult = await validateDuplicateTacticWithUpdatedCoord(newId, selectedTactic, data);
  1639. if (validationResult.status === 'unchanged') {
  1640. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noChangesMadeError);
  1641. return;
  1642. } else if (validationResult.status === 'duplicate') {
  1643. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.duplicateTacticErrorWithName.replace('{}', validationResult.name));
  1644. return;
  1645. }
  1646. const confirmation = await showAlert({
  1647. title: USERSCRIPT_STRINGS.confirmationTitle,
  1648. text: USERSCRIPT_STRINGS.updateConfirmation.replace('{}', selectedTactic.name),
  1649. showCancelButton: true,
  1650. confirmButtonText: USERSCRIPT_STRINGS.updateConfirmButton,
  1651. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton
  1652. });
  1653. if (!confirmation.isConfirmed) return;
  1654. for (const tactic of data.tactics) {
  1655. if (tactic.id === selectedTactic.id) {
  1656. tactic.coordinates = updatedCoordinates;
  1657. tactic.id = newId;
  1658. }
  1659. }
  1660. const memoryTactic = tactics.find(t => t.id === selectedTactic.id);
  1661. if (memoryTactic) {
  1662. memoryTactic.coordinates = updatedCoordinates;
  1663. memoryTactic.id = newId;
  1664. }
  1665. await GM_setValue(FORMATIONS_STORAGE_KEY, data);
  1666. selectedFormationTacticId = newId;
  1667. updateTacticsDropdown(newId);
  1668. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.updateAlert.replace('{}', selectedTactic.name));
  1669. }
  1670.  
  1671. async function clearTactics() {
  1672. const confirmation = await showAlert({
  1673. title: USERSCRIPT_STRINGS.confirmationTitle,
  1674. text: USERSCRIPT_STRINGS.clearConfirmation,
  1675. showCancelButton: true,
  1676. confirmButtonText: USERSCRIPT_STRINGS.clearTacticsConfirmButton,
  1677. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton,
  1678. type: 'error'
  1679. });
  1680. if (!confirmation.isConfirmed) return;
  1681.  
  1682. await GM_deleteValue(FORMATIONS_STORAGE_KEY);
  1683. await GM_deleteValue(OLD_FORMATIONS_STORAGE_KEY);
  1684. await GM_deleteValue(CATEGORIES_STORAGE_KEY);
  1685. await GM_deleteValue(CATEGORY_FILTER_STORAGE_KEY);
  1686.  
  1687. tactics = [];
  1688. selectedFormationTacticId = null;
  1689. currentFilter = 'all';
  1690.  
  1691. loadCategories();
  1692. updateTacticsDropdown();
  1693. updateCategoryFilterDropdown();
  1694. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.clearAlert);
  1695. }
  1696.  
  1697. async function resetTactics() {
  1698. const confirmation = await showAlert({
  1699. title: USERSCRIPT_STRINGS.confirmationTitle,
  1700. text: USERSCRIPT_STRINGS.resetConfirmation,
  1701. showCancelButton: true,
  1702. confirmButtonText: USERSCRIPT_STRINGS.resetTacticsConfirmButton,
  1703. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton,
  1704. type: 'error'
  1705. });
  1706. if (!confirmation.isConfirmed) return;
  1707.  
  1708. await GM_deleteValue(FORMATIONS_STORAGE_KEY);
  1709. await GM_deleteValue(OLD_FORMATIONS_STORAGE_KEY);
  1710. await GM_deleteValue(CATEGORIES_STORAGE_KEY);
  1711. await GM_deleteValue(CATEGORY_FILTER_STORAGE_KEY);
  1712.  
  1713. tactics = [];
  1714. selectedFormationTacticId = null;
  1715. currentFilter = 'all';
  1716.  
  1717. loadCategories();
  1718. await loadInitialTacticsAndCategories();
  1719.  
  1720. updateTacticsDropdown();
  1721. updateCategoryFilterDropdown();
  1722. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.resetAlert);
  1723. }
  1724.  
  1725. async function importTacticsJsonData() {
  1726. try {
  1727. const result = await showAlert({
  1728. title: 'Import Formations (JSON)',
  1729. input: 'text',
  1730. inputValue: '',
  1731. placeholder: 'Paste Formations JSON here',
  1732. showCancelButton: true,
  1733. confirmButtonText: USERSCRIPT_STRINGS.importButton,
  1734. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton
  1735. });
  1736. if (!result.isConfirmed || !result.value) return;
  1737. let importedData;
  1738. try {
  1739. importedData = JSON.parse(result.value);
  1740. } catch (e) {
  1741. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidImportError);
  1742. return;
  1743. }
  1744. if (!importedData || !Array.isArray(importedData.tactics) || !importedData.tactics.every(t => t.name && t.id && Array.isArray(t.coordinates))) {
  1745. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidImportError);
  1746. return;
  1747. }
  1748. const importedTactics = importedData.tactics;
  1749. importedTactics.forEach(t => {
  1750. if (!t.hasOwnProperty('style')) t.style = OTHER_CATEGORY_ID;
  1751. if (!t.hasOwnProperty('description')) t.description = '';
  1752. if (t.style && !categories[t.style] && !DEFAULT_CATEGORIES[t.style] && t.style !== OTHER_CATEGORY_ID) {
  1753. addCategory({
  1754. id: t.style,
  1755. name: t.style,
  1756. color: generateCategoryColor(t.style)
  1757. });
  1758. }
  1759. });
  1760. let existingData = await GM_getValue(FORMATIONS_STORAGE_KEY, {
  1761. tactics: []
  1762. });
  1763. let existingTactics = existingData.tactics || [];
  1764. const mergedTactics = [...existingTactics];
  1765. let addedCount = 0;
  1766. for (const impTactic of importedTactics) {
  1767. if (!existingTactics.some(t => t.id === impTactic.id)) {
  1768. mergedTactics.push(impTactic);
  1769. addedCount++;
  1770. } else {
  1771. const existingIndex = mergedTactics.findIndex(t => t.id === impTactic.id);
  1772. if (existingIndex !== -1) {
  1773. mergedTactics[existingIndex] = { ...mergedTactics[existingIndex], ...impTactic };
  1774. }
  1775. }
  1776. }
  1777. await GM_setValue(FORMATIONS_STORAGE_KEY, {
  1778. tactics: mergedTactics
  1779. });
  1780. mergedTactics.sort((a, b) => a.name.localeCompare(b.name));
  1781. tactics = mergedTactics;
  1782. updateTacticsDropdown();
  1783. updateCategoryFilterDropdown();
  1784. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.importAlert + (addedCount > 0 ? ` (${addedCount} new items added)` : ''));
  1785. } catch (error) {
  1786. console.error('ImportError:', error);
  1787. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidImportError + (error.message ? `: ${error.message}` : ''));
  1788. }
  1789. }
  1790.  
  1791. async function exportTacticsJsonData() {
  1792. try {
  1793. const data = GM_getValue(FORMATIONS_STORAGE_KEY, {
  1794. tactics: []
  1795. });
  1796. const jsonString = JSON.stringify(data, null, 2);
  1797. if (navigator.clipboard?.writeText) {
  1798. try {
  1799. await navigator.clipboard.writeText(jsonString);
  1800. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.exportAlert);
  1801. return;
  1802. } catch (clipError) {
  1803. console.warn('Clipboard write failed, fallback.', clipError);
  1804. }
  1805. }
  1806. const textArea = document.createElement('textarea');
  1807. textArea.value = jsonString;
  1808. textArea.style.width = '100%';
  1809. textArea.style.minHeight = '150px';
  1810. textArea.style.marginTop = '10px';
  1811. textArea.style.backgroundColor = 'rgba(0,0,0,0.2)';
  1812. textArea.style.color = 'var(--text-color)';
  1813. textArea.style.border = '1px solid rgba(255,255,255,0.1)';
  1814. textArea.style.borderRadius = '4px';
  1815. textArea.readOnly = true;
  1816. const container = document.createElement('div');
  1817. container.appendChild(document.createTextNode('Copy the JSON data:'));
  1818. container.appendChild(textArea);
  1819. await showAlert({
  1820. title: 'Export Formations (JSON)',
  1821. htmlContent: container,
  1822. confirmButtonText: 'Done'
  1823. });
  1824. textArea.select();
  1825. textArea.setSelectionRange(0, 99999);
  1826. } catch (error) {
  1827. console.error('Export error:', error);
  1828. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, 'Failed to export formations.');
  1829. }
  1830. }
  1831.  
  1832. async function convertXmlToSimpleFormationJson(xmlString, tacticName) {
  1833. const parser = new DOMParser();
  1834. const xmlDoc = parser.parseFromString(xmlString, 'text/xml');
  1835. const parseErrors = xmlDoc.getElementsByTagName('parsererror');
  1836. if (parseErrors.length > 0) throw new Error(USERSCRIPT_STRINGS.xmlValidationError);
  1837. const positionElements = Array.from(xmlDoc.getElementsByTagName('Pos')).filter(el => el.getAttribute('pos') === 'normal');
  1838. if (positionElements.length !== MIN_PLAYERS_ON_PITCH - 1) throw new Error(`XML must contain exactly ${MIN_PLAYERS_ON_PITCH - 1} outfield players. Found ${positionElements.length}.`);
  1839. const coordinates = positionElements.map(el => {
  1840. const x = parseInt(el.getAttribute('x'));
  1841. const y = parseInt(el.getAttribute('y'));
  1842. if (isNaN(x) || isNaN(y)) throw new Error('Invalid coordinates found in XML.');
  1843. return [x - 7, y - 9];
  1844. });
  1845. return {
  1846. name: tacticName,
  1847. coordinates: coordinates
  1848. };
  1849. }
  1850.  
  1851. function getAttr(element, attributeName, defaultValue = null) {
  1852. return element ? element.getAttribute(attributeName) || defaultValue : defaultValue;
  1853. }
  1854.  
  1855. function parseCompleteTacticXml(xmlString) {
  1856. const parser = new DOMParser();
  1857. const xmlDoc = parser.parseFromString(xmlString, "text/xml");
  1858. if (xmlDoc.getElementsByTagName("parsererror").length > 0) throw new Error("XML parsing error");
  1859. const soccerTactics = xmlDoc.querySelector("SoccerTactics");
  1860. if (!soccerTactics) throw new Error("Missing <SoccerTactics>");
  1861. const teamElement = soccerTactics.querySelector("Team");
  1862. const posElements = Array.from(soccerTactics.querySelectorAll("Pos"));
  1863. const subElements = Array.from(soccerTactics.querySelectorAll("Sub"));
  1864. const ruleElements = Array.from(soccerTactics.querySelectorAll("TacticRule"));
  1865. const data = {
  1866. initialCoords: [],
  1867. alt1Coords: [],
  1868. alt2Coords: [],
  1869. teamSettings: {},
  1870. substitutes: [],
  1871. tacticRules: [],
  1872. originalPlayerIDs: new Set(),
  1873. description: ''
  1874. };
  1875. data.teamSettings = {
  1876. passingStyle: getAttr(teamElement, 'tactics', 'shortpass'),
  1877. mentality: getAttr(teamElement, 'playstyle', 'normal'),
  1878. aggression: getAttr(teamElement, 'aggression', 'normal'),
  1879. captainPID: getAttr(teamElement, 'captain', '0')
  1880. };
  1881. if (data.teamSettings.captainPID !== '0') data.originalPlayerIDs.add(data.teamSettings.captainPID);
  1882. posElements.forEach(el => {
  1883. const pid = getAttr(el, 'pid');
  1884. const posType = getAttr(el, 'pos');
  1885. if (!pid) return;
  1886. data.originalPlayerIDs.add(pid);
  1887. if (posType === 'normal' || posType === 'goalie') {
  1888. const x = parseInt(getAttr(el, 'x', 0));
  1889. const y = parseInt(getAttr(el, 'y', 0));
  1890. const x1 = parseInt(getAttr(el, 'x1', x));
  1891. const y1 = parseInt(getAttr(el, 'y1', y));
  1892. const x2 = parseInt(getAttr(el, 'x2', x1));
  1893. const y2 = parseInt(getAttr(el, 'y2', y1));
  1894. data.initialCoords.push({
  1895. pid: pid,
  1896. pos: posType,
  1897. x: x,
  1898. y: y
  1899. });
  1900. data.alt1Coords.push({
  1901. pid: pid,
  1902. pos: posType,
  1903. x: x1,
  1904. y: y1
  1905. });
  1906. data.alt2Coords.push({
  1907. pid: pid,
  1908. pos: posType,
  1909. x: x2,
  1910. y: y2
  1911. });
  1912. }
  1913. });
  1914. subElements.forEach(el => {
  1915. const pid = getAttr(el, 'pid');
  1916. const posType = getAttr(el, 'pos');
  1917. const x = parseInt(getAttr(el, 'x', 0));
  1918. const y = parseInt(getAttr(el, 'y', 0));
  1919. if (pid) {
  1920. data.originalPlayerIDs.add(pid);
  1921. data.substitutes.push({
  1922. pid: pid,
  1923. pos: posType,
  1924. x: x,
  1925. y: y
  1926. });
  1927. }
  1928. });
  1929. ruleElements.forEach(el => {
  1930. const rule = {};
  1931. for (const attr of el.attributes) {
  1932. rule[attr.name] = attr.value;
  1933. if (attr.name === 'out_player' && attr.value !== 'no_change' && attr.value !== '0') data.originalPlayerIDs.add(attr.value);
  1934. if (attr.name === 'in_player_id' && attr.value !== 'NULL' && attr.value !== '0') data.originalPlayerIDs.add(attr.value);
  1935. }
  1936. data.tacticRules.push(rule);
  1937. });
  1938. data.originalPlayerIDs = Array.from(data.originalPlayerIDs);
  1939. return data;
  1940. }
  1941.  
  1942. function generateCompleteTacticXml(tacticData, playerMapping) {
  1943. let xml = `<?xml version="1.0" ?>\n<SoccerTactics>\n`;
  1944. const mappedCaptain = playerMapping[tacticData.teamSettings.captainPID] || '0';
  1945. xml += `\t<Team tactics="${tacticData.teamSettings.passingStyle || 'shortpass'}" playstyle="${tacticData.teamSettings.mentality || 'normal'}" aggression="${tacticData.teamSettings.aggression || 'normal'}" captain="${mappedCaptain}" />\n`;
  1946. const playerCoords = {};
  1947. tacticData.initialCoords.forEach(p => {
  1948. if (!playerCoords[p.pid]) {
  1949. playerCoords[p.pid] = {
  1950. pos: p.pos
  1951. };
  1952. playerCoords[p.pid].initial = {
  1953. x: p.x,
  1954. y: p.y
  1955. };
  1956. }
  1957. });
  1958. tacticData.alt1Coords.forEach(p => {
  1959. if (!playerCoords[p.pid]) return;
  1960. playerCoords[p.pid].alt1 = {
  1961. x: p.x,
  1962. y: p.y
  1963. };
  1964. });
  1965. tacticData.alt2Coords.forEach(p => {
  1966. if (!playerCoords[p.pid]) return;
  1967. playerCoords[p.pid].alt2 = {
  1968. x: p.x,
  1969. y: p.y
  1970. };
  1971. });
  1972. for (const originalPid in playerCoords) {
  1973. const mappedPid = playerMapping[originalPid];
  1974. if (!mappedPid) continue;
  1975. const playerData = playerCoords[originalPid];
  1976. const initial = playerData.initial || {
  1977. x: 0,
  1978. y: 0
  1979. };
  1980. const alt1 = playerData.alt1 || initial;
  1981. const alt2 = playerData.alt2 || alt1;
  1982. xml += `\t<Pos pos="${playerData.pos}" pid="${mappedPid}" x="${initial.x}" y="${initial.y}" x1="${alt1.x}" y1="${alt1.y}" x2="${alt2.x}" y2="${alt2.y}" />\n`;
  1983. }
  1984. tacticData.substitutes.forEach(s => {
  1985. const mappedPid = playerMapping[s.pid];
  1986. if (mappedPid) xml += `\t<Sub pos="${s.pos}" pid="${mappedPid}" x="${s.x}" y="${s.y}" />\n`;
  1987. });
  1988. tacticData.tacticRules.forEach(rule => {
  1989. const mappedOutPlayer = (rule.out_player && rule.out_player !== 'no_change') ? (playerMapping[rule.out_player] || 'no_change') : 'no_change';
  1990. const mappedInPlayer = (rule.in_player_id && rule.in_player_id !== 'NULL') ? (playerMapping[rule.in_player_id] || 'NULL') : 'NULL';
  1991. let includeRule = true;
  1992. if (rule.out_player && rule.out_player !== 'no_change' && mappedOutPlayer === 'no_change') includeRule = false;
  1993. if (rule.in_player_id && rule.in_player_id !== 'NULL' && mappedInPlayer === 'NULL') includeRule = false;
  1994. if (includeRule) {
  1995. xml += '\t<TacticRule';
  1996. for (const attr in rule) {
  1997. let value = rule[attr];
  1998. if (attr === 'out_player') value = mappedOutPlayer;
  1999. if (attr === 'in_player_id') value = mappedInPlayer;
  2000. xml += ` ${attr}="${value}"`;
  2001. }
  2002. xml += ' />\n';
  2003. }
  2004. });
  2005. xml += '</SoccerTactics>';
  2006. return xml;
  2007. }
  2008.  
  2009. async function saveCompleteTactic() {
  2010. const exportButton = document.getElementById('export_button');
  2011. const importExportWindow = document.getElementById('importExportTacticsWindow');
  2012. const playerInfoWindow = document.getElementById('playerInfoWindow');
  2013. const importExportData = document.getElementById('importExportData');
  2014. if (!exportButton || !importExportWindow || !playerInfoWindow || !importExportData) return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, 'Could not find required MZ UI elements for export.');
  2015. const windowHidden = importExportWindow.style.display === 'none';
  2016. if (windowHidden) {
  2017. const toggleButton = document.getElementById('import_export_button');
  2018. if (toggleButton) toggleButton.click();
  2019. else return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, 'Could not find button to toggle XML view.');
  2020. }
  2021. importExportData.value = '';
  2022. exportButton.click();
  2023. await new Promise(r => setTimeout(r, 200));
  2024. const xmlString = importExportData.value;
  2025. if (!xmlString) {
  2026. if (windowHidden) document.getElementById('close_button')?.click();
  2027. return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, 'Export did not produce XML.');
  2028. }
  2029. let savedData;
  2030. try {
  2031. savedData = parseCompleteTacticXml(xmlString);
  2032. } catch (error) {
  2033. console.error("XML Parse Error:", error);
  2034. if (windowHidden) document.getElementById('close_button')?.click();
  2035. return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.errorXmlExportParse);
  2036. }
  2037. const result = await showAlert({
  2038. title: USERSCRIPT_STRINGS.completeTacticNamePrompt,
  2039. input: 'text',
  2040. inputValue: '',
  2041. placeholder: USERSCRIPT_STRINGS.completeTacticNamePlaceholder,
  2042. inputValidator: (v) => {
  2043. if (!v) return USERSCRIPT_STRINGS.noTacticNameProvidedError;
  2044. if (v.length > MAX_TACTIC_NAME_LENGTH) return USERSCRIPT_STRINGS.tacticNameMaxLengthError;
  2045. if (completeTactics.hasOwnProperty(v)) return USERSCRIPT_STRINGS.alreadyExistingTacticNameError;
  2046. return null;
  2047. },
  2048. descriptionInput: 'textarea',
  2049. descriptionValue: '',
  2050. descriptionPlaceholder: USERSCRIPT_STRINGS.descriptionPlaceholder,
  2051. descriptionValidator: (d) => {
  2052. if (d && d.length > MAX_DESCRIPTION_LENGTH) return USERSCRIPT_STRINGS.descriptionMaxLengthError;
  2053. return null;
  2054. },
  2055. showCancelButton: true,
  2056. confirmButtonText: USERSCRIPT_STRINGS.saveButton,
  2057. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton,
  2058. });
  2059. if (!result.isConfirmed || !result.value) {
  2060. if (windowHidden) document.getElementById('close_button')?.click();
  2061. return;
  2062. }
  2063. const baseName = result.value;
  2064. const description = result.description || '';
  2065. const fullName = `${baseName} (${getFormattedDate()})`;
  2066. savedData.description = description;
  2067. completeTactics[fullName] = savedData;
  2068. saveCompleteTacticsData();
  2069. updateCompleteTacticsDropdown(fullName);
  2070. if (windowHidden) document.getElementById('close_button')?.click();
  2071. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.completeTacticSaveSuccess.replace('{}', fullName));
  2072. }
  2073.  
  2074. async function loadCompleteTactic() {
  2075. const selectedName = selectedCompleteTacticName;
  2076. if (!selectedName || !completeTactics[selectedName]) return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError);
  2077. showLoadingOverlay();
  2078. const originalAlert = window.alert;
  2079. try {
  2080. const dataToLoad = completeTactics[selectedName];
  2081. const currentRoster = await fetchTeamRoster();
  2082. if (!currentRoster) throw new Error(USERSCRIPT_STRINGS.errorFetchingRoster);
  2083. const rosterSet = new Set(currentRoster);
  2084. const originalPids = dataToLoad.originalPlayerIDs || [];
  2085. const mapping = {};
  2086. const missingPids = [];
  2087. const mappedPids = new Set();
  2088. originalPids.forEach(pid => {
  2089. if (rosterSet.has(pid)) {
  2090. mapping[pid] = pid;
  2091. mappedPids.add(pid);
  2092. } else {
  2093. missingPids.push(pid);
  2094. }
  2095. });
  2096. const availablePids = currentRoster.filter(pid => !mappedPids.has(pid));
  2097. let replacementsFound = 0;
  2098. missingPids.forEach(missingPid => {
  2099. if (availablePids.length > 0) {
  2100. const randomIndex = Math.floor(Math.random() * availablePids.length);
  2101. const replacementPid = availablePids.splice(randomIndex, 1)[0];
  2102. mapping[missingPid] = replacementPid;
  2103. replacementsFound++;
  2104. } else {
  2105. mapping[missingPid] = null;
  2106. }
  2107. });
  2108. const assignedPids = new Set();
  2109. dataToLoad.initialCoords.forEach(p => {
  2110. if (mapping[p.pid]) assignedPids.add(mapping[p.pid]);
  2111. });
  2112. dataToLoad.substitutes.forEach(s => {
  2113. if (mapping[s.pid]) assignedPids.add(mapping[s.pid]);
  2114. });
  2115. if (assignedPids.size < MIN_PLAYERS_ON_PITCH) throw new Error(USERSCRIPT_STRINGS.errorInsufficientPlayers);
  2116. let xmlString;
  2117. try {
  2118. xmlString = generateCompleteTacticXml(dataToLoad, mapping);
  2119. } catch (error) {
  2120. console.error("XML Gen Error:", error);
  2121. throw new Error(USERSCRIPT_STRINGS.errorXmlGenerate);
  2122. }
  2123. let alertContent = null;
  2124. window.alert = (msg) => {
  2125. console.warn("Native alert captured:", msg);
  2126. alertContent = msg;
  2127. };
  2128. const importButton = document.getElementById('import_button');
  2129. const importExportWindow = document.getElementById('importExportTacticsWindow');
  2130. const importExportData = document.getElementById('importExportData');
  2131. if (!importButton || !importExportWindow || !importExportData) throw new Error('Could not find required MZ UI elements for import.');
  2132. const windowHidden = importExportWindow.style.display === 'none';
  2133. if (windowHidden) {
  2134. document.getElementById('import_export_button')?.click();
  2135. await new Promise(r => setTimeout(r, 50));
  2136. }
  2137. importExportData.value = xmlString;
  2138. importButton.click();
  2139. await new Promise(r => setTimeout(r, 300));
  2140. window.alert = originalAlert;
  2141. if (alertContent) throw new Error(USERSCRIPT_STRINGS.invalidXmlForImport + (alertContent.length < 100 ? ` MZ Message: ${alertContent}` : ''));
  2142.  
  2143. const observer = new MutationObserver((mutationsList, obs) => {
  2144. for (const mutation of mutationsList) {
  2145. if (mutation.type === 'childList') {
  2146. const errorBox = document.getElementById('lightbox_tactics_rule_error');
  2147. if (errorBox && errorBox.style.display !== 'none') {
  2148. const okButton = errorBox.querySelector('#powerbox_confirm_ok_button');
  2149. if (okButton) {
  2150. okButton.click();
  2151. obs.disconnect();
  2152. break;
  2153. }
  2154. }
  2155. }
  2156. }
  2157. });
  2158. observer.observe(document.body, {
  2159. childList: true,
  2160. subtree: true
  2161. });
  2162. setTimeout(() => observer.disconnect(), 3000);
  2163. if (replacementsFound > 0) {
  2164. showAlert({
  2165. title: 'Warning',
  2166. text: USERSCRIPT_STRINGS.warningPlayersSubstituted,
  2167. type: 'info'
  2168. });
  2169. }
  2170. else showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.completeTacticLoadSuccess.replace('{}', selectedName));
  2171. } catch (error) {
  2172. console.error("Load Complete Tactic Error:", error);
  2173. showErrorMessage(USERSCRIPT_STRINGS.errorTitle, error.message || 'Unknown error during load.');
  2174. if (window.alert !== originalAlert) window.alert = originalAlert;
  2175. } finally {
  2176. hideLoadingOverlay();
  2177. }
  2178. }
  2179.  
  2180. async function deleteCompleteTactic() {
  2181. const selectedName = selectedCompleteTacticName;
  2182. if (!selectedName || !completeTactics[selectedName]) return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError);
  2183. const confirmation = await showAlert({
  2184. title: USERSCRIPT_STRINGS.confirmationTitle,
  2185. text: USERSCRIPT_STRINGS.deleteConfirmation.replace('{}', selectedName),
  2186. showCancelButton: true,
  2187. confirmButtonText: USERSCRIPT_STRINGS.deleteTacticConfirmButton,
  2188. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton,
  2189. type: 'error'
  2190. });
  2191. if (!confirmation.isConfirmed) return;
  2192. delete completeTactics[selectedName];
  2193. selectedCompleteTacticName = null;
  2194. saveCompleteTacticsData();
  2195. updateCompleteTacticsDropdown();
  2196. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.completeTacticDeleteSuccess.replace('{}', selectedName));
  2197. }
  2198.  
  2199. async function editCompleteTactic() {
  2200. const selectedName = selectedCompleteTacticName;
  2201. if (!selectedName || !completeTactics[selectedName]) {
  2202. return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError);
  2203. }
  2204. const originalTacticData = completeTactics[selectedName];
  2205. const originalDescription = originalTacticData.description || '';
  2206.  
  2207. const result = await showAlert({
  2208. title: USERSCRIPT_STRINGS.renameCompleteTacticPrompt,
  2209. input: 'text',
  2210. inputValue: selectedName,
  2211. placeholder: USERSCRIPT_STRINGS.completeTacticNamePlaceholder,
  2212. inputValidator: (v) => {
  2213. if (!v) return USERSCRIPT_STRINGS.noTacticNameProvidedError;
  2214. if (v.length > MAX_TACTIC_NAME_LENGTH) return USERSCRIPT_STRINGS.tacticNameMaxLengthError;
  2215. if (v !== selectedName && completeTactics.hasOwnProperty(v)) return USERSCRIPT_STRINGS.alreadyExistingTacticNameError;
  2216. return null;
  2217. },
  2218. descriptionInput: 'textarea',
  2219. descriptionValue: originalDescription,
  2220. descriptionPlaceholder: USERSCRIPT_STRINGS.descriptionPlaceholder,
  2221. descriptionValidator: (d) => {
  2222. if (d && d.length > MAX_DESCRIPTION_LENGTH) return USERSCRIPT_STRINGS.descriptionMaxLengthError;
  2223. return null;
  2224. },
  2225. showCancelButton: true,
  2226. confirmButtonText: USERSCRIPT_STRINGS.saveButton,
  2227. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton
  2228. });
  2229.  
  2230. if (!result.isConfirmed || !result.value) return;
  2231.  
  2232. const newName = result.value;
  2233. const newDescription = result.description || '';
  2234.  
  2235. if (newName === selectedName && newDescription === originalDescription) return;
  2236.  
  2237. const tacticData = completeTactics[selectedName];
  2238. tacticData.description = newDescription;
  2239.  
  2240. delete completeTactics[selectedName];
  2241. completeTactics[newName] = tacticData;
  2242.  
  2243. saveCompleteTacticsData();
  2244. selectedCompleteTacticName = newName;
  2245. updateCompleteTacticsDropdown(newName);
  2246. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.completeTacticRenameSuccess.replace('{}', newName));
  2247. }
  2248.  
  2249. async function updateCompleteTactic() {
  2250. const selectedName = selectedCompleteTacticName;
  2251. if (!selectedName || !completeTactics[selectedName]) {
  2252. return showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.noTacticSelectedError);
  2253. }
  2254.  
  2255. const confirmation = await showAlert({
  2256. title: USERSCRIPT_STRINGS.confirmationTitle,
  2257. text: USERSCRIPT_STRINGS.updateCompleteTacticConfirmation.replace('{}', selectedName),
  2258. showCancelButton: true,
  2259. confirmButtonText: USERSCRIPT_STRINGS.updateConfirmButton,
  2260. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton
  2261. });
  2262.  
  2263. if (!confirmation.isConfirmed) return;
  2264.  
  2265. const originalAlert = window.alert;
  2266. try {
  2267. const exportButton = document.getElementById('export_button');
  2268. const importExportWindow = document.getElementById('importExportTacticsWindow');
  2269. const importExportData = document.getElementById('importExportData');
  2270. if (!exportButton || !importExportWindow || !importExportData) throw new Error('Could not find required MZ UI elements for export.');
  2271.  
  2272. const windowHidden = importExportWindow.style.display === 'none';
  2273. if (windowHidden) {
  2274. document.getElementById('import_export_button')?.click();
  2275. await new Promise(r => setTimeout(r, 50));
  2276. }
  2277.  
  2278. importExportData.value = '';
  2279. exportButton.click();
  2280. await new Promise(r => setTimeout(r, 200));
  2281. const xmlString = importExportData.value;
  2282. if (windowHidden) document.getElementById('close_button')?.click();
  2283.  
  2284. if (!xmlString) throw new Error('Export did not produce XML.');
  2285.  
  2286. let updatedData;
  2287. try {
  2288. updatedData = parseCompleteTacticXml(xmlString);
  2289. } catch (error) {
  2290. console.error("XML Parse Error on Update:", error);
  2291. throw new Error(USERSCRIPT_STRINGS.errorXmlExportParse);
  2292. }
  2293.  
  2294. updatedData.description = completeTactics[selectedName]?.description || '';
  2295. completeTactics[selectedName] = updatedData;
  2296. saveCompleteTacticsData();
  2297. updateCompleteTacticsDropdown(selectedName);
  2298. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.completeTacticUpdateSuccess.replace('{}', selectedName));
  2299.  
  2300. } catch (error) {
  2301. console.error("Update Complete Tactic Error:", error);
  2302. showErrorMessage(USERSCRIPT_STRINGS.errorTitle, error.message || 'Unknown error during update.');
  2303. if (window.alert !== originalAlert) window.alert = originalAlert;
  2304. const importExportWindow = document.getElementById('importExportTacticsWindow');
  2305. if (importExportWindow && importExportWindow.style.display !== 'none') {
  2306. document.getElementById('close_button')?.click();
  2307. }
  2308. }
  2309. }
  2310.  
  2311. async function importCompleteTactics() {
  2312. try {
  2313. const result = await showAlert({
  2314. title: USERSCRIPT_STRINGS.importCompleteTacticsTitle,
  2315. input: 'text',
  2316. inputValue: '',
  2317. placeholder: USERSCRIPT_STRINGS.importCompleteTacticsPlaceholder,
  2318. showCancelButton: true,
  2319. confirmButtonText: USERSCRIPT_STRINGS.importButton,
  2320. cancelButtonText: USERSCRIPT_STRINGS.cancelConfirmButton
  2321. });
  2322.  
  2323. if (!result.isConfirmed || !result.value) return;
  2324.  
  2325. let importedData;
  2326. try {
  2327. importedData = JSON.parse(result.value);
  2328. } catch (e) {
  2329. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidCompleteImportError);
  2330. return;
  2331. }
  2332.  
  2333. if (typeof importedData !== 'object' || importedData === null || Array.isArray(importedData)) {
  2334. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidCompleteImportError);
  2335. return;
  2336. }
  2337.  
  2338. let addedCount = 0;
  2339. let updatedCount = 0;
  2340. for (const name in importedData) {
  2341. if (importedData.hasOwnProperty(name)) {
  2342. if (typeof importedData[name] === 'object' && importedData[name] !== null) {
  2343. if (!importedData[name].hasOwnProperty('description')) importedData[name].description = '';
  2344. if (!completeTactics.hasOwnProperty(name)) {
  2345. addedCount++;
  2346. } else {
  2347. updatedCount++;
  2348. }
  2349. completeTactics[name] = importedData[name];
  2350. } else {
  2351. console.warn(`MZTM: Skipping invalid tactic data during import for key: ${name}`);
  2352. }
  2353. }
  2354. }
  2355.  
  2356. saveCompleteTacticsData();
  2357. updateCompleteTacticsDropdown();
  2358. let message = USERSCRIPT_STRINGS.importCompleteTacticsAlert;
  2359. if (addedCount > 0 || updatedCount > 0) {
  2360. message += ` (${addedCount > 0 ? `${addedCount} new` : ''}${addedCount > 0 && updatedCount > 0 ? ', ' : ''}${updatedCount > 0 ? `${updatedCount} updated` : ''} items)`;
  2361. }
  2362. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, message);
  2363.  
  2364. } catch (error) {
  2365. console.error('Import Complete Tactics Error:', error);
  2366. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, USERSCRIPT_STRINGS.invalidCompleteImportError + (error.message ? `: ${error.message}` : ''));
  2367. }
  2368. }
  2369.  
  2370. async function exportCompleteTactics() {
  2371. try {
  2372. const jsonString = JSON.stringify(completeTactics, null, 2);
  2373. if (navigator.clipboard?.writeText) {
  2374. try {
  2375. await navigator.clipboard.writeText(jsonString);
  2376. await showSuccessMessage(USERSCRIPT_STRINGS.doneTitle, USERSCRIPT_STRINGS.exportCompleteTacticsAlert);
  2377. return;
  2378. } catch (clipError) {
  2379. console.warn('Clipboard write failed, fallback.', clipError);
  2380. }
  2381. }
  2382. const textArea = document.createElement('textarea');
  2383. textArea.value = jsonString;
  2384. textArea.style.width = '100%';
  2385. textArea.style.minHeight = '150px';
  2386. textArea.style.marginTop = '10px';
  2387. textArea.style.backgroundColor = 'rgba(0,0,0,0.2)';
  2388. textArea.style.color = 'var(--text-color)';
  2389. textArea.style.border = '1px solid rgba(255,255,255,0.1)';
  2390. textArea.style.borderRadius = '4px';
  2391. textArea.readOnly = true;
  2392. const container = document.createElement('div');
  2393. container.appendChild(document.createTextNode('Copy the JSON data:'));
  2394. container.appendChild(textArea);
  2395. await showAlert({
  2396. title: USERSCRIPT_STRINGS.exportCompleteTacticsTitle,
  2397. htmlContent: container,
  2398. confirmButtonText: 'Done'
  2399. });
  2400. textArea.select();
  2401. textArea.setSelectionRange(0, 99999);
  2402. } catch (error) {
  2403. console.error('Export Complete Tactics error:', error);
  2404. await showErrorMessage(USERSCRIPT_STRINGS.errorTitle, 'Failed to export tactics.');
  2405. }
  2406. }
  2407.  
  2408. function createTacticPreviewElement() {
  2409. if (previewElement) return previewElement;
  2410. previewElement = document.createElement('div');
  2411. previewElement.id = 'mztm-tactic-preview';
  2412. previewElement.style.display = 'none';
  2413. previewElement.style.opacity = '0';
  2414. previewElement.addEventListener('mouseenter', () => {
  2415. if (previewHideTimeout) clearTimeout(previewHideTimeout);
  2416. });
  2417. previewElement.addEventListener('mouseleave', hideTacticPreview);
  2418. document.body.appendChild(previewElement);
  2419. return previewElement;
  2420. }
  2421.  
  2422. function updatePreviewPosition(event) {
  2423. if (!previewElement || previewElement.style.display === 'none') return;
  2424. const xOffset = 15;
  2425. const yOffset = 10;
  2426. let x = event.clientX + xOffset;
  2427. let y = event.clientY + yOffset;
  2428. const previewRect = previewElement.getBoundingClientRect();
  2429. const viewportWidth = window.innerWidth;
  2430. const viewportHeight = window.innerHeight;
  2431.  
  2432. if (x + previewRect.width > viewportWidth - 10) {
  2433. x = event.clientX - previewRect.width - xOffset;
  2434. }
  2435. if (y + previewRect.height > viewportHeight - 10) {
  2436. y = event.clientY - previewRect.height - yOffset;
  2437. }
  2438. if (x < 10) x = 10;
  2439. if (y < 10) y = 10;
  2440.  
  2441. previewElement.style.left = `${x}px`;
  2442. previewElement.style.top = `${y}px`;
  2443. }
  2444.  
  2445. function showTacticPreview(event, listItem) {
  2446. if (!listItem || listItem.classList.contains('mztm-custom-select-category') || listItem.classList.contains('mztm-custom-select-no-results')) {
  2447. hideTacticPreview();
  2448. return;
  2449. }
  2450.  
  2451. if (previewHideTimeout) clearTimeout(previewHideTimeout);
  2452.  
  2453. const tacticId = listItem.dataset.tacticId;
  2454. const tacticName = listItem.dataset.tacticName;
  2455. const description = listItem.dataset.description || '';
  2456. const formationString = listItem.dataset.formationString || 'N/A';
  2457. const isCompleteTactic = listItem.closest('#complete_tactics_selector_list');
  2458.  
  2459.  
  2460. if (!tacticId && !tacticName) {
  2461. hideTacticPreview();
  2462. return;
  2463. }
  2464.  
  2465. const previewDiv = createTacticPreviewElement();
  2466. previewDiv.innerHTML = `
  2467. <div class="mztm-preview-formation"><strong>${USERSCRIPT_STRINGS.previewFormationLabel}</strong> ${formationString}</div>
  2468. ${description ? `<div class="mztm-preview-desc">${description.replace(/\n/g, '<br>')}</div>` : '<div class="mztm-preview-no-desc">No description available.</div>'}
  2469. `;
  2470. previewDiv.style.display = 'block';
  2471. requestAnimationFrame(() => {
  2472. updatePreviewPosition(event);
  2473. previewDiv.style.opacity = '1';
  2474. });
  2475.  
  2476. document.addEventListener('mousemove', updatePreviewPosition);
  2477. }
  2478.  
  2479. function hideTacticPreview() {
  2480. if (previewHideTimeout) clearTimeout(previewHideTimeout);
  2481. previewHideTimeout = setTimeout(() => {
  2482. if (previewElement) {
  2483. previewElement.style.opacity = '0';
  2484. setTimeout(() => {
  2485. if (previewElement && previewElement.style.opacity === '0') {
  2486. previewElement.style.display = 'none';
  2487. }
  2488. }, 200);
  2489. document.removeEventListener('mousemove', updatePreviewPosition);
  2490. }
  2491. previewHideTimeout = null;
  2492. }, 100);
  2493. }
  2494.  
  2495. function addPreviewListenersToList(listElement) {
  2496. if (!listElement) return;
  2497.  
  2498. listElement.addEventListener('mouseover', (event) => {
  2499. const listItem = event.target.closest('.mztm-custom-select-item');
  2500. if (listItem && !listItem.classList.contains('disabled')) {
  2501. showTacticPreview(event, listItem);
  2502. } else if (!listItem) {
  2503. hideTacticPreview();
  2504. }
  2505. });
  2506.  
  2507. listElement.addEventListener('mouseout', (event) => {
  2508. const listItem = event.target.closest('.mztm-custom-select-item');
  2509. if (listItem) {
  2510. const related = event.relatedTarget;
  2511. if (!listItem.contains(related) && related !== previewElement) {
  2512. hideTacticPreview();
  2513. }
  2514. } else if (!listElement.contains(event.relatedTarget) && (!previewElement || event.relatedTarget !== previewElement)) {
  2515. hideTacticPreview();
  2516. }
  2517. });
  2518.  
  2519. window.addEventListener('scroll', hideTacticPreview, true);
  2520. }
  2521.  
  2522. function closeAllCustomDropdowns(exceptElement = null) {
  2523. document.querySelectorAll('.mztm-custom-select-list-container.open').forEach(container => {
  2524. const wrapper = container.closest('.mztm-custom-select-wrapper');
  2525. if (wrapper !== exceptElement?.closest('.mztm-custom-select-wrapper')) {
  2526. container.classList.remove('open');
  2527. const trigger = wrapper?.querySelector('.mztm-custom-select-trigger');
  2528. trigger?.classList.remove('open');
  2529. }
  2530. });
  2531. currentOpenDropdown = exceptElement?.closest('.mztm-custom-select-wrapper') || null;
  2532. }
  2533.  
  2534. document.addEventListener('click', (event) => {
  2535. if (currentOpenDropdown && !currentOpenDropdown.contains(event.target)) {
  2536. closeAllCustomDropdowns();
  2537. }
  2538. });
  2539.  
  2540. function createCustomSelect(id, placeholderText) {
  2541. const wrapper = document.createElement('div');
  2542. wrapper.className = 'mztm-custom-select-wrapper';
  2543. wrapper.id = `${id}_wrapper`;
  2544.  
  2545. const trigger = document.createElement('div');
  2546. trigger.className = 'mztm-custom-select-trigger';
  2547. trigger.id = `${id}_trigger`;
  2548. trigger.tabIndex = 0;
  2549. const triggerText = document.createElement('span');
  2550. triggerText.className = 'mztm-custom-select-text mztm-custom-select-placeholder';
  2551. triggerText.textContent = placeholderText;
  2552. trigger.appendChild(triggerText);
  2553.  
  2554. const listContainer = document.createElement('div');
  2555. listContainer.className = 'mztm-custom-select-list-container';
  2556. listContainer.id = `${id}_list_container`;
  2557. const list = document.createElement('ul');
  2558. list.className = 'mztm-custom-select-list';
  2559. list.id = `${id}_list`;
  2560. listContainer.appendChild(list);
  2561.  
  2562. addPreviewListenersToList(list);
  2563.  
  2564. trigger.addEventListener('click', (e) => {
  2565. e.stopPropagation();
  2566. const isOpen = listContainer.classList.contains('open');
  2567. closeAllCustomDropdowns(wrapper);
  2568. if (!isOpen && !trigger.classList.contains('disabled')) {
  2569. listContainer.classList.add('open');
  2570. trigger.classList.add('open');
  2571. currentOpenDropdown = wrapper;
  2572. }
  2573. });
  2574.  
  2575. trigger.addEventListener('keydown', (e) => {
  2576. if (e.key === 'Enter' || e.key === ' ') {
  2577. e.preventDefault();
  2578. trigger.click();
  2579. }
  2580. });
  2581.  
  2582. list.addEventListener('click', (e) => {
  2583. const item = e.target.closest('.mztm-custom-select-item');
  2584. if (item && !item.classList.contains('disabled')) {
  2585. const value = item.dataset.value || item.dataset.tacticId || item.dataset.tacticName;
  2586. const text = item.textContent;
  2587.  
  2588. triggerText.textContent = text;
  2589. triggerText.classList.remove('mztm-custom-select-placeholder');
  2590. trigger.dataset.selectedValue = value;
  2591.  
  2592. if (id === 'tactics_selector') {
  2593. handleTacticSelection(value);
  2594. } else if (id === 'complete_tactics_selector') {
  2595. selectedCompleteTacticName = value;
  2596. }
  2597.  
  2598. closeAllCustomDropdowns();
  2599.  
  2600. const changeEvent = new Event('change', { bubbles: true });
  2601. trigger.dispatchEvent(changeEvent);
  2602. }
  2603. });
  2604.  
  2605. wrapper.appendChild(trigger);
  2606. wrapper.appendChild(listContainer);
  2607. return wrapper;
  2608. }
  2609.  
  2610. function createTacticsSelector() {
  2611. const container = document.createElement('div');
  2612. container.className = 'tactics-selector-section';
  2613. const controlsContainer = document.createElement('div');
  2614. controlsContainer.className = 'formations-controls-container';
  2615.  
  2616. const dropdownWrapper = createCustomSelect('tactics_selector', USERSCRIPT_STRINGS.tacticsDropdownMenuLabel);
  2617.  
  2618. const searchBox = document.createElement('input');
  2619. searchBox.type = 'text';
  2620. searchBox.className = 'tactics-search-box';
  2621. searchBox.placeholder = USERSCRIPT_STRINGS.searchPlaceholder;
  2622. searchBox.addEventListener('input', (e) => {
  2623. searchTerm = e.target.value.toLowerCase();
  2624. updateTacticsDropdown(selectedFormationTacticId);
  2625. });
  2626.  
  2627. const filterDropdownWrapper = document.createElement('div');
  2628. filterDropdownWrapper.className = 'category-filter-wrapper';
  2629. const filterSelect = document.createElement('select');
  2630. filterSelect.id = 'category_filter_selector';
  2631. filterSelect.addEventListener('change', (e) => {
  2632. currentFilter = e.target.value;
  2633. GM_setValue(CATEGORY_FILTER_STORAGE_KEY, currentFilter);
  2634. updateTacticsDropdown(selectedFormationTacticId);
  2635. });
  2636.  
  2637. const manageBtn = document.createElement('button');
  2638. manageBtn.id = 'manage_items_btn';
  2639. manageBtn.className = 'mzbtn manage-items-btn';
  2640. manageBtn.innerHTML = '⚙️';
  2641. manageBtn.title = USERSCRIPT_STRINGS.managementModalTitle;
  2642. manageBtn.addEventListener('click', showManagementModal);
  2643. filterDropdownWrapper.appendChild(filterSelect);
  2644. filterDropdownWrapper.appendChild(manageBtn);
  2645.  
  2646. appendChildren(controlsContainer, [dropdownWrapper, searchBox, filterDropdownWrapper]);
  2647. container.appendChild(controlsContainer);
  2648. return container;
  2649. }
  2650.  
  2651. function updateCategoryFilterDropdown() {
  2652. const filterSelect = document.getElementById('category_filter_selector');
  2653. if (!filterSelect) return;
  2654. filterSelect.innerHTML = '';
  2655. const usedCategoryIds = new Set(tactics.map(t => t.style || OTHER_CATEGORY_ID));
  2656. let categoriesToShow = [{
  2657. id: 'all',
  2658. name: USERSCRIPT_STRINGS.allTacticsFilter
  2659. }];
  2660. Object.values(categories)
  2661. .filter(cat => cat.id !== 'all' && (usedCategoryIds.has(cat.id) || Object.keys(DEFAULT_CATEGORIES).includes(cat.id) || cat.id === OTHER_CATEGORY_ID))
  2662. .sort((a, b) => {
  2663. if (a.id === OTHER_CATEGORY_ID) return 1;
  2664. if (b.id === OTHER_CATEGORY_ID) return -1;
  2665. return a.name.localeCompare(b.name);
  2666. })
  2667. .forEach(cat => categoriesToShow.push({ id: cat.id, name: getCategoryName(cat.id) }));
  2668.  
  2669. let foundCurrentFilter = false;
  2670. categoriesToShow.forEach(categoryInfo => {
  2671. const option = document.createElement('option');
  2672. option.value = categoryInfo.id;
  2673. option.textContent = categoryInfo.name;
  2674. filterSelect.appendChild(option);
  2675. if (categoryInfo.id === currentFilter) {
  2676. foundCurrentFilter = true;
  2677. }
  2678. });
  2679.  
  2680. if (foundCurrentFilter) {
  2681. filterSelect.value = currentFilter;
  2682. } else {
  2683. filterSelect.value = 'all';
  2684. currentFilter = 'all';
  2685. GM_setValue(CATEGORY_FILTER_STORAGE_KEY, currentFilter);
  2686. }
  2687.  
  2688. filterSelect.disabled = categoriesToShow.length <= 1;
  2689. }
  2690.  
  2691. function updateTacticsDropdown(currentSelectedId = null) {
  2692. const listElement = document.getElementById('tactics_selector_list');
  2693. const triggerElement = document.getElementById('tactics_selector_trigger');
  2694. const triggerTextElement = triggerElement?.querySelector('.mztm-custom-select-text');
  2695. const wrapper = document.getElementById('tactics_selector_wrapper');
  2696. const searchBox = document.querySelector('.tactics-search-box');
  2697.  
  2698. if (!listElement || !triggerElement || !triggerTextElement || !wrapper) return;
  2699.  
  2700. listElement.innerHTML = '';
  2701.  
  2702. if (searchTerm.length > 0) {
  2703. wrapper.classList.add('filtering');
  2704. searchBox?.classList.add('filtering');
  2705. } else {
  2706. wrapper.classList.remove('filtering');
  2707. searchBox?.classList.remove('filtering');
  2708. }
  2709.  
  2710. const filteredTactics = tactics.filter(t => {
  2711. const nameMatch = searchTerm === '' || t.name.toLowerCase().includes(searchTerm);
  2712. const categoryMatch = currentFilter === 'all' || (currentFilter === OTHER_CATEGORY_ID && (!t.style || t.style === OTHER_CATEGORY_ID)) || t.style === currentFilter;
  2713. return nameMatch && categoryMatch;
  2714. });
  2715.  
  2716. const groupedTactics = {};
  2717. Object.keys(categories).forEach(id => {
  2718. if (id !== 'all') groupedTactics[id] = [];
  2719. });
  2720. if (!groupedTactics[OTHER_CATEGORY_ID]) groupedTactics[OTHER_CATEGORY_ID] = [];
  2721.  
  2722. filteredTactics.forEach(t => {
  2723. const categoryId = t.style || OTHER_CATEGORY_ID;
  2724. if (!groupedTactics[categoryId]) {
  2725. if (!groupedTactics[OTHER_CATEGORY_ID]) groupedTactics[OTHER_CATEGORY_ID] = [];
  2726. groupedTactics[OTHER_CATEGORY_ID].push(t);
  2727. }
  2728. else groupedTactics[categoryId].push(t);
  2729. });
  2730.  
  2731. const categoryOrder = Object.keys(groupedTactics).filter(id => groupedTactics[id].length > 0).sort((a, b) => {
  2732. if (a === currentFilter) return -1;
  2733. if (b === currentFilter) return 1;
  2734. if (DEFAULT_CATEGORIES[a] && !DEFAULT_CATEGORIES[b]) return -1;
  2735. if (!DEFAULT_CATEGORIES[a] && DEFAULT_CATEGORIES[b]) return 1;
  2736. if (a === OTHER_CATEGORY_ID) return 1;
  2737. if (b === OTHER_CATEGORY_ID) return -1;
  2738. return (getCategoryName(a) || '').localeCompare(getCategoryName(b) || '');
  2739. });
  2740.  
  2741. let itemsAdded = 0;
  2742. categoryOrder.forEach(categoryId => {
  2743. if (groupedTactics[categoryId].length > 0) {
  2744. addTacticItemsGroup(listElement, groupedTactics[categoryId], getCategoryName(categoryId), categoryId);
  2745. itemsAdded += groupedTactics[categoryId].length;
  2746. }
  2747. });
  2748.  
  2749. if (itemsAdded === 0) {
  2750. const noResultsItem = document.createElement('li');
  2751. noResultsItem.className = 'mztm-custom-select-no-results';
  2752. noResultsItem.textContent = tactics.length === 0 ? USERSCRIPT_STRINGS.noTacticsSaved : USERSCRIPT_STRINGS.noTacticsFound;
  2753. listElement.appendChild(noResultsItem);
  2754. triggerElement.classList.add('disabled');
  2755. triggerTextElement.textContent = tactics.length === 0 ? USERSCRIPT_STRINGS.noTacticsSaved : USERSCRIPT_STRINGS.tacticsDropdownMenuLabel;
  2756. triggerTextElement.classList.add('mztm-custom-select-placeholder');
  2757. delete triggerElement.dataset.selectedValue;
  2758. selectedFormationTacticId = null;
  2759. } else {
  2760. triggerElement.classList.remove('disabled');
  2761. const currentSelection = tactics.find(t => t.id === currentSelectedId);
  2762. if (currentSelection) {
  2763. triggerTextElement.textContent = currentSelection.name;
  2764. triggerTextElement.classList.remove('mztm-custom-select-placeholder');
  2765. triggerElement.dataset.selectedValue = currentSelection.id;
  2766. selectedFormationTacticId = currentSelection.id;
  2767. } else {
  2768. triggerTextElement.textContent = USERSCRIPT_STRINGS.tacticsDropdownMenuLabel;
  2769. triggerTextElement.classList.add('mztm-custom-select-placeholder');
  2770. delete triggerElement.dataset.selectedValue;
  2771. selectedFormationTacticId = null;
  2772. }
  2773. }
  2774. }
  2775.  
  2776. function addTacticItemsGroup(listElement, tacticsList, groupLabel, categoryId) {
  2777. if (tacticsList.length === 0) return;
  2778.  
  2779. const categoryHeader = document.createElement('li');
  2780. categoryHeader.className = 'mztm-custom-select-category';
  2781. categoryHeader.textContent = groupLabel;
  2782. listElement.appendChild(categoryHeader);
  2783.  
  2784. tacticsList.sort((a, b) => a.name.localeCompare(b.name));
  2785. tacticsList.forEach(tactic => {
  2786. const item = document.createElement('li');
  2787. item.className = 'mztm-custom-select-item';
  2788. item.textContent = tactic.name;
  2789. item.dataset.tacticId = tactic.id;
  2790. item.dataset.value = tactic.id;
  2791. item.dataset.description = tactic.description || '';
  2792. item.dataset.style = tactic.style || OTHER_CATEGORY_ID;
  2793. item.dataset.formationString = formatFormationString(getFormation(tactic.coordinates));
  2794. listElement.appendChild(item);
  2795. });
  2796. }
  2797.  
  2798. function createCompleteTacticsSelector() {
  2799. const container = document.createElement('div');
  2800. container.className = 'tactics-selector-section';
  2801. const label = document.createElement('label');
  2802. label.textContent = '';
  2803. label.className = 'tactics-selector-label';
  2804.  
  2805. const dropdownWrapper = createCustomSelect('complete_tactics_selector', USERSCRIPT_STRINGS.completeTacticsDropdownMenuLabel);
  2806.  
  2807. container.appendChild(label);
  2808. container.appendChild(dropdownWrapper);
  2809. return container;
  2810. }
  2811.  
  2812. function updateCompleteTacticsDropdown(currentSelectedName = null) {
  2813. const listElement = document.getElementById('complete_tactics_selector_list');
  2814. const triggerElement = document.getElementById('complete_tactics_selector_trigger');
  2815. const triggerTextElement = triggerElement?.querySelector('.mztm-custom-select-text');
  2816. const wrapper = document.getElementById('complete_tactics_selector_wrapper');
  2817.  
  2818. if (!listElement || !triggerElement || !triggerTextElement || !wrapper) return;
  2819.  
  2820. listElement.innerHTML = '';
  2821.  
  2822. const names = Object.keys(completeTactics).sort((a, b) => a.localeCompare(b));
  2823.  
  2824. if (names.length === 0) {
  2825. const noResultsItem = document.createElement('li');
  2826. noResultsItem.className = 'mztm-custom-select-no-results';
  2827. noResultsItem.textContent = USERSCRIPT_STRINGS.noCompleteTacticsSaved;
  2828. listElement.appendChild(noResultsItem);
  2829. triggerElement.classList.add('disabled');
  2830. triggerTextElement.textContent = USERSCRIPT_STRINGS.noCompleteTacticsSaved;
  2831. triggerTextElement.classList.add('mztm-custom-select-placeholder');
  2832. delete triggerElement.dataset.selectedValue;
  2833. selectedCompleteTacticName = null;
  2834. } else {
  2835. triggerElement.classList.remove('disabled');
  2836. names.forEach(name => {
  2837. const tactic = completeTactics[name];
  2838. const item = document.createElement('li');
  2839. item.className = 'mztm-custom-select-item';
  2840. item.textContent = name;
  2841. item.dataset.tacticName = name;
  2842. item.dataset.value = name;
  2843. item.dataset.description = tactic.description || '';
  2844. item.dataset.formationString = formatFormationString(getFormationFromCompleteTactic(tactic));
  2845. listElement.appendChild(item);
  2846. });
  2847.  
  2848. const currentSelection = currentSelectedName && completeTactics[currentSelectedName] ? currentSelectedName : null;
  2849.  
  2850. if (currentSelection) {
  2851. triggerTextElement.textContent = currentSelection;
  2852. triggerTextElement.classList.remove('mztm-custom-select-placeholder');
  2853. triggerElement.dataset.selectedValue = currentSelection;
  2854. selectedCompleteTacticName = currentSelection;
  2855. } else {
  2856. triggerTextElement.textContent = USERSCRIPT_STRINGS.completeTacticsDropdownMenuLabel;
  2857. triggerTextElement.classList.add('mztm-custom-select-placeholder');
  2858. delete triggerElement.dataset.selectedValue;
  2859. selectedCompleteTacticName = null;
  2860. }
  2861. }
  2862. }
  2863.  
  2864. function createButton(id, text, clickHandler) {
  2865. const button = document.createElement('button');
  2866. setUpButton(button, id, text);
  2867. if (clickHandler) {
  2868. button.addEventListener('click', async (e) => {
  2869. e.stopPropagation();
  2870. try {
  2871. await clickHandler();
  2872. } catch (err) {
  2873. console.error('Button click failed:', err);
  2874. showErrorMessage('Action Failed', `${err.message || err}`);
  2875. }
  2876. });
  2877. }
  2878. return button;
  2879. }
  2880.  
  2881. async function checkVersion() {
  2882. const savedVersion = GM_getValue(VERSION_KEY, null);
  2883. if (!savedVersion || savedVersion !== SCRIPT_VERSION) {
  2884. await showWelcomeMessage();
  2885. GM_setValue(VERSION_KEY, SCRIPT_VERSION);
  2886. }
  2887. }
  2888.  
  2889. function createModeToggleSwitch() {
  2890. const label = document.createElement('label');
  2891. label.className = 'mode-toggle-switch';
  2892. const input = document.createElement('input');
  2893. input.type = 'checkbox';
  2894. input.id = 'view-mode-toggle';
  2895. input.addEventListener('change', (e) => setViewMode(e.target.checked ? 'complete' : 'normal'));
  2896. const slider = document.createElement('span');
  2897. slider.className = 'mode-toggle-slider';
  2898. label.appendChild(input);
  2899. label.appendChild(slider);
  2900. return label;
  2901. }
  2902.  
  2903. function createModeLabel(mode, isPrefix = false) {
  2904. const span = document.createElement('span');
  2905. span.className = 'mode-toggle-label';
  2906. span.textContent = isPrefix ? USERSCRIPT_STRINGS.modeLabel : (mode === 'normal' ? USERSCRIPT_STRINGS.normalModeLabel : USERSCRIPT_STRINGS.completeModeLabel);
  2907. span.id = `mode-label-${mode}`;
  2908. return span;
  2909. }
  2910.  
  2911. function setViewMode(mode) {
  2912. currentViewMode = mode;
  2913. GM_setValue(VIEW_MODE_KEY, mode);
  2914. const normalContent = document.getElementById('normal-tactics-content');
  2915. const completeContent = document.getElementById('complete-tactics-content');
  2916. const toggleInput = document.getElementById('view-mode-toggle');
  2917. const normalLabel = document.getElementById('mode-label-normal');
  2918. const completeLabel = document.getElementById('mode-label-complete');
  2919. const isNormal = mode === 'normal';
  2920. if (normalContent) normalContent.style.display = isNormal ? 'block' : 'none';
  2921. if (completeContent) completeContent.style.display = isNormal ? 'none' : 'block';
  2922. if (toggleInput) toggleInput.checked = !isNormal;
  2923. if (normalLabel) normalLabel.classList.toggle('active', isNormal);
  2924. if (completeLabel) completeLabel.classList.toggle('active', !isNormal);
  2925. }
  2926.  
  2927. function createMainContainer() {
  2928. const container = document.createElement('div');
  2929. container.id = 'mz_tactics_panel';
  2930. container.classList.add('mz-panel');
  2931. const header = document.createElement('div');
  2932. header.classList.add('mz-group-main-title');
  2933. const titleContainer = document.createElement('div');
  2934. titleContainer.className = 'mz-title-container';
  2935. const titleText = document.createElement('span');
  2936. titleText.textContent = USERSCRIPT_STRINGS.managerTitle;
  2937. titleText.classList.add('mz-main-title');
  2938. const versionText = document.createElement('span');
  2939. versionText.textContent = 'v' + DISPLAY_VERSION;
  2940. versionText.classList.add('mz-version-text');
  2941. const modeToggleContainer = document.createElement('div');
  2942. modeToggleContainer.className = 'mode-toggle-container';
  2943. const prefixLabel = createModeLabel('', true);
  2944. const modeLabelNormal = createModeLabel('normal');
  2945. const toggleSwitch = createModeToggleSwitch();
  2946. const modeLabelComplete = createModeLabel('complete');
  2947. appendChildren(modeToggleContainer, [prefixLabel, modeLabelNormal, toggleSwitch, modeLabelComplete]);
  2948. appendChildren(titleContainer, [titleText, versionText, modeToggleContainer]);
  2949. header.appendChild(titleContainer);
  2950. const toggleButton = createToggleButton();
  2951. header.appendChild(toggleButton);
  2952. container.appendChild(header);
  2953. const group = document.createElement('div');
  2954. group.classList.add('mz-group');
  2955. container.appendChild(group);
  2956.  
  2957. const normalContent = document.createElement('div');
  2958. normalContent.id = 'normal-tactics-content';
  2959. normalContent.className = 'section-content';
  2960. const tacticsSelectorSection = createTacticsSelector();
  2961. const normalButtonsSection = document.createElement('div');
  2962. normalButtonsSection.className = 'action-buttons-section';
  2963. const addCurrentBtn = createButton('add_current_tactic_btn', USERSCRIPT_STRINGS.addCurrentTactic, addNewTactic);
  2964. const addXmlBtn = createButton('add_xml_tactic_btn', USERSCRIPT_STRINGS.addWithXmlButton, addNewTacticWithXml);
  2965. const editBtn = createButton('edit_tactic_button', USERSCRIPT_STRINGS.renameButton, () => editTactic());
  2966. const updateBtn = createButton('update_tactic_button', USERSCRIPT_STRINGS.updateButton, updateTactic);
  2967. const deleteBtn = createButton('delete_tactic_button', USERSCRIPT_STRINGS.deleteButton, deleteTactic);
  2968. const importBtn = createButton('import_tactics_btn', USERSCRIPT_STRINGS.importButton, importTacticsJsonData);
  2969. const exportBtn = createButton('export_tactics_btn', USERSCRIPT_STRINGS.exportButton, exportTacticsJsonData);
  2970. const resetBtn = createButton('reset_tactics_btn', USERSCRIPT_STRINGS.resetButton, resetTactics);
  2971. const clearBtn = createButton('clear_tactics_btn', USERSCRIPT_STRINGS.clearButton, clearTactics);
  2972. const normalButtonsRow1 = document.createElement('div');
  2973. normalButtonsRow1.className = 'action-buttons-row';
  2974. appendChildren(normalButtonsRow1, [addCurrentBtn, addXmlBtn, editBtn, updateBtn, deleteBtn]);
  2975. const normalButtonsRow2 = document.createElement('div');
  2976. normalButtonsRow2.className = 'action-buttons-row';
  2977. appendChildren(normalButtonsRow2, [importBtn, exportBtn, resetBtn, clearBtn]);
  2978. appendChildren(normalButtonsSection, [normalButtonsRow1, normalButtonsRow2]);
  2979. appendChildren(normalContent, [tacticsSelectorSection, normalButtonsSection, createHiddenTriggerButton(), createCombinedInfoButton()]);
  2980. group.appendChild(normalContent);
  2981.  
  2982. const completeContent = document.createElement('div');
  2983. completeContent.id = 'complete-tactics-content';
  2984. completeContent.className = 'section-content';
  2985. completeContent.style.display = 'none';
  2986. const completeTacticsSelectorSection = createCompleteTacticsSelector();
  2987. const completeButtonsSection = document.createElement('div');
  2988. completeButtonsSection.className = 'action-buttons-section';
  2989. const completeButtonsRow1 = document.createElement('div');
  2990. completeButtonsRow1.className = 'action-buttons-row';
  2991. const saveCompleteBtn = createButton('save_complete_tactic_button', USERSCRIPT_STRINGS.saveCompleteTacticButton, saveCompleteTactic);
  2992. const loadCompleteBtn = createButton('load_complete_tactic_button', USERSCRIPT_STRINGS.loadCompleteTacticButton, loadCompleteTactic);
  2993. const renameCompleteBtn = createButton('rename_complete_tactic_button', USERSCRIPT_STRINGS.renameCompleteTacticButton, editCompleteTactic);
  2994. const updateCompleteBtn = createButton('update_complete_tactic_button', USERSCRIPT_STRINGS.updateCompleteTacticButton, updateCompleteTactic);
  2995. const deleteCompleteBtn = createButton('delete_complete_tactic_button', USERSCRIPT_STRINGS.deleteCompleteTacticButton, deleteCompleteTactic);
  2996. appendChildren(completeButtonsRow1, [saveCompleteBtn, loadCompleteBtn, renameCompleteBtn, updateCompleteBtn, deleteCompleteBtn]);
  2997. const completeButtonsRow2 = document.createElement('div');
  2998. completeButtonsRow2.className = 'action-buttons-row';
  2999. const importCompleteBtn = createButton('import_complete_tactics_btn', USERSCRIPT_STRINGS.importCompleteTacticsButton, importCompleteTactics);
  3000. const exportCompleteBtn = createButton('export_complete_tactics_btn', USERSCRIPT_STRINGS.exportCompleteTacticsButton, exportCompleteTactics);
  3001. appendChildren(completeButtonsRow2, [importCompleteBtn, exportCompleteBtn]);
  3002. appendChildren(completeButtonsSection, [completeButtonsRow1, completeButtonsRow2]);
  3003. appendChildren(completeContent, [completeTacticsSelectorSection, completeButtonsSection, createCombinedInfoButton()]);
  3004. group.appendChild(completeContent);
  3005.  
  3006. return container;
  3007. }
  3008.  
  3009. function createHiddenTriggerButton() {
  3010. const button = document.createElement('button');
  3011. button.id = 'hidden_trigger_button';
  3012. button.textContent = '';
  3013. button.style.cssText = 'position:absolute; opacity:0; pointer-events:none; width:0; height:0; padding:0; margin:0; border:0;';
  3014. button.addEventListener('click', function () {
  3015. const presetSelect = document.getElementById('tactics_preset');
  3016. if (presetSelect) {
  3017. presetSelect.value = '5-3-2';
  3018. presetSelect.dispatchEvent(new Event('change'));
  3019. }
  3020. });
  3021. return button;
  3022. }
  3023.  
  3024. function setUpButton(button, id, text) {
  3025. button.id = id;
  3026. button.classList.add('mzbtn');
  3027. button.textContent = text;
  3028. }
  3029.  
  3030. function createModalTabs(tabsConfig, modalBody) {
  3031. const tabsContainer = document.createElement('div');
  3032. tabsContainer.className = 'modal-tabs';
  3033. tabsConfig.forEach((tab, index) => {
  3034. const tabButton = document.createElement('button');
  3035. tabButton.className = 'modal-tab';
  3036. tabButton.textContent = tab.title;
  3037. tabButton.dataset.tabId = tab.id;
  3038. if (index === 0) tabButton.classList.add('active');
  3039. tabButton.addEventListener('click', () => {
  3040. modalBody.querySelectorAll('.modal-tab').forEach(t => t.classList.remove('active'));
  3041. modalBody.querySelectorAll('.management-modal-content, .modal-tab-content').forEach(c => c.classList.remove('active'));
  3042. tabButton.classList.add('active');
  3043. const content = modalBody.querySelector(`.management-modal-content[data-tab-id="${tab.id}"], .modal-tab-content[data-tab-id="${tab.id}"]`);
  3044. if (content) content.classList.add('active');
  3045. });
  3046. tabsContainer.appendChild(tabButton);
  3047. });
  3048. return tabsContainer;
  3049. }
  3050.  
  3051. function createTabbedModalContent(tabsConfig) {
  3052. const wrapper = document.createElement('div');
  3053. wrapper.className = 'modal-info-wrapper';
  3054. const tabs = createModalTabs(tabsConfig, wrapper);
  3055. wrapper.appendChild(tabs);
  3056. tabsConfig.forEach((tab, index) => {
  3057. const contentDiv = document.createElement('div');
  3058. contentDiv.className = 'modal-tab-content';
  3059. contentDiv.dataset.tabId = tab.id;
  3060. if (index === 0) contentDiv.classList.add('active');
  3061. const content = tab.contentGenerator();
  3062. contentDiv.appendChild(content);
  3063. wrapper.appendChild(contentDiv);
  3064. });
  3065. return wrapper;
  3066. }
  3067.  
  3068. function createAboutTabContent() {
  3069. const content = document.createElement('div');
  3070. const aboutSection = document.createElement('div');
  3071. const aboutTitle = document.createElement('h3');
  3072. aboutTitle.textContent = 'About';
  3073. const infoText = document.createElement('p');
  3074. infoText.id = 'info_modal_info_text';
  3075. infoText.innerHTML = USERSCRIPT_STRINGS.modalContentInfoText;
  3076. const feedbackText = document.createElement('p');
  3077. feedbackText.id = 'info_modal_feedback_text';
  3078. feedbackText.innerHTML = USERSCRIPT_STRINGS.modalContentFeedbackText;
  3079. appendChildren(aboutSection, [aboutTitle, infoText, feedbackText]);
  3080. content.appendChild(aboutSection);
  3081. const faqSection = document.createElement('div');
  3082. faqSection.className = 'faq-section';
  3083. const faqTitle = document.createElement('h3');
  3084. faqTitle.textContent = 'FAQ/Function Explanations';
  3085. faqSection.appendChild(faqTitle);
  3086.  
  3087. const formationItems = [
  3088. { q: "<code>Add Current</code> Button (Formations Mode)", a: "Saves the player positions currently visible on the pitch as a new formation. You'll be prompted for a name, category, and an optional description." },
  3089. { q: "<code>Add via XML</code> Button (Formations Mode)", a: "Allows pasting XML to add a new formation. Only player positions are saved from the XML. Prompted for name, category, and description." },
  3090. { q: "Category Filter Dropdown & <code>⚙️</code> Button (Formations Mode)", a: "Use the dropdown to filter formations by category (your last selection is remembered). Click the gear icon (⚙️) to open the Management Modal (Formations & Categories)." },
  3091. { q: "<code>Edit</code> Button (Formations Mode)", a: "Allows renaming the selected formation, changing its assigned category, and editing its description via a popup." },
  3092. { q: "<code>Update Coords</code> Button (Formations Mode)", a: "Updates the coordinates of the selected formation to match the current player positions on the pitch (description and category remain unchanged)." },
  3093. { q: "<code>Delete</code> Button (Formations Mode)", a: "Permanently removes the selected formation from the storage." },
  3094. { q: "<code>Import</code> Button (Formations Mode)", a: "Imports multiple formations from a JSON text format. Merges with existing formations (updates name/category/description if ID matches)." },
  3095. { q: "<code>Export</code> Button (Formations Mode)", a: "Exports all saved formations (including descriptions) into a JSON text format (copied to clipboard)." },
  3096. { q: "<code>Reset</code> Button (Formations Mode)", a: "Deletes all saved formations and custom categories, restoring defaults. Also resets the saved category filter." },
  3097. { q: "<code>Clear</code> Button (Formations Mode)", a: "Deletes all saved formations. Also resets the saved category filter." },
  3098. { q: "Management Modal (Gear Icon ⚙️)", a: "Opens a dedicated window to manage formations (edit name/description/category, delete) and categories (add, remove) in bulk." },
  3099. { q: "Preview on Hover (Formations Mode)", a: "Hover your mouse over a formation name in the dropdown list to see its numerical formation (e.g., 4-4-2) and its description in a small pop-up." }
  3100. ];
  3101.  
  3102. const tacticItems = [
  3103. { q: "<code>Save Current</code> Button (Tactics Mode)", a: "Exports the entire current tactic setup (positions, alts, rules, settings) using MZ's native export, parses it, prompts for a name and description, then saves it as a new complete tactic." },
  3104. { q: "<code>Load</code> Button (Tactics Mode)", a: "Loads a saved complete tactic using MZ's native import. Shows a spinner during load. Matches players or substitutes if needed. Updates everything on the pitch." },
  3105. { q: "<code>Rename</code> Button (Tactics Mode)", a: "Allows renaming the selected complete tactic and editing its description via a popup." },
  3106. { q: "<code>Update with Current</code> Button (Tactics Mode)", a: "Overwrites the selected complete tactic's positions, rules, and settings with the setup currently on the pitch (using native export). The existing description is kept." },
  3107. { q: "<code>Delete</code> Button (Tactics Mode)", a: "Permanently removes the selected complete tactic." },
  3108. { q: "<code>Import</code> Button (Tactics Mode)", a: "Imports multiple complete tactics from a JSON text format. Merges with existing tactics, overwriting any with the same name (including description)." },
  3109. { q: "<code>Export</code> Button (Tactics Mode)", a: "Exports all saved complete tactics (including descriptions) into a JSON text format (copied to clipboard)." },
  3110. { q: "Preview on Hover (Tactics Mode)", a: "Hover your mouse over a tactic name in the dropdown list to see its numerical formation (e.g., 5-3-2, based on initial positions) and its description in a small pop-up." }
  3111. ];
  3112.  
  3113. const combinedItems = [...formationItems, ...tacticItems].sort((a, b) => {
  3114. const modeA = a.q.includes("Formations Mode") || a.q.includes("Category Filter") || a.q.includes("Management Modal") ? 0 : (a.q.includes("Tactics Mode") ? 1 : 2);
  3115. const modeB = b.q.includes("Formations Mode") || b.q.includes("Category Filter") || b.q.includes("Management Modal") ? 0 : (b.q.includes("Tactics Mode") ? 1 : 2);
  3116. if (modeA !== modeB) return modeA - modeB;
  3117. return a.q.localeCompare(b.q);
  3118. });
  3119.  
  3120. combinedItems.forEach(item => {
  3121. const faqItemDiv = document.createElement('div');
  3122. faqItemDiv.className = 'faq-item';
  3123. const question = document.createElement('h4');
  3124. question.innerHTML = item.q;
  3125. const answer = document.createElement('p');
  3126. answer.textContent = item.a;
  3127. appendChildren(faqItemDiv, [question, answer]);
  3128. faqSection.appendChild(faqItemDiv);
  3129. });
  3130. content.appendChild(faqSection);
  3131. return content;
  3132. }
  3133.  
  3134. function createLinksTabContent() {
  3135. const content = document.createElement('div');
  3136. const linksSection = document.createElement('div');
  3137. const linksTitle = document.createElement('h3');
  3138. linksTitle.textContent = 'Useful Links';
  3139. const resourcesText = createUsefulContent();
  3140. const linksMap = new Map([
  3141. ['gewlaht - BoooM', 'https://www.managerzone.com/?p=forum&sub=topic&topic_id=11415137&forum_id=49&sport=soccer'],
  3142. ['taktikskola by honken91', 'https://www.managerzone.com/?p=forum&sub=topic&topic_id=12653892&forum_id=4&sport=soccer'],
  3143. ['peto - mix de dibujos', 'https://www.managerzone.com/?p=forum&sub=topic&topic_id=12196312&forum_id=255&sport=soccer'],
  3144. ['The Zone Chile', 'https://www.managerzone.com/thezone/paper.php?paper_id=18036&page=9&sport=soccer'],
  3145. ['Tactics guide by lukasz87o/filipek4', 'https://www.managerzone.com/?p=forum&sub=topic&topic_id=12766444&forum_id=12&sport=soccer&share_sport=soccer'],
  3146. ['MZExtension/van.mz.playerAdvanced by vanjoge', 'https://greasyfork.org/en/scripts/373382-van-mz-playeradvanced'],
  3147. ['Mazyar Userscript', 'https://greasyfork.org/en/scripts/476290-mazyar'],
  3148. ['Stats Xente Userscript', 'https://greasyfork.org/en/scripts/491442-stats-xente-script'],
  3149. ['More userscripts', 'https://greasyfork.org/en/users/1088808-douglasdotv']
  3150. ]);
  3151. const linksList = createLinksList(linksMap);
  3152. appendChildren(linksSection, [linksTitle, resourcesText, linksList]);
  3153. content.appendChild(linksSection);
  3154. return content;
  3155. }
  3156.  
  3157. function createCombinedInfoButton() {
  3158. const button = createButton('info_button', USERSCRIPT_STRINGS.infoButton, null);
  3159. button.classList.add('footer-actions');
  3160. button.style.background = 'transparent';
  3161. button.style.border = 'none';
  3162. button.style.boxShadow = 'none';
  3163. button.style.fontFamily = '"Quicksand", sans-serif';
  3164. button.style.color = 'gold';
  3165. button.addEventListener('click', (e) => {
  3166. e.stopPropagation();
  3167. const tabsConfig = [{
  3168. id: 'about',
  3169. title: 'About & FAQ',
  3170. contentGenerator: createAboutTabContent
  3171. }, {
  3172. id: 'links',
  3173. title: 'Useful Links',
  3174. contentGenerator: createLinksTabContent
  3175. }];
  3176. const modalContent = createTabbedModalContent(tabsConfig);
  3177. showAlert({
  3178. title: 'MZ Tactics Manager Info',
  3179. htmlContent: modalContent,
  3180. confirmButtonText: DEFAULT_MODAL_STRINGS.ok
  3181. });
  3182. });
  3183. return button;
  3184. }
  3185.  
  3186. function createUsefulContent() {
  3187. const p = document.createElement('p');
  3188. p.id = 'useful_content';
  3189. p.textContent = USERSCRIPT_STRINGS.usefulContent;
  3190. return p;
  3191. }
  3192.  
  3193. function createLinksList(linksMap) {
  3194. const list = document.createElement('ul');
  3195. linksMap.forEach((href, text) => {
  3196. const listItem = document.createElement('li');
  3197. const anchor = document.createElement('a');
  3198. anchor.href = href;
  3199. anchor.target = '_blank';
  3200. anchor.rel = 'noopener noreferrer';
  3201. anchor.textContent = text;
  3202. listItem.appendChild(anchor);
  3203. list.appendChild(listItem);
  3204. });
  3205. return list;
  3206. }
  3207.  
  3208. function createToggleButton() {
  3209. const button = document.createElement('button');
  3210. button.id = 'toggle_panel_btn';
  3211. button.innerHTML = '✕';
  3212. button.title = 'Hide panel';
  3213. return button;
  3214. }
  3215.  
  3216. function createCollapsedIcon() {
  3217. const icon = document.createElement('div');
  3218. icon.id = 'collapsed_icon';
  3219. icon.innerHTML = 'TM';
  3220. icon.title = 'Show MZ Tactics Manager';
  3221. collapsedIconElement = icon;
  3222. return icon;
  3223. }
  3224.  
  3225. async function initializeUsData() {
  3226. loadCategories();
  3227. currentFilter = GM_getValue(CATEGORY_FILTER_STORAGE_KEY, 'all');
  3228. const ids = await fetchTeamIdAndUsername();
  3229. if (!ids.teamId) {
  3230. console.warn("MZTM: Failed to get Team ID.");
  3231. }
  3232.  
  3233. tactics = [];
  3234.  
  3235. const rawTacticDataFromStorage = GM_getValue(FORMATIONS_STORAGE_KEY);
  3236. const oldRawTacticDataFromStorage = GM_getValue(OLD_FORMATIONS_STORAGE_KEY);
  3237.  
  3238. if (!rawTacticDataFromStorage && oldRawTacticDataFromStorage && oldRawTacticDataFromStorage.tactics && Array.isArray(oldRawTacticDataFromStorage.tactics)) {
  3239. console.log(`MZTM: Migrating tactics from old storage key '${OLD_FORMATIONS_STORAGE_KEY}' to '${FORMATIONS_STORAGE_KEY}'.`);
  3240. let migratedTactics = oldRawTacticDataFromStorage.tactics.filter(t => t && t.name && t.id && Array.isArray(t.coordinates));
  3241. migratedTactics.forEach(t => {
  3242. if (!t.hasOwnProperty('style')) t.style = OTHER_CATEGORY_ID;
  3243. if (!t.hasOwnProperty('description')) t.description = '';
  3244. });
  3245. tactics = migratedTactics;
  3246. await GM_setValue(FORMATIONS_STORAGE_KEY, { tactics: tactics });
  3247. await GM_deleteValue(OLD_FORMATIONS_STORAGE_KEY);
  3248. console.log(`MZTM: Migration complete. Deleted old key '${OLD_FORMATIONS_STORAGE_KEY}'.`);
  3249. } else if (!rawTacticDataFromStorage) {
  3250. console.log("MZTM: No existing formations data found. Loading initial default formations.");
  3251. await loadInitialTacticsAndCategories();
  3252. } else {
  3253. if (!rawTacticDataFromStorage.tactics || !Array.isArray(rawTacticDataFromStorage.tactics)) {
  3254. rawTacticDataFromStorage.tactics = [];
  3255. }
  3256. let loadedTactics = rawTacticDataFromStorage.tactics.filter(t => t && t.name && t.id && Array.isArray(t.coordinates));
  3257. let dataWasChangedDuringValidation = false;
  3258. loadedTactics.forEach(t => {
  3259. if (!t.hasOwnProperty('style')) {
  3260. t.style = OTHER_CATEGORY_ID;
  3261. dataWasChangedDuringValidation = true;
  3262. }
  3263. if (!t.hasOwnProperty('description')) {
  3264. t.description = '';
  3265. dataWasChangedDuringValidation = true;
  3266. }
  3267. });
  3268. if (dataWasChangedDuringValidation) {
  3269. await GM_setValue(FORMATIONS_STORAGE_KEY, { tactics: loadedTactics });
  3270. }
  3271. tactics = loadedTactics;
  3272. }
  3273.  
  3274. tactics.sort((a, b) => a.name.localeCompare(b.name));
  3275.  
  3276. loadCompleteTacticsData();
  3277. const storedCompleteTactics = GM_getValue(COMPLETE_TACTICS_STORAGE_KEY, {});
  3278. let completeTacticsChanged = false;
  3279. for (const name in storedCompleteTactics) {
  3280. if (storedCompleteTactics.hasOwnProperty(name)) {
  3281. if (storedCompleteTactics[name] && typeof storedCompleteTactics[name] === 'object' && !storedCompleteTactics[name].hasOwnProperty('description')) {
  3282. storedCompleteTactics[name].description = '';
  3283. completeTacticsChanged = true;
  3284. }
  3285. }
  3286. }
  3287. if (completeTacticsChanged) {
  3288. GM_setValue(COMPLETE_TACTICS_STORAGE_KEY, storedCompleteTactics);
  3289. }
  3290. completeTactics = storedCompleteTactics;
  3291.  
  3292. await checkVersion();
  3293. }
  3294.  
  3295. function setUpTacticsInterface(mainContainer) {
  3296. const toggleButton = mainContainer.querySelector('#toggle_panel_btn');
  3297. const collapsedIcon = collapsedIconElement || createCollapsedIcon();
  3298. let isCollapsed = GM_getValue(COLLAPSED_KEY, false);
  3299. const anchorButtonId = 'replace-player-btn';
  3300. const applyCollapseState = (instant = false) => {
  3301. const anchorButton = document.getElementById(anchorButtonId);
  3302. if (collapsedIcon && collapsedIcon.parentNode) {
  3303. collapsedIcon.parentNode.removeChild(collapsedIcon);
  3304. }
  3305. if (isCollapsed) {
  3306. if (instant) {
  3307. mainContainer.style.transition = 'none';
  3308. mainContainer.classList.add('collapsed');
  3309. void mainContainer.offsetHeight;
  3310. mainContainer.style.transition = '';
  3311. } else {
  3312. mainContainer.classList.add('collapsed');
  3313. }
  3314. toggleButton.innerHTML = '☰';
  3315. toggleButton.title = 'Show panel';
  3316. if (anchorButton) {
  3317. insertAfterElement(collapsedIcon, anchorButton);
  3318. collapsedIcon.classList.add('visible');
  3319. } else {
  3320. console.warn(`MZTM: Anchor button #${anchorButtonId} not found for collapsed icon.`);
  3321. collapsedIcon.classList.remove('visible');
  3322. }
  3323. } else {
  3324. mainContainer.classList.remove('collapsed');
  3325. toggleButton.innerHTML = '✕';
  3326. toggleButton.title = 'Hide panel';
  3327. collapsedIcon.classList.remove('visible');
  3328. }
  3329. };
  3330. applyCollapseState(true);
  3331.  
  3332. function togglePanel() {
  3333. isCollapsed = !isCollapsed;
  3334. GM_setValue(COLLAPSED_KEY, isCollapsed);
  3335. applyCollapseState();
  3336. }
  3337. toggleButton.addEventListener('click', (e) => {
  3338. e.stopPropagation();
  3339. togglePanel();
  3340. });
  3341. collapsedIcon.addEventListener('click', () => {
  3342. togglePanel();
  3343. });
  3344. }
  3345.  
  3346. async function initialize() {
  3347. const tacticsBox = document.getElementById('tactics_box');
  3348. if (!tacticsBox || !isFootball()) {
  3349. console.log("MZTM: Not on valid page or tactics box not found.");
  3350. return;
  3351. }
  3352. const cachedUserInfo = GM_getValue(USER_INFO_CACHE_KEY);
  3353. if (cachedUserInfo && typeof cachedUserInfo === 'object' && cachedUserInfo.teamId && cachedUserInfo.username && cachedUserInfo.timestamp) {
  3354. userInfoCache = cachedUserInfo;
  3355. if (Date.now() - userInfoCache.timestamp < USER_INFO_CACHE_DURATION_MS) {
  3356. teamId = userInfoCache.teamId;
  3357. username = userInfoCache.username;
  3358. }
  3359. }
  3360. const cachedRoster = GM_getValue(ROSTER_CACHE_KEY);
  3361. if (cachedRoster && typeof cachedRoster === 'object' && cachedRoster.data && cachedRoster.timestamp) {
  3362. rosterCache = cachedRoster;
  3363. }
  3364. try {
  3365. collapsedIconElement = createCollapsedIcon();
  3366. createTacticPreviewElement();
  3367. await initializeUsData();
  3368. const mainContainer = createMainContainer();
  3369. setUpTacticsInterface(mainContainer);
  3370. insertAfterElement(mainContainer, tacticsBox);
  3371. updateTacticsDropdown();
  3372. updateCategoryFilterDropdown();
  3373. updateCompleteTacticsDropdown();
  3374. const savedMode = GM_getValue(VIEW_MODE_KEY, 'normal');
  3375. setViewMode(savedMode);
  3376. } catch (error) {
  3377. console.error('MZTM Initialization Error:', error);
  3378. const errorDiv = document.createElement('div');
  3379. errorDiv.textContent = 'Error initializing MZ Tactics Manager. Check console for details.';
  3380. errorDiv.style.cssText = 'color:red; padding:10px; border:1px solid red; margin:10px;';
  3381. insertAfterElement(errorDiv, tacticsBox);
  3382. }
  3383. }
  3384.  
  3385. window.addEventListener('load', initialize);
  3386. })();