deckById = ratings ? new Map(ratings.filter(r => r.player_id).map(r => [r.player_id, r])) : new Map()
deckHandTray = {
if (!ratings) return html``
const KEY = "ig-fb-hand", MAX = 30
let hand = []
try { hand = (JSON.parse(localStorage.getItem(KEY) || "[]") || []).filter(id => deckById.has(id)).slice(0, MAX) } catch (e) {}
const save = () => { try { localStorage.setItem(KEY, JSON.stringify(hand)) } catch (e) {} }
// Re-runs (e.g. bio loading) must not stack trays — remove any prior one.
const prior = document.querySelector(".deck-tray"); if (prior) prior.remove()
const tray = document.createElement("div"); tray.className = "deck-tray"
const head = document.createElement("div"); head.className = "deck-tray-head"
const title = document.createElement("span"); title.className = "deck-tray-title"
const actions = document.createElement("span"); actions.className = "deck-tray-actions"
const clearBtn = document.createElement("button"); clearBtn.type = "button"; clearBtn.className = "deck-tray-btn"; clearBtn.textContent = "Clear"
const playBtn = document.createElement("button"); playBtn.type = "button"; playBtn.className = "deck-tray-btn play"; playBtn.textContent = "Play"; playBtn.title = "Duel your hand as Top Trumps (need 2+ cards)"
playBtn.addEventListener("click", () => { if (hand.length >= 2 && window._deckStartDuel) window._deckStartDuel([...hand]) })
// Quick Play — deal a random even-sized hand from the CURRENT filter (so
// filtering to a league/position first scopes the duel) and start immediately.
const quickBtn = document.createElement("button"); quickBtn.type = "button"; quickBtn.className = "deck-tray-btn quick"; quickBtn.textContent = "Quick Play"; quickBtn.title = "Deal a random hand (players with stats) from the current filter and duel the CPU"
quickBtn.addEventListener("click", () => { if (window._deckStartQuick) window._deckStartQuick() })
// Higher / Lower — endless streak game from the current filter pool.
const hlBtn = document.createElement("button"); hlBtn.type = "button"; hlBtn.className = "deck-tray-btn hl"; hlBtn.textContent = "Higher / Lower"; hlBtn.title = "Endless streak: guess if the next card's stat is higher or lower"
hlBtn.addEventListener("click", () => {
if (!window._deckStartHL) return
window._deckStartHL({ pool: (window._deckCurrentPool && window._deckCurrentPool.length) ? window._deckCurrentPool : null })
})
// Gaffer's Run — the roguelike deckbuilder (flagship).
const runBtn = document.createElement("button"); runBtn.type = "button"; runBtn.className = "deck-tray-btn run"; runBtn.textContent = "Gaffer's Run"; runBtn.title = "Roguelike: climb a ladder of CPUs, draft a card after each win, lose and it's over"
runBtn.addEventListener("click", () => { if (window._deckStartRun) window._deckStartRun() })
// Daily Run — today's seeded run (same for everyone), best depth + streak tracked.
const dailyBtn = document.createElement("button"); dailyBtn.type = "button"; dailyBtn.className = "deck-tray-btn daily"; dailyBtn.textContent = "Daily"; dailyBtn.title = "Today's seeded Gaffer's Run — same opponents for everyone; your best depth + streak are saved"
dailyBtn.addEventListener("click", () => { if (window._deckStartDaily) window._deckStartDaily() })
actions.append(dailyBtn, runBtn, hlBtn, quickBtn, clearBtn, playBtn)
head.append(title, actions)
const strip = document.createElement("div"); strip.className = "deck-tray-strip"
tray.append(head, strip)
function miniCard(id) {
const r = deckById.get(id); if (!r) return null
const name = r.player_name || "?"
const code = fm.optaToPanna[r.position] || r.position || "?"
const colour = fm.pannaBadge[code] || "#5a9a7a"
const initials = name.split(" ").map(w => w[0] || "").join("").slice(0, 2).toUpperCase()
const photo = `${base_url}football/headshots/${id}.webp` // tight circle variant
const el = document.createElement("div"); el.className = "tray-card"; el.style.setProperty("--c", colour)
el.dataset.id = id
el.innerHTML = `
<div class="tray-thumb">
<img src="${photo}" alt="" draggable="false" onerror="this.style.display='none';this.nextElementSibling.style.display=''">
<span class="tray-mono" style="display:none">${statsEsc(initials)}</span>
</div>
<div class="tray-meta"><span class="tray-name">${statsEsc(name)}</span><span class="tray-panna">${r.panna == null ? "—" : r.panna.toFixed(3)}</span></div>
<button type="button" class="tray-x" title="Remove">×</button>`
const goProfile = () => { if (window._deckDidDrag) return; location.href = "player.html#name=" + encodeURIComponent(name) }
el.querySelector(".tray-thumb").addEventListener("click", goProfile)
el.querySelector(".tray-meta").addEventListener("click", goProfile)
el.querySelector(".tray-x").addEventListener("click", e => { e.stopPropagation(); hand = hand.filter(x => x !== id); save(); render() })
el.style.touchAction = "none"
el.draggable = false
el.addEventListener("dragstart", e => e.preventDefault())
el.addEventListener("pointerdown", e => {
if (e.pointerType === "mouse" && e.button !== 0) return
if (e.target.closest(".tray-x")) return
beginDrag(e, id, name, "reorder", el, el)
})
return el
}
function render() {
title.textContent = hand.length ? `Your Hand · ${hand.length}${hand.length >= MAX ? " (max)" : ""}` : "Your Hand"
clearBtn.disabled = !hand.length
playBtn.disabled = hand.length < 2
playBtn.title = hand.length < 2 ? "Add 2+ cards to duel" : "Duel your hand as Top Trumps"
strip.replaceChildren()
if (!hand.length) {
const hint = document.createElement("span"); hint.className = "deck-tray-hint"
hint.textContent = "Drag cards here (or tap “+ Hand”) to build your deck"
strip.appendChild(hint); return
}
for (const id of hand) { const m = miniCard(id); if (m) strip.appendChild(m) }
}
window._deckAddToHand = (id) => {
if (!deckById.has(id) || hand.includes(id) || hand.length >= MAX) return false
hand.push(id); save(); render(); return true
}
clearBtn.addEventListener("click", () => { hand = []; save(); render() })
// ── Pointer-drag controller (mouse + touch + pen) ───────────
// A gallery card drags with kind="add"; a hand mini-card drags with
// kind="reorder". A floating clone of the card follows the pointer, and the
// hand cards slide aside to open a gap at the drop slot. Drop inside the tray
// adds (or reorders at that slot); a reorder dropped OUTSIDE the tray removes
// the card. window._deckDidDrag (set once past the 6px threshold) tells the
// gallery <a>'s click handler to suppress navigation after a drag.
const overTray = (x, y) => { const el = document.elementFromPoint(x, y); return !!(el && el.closest(".deck-tray")) }
// The "live" cards are those NOT currently being dragged; the gap index is
// computed in their coordinate space, so it's already the post-removal slot.
const liveCards = () => [...strip.querySelectorAll(".tray-card:not(.dragging)")]
const gapIndex = clientX => {
const items = liveCards()
for (let i = 0; i < items.length; i++) { const b = items[i].getBoundingClientRect(); if (clientX < b.left + b.width / 2) return i }
return items.length
}
let gap = null
function showGap(idx) {
if (!gap) { gap = document.createElement("div"); gap.className = "tray-gap" }
const ref = liveCards()[idx] || null
if (gap.nextElementSibling !== ref || gap.parentNode !== strip) strip.insertBefore(gap, ref)
requestAnimationFrame(() => gap && gap.classList.add("open"))
}
const hideGap = () => { if (gap) { gap.remove(); gap = null } }
let drag = null
function makeGhost(d) {
if (d.cloneEl) {
const c = d.cloneEl.cloneNode(true)
c.className = (d.kind === "add" ? "ttc" : "tray-card") + " deck-ghost-card"
c.style.setProperty("--c", d.colour)
c.querySelectorAll(".ttc-add, .tray-x").forEach(b => b.remove())
if (c.removeAttribute) c.removeAttribute("href")
return c
}
const g = document.createElement("div"); g.className = "deck-ghost"; g.style.setProperty("--c", d.colour); g.textContent = d.name
return g
}
function onMove(e) {
if (!drag) return
if (!drag.moved && Math.hypot(e.clientX - drag.x0, e.clientY - drag.y0) < 6) return
if (!drag.moved) {
drag.moved = true; window._deckDidDrag = true
if (drag.sourceEl) drag.sourceEl.classList.add("dragging") // dim the source first so the clone matches the post-removal layout
drag.ghost = makeGhost(drag); document.body.appendChild(drag.ghost)
document.body.style.userSelect = "none"
}
drag.ghost.style.left = e.clientX + "px"; drag.ghost.style.top = e.clientY + "px"
const inTray = overTray(e.clientX, e.clientY)
tray.classList.toggle("drop-hot", inTray)
if (inTray && hand.length) showGap(gapIndex(e.clientX)); else hideGap()
e.preventDefault()
}
function onUp(e) {
document.removeEventListener("pointermove", onMove)
tray.classList.remove("drop-hot"); document.body.style.userSelect = ""
const inTray = drag && drag.moved && overTray(e.clientX, e.clientY)
const slot = inTray ? gapIndex(e.clientX) : -1
hideGap()
if (drag) {
if (drag.sourceEl) drag.sourceEl.classList.remove("dragging")
if (drag.ghost) drag.ghost.remove()
if (drag.moved) {
if (drag.kind === "add" && inTray) {
if (window._deckAddToHand(drag.id)) { // appended; move to the requested slot
const cur = hand.indexOf(drag.id), to = Math.min(slot, hand.length - 1)
if (cur !== to && to >= 0) { hand.splice(cur, 1); hand.splice(to, 0, drag.id); save(); render() }
}
} else if (drag.kind === "reorder") {
const from = hand.indexOf(drag.id)
if (from >= 0 && inTray) { hand.splice(from, 1); hand.splice(Math.max(0, Math.min(hand.length, slot)), 0, drag.id); save(); render() }
else if (from >= 0 && !inTray) { hand = hand.filter(x => x !== drag.id); save(); render() } // dragged out → remove
}
}
}
setTimeout(() => { window._deckDidDrag = false }, 0) // survive the click that fires right after pointerup
drag = null
}
function beginDrag(e, id, name, kind, sourceEl, cloneEl) {
const rr = deckById.get(id), code = rr ? (fm.optaToPanna[rr.position] || rr.position) : null
drag = { id, name, kind, x0: e.clientX, y0: e.clientY, moved: false, colour: fm.pannaBadge[code] || "#5a9a7a", sourceEl: sourceEl || null, cloneEl: cloneEl || null }
window._deckDidDrag = false
document.addEventListener("pointermove", onMove, { passive: false })
document.addEventListener("pointerup", onUp, { once: true })
document.addEventListener("pointercancel", onUp, { once: true })
}
window._deckBeginCardDrag = (e, id, name, cardEl) => beginDrag(e, id, name, "add", null, cardEl)
render()
document.body.appendChild(tray) // body-level so position:fixed can't be trapped by an ancestor transform
return html``
}