Polish battle HUD notices

This commit is contained in:
Horoli 2026-06-02 01:22:51 +09:00
parent 7814ed3951
commit da8ae49a72
8 changed files with 235 additions and 19 deletions

View File

@ -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`.

View File

@ -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.

View File

@ -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 도메인

View File

@ -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;

View File

@ -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 {

View File

@ -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 {

View File

@ -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");

View File

@ -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;
}
}