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
+1 -1
View File
@@ -12,7 +12,7 @@ updatedAt: 2026-09-08T12:00:00+09:00
### 액티브 미발동 시뮬레이션 ### 액티브 미발동 시뮬레이션
`NEW` 배치 분석의 카드별 액티브 구간을 클릭해 해당 발동을 제외한 예상 점수·이론상 고점과 그래프를 확인할 수 있습니다. 다시 클릭하거나 모두 복원하면 원래 결과로 돌아옵니다. `NEW` 배치 분석의 카드별 액티브 구간을 클릭해 해당 발동을 제외한 예상 점수·이론상 고점과 그래프를 확인할 수 있습니다. 다시 클릭하거나 모두 복원하면 원래 결과로 돌아옵니다. 그래프 근처에서 현재 최고점·원래 대비 변화와 각 토글 직전·직후의 점수 차이를 확인할 수 있습니다.
### 연결 카드 최적화의 최고점 우선 적용 ### 연결 카드 최적화의 최고점 우선 적용
+48 -1
View File
@@ -118,8 +118,10 @@
let simulationBase: ExactOptimizationResult | null = null; let simulationBase: ExactOptimizationResult | null = null;
let simulatedResult: ExactOptimizationResult | null = null; let simulatedResult: ExactOptimizationResult | null = null;
let excludedActiveWindows: SkillWindow[] = []; let excludedActiveWindows: SkillWindow[] = [];
let toggleScoreChanges: { window: SkillWindow | null; excluded: boolean; before: number; after: number }[] = [];
$: if (result !== simulationBase) { $: if (result !== simulationBase) {
simulationBase = result; simulationBase = result;
toggleScoreChanges = [];
simulatedResult = null; simulatedResult = null;
excludedActiveWindows = []; excludedActiveWindows = [];
} }
@@ -131,11 +133,14 @@
function toggleActiveWindow(window: SkillWindow): void { function toggleActiveWindow(window: SkillWindow): void {
if (!result || !simulationContext) return; 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.filter((excluded) => excluded.cardId !== window.cardId || excluded.startSeconds !== window.startSeconds)
: [...excludedActiveWindows, window]; : [...excludedActiveWindows, window];
if (!excludedActiveWindows.length) { if (!excludedActiveWindows.length) {
simulatedResult = null; simulatedResult = null;
toggleScoreChanges = [{ window, excluded, before, after: result.theoreticalMaxScore }, ...toggleScoreChanges];
return; return;
} }
const { cards, chart, options } = simulationContext; const { cards, chart, options } = simulationContext;
@@ -145,13 +150,21 @@
...calculateExactScore(order, result.cooldownReductions, chart, { ...options, excludedActiveWindows }), ...calculateExactScore(order, result.cooldownReductions, chart, { ...options, excludedActiveWindows }),
...buildExactSkillWindows(order, result.cooldownReductions, chart, excludedActiveWindows) ...buildExactSkillWindows(order, result.cooldownReductions, chart, excludedActiveWindows)
}; };
toggleScoreChanges = [{ window, excluded, before, after: simulatedResult.theoreticalMaxScore }, ...toggleScoreChanges];
} }
function resetActiveWindows(): void { function resetActiveWindows(): void {
if (result && simulatedResult) {
toggleScoreChanges = [{ window: null, excluded: false, before: simulatedResult.theoreticalMaxScore, after: result.theoreticalMaxScore }, ...toggleScoreChanges];
}
excludedActiveWindows = []; excludedActiveWindows = [];
simulatedResult = null; simulatedResult = null;
} }
function signedScoreChange(value: number): string {
return (value > 0 ? '+' : value < 0 ? '−' : '') + Math.abs(value).toLocaleString('ko-KR') + '점';
}
let ratingResultKey = ''; let ratingResultKey = '';
let resultCapture: AnalysisCapture | null = null; let resultCapture: AnalysisCapture | null = null;
let pendingCapture: 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} {#each result.order as cardId}<span><i style={`background: ${memberColor(cardId)};`}></i>{resultCardName(cardId)}</span>{/each}
</div> </div>
</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 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">카드별 색칠된 구간을 클릭하면 해당 액티브만 미발동으로 계산합니다. 다시 클릭하면 복원됩니다. 배치와 쿨타임은 유지됩니다. <p class="simulation-guide" aria-live="polite">카드별 색칠된 구간을 클릭하면 해당 액티브만 미발동으로 계산합니다. 다시 클릭하면 복원됩니다. 배치와 쿨타임은 유지됩니다.
{#if excludedActiveWindows.length} {#if excludedActiveWindows.length}
<strong>{excludedActiveWindows.length}회 미발동 적용 중</strong> <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: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: .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; } .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 { color: #adb5c2; font-size: 13px; line-height: 1.7; }
.simulation-guide strong, .simulation-guide span { display: block; } .simulation-guide strong, .simulation-guide span { display: block; }
.simulation-guide button { margin: 4px 0; } .simulation-guide button { margin: 4px 0; }