feat: release version 1.0.5

This commit is contained in:
2026-08-20 14:30:22 +09:00
parent f910db8901
commit 6537ab1f20
35 changed files with 1151 additions and 181 deletions
+48 -2
View File
@@ -1,10 +1,14 @@
<script lang="ts">
import { onMount } from 'svelte';
import { page } from '$app/stores';
import cardSummary from '$lib/data/cards-summary.json';
import boardSummary from '$lib/data/boards-summary.json';
import memorySummary from '$lib/data/memory-summary.json';
import exactChartCatalog from '$lib/data/exact-chart-catalog.json';
import { loadExactSongChart } from '$lib/data/chart-loader.ts';
import { createInitialReleaseClock, synchronizeReleaseClock } from '$lib/data/release-clock.ts';
import { releasedSongCharts } from '$lib/data/song-release.ts';
import PresetManager from '$lib/components/PresetManager.svelte';
import { calculateBoardEffects, calculateUnitPower, countOptimizationCandidates, memoryParameterPermil, perfectNoteScoreFromPower, toOptimizerCard } 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';
@@ -33,13 +37,18 @@
};
const ANALYSIS_STORAGE_KEY = 'holodori.analysis.v2';
const ANALYSIS_PRESET_STORAGE_KEY = 'holodori.analysis.presets.v1';
const LEGACY_ANALYSIS_STORAGE_KEY = 'holodori.analysis.v1';
const MEMBER_COLORS = ['#f08db8', '#64cbe4', '#f2b85b', '#a995f5', '#69cf9b'] as const;
const REDUCTION_LOCK_OPTIONS = [null, 0, 4, 8, 12] as const;
const fiveStarCards = cardSummary.cards.filter((card) => card.rarity === 5);
const charts = exactChartCatalog.charts;
const songs = Array.from(new Map(charts.map((chart) => [chart.songId, { id: chart.songId, title: chart.title }])).values());
const allCharts = exactChartCatalog.charts;
const initialReleaseClock = createInitialReleaseClock($page.data?.serverNow);
let currentReleaseClock = initialReleaseClock;
let releaseNow = currentReleaseClock.now();
let charts = releasedSongCharts(allCharts, releaseNow);
let songs = Array.from(new Map(charts.map((chart) => [chart.songId, { id: chart.songId, title: chart.title }])).values());
const cardById = new Map(fiveStarCards.map((card) => [card.id, card]));
function uniqueMemberCards(cards: readonly CardRow[], limit = Number.POSITIVE_INFINITY): CardRow[] {
@@ -73,6 +82,9 @@
let errorMessage = '';
let analysisReady = false;
$: charts = releasedSongCharts(allCharts, releaseNow);
$: songs = Array.from(new Map(charts.map((chart) => [chart.songId, { id: chart.songId, title: chart.title }])).values());
$: if (!songs.some((song) => song.id === selectedSongId)) selectedSongId = songs[0]?.id ?? '';
$: selectedSong = songs.find((song) => song.id === selectedSongId) ?? songs[0];
$: selectedChart = charts.find((chart) => chart.songId === selectedSongId && chart.difficulty === difficulty);
$: leaderMember = leaderMembers.find((card) => card.characterId === leaderCharacterId);
@@ -127,6 +139,16 @@
return 0;
}
})();
$: presetSnapshot = {
schemaVersion: 2,
songId: selectedSongId,
difficulty,
cardIds: selectedCardIds,
awakenings: selectedAwakenings,
fixedCooldownReductions,
leaderCharacterId,
leaderOutfitCardId
} satisfies SavedAnalysisState;
$: if (analysisReady) saveAnalysisState({
schemaVersion: 2,
songId: selectedSongId,
@@ -139,6 +161,15 @@
});
onMount(() => {
let mounted = true;
void synchronizeReleaseClock().then((clock) => {
if (!mounted) return;
currentReleaseClock = clock;
releaseNow = currentReleaseClock.now();
});
const releaseClock = window.setInterval(() => {
releaseNow = currentReleaseClock.now();
}, 1_000);
const applyStoredUserData = (stored: string | null) => {
const parsed = stored ? JSON.parse(stored) : null;
collection = parsed?.collection && typeof parsed.collection === 'object' ? parsed.collection : {};
@@ -202,6 +233,8 @@
};
window.addEventListener('storage', syncStorage);
return () => {
mounted = false;
window.clearInterval(releaseClock);
window.removeEventListener('storage', syncStorage);
worker?.terminate();
};
@@ -256,6 +289,17 @@
}
}
function applyPreset(event: CustomEvent<unknown>): void {
try {
restoreAnalysisState(JSON.stringify(event.detail));
result = null;
achievementProbabilities = undefined;
errorMessage = '';
} catch {
errorMessage = '이 프리셋의 분석 조건을 불러오지 못했습니다.';
}
}
function updateAwakening(slot: number, awakening: number): void {
const normalized = normalizeAwakening(awakening);
selectedAwakenings = selectedAwakenings.map((value, index) => index === slot ? normalized : value);
@@ -538,6 +582,8 @@
</div>
<p class="power-detail">최대 레벨 기준 · 기본 {unitPower.baseParameterTotal.toLocaleString('ko-KR')} + 개화 {unitPower.potentialTotal.toLocaleString('ko-KR')} + 패시브 {unitPower.passiveTotal.toLocaleString('ko-KR')} + 의상 {unitPower.outfitTotal.toLocaleString('ko-KR')} + 보드 {unitPower.boardTotal.toLocaleString('ko-KR')} + 메모리 {unitPower.memoryTotal.toLocaleString('ko-KR')} ({memoryCount}개) + 멤버 강화 {unitPower.memberPowerUpTotal.toLocaleString('ko-KR')} ({memberPowerUpPercent.toLocaleString('ko-KR')}%)</p>
<PresetManager storageKey={ANALYSIS_PRESET_STORAGE_KEY} snapshot={presetSnapshot} disabled={running} on:apply={applyPreset} />
{#if errorMessage}<p class="error" role="alert">{errorMessage}</p>{/if}
<button class="analyze" type="button" disabled={running || candidateCount === 0} on:click={analyze}>
{running ? `분석 중 ${Math.round(progress.evaluatedCandidates / Math.max(1, progress.totalCandidates) * 100)}%` : '최적 배치 분석'}