feat: show timeline maximum score and per-toggle changes
Publish GitHub Pages output / publish (push) Successful in 43s
Publish GitHub Pages output / publish (push) Successful in 43s
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@ updatedAt: 2026-09-08T12:00:00+09:00
|
|||||||
|
|
||||||
### 액티브 미발동 시뮬레이션
|
### 액티브 미발동 시뮬레이션
|
||||||
|
|
||||||
`NEW` 배치 분석의 카드별 액티브 구간을 클릭해 해당 발동을 제외한 예상 점수·이론상 고점과 그래프를 확인할 수 있습니다. 다시 클릭하거나 모두 복원하면 원래 결과로 돌아옵니다.
|
`NEW` 배치 분석의 카드별 액티브 구간을 클릭해 해당 발동을 제외한 예상 점수·이론상 고점과 그래프를 확인할 수 있습니다. 다시 클릭하거나 모두 복원하면 원래 결과로 돌아옵니다. 그래프 근처에서 현재 최고점·원래 대비 변화와 각 토글 직전·직후의 점수 차이를 확인할 수 있습니다.
|
||||||
|
|
||||||
### 연결 카드 최적화의 최고점 우선 적용
|
### 연결 카드 최적화의 최고점 우선 적용
|
||||||
|
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user