feat: show timeline maximum score and per-toggle changes
Publish GitHub Pages output / publish (push) Successful in 43s

This commit is contained in:
2026-09-08 13:10:28 +09:00
parent 0505ebd1fb
commit be94ee4be6
2 changed files with 49 additions and 2 deletions
+48 -1
View File
@@ -118,8 +118,10 @@
let simulationBase: ExactOptimizationResult | null = null;
let simulatedResult: ExactOptimizationResult | null = null;
let excludedActiveWindows: SkillWindow[] = [];
let toggleScoreChanges: { window: SkillWindow | null; excluded: boolean; before: number; after: number }[] = [];
$: if (result !== simulationBase) {
simulationBase = result;
toggleScoreChanges = [];
simulatedResult = null;
excludedActiveWindows = [];
}
@@ -131,11 +133,14 @@
function toggleActiveWindow(window: SkillWindow): void {
if (!result || !simulationContext) return;
excludedActiveWindows = isExcluded(window)
const before = (simulatedResult ?? result).theoreticalMaxScore;
const excluded = !isExcluded(window);
excludedActiveWindows = !excluded
? excludedActiveWindows.filter((excluded) => excluded.cardId !== window.cardId || excluded.startSeconds !== window.startSeconds)
: [...excludedActiveWindows, window];
if (!excludedActiveWindows.length) {
simulatedResult = null;
toggleScoreChanges = [{ window, excluded, before, after: result.theoreticalMaxScore }, ...toggleScoreChanges];
return;
}
const { cards, chart, options } = simulationContext;
@@ -145,13 +150,21 @@
...calculateExactScore(order, result.cooldownReductions, chart, { ...options, excludedActiveWindows }),
...buildExactSkillWindows(order, result.cooldownReductions, chart, excludedActiveWindows)
};
toggleScoreChanges = [{ window, excluded, before, after: simulatedResult.theoreticalMaxScore }, ...toggleScoreChanges];
}
function resetActiveWindows(): void {
if (result && simulatedResult) {
toggleScoreChanges = [{ window: null, excluded: false, before: simulatedResult.theoreticalMaxScore, after: result.theoreticalMaxScore }, ...toggleScoreChanges];
}
excludedActiveWindows = [];
simulatedResult = null;
}
function signedScoreChange(value: number): string {
return (value > 0 ? '+' : value < 0 ? '−' : '') + Math.abs(value).toLocaleString('ko-KR') + '점';
}
let ratingResultKey = '';
let resultCapture: AnalysisCapture | null = null;
let pendingCapture: AnalysisCapture | null = null;
@@ -1135,7 +1148,27 @@
{#each result.order as cardId}<span><i style={`background: ${memberColor(cardId)};`}></i>{resultCardName(cardId)}</span>{/each}
</div>
</div>
<div class="timeline-score-summary" aria-live="polite" aria-atomic="true">
<div><span>현재 설정 최고점</span><strong>{displayedResult!.theoreticalMaxScore.toLocaleString('ko-KR')}점</strong></div>
<div><span>원래 최고점</span><b>{result.theoreticalMaxScore.toLocaleString('ko-KR')}점</b></div>
<div><span>원래 대비 변화</span><b class:score-loss={displayedResult!.theoreticalMaxScore < result.theoreticalMaxScore}>{signedScoreChange(displayedResult!.theoreticalMaxScore - result.theoreticalMaxScore)}</b></div>
</div>
{#if mode === 'analysis'}
{#if toggleScoreChanges.length}
<details class="toggle-score-changes" open>
<summary>토글별 최고점 변화 · {toggleScoreChanges.length}건</summary>
<p>최근 조작부터 표시합니다. 각 변화량은 클릭 직전 설정과 비교하며, 겹친 액티브에 따라 달라집니다.</p>
<ol aria-label="토글별 최고점 변화">
{#each toggleScoreChanges as change}
<li>
<span>{#if change.window}{resultCardName(change.window.cardId)} · {formatSeconds(change.window.startSeconds)}~{formatSeconds(change.window.endSeconds)} · {change.excluded ? '미발동' : '복원'}{:else}모두 복원{/if}</span>
<strong class:score-loss={change.after < change.before} class:score-gain={change.after > change.before}>{signedScoreChange(change.after - change.before)}</strong>
<small>{change.before.toLocaleString('ko-KR')} → {change.after.toLocaleString('ko-KR')}점</small>
</li>
{/each}
</ol>
</details>
{/if}
<p class="simulation-guide" aria-live="polite">카드별 색칠된 구간을 클릭하면 해당 액티브만 미발동으로 계산합니다. 다시 클릭하면 복원됩니다. 배치와 쿨타임은 유지됩니다.
{#if excludedActiveWindows.length}
<strong>{excludedActiveWindows.length}회 미발동 적용 중</strong>
@@ -1346,6 +1379,20 @@
.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: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; }
.timeline-score-summary span { color: #adb5c2; font-size: 12px; }
.timeline-score-summary strong { color: #f2a3c4; font-size: 22px; }
.timeline-score-summary b { color: #e1e5ec; font-size: 16px; }
.toggle-score-changes { margin: 12px 0; padding: 12px 16px; border: 1px solid #343c49; border-radius: 10px; }
.toggle-score-changes summary { cursor: pointer; color: #e1e5ec; font-size: 14px; }
.toggle-score-changes p { color: #adb5c2; font-size: 12px; line-height: 1.6; }
.toggle-score-changes ol { list-style: none; max-height: 180px; overflow-y: auto; margin: 0; padding: 0; }
.toggle-score-changes li { display: flex; flex-wrap: wrap; gap: 6px 16px; align-items: baseline; padding: 8px 0; border-top: 1px solid #343c49; font-size: 13px; }
.toggle-score-changes li > span { flex: 1 1 240px; color: #e1e5ec; }
.toggle-score-changes small { color: #adb5c2; }
.timeline-score-summary .score-loss, .toggle-score-changes .score-loss { color: #f2a3a3; }
.toggle-score-changes .score-gain { color: #82d09e; }
.simulation-guide { color: #adb5c2; font-size: 13px; line-height: 1.7; }
.simulation-guide strong, .simulation-guide span { display: block; }
.simulation-guide button { margin: 4px 0; }