Polish battle HUD notices
This commit is contained in:
parent
7814ed3951
commit
da8ae49a72
12
agent.md
12
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`.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 도메인
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue