From ef739e1f499115b28280fce17cd1dde052d0076c Mon Sep 17 00:00:00 2001 From: rgriffith Date: Sat, 4 Apr 2026 16:50:56 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20UI=20improvements=20=E2=80=94=20silver?= =?UTF-8?q?=20commander=20border,=20life=20panel=20border,=20hand=20drag-a?= =?UTF-8?q?nd-drop,=20card=20placement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add bright silver 3D smoky border around commander card zones - Wrap all 4 life panels in a single bordered container - Add drag-and-drop from hand zone to battlefield with ghost preview - Update card placement to center of battlefield, avoiding overlaid zones - Fix card dimensions in placement calc (110x150 -> 63x84) Co-Authored-By: Claude Opus 4.6 --- lan-client/engine.js | 18 +++++------- lan-client/style.css | 59 ++++++++++++++++++++++++++++++---------- lan-client/ui.js | 65 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 25 deletions(-) diff --git a/lan-client/engine.js b/lan-client/engine.js index 24db745..3093d28 100644 --- a/lan-client/engine.js +++ b/lan-client/engine.js @@ -3247,19 +3247,15 @@ function addCardToBattlefield(name, imageUrl, typeLine, ownerIndex, x, y, oracle } else { // Place cards by type in designated battlefield zones var zone = getCardTypeZone(typeLine || ''); - var cardW = 110, cardH = 150; + var cardW = 63, cardH = 84; // Zone regions (percentage of section area) - // Creatures: top center (30%-70% x, 5%-45% y) - // Artifacts/Enchantments: top right (70%-95% x, 5%-45% y) - // Lands: bottom left (5%-35% x, 55%-90% y) - // Instants/Sorceries: bottom center (35%-65% x, 55%-90% y) - // Other: center (30%-70% x, 25%-75% y) + // Offset from edges to avoid overlapping the overlaid zone-row/commander-row var zones = { - creature: { x1: 0.25, x2: 0.65, y1: 0.05, y2: 0.42 }, - artifact: { x1: 0.68, x2: 0.95, y1: 0.05, y2: 0.42 }, - land: { x1: 0.03, x2: 0.32, y1: 0.55, y2: 0.90 }, - spell: { x1: 0.35, x2: 0.65, y1: 0.55, y2: 0.90 }, - other: { x1: 0.25, x2: 0.70, y1: 0.25, y2: 0.75 } + creature: { x1: 0.25, x2: 0.70, y1: 0.20, y2: 0.55 }, + artifact: { x1: 0.70, x2: 0.95, y1: 0.20, y2: 0.55 }, + land: { x1: 0.03, x2: 0.35, y1: 0.55, y2: 0.85 }, + spell: { x1: 0.35, x2: 0.65, y1: 0.55, y2: 0.85 }, + other: { x1: 0.25, x2: 0.70, y1: 0.30, y2: 0.70 } }; var z = zones[zone] || zones.other; var areaW = rect.width; diff --git a/lan-client/style.css b/lan-client/style.css index c0a3378..4abbabd 100644 --- a/lan-client/style.css +++ b/lan-client/style.css @@ -642,25 +642,33 @@ input { font-family: inherit; } /* Life Panels Overlay — sits above everything */ #life-panels-overlay { position: absolute; - inset: 44px 0 0 0; + top: calc(44px + 50%); + left: 50%; + transform: translate(-50%, -50%); z-index: 50; - pointer-events: none; + pointer-events: auto; + display: grid; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; + gap: 6px; + padding: 8px; + border: 2px solid var(--felt-line-strong); + border-radius: 8px; + background: rgba(10,10,20,0.7); } .life-panel { - position: absolute; display: flex; flex-direction: column; gap: 2px; width: 130px; - pointer-events: auto; } -/* Position each panel near the center crossing */ -.lp-p1 { bottom: calc(50% + 4px); right: calc(50% + 4px); } -.lp-p2 { bottom: calc(50% + 4px); left: calc(50% + 4px); } -.lp-p3 { top: calc(50% + 4px); right: calc(50% + 4px); } -.lp-p4 { top: calc(50% + 4px); left: calc(50% + 4px); } +/* Grid positions */ +.lp-p1 { grid-area: 1 / 1; } +.lp-p2 { grid-area: 1 / 2; } +.lp-p3 { grid-area: 2 / 1; } +.lp-p4 { grid-area: 2 / 2; } .life-panel-box { border-radius: 4px; @@ -881,7 +889,12 @@ input { font-family: inherit; } .zone.commander { width: calc(var(--card-w) + 12px); height: calc(var(--card-h) + 5px); - border-width: 2px; + border: 2px solid rgba(200,200,210,0.6); + box-shadow: + 0 0 6px rgba(192,192,200,0.4), + 0 0 14px rgba(192,192,200,0.25), + 0 0 24px rgba(192,192,200,0.15), + inset 0 0 8px rgba(192,192,200,0.1); } /* Commander card image in zone */ @@ -895,22 +908,22 @@ input { font-family: inherit; } /* Player-colored zone borders */ .p1 .zone { border-color: rgba(192,57,43,0.12); } .p1 .zone:hover { border-color: rgba(192,57,43,0.3); box-shadow: 0 0 10px var(--p1-soft); } -.p1 .zone.commander { border-color: rgba(192,57,43,0.3); background: var(--p1-soft); } +.p1 .zone.commander { border-color: rgba(200,200,210,0.6); background: var(--p1-soft); } .p1 .zone-icon { color: var(--p1-text); } .p2 .zone { border-color: rgba(41,128,185,0.12); } .p2 .zone:hover { border-color: rgba(41,128,185,0.3); box-shadow: 0 0 10px var(--p2-soft); } -.p2 .zone.commander { border-color: rgba(41,128,185,0.3); background: var(--p2-soft); } +.p2 .zone.commander { border-color: rgba(200,200,210,0.6); background: var(--p2-soft); } .p2 .zone-icon { color: var(--p2-text); } .p3 .zone { border-color: rgba(39,174,96,0.12); } .p3 .zone:hover { border-color: rgba(39,174,96,0.3); box-shadow: 0 0 10px var(--p3-soft); } -.p3 .zone.commander { border-color: rgba(39,174,96,0.3); background: var(--p3-soft); } +.p3 .zone.commander { border-color: rgba(200,200,210,0.6); background: var(--p3-soft); } .p3 .zone-icon { color: var(--p3-text); } .p4 .zone { border-color: rgba(212,147,13,0.12); } .p4 .zone:hover { border-color: rgba(212,147,13,0.3); box-shadow: 0 0 10px var(--p4-soft); } -.p4 .zone.commander { border-color: rgba(212,147,13,0.3); background: var(--p4-soft); } +.p4 .zone.commander { border-color: rgba(200,200,210,0.6); background: var(--p4-soft); } .p4 .zone-icon { color: var(--p4-text); } /* Card count in zone */ @@ -950,6 +963,24 @@ input { font-family: inherit; } pointer-events: none; } +/* Hand drag ghost */ +.hand-drag-ghost { + position: fixed; + z-index: 9999; + width: 80px; + height: 112px; + pointer-events: none; + transform: translate(-50%, -50%); + opacity: 0.85; + filter: drop-shadow(0 4px 12px rgba(0,0,0,0.6)); +} +.hand-drag-ghost img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 4px; +} + /* Zone tooltip */ .zone[data-tooltip]:hover::after { content: attr(data-tooltip); diff --git a/lan-client/ui.js b/lan-client/ui.js index a931f11..f87a870 100644 --- a/lan-client/ui.js +++ b/lan-client/ui.js @@ -17,6 +17,12 @@ let undoHistoryVisible = false; let aiTurnActive = false; let floatingPreviewTimeout = null; +// Hand-to-battlefield drag state +let isDraggingFromHand = false; +let handDragPlayerIdx = null; +let handDragCardIdx = null; +let handDragGhost = null; + // ============================================================ // LOG SYSTEM (override engine stub) // ============================================================ @@ -246,7 +252,9 @@ function renderHandZone(playerIdx) { if (imgSrc) { thumbsHtml += '
' + @@ -521,6 +529,63 @@ document.addEventListener('mouseup', e => { } }); +// ============================================================ +// HAND-TO-BATTLEFIELD DRAG & DROP +// ============================================================ +function startHandDrag(e, playerIdx, handIndex, imgSrc) { + // Only left mouse button + if (e.button !== 0) return; + e.preventDefault(); + e.stopPropagation(); + + isDraggingFromHand = true; + handDragPlayerIdx = playerIdx; + handDragCardIdx = handIndex; + hideFloatingCardPreview(); + + // Create ghost element + handDragGhost = document.createElement('div'); + handDragGhost.className = 'hand-drag-ghost'; + handDragGhost.innerHTML = ''; + handDragGhost.style.left = e.clientX + 'px'; + handDragGhost.style.top = e.clientY + 'px'; + document.body.appendChild(handDragGhost); +} + +document.addEventListener('mousemove', function(e) { + if (!isDraggingFromHand || !handDragGhost) return; + handDragGhost.style.left = e.clientX + 'px'; + handDragGhost.style.top = e.clientY + 'px'; +}); + +document.addEventListener('mouseup', function(e) { + if (!isDraggingFromHand) return; + + // Clean up ghost + if (handDragGhost) { + handDragGhost.remove(); + handDragGhost = null; + } + + // Check if dropped over a battle-zone + const pNum = handDragPlayerIdx + 1; + const battleZone = document.getElementById('battle-zone-' + pNum); + if (battleZone) { + const rect = battleZone.getBoundingClientRect(); + if (e.clientX >= rect.left && e.clientX <= rect.right && + e.clientY >= rect.top && e.clientY <= rect.bottom) { + // Play the card + if (typeof playCardFromHand === 'function') { + playCardFromHand(handDragPlayerIdx, handDragCardIdx); + } + } + } + + isDraggingFromHand = false; + handDragPlayerIdx = null; + handDragCardIdx = null; +}); + // ============================================================ // PHASE TRACKER // ============================================================