feat: improve analysis and release v1.0.2
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
import exactChartCatalog from '$lib/data/exact-chart-catalog.json';
|
||||
import { loadExactSongChart } from '$lib/data/chart-loader.ts';
|
||||
import { calculateBoardEffects, calculateUnitPower, countOptimizationCandidates, memoryParameterPermil, perfectNoteScoreFromPower, toOptimizerCard } from '$lib/engine/index.ts';
|
||||
import type { CooldownReduction, EffectiveSkillWindow, ExactOptimizationResult, ExactSongChart, OptimizationProgress, SkillWindow } from '$lib/engine/index.ts';
|
||||
import type { CooldownReduction, EffectiveSkillWindow, ExactOptimizationResult, ExactSongChart, OptimizationProgress, ScoreAchievementProbabilities, SkillWindow } from '$lib/engine/index.ts';
|
||||
import { USER_DATA_STORAGE_KEY } from '$lib/storage/browser-storage';
|
||||
import { createScoreOptimizerWorker } from '$lib/workers/create-score-optimizer-worker.ts';
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
let running = false;
|
||||
let progress: OptimizationProgress = { evaluatedCandidates: 0, totalCandidates: 0 };
|
||||
let result: ExactOptimizationResult | null = null;
|
||||
let achievementProbabilities: ScoreAchievementProbabilities | undefined;
|
||||
let errorMessage = '';
|
||||
let analysisReady = false;
|
||||
|
||||
@@ -182,6 +183,7 @@
|
||||
if (event.data.type === 'progress') progress = event.data.progress;
|
||||
if (event.data.type === 'result') {
|
||||
result = event.data.result;
|
||||
achievementProbabilities = event.data.achievementProbabilities;
|
||||
running = false;
|
||||
}
|
||||
if (event.data.type === 'error') {
|
||||
@@ -315,6 +317,7 @@
|
||||
async function analyze(): Promise<void> {
|
||||
errorMessage = '';
|
||||
result = null;
|
||||
achievementProbabilities = undefined;
|
||||
if (!worker || !selectedSong || !selectedChart) return;
|
||||
if (new Set(selectedCardIds).size !== 5) {
|
||||
errorMessage = '서로 다른 카드 5장을 선택해 주세요.';
|
||||
@@ -342,7 +345,8 @@
|
||||
cards,
|
||||
chart,
|
||||
perfectNoteScore,
|
||||
directSongScoreBonusPercent: boardEffects.directSongScoreBonusPercent
|
||||
directSongScoreBonusPercent: boardEffects.directSongScoreBonusPercent,
|
||||
includeAchievementProbabilities: true
|
||||
});
|
||||
} catch (error) {
|
||||
errorMessage = error instanceof Error ? error.message : '분석 입력을 준비하지 못했습니다.';
|
||||
@@ -354,6 +358,19 @@
|
||||
return card ? cardLabel(card) : cardId;
|
||||
}
|
||||
|
||||
function probabilityLabel(value: number | undefined): string {
|
||||
if (value === undefined) return '—';
|
||||
if (value <= 0) return '0%';
|
||||
if (value < 0.000001) return '<0.0001%';
|
||||
if (value < 0.0001) return `${(value * 100).toFixed(4)}%`;
|
||||
if (value < 0.01) return `${(value * 100).toFixed(2)}%`;
|
||||
return `${(value * 100).toFixed(1)}%`;
|
||||
}
|
||||
|
||||
function scoreThreshold(maximumScore: number, ratio: number): string {
|
||||
return Math.ceil(maximumScore * ratio).toLocaleString('ko-KR');
|
||||
}
|
||||
|
||||
function countNotesBySecond(scoreResult: ExactOptimizationResult, durationSeconds: number): number[] {
|
||||
const counts = Array.from({ length: Math.ceil(durationSeconds) }, () => 0);
|
||||
for (const note of scoreResult.notes) {
|
||||
@@ -430,7 +447,7 @@
|
||||
<header class="page-header">
|
||||
<div>
|
||||
<h1>배치 분석</h1>
|
||||
<p class="page-description">특정 악곡과 카드 조합에서 최고 점수를 낼 수 있는 멤버 배치 순서와 빈도 감소 조합을 찾습니다.</p>
|
||||
<p class="page-description">특정 악곡과 카드 조합에서 최고 점수를 낼 수 있는 스페셜 발동 순서와 빈도 감소 조합을 찾습니다.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -528,16 +545,27 @@
|
||||
<section class="panel result" aria-labelledby="result-title">
|
||||
<div class="section-title"><div><span>02</span><h2 id="result-title">분석 결과</h2></div></div>
|
||||
{#if result}
|
||||
<ol class="order-list">
|
||||
{#each result.order as cardId, slot}
|
||||
<li><span class="slot">{slot + 1}</span><strong>{resultCardName(cardId)}</strong><span class="reduction">쿨타임 {result.cooldownReductions[cardId]}% 감소</span></li>
|
||||
{/each}
|
||||
</ol>
|
||||
<div class="order-sections">
|
||||
<section class="order-section">
|
||||
<h3>추천 스페셜 발동 순서</h3>
|
||||
<ol class="order-list">
|
||||
{#each result.order as cardId, slot}
|
||||
<li><span class="slot">{slot + 1}</span><strong>{resultCardName(cardId)}</strong><span class="reduction">쿨타임 {result.cooldownReductions[cardId]}% 감소</span></li>
|
||||
{/each}
|
||||
</ol>
|
||||
</section>
|
||||
</div>
|
||||
<div class="score-grid">
|
||||
<div class="score-summary"><span>예상 점수</span><strong>{result.expectedScore.toLocaleString('ko-KR')}</strong></div>
|
||||
<div class="score-summary"><span>이론상 고점</span><strong>{result.theoreticalMaxScore.toLocaleString('ko-KR')}</strong></div>
|
||||
<div class="score-summary"><span>스킬 미발동 기준</span><strong>{result.baselineScore.toLocaleString('ko-KR')}</strong></div>
|
||||
</div>
|
||||
<div class="probability-grid">
|
||||
<div><span>최고점 100%</span><strong>{probabilityLabel(achievementProbabilities?.maximum)}</strong></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>
|
||||
<p class="probability-guide">모든 노트를 PERFECT로 처리한다고 가정하고 액티브 발동 확률만 반영합니다. 100%는 정확 계산, 99%·95% 이상은 25,000회 반복 계산값이며 최고점에 영향을 주지 않는 발동 실패는 100% 달성 확률을 낮추지 않습니다.</p>
|
||||
|
||||
<section class="skill-timeline" aria-labelledby="timeline-title">
|
||||
<div class="timeline-heading">
|
||||
@@ -662,7 +690,10 @@
|
||||
.analyze:disabled { opacity: .55; cursor: default; }
|
||||
progress { display: block; width: 100%; height: 4px; margin-top: 8px; accent-color: #ed8bb5; }
|
||||
.error { margin: 16px 0 0; color: #ff9f9f; font-size: 13px; }
|
||||
.order-list { display: grid; gap: 8px; padding: 0; margin: 22px 0 0; list-style: none; }
|
||||
.order-sections { display: grid; grid-template-columns: 1fr; gap: 14px; margin-top: 22px; }
|
||||
.order-section { min-width: 0; }
|
||||
.order-section h3 { margin: 0 0 9px; color: #aeb7c5; font-size: 14px; }
|
||||
.order-list { display: grid; gap: 8px; padding: 0; margin: 0; list-style: none; }
|
||||
.order-list li { display: grid; grid-template-columns: 34px minmax(0, 1fr) auto; align-items: center; gap: 12px; min-height: 50px; padding: 8px 12px; border: 1px solid #2d3440; border-radius: 10px; background: #12161c; }
|
||||
.slot { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; color: #171019; background: #e98ab2; font-weight: 900; }
|
||||
.order-list strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
@@ -671,6 +702,12 @@
|
||||
.score-summary { display: grid; gap: 7px; padding: 16px; border-radius: 10px; background: #22202a; }
|
||||
.score-summary span { color: #a9b1be; font-size: 14px; }
|
||||
.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 > 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 strong { color: #e5e9ef; font-size: 20px; }
|
||||
.probability-grid small { color: #8d96a5; font-size: 12px; font-weight: 700; }
|
||||
.probability-guide { margin: 9px 2px 0; color: #7f8998; font-size: 12px; line-height: 1.55; }
|
||||
.empty { color: #858f9f; font-size: 14px; line-height: 1.65; }
|
||||
.skill-timeline { margin-top: 22px; padding-top: 20px; border-top: 1px solid #2b313c; }
|
||||
.timeline-heading { display: flex; align-items: flex-end; justify-content: space-between; gap: 18px; margin-bottom: 14px; }
|
||||
@@ -710,6 +747,7 @@
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.order-sections { grid-template-columns: 1fr; }
|
||||
main { width: calc(100% - 24px); padding-top: var(--page-content-top); }
|
||||
.page-header { align-items: flex-start; }
|
||||
.panel { padding: 17px; }
|
||||
|
||||
Reference in New Issue
Block a user