diff --git a/agent.md b/agent.md index 5bbbec5..5273044 100644 --- a/agent.md +++ b/agent.md @@ -1,3 +1,15 @@ +# Update: Restrained Team Card Styling + +- Team score cards keep the existing label, elite/normal count, click behavior, and focused-team state. +- Team color is limited to a compact team marker and muted inner divider instead of a full-height side stripe or filled card background. +- Hover and focus states are quieter: no raised hover motion, reduced brightness, and a subtle inset focus treatment instead of an outer glow. + +# Update: Battle Notice Rolling Text + +- `battleDeathNotice.js` now renders notice text inside a message span, measures the rendered content width on the next animation frame, and switches to a rolling track only when the text exceeds the notice box content width. +- Overflowing battle notices duplicate the message in an `aria-hidden` track and use `aria-label` on the status node so assistive text is not repeated. +- Rolling speed, gap, and duration clamps are tuned by `UI.BATTLE_NOTICE_ROLL_*` constants; non-overflowing notices keep the normal centered display. + # Update: Special Projectile Trail - Special projectile movement can leave short-lived visual afterimages controlled by `SPECIAL_EFFECT.PROJECTILE.TRAIL`. diff --git a/context/match-ui.md b/context/match-ui.md index d0ac8cf..f89aacc 100644 --- a/context/match-ui.md +++ b/context/match-ui.md @@ -1,3 +1,15 @@ +# Update: Restrained Team Card Styling + +- Team score cards preserve their existing content and selection behavior while using a neutral dark card surface. +- Per-team color now reads mainly through a compact marker and a small divider segment, making the HUD feel less saturated. +- Focus and hover feedback uses subtle inset emphasis without the previous raised motion or strong glow. + +# Update: Battle Notice Rolling Text + +- `battleDeathNotice.js` measures the rendered message against the visible notice content width and only enables rolling text when the message would overflow. +- Rolling notices render an internal duplicated track for continuous movement while exposing the single message through the status node's `aria-label`. +- `UI.BATTLE_NOTICE_ROLL_GAP_PX`, `BATTLE_NOTICE_ROLL_SPEED_PX_PER_SECOND`, and min/max duration constants tune the marquee behavior. + # Update: Elite Compression And Population Display - Below `FIGHTER.ELITE.RANDOMIZED_COMPRESSION.MIN_TEAM_SIZE`, `matchSetup.js` converts each complete `FIGHTER.ELITE.STACK_SIZE = 100` block into one elite plan and keeps the remainder as individual normal plans. With the current threshold of `100`, complete blocks are randomized. diff --git a/src/constants.js b/src/constants.js index e20fc27..599414a 100644 --- a/src/constants.js +++ b/src/constants.js @@ -365,6 +365,10 @@ export const UI = { BATTLE_NOTICE_DELAY_MS: 5000, BATTLE_NOTICE_VISIBLE_MS: 2000, BATTLE_NOTICE_INTERVAL_MS: 10000, + BATTLE_NOTICE_ROLL_GAP_PX: 48, + BATTLE_NOTICE_ROLL_SPEED_PX_PER_SECOND: 58, + BATTLE_NOTICE_ROLL_MIN_DURATION_MS: 7000, + BATTLE_NOTICE_ROLL_MAX_DURATION_MS: 18000, }; // 9. TEAM 도메인 diff --git a/src/styles/animations.css b/src/styles/animations.css index d916d07..ce15199 100644 --- a/src/styles/animations.css +++ b/src/styles/animations.css @@ -46,6 +46,15 @@ } } +@keyframes battle-notice-roll { + from { + transform: translateX(0); + } + to { + transform: translateX(calc(-1 * (var(--battle-notice-message-width) + var(--battle-notice-roll-gap)))); + } +} + @keyframes kill-log-entry { from { opacity: 0; diff --git a/src/styles/game-ui.css b/src/styles/game-ui.css index 7fc9da6..568d5b5 100644 --- a/src/styles/game-ui.css +++ b/src/styles/game-ui.css @@ -13,9 +13,9 @@ scrollbar-width: thin; scrollbar-color: rgb(238 185 73 / 0.3) transparent; padding: 8px; - border: 1px solid rgb(238 185 73 / 0.18); + border: 1px solid rgb(238 185 73 / 0.12); border-radius: 8px; - background: rgb(4 6 4 / 0.5); + background: rgb(4 6 4 / 0.46); opacity: 0; pointer-events: none; transform: translateY(-18px); @@ -56,33 +56,54 @@ } .team-score { + position: relative; display: grid; grid-template-rows: 1fr 1px auto; gap: 6px; width: 100%; min-height: 72px; overflow: hidden; + border: 1px solid rgb(255 244 209 / 0.08); border-radius: 6px; padding: 8px 7px; + background: rgb(8 10 7 / 0.58); color: #fff; font-size: 0.8rem; font-weight: 900; text-align: left; - text-shadow: 1px 1px 2px #000; + text-shadow: 0 1px 1px rgb(0 0 0 / 0.78); transition: - filter 160ms ease, - transform 160ms ease; + background-color 160ms ease, + border-color 160ms ease, + filter 160ms ease; +} + +.team-score::before { + content: ""; + position: absolute; + top: 9px; + left: 8px; + width: 10px; + height: 7px; + border-radius: 1px; + background: var(--team-color); + box-shadow: + 0 0 0 1px rgb(0 0 0 / 0.72), + inset 0 -1px 0 rgb(0 0 0 / 0.28); + opacity: 0.9; + transform: skewX(-14deg); } .team-score:hover { - filter: brightness(1.16); - transform: translateY(-1px); + border-color: rgb(255 244 209 / 0.16); + background: rgb(12 14 10 / 0.68); + filter: brightness(1.06); } .team-score.is-focused { box-shadow: - inset 0 0 0 2px rgb(255 244 209 / 0.92), - 0 0 18px rgb(227 178 79 / 0.26); + inset 0 0 0 1px rgb(255 244 209 / 0.72), + inset 0 0 0 999px rgb(255 244 209 / 0.035); } .team-score:disabled { @@ -91,7 +112,7 @@ } .team-score:disabled:hover { - transform: none; + background: rgb(8 10 7 / 0.58); } .team-score-name { @@ -99,6 +120,7 @@ -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; + padding-left: 16px; text-overflow: ellipsis; white-space: normal; line-height: 1.15; @@ -106,15 +128,18 @@ .team-score-rule { width: 100%; - background: var(--team-color); - opacity: 0.9; + background: linear-gradient( + 90deg, + var(--team-color) 0 34%, + rgb(255 244 209 / 0.1) 34% 100% + ); + opacity: 0.68; } .team-score-count { justify-self: end; - color: #fff2c8; + color: #ead9b3; font-size: 0.68rem; - letter-spacing: -0.02em; white-space: nowrap; } @@ -140,13 +165,51 @@ text-align: center; text-shadow: 1px 1px 2px #000; white-space: nowrap; + overflow: hidden; opacity: 0; pointer-events: none; transform: translate(-50%, -10px); transition: opacity 260ms ease, transform 260ms ease; - backdrop-filter: blur(10px); + backdrop-filter: blur(7px); +} + +.battle-notice-message { + display: block; + max-width: 100%; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.battle-notice.is-rolling { + justify-content: flex-start; +} + +.battle-notice-track { + display: flex; + flex: 0 0 auto; + gap: var(--battle-notice-roll-gap, 48px); + width: max-content; + max-width: none; + animation: battle-notice-roll var(--battle-notice-roll-duration, 12s) linear infinite; + will-change: transform; +} + +.battle-notice.is-rolling .battle-notice-message { + flex: 0 0 auto; + max-width: none; + overflow: visible; + text-overflow: clip; +} + +@media (prefers-reduced-motion: reduce) { + .battle-notice-track { + animation: none; + transform: none; + } } #app.match-live .battle-notice.is-visible { diff --git a/src/styles/mobile.css b/src/styles/mobile.css index d060e9d..4b5e4e8 100644 --- a/src/styles/mobile.css +++ b/src/styles/mobile.css @@ -204,12 +204,25 @@ align-content: center; } + .team-score::before { + top: 6px; + left: 6px; + width: 8px; + height: 6px; + } + + .team-score-name { + padding-left: 13px; + } + .team-score-count { font-size: 0.64rem; } .team-score.is-focused { - box-shadow: inset 0 0 0 2px rgb(255 244 209 / 0.92); + box-shadow: + inset 0 0 0 1px rgb(255 244 209 / 0.72), + inset 0 0 0 999px rgb(255 244 209 / 0.035); } .battle-notice { diff --git a/src/ui/arenaScoreboard.js b/src/ui/arenaScoreboard.js index 8c39a97..664e018 100644 --- a/src/ui/arenaScoreboard.js +++ b/src/ui/arenaScoreboard.js @@ -33,8 +33,8 @@ export function updateScoreboard( teamEl.disabled = livingFighters.length === 0; teamEl.setAttribute("aria-label", `${team.label} 생존 캐릭터 무작위 시점 고정`); teamEl.style.setProperty("--team-color", team.color); - teamEl.style.backgroundColor = `${team.color}33`; - teamEl.style.borderLeft = `4px solid ${team.color}`; + teamEl.style.removeProperty("background-color"); + teamEl.style.removeProperty("border-left"); teamEl.classList.toggle("is-focused", selectedFighterTeamId === team.id); const labelEl = teamEl.querySelector(".team-score-name"); diff --git a/src/ui/battleDeathNotice.js b/src/ui/battleDeathNotice.js index 2d9da55..4ceaffb 100644 --- a/src/ui/battleDeathNotice.js +++ b/src/ui/battleDeathNotice.js @@ -31,6 +31,9 @@ const SYSTEM_TIP_TEMPLATES = [ "엘리트 전투: 처치 보너스는 비활성화되어 전투 중 체력 회복이나 성장 효과가 없습니다.", ]; +const NOTICE_MESSAGE_CLASS = "battle-notice-message"; +const NOTICE_TRACK_CLASS = "battle-notice-track"; + export function createDeathCounts() { return SPECIES_KEYS.reduce((counts, species) => { counts[species] = 0; @@ -90,9 +93,21 @@ export function showBattleDeathNotice(noticeNode, message) { return; } - noticeNode.textContent = message; + cancelBattleNoticeMeasure(noticeNode); + + const text = String(message ?? ""); + const messageNode = createBattleNoticeMessage(text); + + noticeNode.classList.remove("is-rolling"); + noticeNode.removeAttribute("aria-label"); + clearBattleNoticeRollStyles(noticeNode); + noticeNode.replaceChildren(messageNode); noticeNode.classList.add("is-visible"); noticeNode.setAttribute("aria-hidden", "false"); + noticeNode.battleNoticeMeasureFrame = requestAnimationFrame(() => { + noticeNode.battleNoticeMeasureFrame = null; + applyBattleNoticeRollingIfNeeded(noticeNode, text, messageNode); + }); } export function clearBattleNotice(noticeNode) { @@ -100,6 +115,94 @@ export function clearBattleNotice(noticeNode) { return; } + cancelBattleNoticeMeasure(noticeNode); noticeNode.classList.remove("is-visible"); noticeNode.setAttribute("aria-hidden", "true"); } + +function createBattleNoticeMessage(message) { + const messageNode = document.createElement("span"); + + messageNode.className = NOTICE_MESSAGE_CLASS; + messageNode.textContent = message; + + return messageNode; +} + +function applyBattleNoticeRollingIfNeeded(noticeNode, message, messageNode) { + if ( + !noticeNode.isConnected || + !noticeNode.classList.contains("is-visible") || + !messageNode.isConnected + ) { + return; + } + + const availableWidth = resolveBattleNoticeContentWidth(noticeNode); + const messageWidth = Math.ceil(messageNode.scrollWidth); + + if (availableWidth <= 0 || messageWidth <= availableWidth + 1) { + return; + } + + const gap = resolveBattleNoticeRollGap(); + const durationMs = resolveBattleNoticeRollDuration( + messageWidth + gap + availableWidth, + ); + const track = document.createElement("span"); + + track.className = NOTICE_TRACK_CLASS; + track.setAttribute("aria-hidden", "true"); + track.append(createBattleNoticeMessage(message), createBattleNoticeMessage(message)); + + noticeNode.classList.add("is-rolling"); + noticeNode.setAttribute("aria-label", message); + noticeNode.style.setProperty("--battle-notice-message-width", `${messageWidth}px`); + noticeNode.style.setProperty("--battle-notice-roll-gap", `${gap}px`); + noticeNode.style.setProperty("--battle-notice-roll-duration", `${durationMs}ms`); + noticeNode.replaceChildren(track); +} + +function resolveBattleNoticeContentWidth(noticeNode) { + const style = getComputedStyle(noticeNode); + const paddingX = + (Number.parseFloat(style.paddingLeft) || 0) + + (Number.parseFloat(style.paddingRight) || 0); + + return Math.max(0, noticeNode.clientWidth - paddingX); +} + +function resolveBattleNoticeRollGap() { + return Math.max(0, Math.round(Number(UI.BATTLE_NOTICE_ROLL_GAP_PX) || 48)); +} + +function resolveBattleNoticeRollDuration(distancePx) { + const speed = Math.max( + 1, + Number(UI.BATTLE_NOTICE_ROLL_SPEED_PX_PER_SECOND) || 58, + ); + const duration = Math.round((Math.max(1, distancePx) / speed) * 1000); + const minimum = Math.max( + 1, + Number(UI.BATTLE_NOTICE_ROLL_MIN_DURATION_MS) || 7000, + ); + const maximum = Math.max( + minimum, + Number(UI.BATTLE_NOTICE_ROLL_MAX_DURATION_MS) || 18000, + ); + + return Math.min(maximum, Math.max(minimum, duration)); +} + +function clearBattleNoticeRollStyles(noticeNode) { + noticeNode.style.removeProperty("--battle-notice-message-width"); + noticeNode.style.removeProperty("--battle-notice-roll-gap"); + noticeNode.style.removeProperty("--battle-notice-roll-duration"); +} + +function cancelBattleNoticeMeasure(noticeNode) { + if (noticeNode.battleNoticeMeasureFrame) { + cancelAnimationFrame(noticeNode.battleNoticeMeasureFrame); + noticeNode.battleNoticeMeasureFrame = null; + } +}