feat: save analysis results and compare growth snapshots
Publish GitHub Pages output / publish (push) Successful in 46s
Publish GitHub Pages output / publish (push) Successful in 46s
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import AnalysisHistory from '$lib/components/AnalysisHistory.svelte';
|
||||
import type { AnalysisCapture, AnalysisSettings, GrowthSnapshot } from '$lib/models/analysis-history.ts';
|
||||
import { page } from '$app/stores';
|
||||
import cardSummary from '$lib/data/cards-summary.json';
|
||||
import SpecialNoteSummary from '$lib/components/SpecialNoteSummary.svelte';
|
||||
@@ -107,6 +109,9 @@
|
||||
let running = false;
|
||||
let progress: OptimizationProgress = { evaluatedCandidates: 0, totalCandidates: 0 };
|
||||
let result: ExactOptimizationResult | null = null;
|
||||
let resultCapture: AnalysisCapture | null = null;
|
||||
let pendingCapture: AnalysisCapture | null = null;
|
||||
const comparisonWorkers = new Set<Worker>();
|
||||
let achievementProbabilities: ScoreAchievementProbabilities | undefined;
|
||||
let errorMessage = '';
|
||||
let analysisReady = false;
|
||||
@@ -259,6 +264,7 @@
|
||||
if (event.data.type === 'progress') progress = event.data.progress;
|
||||
if (event.data.type === 'result') {
|
||||
result = event.data.result;
|
||||
resultCapture = pendingCapture;
|
||||
achievementProbabilities = event.data.achievementProbabilities;
|
||||
running = false;
|
||||
}
|
||||
@@ -282,6 +288,7 @@
|
||||
window.clearInterval(releaseClock);
|
||||
window.removeEventListener('storage', syncStorage);
|
||||
worker?.terminate();
|
||||
for (const comparisonWorker of comparisonWorkers) comparisonWorker.terminate();
|
||||
};
|
||||
});
|
||||
|
||||
@@ -660,9 +667,11 @@
|
||||
}
|
||||
|
||||
async function analyze(): Promise<void> {
|
||||
if (running) return;
|
||||
errorMessage = '';
|
||||
clearConnectionOptimizationProposal();
|
||||
result = null;
|
||||
resultCapture = null;
|
||||
achievementProbabilities = undefined;
|
||||
if (!worker || !selectedSong || !selectedChart) return;
|
||||
if (new Set(selectedCardIds).size !== 5) {
|
||||
@@ -683,22 +692,76 @@
|
||||
return optimizerCardWithBoard(card, index, unitMembers);
|
||||
});
|
||||
if (!Number.isFinite(overallPower) || overallPower <= 0) throw new Error('선택한 카드의 종합력을 계산하지 못했습니다.');
|
||||
const chart = await loadExactSongChart(selectedChart.path);
|
||||
running = true;
|
||||
pendingCapture = structuredClone({
|
||||
settings: presetSnapshot,
|
||||
growth: { collection, memberBoards, boardCards, memoryCount, memberPowerUpPercent },
|
||||
title: selectedChart.title,
|
||||
durationSeconds: selectedChart.durationSeconds,
|
||||
overallPower,
|
||||
analyzedAt: new Date().toISOString()
|
||||
});
|
||||
const noteScore = perfectNoteScore;
|
||||
const songBonus = boardEffects.directSongScoreBonusPercent;
|
||||
const chart = await loadExactSongChart(selectedChart.path);
|
||||
progress = { evaluatedCandidates: 0, totalCandidates: countOptimizationCandidates(cards) };
|
||||
worker.postMessage({
|
||||
type: 'optimize-exact',
|
||||
cards,
|
||||
chart,
|
||||
perfectNoteScore,
|
||||
directSongScoreBonusPercent: boardEffects.directSongScoreBonusPercent,
|
||||
perfectNoteScore: noteScore,
|
||||
directSongScoreBonusPercent: songBonus,
|
||||
includeAchievementProbabilities: true
|
||||
});
|
||||
} catch (error) {
|
||||
running = false;
|
||||
errorMessage = error instanceof Error ? error.message : '분석 입력을 준비하지 못했습니다.';
|
||||
}
|
||||
}
|
||||
|
||||
function latestGrowth(): GrowthSnapshot {
|
||||
const value = JSON.parse(localStorage.getItem(USER_DATA_STORAGE_KEY) ?? '{}');
|
||||
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 }> {
|
||||
const chartSummary = charts.find((chart) => chart.songId === settings.songId && chart.difficulty === settings.difficulty);
|
||||
if (!chartSummary) throw new Error('현재 공개된 악곡 데이터에서 저장한 곡을 찾을 수 없습니다.');
|
||||
const sources = settings.cardIds.map((id) => {
|
||||
const card = allCardById.get(id);
|
||||
if (!card) throw new Error('현재 데이터에서 저장된 편성 카드를 찾을 수 없습니다.');
|
||||
return card;
|
||||
});
|
||||
const outfit = allCardById.get(settings.leaderOutfitCardId)?.details.costume;
|
||||
if (!outfit) throw new Error('저장된 리더 의상을 찾을 수 없습니다.');
|
||||
const effectiveBoards = boardsForSelectedCards(growth.memberBoards, settings.cardIds, settings.allBlueNodes ?? settings.cardIds.map(() => false));
|
||||
const effects = calculateBoardEffects(boardSummary, effectiveBoards, sources, settings.leaderCharacterId, chartSummary, {
|
||||
placements: growth.boardCards,
|
||||
cards: cardSummary.cards.filter((card) => card.details.connect.length > 0).map((card) => ({ id: card.id, awakening: growth.collection[card.id]?.awakening ?? 0, connect: card.details.connect }))
|
||||
}, {
|
||||
generationBonusByCardId: Object.fromEntries(settings.cardIds.flatMap((id, i) => settings.generationBonusOverrides?.[i] == null ? [] : [[id, settings.generationBonusOverrides[i]]])),
|
||||
...(settings.songScoreBonusOverride == null ? {} : { directSongScoreBonusPercent: settings.songScoreBonusOverride })
|
||||
});
|
||||
const power = calculateUnitPower(sources.map((card, i) => ({ card, awakening: settings.awakenings[i] })), outfit, effects, memoryParameterPermil(growth.memoryCount, memorySummary.effects), growth.memberPowerUpPercent);
|
||||
const cards = sources.map((card, i) => {
|
||||
const fixed = settings.fixedCooldownReductions?.[i];
|
||||
const adapted = toOptimizerCard(card, settings.awakenings[i], fixed == null ? effects.allowedCooldownReductionsByCardId[card.id] : [fixed], sources);
|
||||
return { ...adapted, active: { ...adapted.active, activationRateUpPercent: effects.activationRateUpByCardId[card.id] ?? 0 }, scoreSupportPercent: power.outfitScoreSupportPercent + effects.scoreSupportPercent };
|
||||
});
|
||||
const chart = await loadExactSongChart(chartSummary.path);
|
||||
return new Promise((resolve, reject) => {
|
||||
const comparisonWorker = createScoreOptimizerWorker();
|
||||
comparisonWorkers.add(comparisonWorker);
|
||||
const finish = () => { comparisonWorker.terminate(); comparisonWorkers.delete(comparisonWorker); };
|
||||
comparisonWorker.onerror = () => { finish(); reject(new Error('성장 비교 작업을 실행하지 못했습니다.')); };
|
||||
comparisonWorker.onmessage = (event) => {
|
||||
if (event.data.type === 'result') { finish(); resolve({ result: event.data.result, overallPower: power.overallPower }); }
|
||||
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 });
|
||||
});
|
||||
}
|
||||
|
||||
function resultCardName(cardId: string): string {
|
||||
const card = cardById.get(cardId);
|
||||
return card ? cardLabel(card) : cardId;
|
||||
@@ -1025,6 +1088,7 @@
|
||||
<div class="empty">조건을 선택하고 분석을 시작하면 최적 배치와 각 멤버의 쿨타임 감소량이 여기에 표시됩니다.</div>
|
||||
{/if}
|
||||
</section>
|
||||
<AnalysisHistory capture={resultCapture} {result} cardName={resultCardName} currentGrowth={latestGrowth} evaluate={evaluateHistory} />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user