feat: show 99.5 percent score achievement probability
Publish GitHub Pages output / publish (push) Successful in 35s

This commit is contained in:
2026-09-25 08:22:37 +09:00
parent 8edb732dd9
commit 167c933032
8 changed files with 72 additions and 11 deletions
+24 -4
View File
@@ -3,12 +3,12 @@
import AnalysisResultView from './AnalysisResultView.svelte';
import { saveAnalysis, listAnalyses, readGrowth, deleteAnalysis, exportAnalyses, importAnalyses } from '$lib/storage/analysis-history.ts';
import { currentGrowthSettings, snapshotKey, type AnalysisCapture, type AnalysisRecord, type AnalysisSettings, type GrowthSnapshot } from '$lib/models/analysis-history.ts';
import type { ExactOptimizationResult } from '$lib/engine/types.ts';
import type { ExactOptimizationResult, ScoreAchievementProbabilities } from '$lib/engine/types.ts';
export let capture: AnalysisCapture | null = null;
export let result: ExactOptimizationResult | null = null;
export let cardName: (id: string) => string;
export let currentGrowth: () => GrowthSnapshot;
export let evaluate: (settings: AnalysisSettings, growth: GrowthSnapshot) => Promise<{ result: ExactOptimizationResult; overallPower: number }>;
export let evaluate: (settings: AnalysisSettings, growth: GrowthSnapshot, includeAchievementProbabilities?: boolean) => Promise<{ result: ExactOptimizationResult; overallPower: number; achievementProbabilities?: ScoreAchievementProbabilities }>;
let name = '';
let records: AnalysisRecord[] = [];
let selected: AnalysisRecord | null = null;
@@ -19,6 +19,12 @@
let comparison: { before: { result: ExactOptimizationResult; overallPower: number }; after: { result: ExactOptimizationResult; overallPower: number }; changes: string[] } | null = null;
let phase = '';
let detail: ExactOptimizationResult | null = null;
let detailProbabilities: ScoreAchievementProbabilities | undefined;
function probabilityLabel(value: number): string {
if (value <= 0) return '0%';
if (value < 0.000001) return '<0.0001%';
return `${(value * 100).toFixed(value < 0.0001 ? 4 : value < 0.01 ? 2 : 1)}%`;
}
onMount(() => { void refresh(); });
async function refresh() {
try { records = await listAnalyses(); } catch { error = '저장 기록을 열지 못했습니다. 브라우저 저장 공간 사용을 허용해 주세요.'; }
@@ -79,8 +85,12 @@
}
async function showDetail() {
if (!selected || !savedGrowth) return;
busy = true; error = ''; phase = '당시 성장으로 상세 결과 재계산 중';
try { detail = (await evaluate(selected.settings, savedGrowth)).result; }
busy = true; error = ''; detail = null; detailProbabilities = undefined; phase = '당시 성장으로 상세 결과·고점 달성 확률 재계산 중';
try {
const evaluated = await evaluate(selected.settings, savedGrowth, true);
detail = evaluated.result;
detailProbabilities = evaluated.achievementProbabilities;
}
catch (e) { error = e instanceof Error ? e.message : '상세 결과를 계산하지 못했습니다.'; }
finally { busy = false; phase = ''; }
}
@@ -168,6 +178,15 @@
{#if detail}
<h4>당시 성장으로 상세 재분석한 결과</h4>
<p class="hint">노트별 그래프는 저장 용량을 줄이기 위해 보관하지 않습니다. 아래는 현재 계산식·악곡 데이터로 재계산한 결과이며 저장 원본 점수와 다를 수 있습니다.</p>
{#if detailProbabilities}
<div class="score-cards achievement-cards" aria-label="상세 재분석 고점 달성 확률">
<div><span>최고점 100%</span><strong>{probabilityLabel(detailProbabilities.maximum)}</strong></div>
<div><span>최고점의 99.5% 이상</span><strong>{probabilityLabel(detailProbabilities.atLeast995Percent)}</strong><small>{Math.ceil(detail.theoreticalMaxScore * 0.995).toLocaleString('ko-KR')}점 이상</small></div>
<div><span>최고점의 99% 이상</span><strong>{probabilityLabel(detailProbabilities.atLeast99Percent)}</strong><small>{Math.ceil(detail.theoreticalMaxScore * 0.99).toLocaleString('ko-KR')}점 이상</small></div>
<div><span>최고점의 95% 이상</span><strong>{probabilityLabel(detailProbabilities.atLeast95Percent)}</strong><small>{Math.ceil(detail.theoreticalMaxScore * 0.95).toLocaleString('ko-KR')}점 이상</small></div>
</div>
<p class="hint">모든 노트를 PERFECT로 처리한다고 가정하고 액티브 발동 확률만 반영합니다. 100%는 정확 계산, 99.5%·99%·95% 이상은 25,000회 반복 계산값이며 최고점에 영향을 주지 않는 발동 실패는 100% 달성 확률을 낮추지 않습니다.</p>
{/if}
<AnalysisResultView result={detail} durationSeconds={selected.durationSeconds} {cardName} instanceId={`history-${selected.id}`} />
{/if}
</article>
@@ -204,6 +223,7 @@
button.delete-button { position: absolute; bottom: 9px; right: 10px; padding: 7px 10px; font-size: 11px; color: #9ca6b5; background: transparent; border-color: transparent; }
article { margin-top: 28px; border-top: 1px solid #394353; padding-top: 26px; } .detail-title { font-size: 23px; letter-spacing: -.03em; }
.score-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 20px 0; } .score-cards > div { display: grid; gap: 10px; padding: 18px; background: #25212e; border: 1px solid #3b3044; border-radius: 10px; } .score-cards span { color: #b2a6bc; font-size: 12px; } .score-cards strong { color: #f2b0cf; font-size: clamp(18px, 2.1vw, 26px); font-variant-numeric: tabular-nums; }
.achievement-cards { grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr)); }
.order-cards { list-style: none; padding: 0; display: grid; gap: 7px; } .order-cards li { display: flex; align-items: center; gap: 12px; background: #121820; border: 1px solid #303946; padding: 10px 12px; border-radius: 9px; } .slot { display: grid; place-items: center; width: 26px; height: 26px; flex-shrink: 0; color: #261724; background: #dd97b9; font-weight: 800; border-radius: 7px; } .order-cards strong { font-size: 13px; } .cooldown { margin-left: auto; color: #a9b5c5; font-size: 11px; white-space: nowrap; }
details { margin: 18px 0; border: 1px solid #333d4a; border-radius: 10px; padding: 14px; background: #141a22; } summary { cursor: pointer; font-size: 13px; color: #bbc6d4; } details p { font-size: 12px; }
.compare-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 20px; } .comparison-heading { margin-top: 30px; padding-top: 22px; border-top: 1px solid #394353; }