feat: show 99.5 percent score achievement probability
Publish GitHub Pages output / publish (push) Successful in 35s
Publish GitHub Pages output / publish (push) Successful in 35s
This commit is contained in:
+7
-1
@@ -1,7 +1,13 @@
|
|||||||
---
|
---
|
||||||
updatedAt: 2026-09-21T11:11:21Z
|
updatedAt: 2026-09-24T23:21:41Z
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026년 9월 25일
|
||||||
|
|
||||||
|
### 배치 분석 고점 달성 확률 추가
|
||||||
|
|
||||||
|
`IMPROVED` 배치 분석 결과와 저장 기록 상세에 최고점의 99.5% 이상을 얻을 확률과 기준 점수를 추가했습니다. 기존 99%·95% 확률과 함께 25,000회 반복 계산합니다.
|
||||||
|
|
||||||
## 2026년 9월 23일
|
## 2026년 9월 23일
|
||||||
|
|
||||||
### Lethal Dose Paranoid 악곡 추가
|
### Lethal Dose Paranoid 악곡 추가
|
||||||
|
|||||||
@@ -3,12 +3,12 @@
|
|||||||
import AnalysisResultView from './AnalysisResultView.svelte';
|
import AnalysisResultView from './AnalysisResultView.svelte';
|
||||||
import { saveAnalysis, listAnalyses, readGrowth, deleteAnalysis, exportAnalyses, importAnalyses } from '$lib/storage/analysis-history.ts';
|
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 { 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 capture: AnalysisCapture | null = null;
|
||||||
export let result: ExactOptimizationResult | null = null;
|
export let result: ExactOptimizationResult | null = null;
|
||||||
export let cardName: (id: string) => string;
|
export let cardName: (id: string) => string;
|
||||||
export let currentGrowth: () => GrowthSnapshot;
|
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 name = '';
|
||||||
let records: AnalysisRecord[] = [];
|
let records: AnalysisRecord[] = [];
|
||||||
let selected: AnalysisRecord | null = null;
|
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 comparison: { before: { result: ExactOptimizationResult; overallPower: number }; after: { result: ExactOptimizationResult; overallPower: number }; changes: string[] } | null = null;
|
||||||
let phase = '';
|
let phase = '';
|
||||||
let detail: ExactOptimizationResult | null = null;
|
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(); });
|
onMount(() => { void refresh(); });
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
try { records = await listAnalyses(); } catch { error = '저장 기록을 열지 못했습니다. 브라우저 저장 공간 사용을 허용해 주세요.'; }
|
try { records = await listAnalyses(); } catch { error = '저장 기록을 열지 못했습니다. 브라우저 저장 공간 사용을 허용해 주세요.'; }
|
||||||
@@ -79,8 +85,12 @@
|
|||||||
}
|
}
|
||||||
async function showDetail() {
|
async function showDetail() {
|
||||||
if (!selected || !savedGrowth) return;
|
if (!selected || !savedGrowth) return;
|
||||||
busy = true; error = ''; phase = '당시 성장으로 상세 결과 재계산 중';
|
busy = true; error = ''; detail = null; detailProbabilities = undefined; phase = '당시 성장으로 상세 결과·고점 달성 확률 재계산 중';
|
||||||
try { detail = (await evaluate(selected.settings, savedGrowth)).result; }
|
try {
|
||||||
|
const evaluated = await evaluate(selected.settings, savedGrowth, true);
|
||||||
|
detail = evaluated.result;
|
||||||
|
detailProbabilities = evaluated.achievementProbabilities;
|
||||||
|
}
|
||||||
catch (e) { error = e instanceof Error ? e.message : '상세 결과를 계산하지 못했습니다.'; }
|
catch (e) { error = e instanceof Error ? e.message : '상세 결과를 계산하지 못했습니다.'; }
|
||||||
finally { busy = false; phase = ''; }
|
finally { busy = false; phase = ''; }
|
||||||
}
|
}
|
||||||
@@ -168,6 +178,15 @@
|
|||||||
{#if detail}
|
{#if detail}
|
||||||
<h4>당시 성장으로 상세 재분석한 결과</h4>
|
<h4>당시 성장으로 상세 재분석한 결과</h4>
|
||||||
<p class="hint">노트별 그래프는 저장 용량을 줄이기 위해 보관하지 않습니다. 아래는 현재 계산식·악곡 데이터로 재계산한 결과이며 저장 원본 점수와 다를 수 있습니다.</p>
|
<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}`} />
|
<AnalysisResultView result={detail} durationSeconds={selected.durationSeconds} {cardName} instanceId={`history-${selected.id}`} />
|
||||||
{/if}
|
{/if}
|
||||||
</article>
|
</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; }
|
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; }
|
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; }
|
.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; }
|
.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; }
|
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; }
|
.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; }
|
||||||
|
|||||||
@@ -1,10 +1,23 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
import { compactResult, currentGrowthSettings, snapshotKey, type AnalysisSettings, type GrowthSnapshot } from '../models/analysis-history.ts';
|
import { compactResult, currentGrowthSettings, snapshotKey, type AnalysisSettings, type GrowthSnapshot } from '../models/analysis-history.ts';
|
||||||
import { parseAnalysisArchive } from '../storage/analysis-history.ts';
|
import { parseAnalysisArchive } from '../storage/analysis-history.ts';
|
||||||
import type { ExactOptimizationResult } from './types.ts';
|
import type { ExactOptimizationResult } from './types.ts';
|
||||||
|
|
||||||
const growth: GrowthSnapshot = { collection: { a: { owned: true, awakening: 2 } }, memberBoards: { a: ['B1', 'G1'] }, boardCards: {}, memoryCount: 10, memberPowerUpPercent: 1 };
|
const growth: GrowthSnapshot = { collection: { a: { owned: true, awakening: 2 } }, memberBoards: { a: ['B1', 'G1'] }, boardCards: {}, memoryCount: 10, memberPowerUpPercent: 1 };
|
||||||
|
test('기록 상세 재분석은 저장 성장으로 확률을 요청하고 재계산 고점 기준으로 표시한다', () => {
|
||||||
|
const history = readFileSync(new URL('../components/AnalysisHistory.svelte', import.meta.url), 'utf8');
|
||||||
|
const page = readFileSync(new URL('../../routes/analysis/+page.svelte', import.meta.url), 'utf8');
|
||||||
|
assert.match(history, /evaluate\(selected\.settings, savedGrowth, true\)/);
|
||||||
|
assert.match(history, /detailProbabilities = evaluated\.achievementProbabilities/);
|
||||||
|
for (const key of ['maximum', 'atLeast99Percent', 'atLeast95Percent']) assert.ok(history.includes(`probabilityLabel(detailProbabilities.${key})`));
|
||||||
|
assert.match(history, /detail\.theoreticalMaxScore \* 0\.99/);
|
||||||
|
assert.match(history, /detail\.theoreticalMaxScore \* 0\.95/);
|
||||||
|
assert.match(page, /includeAchievementProbabilities = false/);
|
||||||
|
assert.match(page, /achievementProbabilities: event\.data\.achievementProbabilities/);
|
||||||
|
assert.match(page, /comparisonWorker\.postMessage\([^\n]*includeAchievementProbabilities/);
|
||||||
|
});
|
||||||
test('동일한 성장 상태는 입력 순서와 무관하게 공유하고 성장 차이는 구분한다', () => {
|
test('동일한 성장 상태는 입력 순서와 무관하게 공유하고 성장 차이는 구분한다', () => {
|
||||||
const reordered = { ...growth, memberBoards: { a: ['G1', 'B1'] } };
|
const reordered = { ...growth, memberBoards: { a: ['G1', 'B1'] } };
|
||||||
assert.equal(snapshotKey(growth), snapshotKey(reordered));
|
assert.equal(snapshotKey(growth), snapshotKey(reordered));
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ test('전달 결과는 재탐색 없이 점수·확률을 유지하고 원래
|
|||||||
const capture = { settings: combinationAnalysisSettings(state, cards.map(c => c.id), growth), growth,
|
const capture = { settings: combinationAnalysisSettings(state, cards.map(c => c.id), growth), growth,
|
||||||
title: 'song', durationSeconds: 8, overallPower: 100, analyzedAt: '2026-09-09T09:00:00Z' };
|
title: 'song', durationSeconds: 8, overallPower: 100, analyzedAt: '2026-09-09T09:00:00Z' };
|
||||||
const transfer: AnalysisTransfer = { schemaVersion: 1, capture, result, cards, chart, directSongScoreBonusPercent: 10,
|
const transfer: AnalysisTransfer = { schemaVersion: 1, capture, result, cards, chart, directSongScoreBonusPercent: 10,
|
||||||
achievementProbabilities: { maximum: 0.1, atLeast99Percent: 0.2, atLeast95Percent: 0.4, samples: 25000 } };
|
achievementProbabilities: { maximum: 0.1, atLeast995Percent: 0.15, atLeast99Percent: 0.2, atLeast95Percent: 0.4, samples: 25000 } };
|
||||||
const loaded = parseAnalysisTransfer(JSON.stringify(transfer));
|
const loaded = parseAnalysisTransfer(JSON.stringify(transfer));
|
||||||
assert.deepEqual(loaded, transfer);
|
assert.deepEqual(loaded, transfer);
|
||||||
const score = calculateExactScore(loaded.result.order.map(id => loaded.cards.find(c => c.id === id)!), loaded.result.cooldownReductions, loaded.chart,
|
const score = calculateExactScore(loaded.result.order.map(id => loaded.cards.find(c => c.id === id)!), loaded.result.cooldownReductions, loaded.chart,
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ test('확정 발동이면 모든 달성 확률이 100%다', () => {
|
|||||||
);
|
);
|
||||||
assert.deepEqual(result, {
|
assert.deepEqual(result, {
|
||||||
maximum: 1,
|
maximum: 1,
|
||||||
|
atLeast995Percent: 1,
|
||||||
atLeast99Percent: 1,
|
atLeast99Percent: 1,
|
||||||
atLeast95Percent: 1,
|
atLeast95Percent: 1,
|
||||||
samples: 1_000
|
samples: 1_000
|
||||||
@@ -134,3 +135,18 @@ test('미발동 토글 시 현재 최고점 기준으로 확률을 계산하고
|
|||||||
assert.deepEqual(calculateMaximumScoreProbabilityTimeline(cards, {}, chart([1.1]), options), [1]);
|
assert.deepEqual(calculateMaximumScoreProbabilityTimeline(cards, {}, chart([1.1]), options), [1]);
|
||||||
assert.equal(estimateScoreAchievementProbabilities(cards, {}, chart([1.1]), options).maximum, 1);
|
assert.equal(estimateScoreAchievementProbabilities(cards, {}, chart([1.1]), options).maximum, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
test('99.5% 경계 점수를 포함하고 99% 이상 점수와 구분한다', () => {
|
||||||
|
const estimate = (fallbackBoost: number) => estimateScoreAchievementProbabilities(
|
||||||
|
[card('strong', 100, 50), card('fallback', fallbackBoost, 100)],
|
||||||
|
{ strong: 0, fallback: 0 },
|
||||||
|
chart([1.1]),
|
||||||
|
{ perfectNoteScore: 10_000, sampleCount: 25_000, seed: 7 }
|
||||||
|
);
|
||||||
|
// 최고점 20,000점에서 19,900점은 포함하고 19,850점은 제외한다.
|
||||||
|
assert.equal(estimate(99).atLeast995Percent, 1);
|
||||||
|
const below = estimate(98.5);
|
||||||
|
assert.ok(Math.abs(below.atLeast995Percent - 0.5) < 0.02);
|
||||||
|
assert.equal(below.atLeast99Percent, 1);
|
||||||
|
});
|
||||||
|
|||||||
@@ -230,8 +230,10 @@ export function estimateScoreAchievementProbabilities(
|
|||||||
const samples = Math.max(1, Math.round(options.sampleCount ?? 25_000));
|
const samples = Math.max(1, Math.round(options.sampleCount ?? 25_000));
|
||||||
const random = randomGenerator(options.seed ?? hashSeed(cards, reductions));
|
const random = randomGenerator(options.seed ?? hashSeed(cards, reductions));
|
||||||
const active = new Uint8Array(model.attempts.length);
|
const active = new Uint8Array(model.attempts.length);
|
||||||
|
let atLeast995 = 0;
|
||||||
let atLeast99 = 0;
|
let atLeast99 = 0;
|
||||||
let atLeast95 = 0;
|
let atLeast95 = 0;
|
||||||
|
const threshold995 = model.maximumScore * 0.995;
|
||||||
const threshold99 = model.maximumScore * 0.99;
|
const threshold99 = model.maximumScore * 0.99;
|
||||||
const threshold95 = model.maximumScore * 0.95;
|
const threshold95 = model.maximumScore * 0.95;
|
||||||
|
|
||||||
@@ -248,12 +250,14 @@ export function estimateScoreAchievementProbabilities(
|
|||||||
}
|
}
|
||||||
score += group.scores[scoreIndex];
|
score += group.scores[scoreIndex];
|
||||||
}
|
}
|
||||||
|
if (score >= threshold995) atLeast995 += 1;
|
||||||
if (score >= threshold99) atLeast99 += 1;
|
if (score >= threshold99) atLeast99 += 1;
|
||||||
if (score >= threshold95) atLeast95 += 1;
|
if (score >= threshold95) atLeast95 += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
maximum: exactClauseProbability(model.maximumClauses, model.attempts),
|
maximum: exactClauseProbability(model.maximumClauses, model.attempts),
|
||||||
|
atLeast995Percent: atLeast995 / samples,
|
||||||
atLeast99Percent: atLeast99 / samples,
|
atLeast99Percent: atLeast99 / samples,
|
||||||
atLeast95Percent: atLeast95 / samples,
|
atLeast95Percent: atLeast95 / samples,
|
||||||
samples
|
samples
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ export type ExactScoreOptions = {
|
|||||||
|
|
||||||
export type ScoreAchievementProbabilities = {
|
export type ScoreAchievementProbabilities = {
|
||||||
maximum: number;
|
maximum: number;
|
||||||
|
atLeast995Percent: number;
|
||||||
atLeast99Percent: number;
|
atLeast99Percent: number;
|
||||||
atLeast95Percent: number;
|
atLeast95Percent: number;
|
||||||
samples: number;
|
samples: number;
|
||||||
|
|||||||
@@ -877,7 +877,7 @@
|
|||||||
return structuredClone({ collection: value.collection ?? {}, memberBoards: value.memberBoards ?? {}, boardCards: value.boardCards ?? {}, memoryCount: value.memoryCount ?? 0, memberPowerUpPercent: value.memberPowerUpPercent ?? 0 });
|
return structuredClone({ collection: value.collection ?? {}, memberBoards: value.memberBoards ?? {}, boardCards: value.boardCards ?? {}, memoryCount: value.memoryCount ?? 0, memberPowerUpPercent: value.memberPowerUpPercent ?? 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function evaluateHistory(settings: AnalysisSettings, growth: GrowthSnapshot): Promise<{ result: ExactOptimizationResult; overallPower: number }> {
|
async function evaluateHistory(settings: AnalysisSettings, growth: GrowthSnapshot, includeAchievementProbabilities = false): Promise<{ result: ExactOptimizationResult; overallPower: number; achievementProbabilities?: ScoreAchievementProbabilities }> {
|
||||||
const chartSummary = charts.find((chart) => chart.songId === settings.songId && chart.difficulty === settings.difficulty);
|
const chartSummary = charts.find((chart) => chart.songId === settings.songId && chart.difficulty === settings.difficulty);
|
||||||
if (!chartSummary) throw new Error('현재 공개된 악곡 데이터에서 저장한 곡을 찾을 수 없습니다.');
|
if (!chartSummary) throw new Error('현재 공개된 악곡 데이터에서 저장한 곡을 찾을 수 없습니다.');
|
||||||
const sources = settings.cardIds.map((id) => {
|
const sources = settings.cardIds.map((id) => {
|
||||||
@@ -908,10 +908,10 @@
|
|||||||
const finish = () => { comparisonWorker.terminate(); comparisonWorkers.delete(comparisonWorker); };
|
const finish = () => { comparisonWorker.terminate(); comparisonWorkers.delete(comparisonWorker); };
|
||||||
comparisonWorker.onerror = () => { finish(); reject(new Error('성장 비교 작업을 실행하지 못했습니다.')); };
|
comparisonWorker.onerror = () => { finish(); reject(new Error('성장 비교 작업을 실행하지 못했습니다.')); };
|
||||||
comparisonWorker.onmessage = (event) => {
|
comparisonWorker.onmessage = (event) => {
|
||||||
if (event.data.type === 'result') { finish(); resolve({ result: event.data.result, overallPower: power.overallPower }); }
|
if (event.data.type === 'result') { finish(); resolve({ result: event.data.result, overallPower: power.overallPower, achievementProbabilities: event.data.achievementProbabilities }); }
|
||||||
if (event.data.type === 'error') { finish(); reject(new Error(event.data.message)); }
|
if (event.data.type === 'error') { finish(); reject(new Error(event.data.message)); }
|
||||||
};
|
};
|
||||||
comparisonWorker.postMessage({ type: 'optimize-exact', cards, chart, perfectNoteScore: perfectNoteScoreFromPower(power.overallPower, chartSummary.scoreRatio), directSongScoreBonusPercent: effects.directSongScoreBonusPercent });
|
comparisonWorker.postMessage({ type: 'optimize-exact', cards, chart, perfectNoteScore: perfectNoteScoreFromPower(power.overallPower, chartSummary.scoreRatio), directSongScoreBonusPercent: effects.directSongScoreBonusPercent, includeAchievementProbabilities });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1160,10 +1160,11 @@
|
|||||||
{#if !simulatedResult && (mode === 'analysis' || achievementProbabilities)}
|
{#if !simulatedResult && (mode === 'analysis' || achievementProbabilities)}
|
||||||
<div class="probability-grid">
|
<div class="probability-grid">
|
||||||
<div><span>최고점 100%</span><strong>{probabilityLabel(achievementProbabilities?.maximum)}</strong></div>
|
<div><span>최고점 100%</span><strong>{probabilityLabel(achievementProbabilities?.maximum)}</strong></div>
|
||||||
|
<div><span>최고점의 99.5% 이상</span><strong>{probabilityLabel(achievementProbabilities?.atLeast995Percent)}</strong><small>{scoreThreshold(result.theoreticalMaxScore, 0.995)}점 이상</small></div>
|
||||||
<div><span>최고점의 99% 이상</span><strong>{probabilityLabel(achievementProbabilities?.atLeast99Percent)}</strong><small>{scoreThreshold(result.theoreticalMaxScore, 0.99)}점 이상</small></div>
|
<div><span>최고점의 99% 이상</span><strong>{probabilityLabel(achievementProbabilities?.atLeast99Percent)}</strong><small>{scoreThreshold(result.theoreticalMaxScore, 0.99)}점 이상</small></div>
|
||||||
<div><span>최고점의 95% 이상</span><strong>{probabilityLabel(achievementProbabilities?.atLeast95Percent)}</strong><small>{scoreThreshold(result.theoreticalMaxScore, 0.95)}점 이상</small></div>
|
<div><span>최고점의 95% 이상</span><strong>{probabilityLabel(achievementProbabilities?.atLeast95Percent)}</strong><small>{scoreThreshold(result.theoreticalMaxScore, 0.95)}점 이상</small></div>
|
||||||
</div>
|
</div>
|
||||||
<p class="probability-guide">모든 노트를 PERFECT로 처리한다고 가정하고 액티브 발동 확률만 반영합니다. 100%는 정확 계산, 99%·95% 이상은 25,000회 반복 계산값이며 최고점에 영향을 주지 않는 발동 실패는 100% 달성 확률을 낮추지 않습니다.</p>
|
<p class="probability-guide">모든 노트를 PERFECT로 처리한다고 가정하고 액티브 발동 확률만 반영합니다. 100%는 정확 계산, 99.5%·99%·95% 이상은 25,000회 반복 계산값이며 최고점에 영향을 주지 않는 발동 실패는 100% 달성 확률을 낮추지 않습니다.</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<section class="connection-optimizer" aria-labelledby="connection-optimizer-title">
|
<section class="connection-optimizer" aria-labelledby="connection-optimizer-title">
|
||||||
@@ -1418,7 +1419,7 @@
|
|||||||
.score-summary { display: grid; gap: 7px; padding: 16px; border-radius: 10px; background: #22202a; }
|
.score-summary { display: grid; gap: 7px; padding: 16px; border-radius: 10px; background: #22202a; }
|
||||||
.score-summary span { color: #a9b1be; font-size: 14px; }
|
.score-summary span { color: #a9b1be; font-size: 14px; }
|
||||||
.score-summary strong { color: #f2a3c4; font-size: 20px; }
|
.score-summary strong { color: #f2a3c4; font-size: 20px; }
|
||||||
.probability-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-top: 8px; }
|
.probability-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr)); gap: 8px; margin-top: 8px; }
|
||||||
.probability-grid > div { display: grid; gap: 7px; padding: 16px; border: 1px solid #303744; border-radius: 10px; background: #171d25; }
|
.probability-grid > div { display: grid; gap: 7px; padding: 16px; border: 1px solid #303744; border-radius: 10px; background: #171d25; }
|
||||||
.probability-grid span { color: #9ca5b3; font-size: 14px; }
|
.probability-grid span { color: #9ca5b3; font-size: 14px; }
|
||||||
.probability-grid strong { color: #e5e9ef; font-size: 20px; }
|
.probability-grid strong { color: #e5e9ef; font-size: 20px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user