feat: emphasize active skill windows by score impact
Publish GitHub Pages output / publish (push) Successful in 50s

This commit is contained in:
2026-09-10 11:00:16 +09:00
parent 089bb69ffd
commit 3f21819b39
5 changed files with 150 additions and 7 deletions
+15 -6
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import { ANALYSIS_TRANSFER_KEY, parseAnalysisTransfer, transferredAnalysisKey } from '$lib/models/analysis-transfer.ts';
import { onMount, tick } from 'svelte';
import { activeWindowKey, calculateActiveImpacts } from '$lib/engine/active-impact.ts';
import LeaderSongRanking from '$lib/components/LeaderSongRanking.svelte';
import { analytics } from '$lib/analytics.ts';
import AnalysisHistory from '$lib/components/AnalysisHistory.svelte';
@@ -133,6 +134,13 @@
excludedActiveWindows = [];
}
$: displayedResult = simulatedResult ?? result;
$: activeImpacts = result && simulationContext && mode === 'analysis'
? calculateActiveImpacts(
result.order.map((id) => simulationContext!.cards.find((card) => card.id === id)!),
result.cooldownReductions, simulationContext.chart,
{ ...simulationContext.options, excludedActiveWindows }, result.activeWindows
)
: new Map<string, { scoreChange: number; strength: number }>();
function isExcluded(window: SkillWindow): boolean {
return excludedActiveWindows.some((excluded) => excluded.cardId === window.cardId && excluded.startSeconds === window.startSeconds);
@@ -1197,7 +1205,7 @@
</ol>
</details>
{/if}
<p class="simulation-guide" aria-live="polite">카드별 색칠된 구간을 클릭하면 해당 액티브만 미발동으로 계산합니다. 다시 클릭하면 복원됩니다. 배치와 쿨타임은 유지됩니다.
<p class="simulation-guide" aria-live="polite">카드별 색칠된 구간을 클릭하면 해당 액티브만 미발동으로 계산합니다. 다시 클릭하면 복원됩니다. 배치와 쿨타임은 유지됩니다. 색이 진하고 테두리가 굵을수록 현재 설정에서 토글할 때 최고점 변화가 큽니다. 빗금은 미발동 상태이며, 해당 구간은 복원 시 영향력을 표시합니다.
{#if excludedActiveWindows.length}
<strong>{excludedActiveWindows.length}회 미발동 적용 중</strong>
<button type="button" on:click={resetActiveWindows}>모두 복원</button>
@@ -1213,14 +1221,15 @@
<strong class="lane-label"><small style={`--member-color: ${memberColor(cardId)};`}>{slot + 1}</small>{resultCardName(cardId)}</strong>
<div class="timeline-track skill-track" style={timelineTrackStyle(timelineWidth, timelineDuration)}>
{#each result.activeWindows.filter((window) => window.cardId === cardId) as window}
{@const impact = activeImpacts.get(activeWindowKey(window))}
<button
type="button"
class="skill-window active-window"
class:excluded={excludedActiveWindows.some((excluded) => excluded.cardId === window.cardId && excluded.startSeconds === window.startSeconds)}
disabled={mode !== 'analysis' || !simulationContext || running}
style={coloredWindowStyle(window, timelineDuration)}
title={`${resultCardName(cardId)} 액티브 ${formatSeconds(window.startSeconds)}~${formatSeconds(window.endSeconds)} · ${window.scoreUpPercent}% · 클릭하여 미발동/복원`}
aria-label={`${resultCardName(cardId)} 액티브 ${formatSeconds(window.startSeconds)}부터 ${formatSeconds(window.endSeconds)}까지 미발동 처리`}
style={coloredWindowStyle(window, timelineDuration) + (impact ? ` --impact-opacity: ${0.25 + 0.75 * Math.sqrt(impact.strength)}; --impact-border: ${impact.strength > 0 ? 1 + 2 * impact.strength : 0}px;` : '')}
title={`${resultCardName(cardId)} 액티브 ${formatSeconds(window.startSeconds)}~${formatSeconds(window.endSeconds)} · ${window.scoreUpPercent}% · 클릭하여 미발동/복원${impact ? ` · 토글 시 최고점 ${signedScoreChange(impact.scoreChange)}` : ''}`}
aria-label={`${resultCardName(cardId)} 액티브 ${formatSeconds(window.startSeconds)}부터 ${formatSeconds(window.endSeconds)}까지 미발동 처리${impact ? ` · 토글 시 최고점 ${signedScoreChange(impact.scoreChange)}` : ''}`}
aria-pressed={excludedActiveWindows.some((excluded) => excluded.cardId === window.cardId && excluded.startSeconds === window.startSeconds)}
on:click={() => toggleActiveWindow(window)}
></button>
@@ -1405,9 +1414,9 @@
.timeline-track::before { content: ''; position: absolute; inset: 0; background: linear-gradient(90deg, rgb(255 255 255 / 9%) 1px, transparent 1px); background-size: var(--time-grid-step, 10%) 100%; pointer-events: none; }
.skill-track { height: 36px; }
.skill-window { position: absolute; min-width: 2px; border-radius: 3px; background: var(--member-color); }
.active-window { top: 8px; height: 20px; opacity: .72; padding: 0; border: 0; cursor: pointer; }
.active-window { top: 8px; height: 20px; opacity: var(--impact-opacity, .72); padding: 0; border: 0; box-shadow: inset 0 0 0 var(--impact-border, 0px) rgb(255 255 255 / 85%); cursor: pointer; }
.active-window:hover:not(:disabled), .active-window:focus-visible { opacity: 1; outline: 2px solid #fff; outline-offset: 2px; }
.active-window.excluded { opacity: .28; border: 1px dashed var(--member-color); background: repeating-linear-gradient(135deg, transparent 0 3px, var(--member-color) 3px 4px); }
.active-window.excluded { opacity: var(--impact-opacity, .28); border: 1px dashed var(--member-color); background: repeating-linear-gradient(135deg, transparent 0 3px, var(--member-color) 3px 4px); }
.active-window:disabled { cursor: default; }
.timeline-score-summary { display: flex; flex-wrap: wrap; gap: 16px 32px; padding: 16px; margin-bottom: 12px; border: 1px solid #454052; border-radius: 10px; background: #22202a; }
.timeline-score-summary > div { display: grid; gap: 6px; }