feat: release version 1.0.9
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
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 { events } from '$lib/data/events.ts';
|
||||
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';
|
||||
@@ -18,6 +19,9 @@
|
||||
combinationSearchLimits,
|
||||
countOptimizationCandidates,
|
||||
effectiveActiveProbability,
|
||||
eventBonusBreakdown,
|
||||
eventChallengeSongScoreBonusPercent,
|
||||
eventPointIndex,
|
||||
generateAnchoredCombinationCandidates,
|
||||
generateCombinationCandidates,
|
||||
memoryParameterPermil,
|
||||
@@ -30,6 +34,8 @@
|
||||
import { USER_DATA_STORAGE_KEY } from '$lib/storage/browser-storage';
|
||||
import { createScoreOptimizerWorker } from '$lib/workers/create-score-optimizer-worker.ts';
|
||||
|
||||
export let eventMode = false;
|
||||
|
||||
type CardRow = (typeof cardSummary.cards)[number];
|
||||
type DifficultyKey = 'hard' | 'expert';
|
||||
type CollectionState = Record<string, { owned?: boolean; awakening?: number }>;
|
||||
@@ -42,12 +48,17 @@
|
||||
directSongScoreBonusPercent: number;
|
||||
heuristic: number;
|
||||
skillPotential: number;
|
||||
eventBonusPercent?: number;
|
||||
rankingMultiplier?: number;
|
||||
optimizationObjective?: 'maximum' | 'expected';
|
||||
};
|
||||
type RankedResult = {
|
||||
result: ExactOptimizationResult;
|
||||
overallPower: number;
|
||||
cards?: OptimizerCard[];
|
||||
directSongScoreBonusPercent?: number;
|
||||
eventBonusPercent?: number;
|
||||
rankingMultiplier?: number;
|
||||
achievementProbabilities?: ScoreAchievementProbabilities;
|
||||
};
|
||||
type DisplayRankedResult = RankedResult & { isBaseline: boolean };
|
||||
@@ -83,12 +94,17 @@
|
||||
leaderOutfitCardId: string;
|
||||
};
|
||||
|
||||
const STORAGE_KEY = 'holodori.combination.v1';
|
||||
const PRESET_STORAGE_KEY = 'holodori.combination.presets.v1';
|
||||
const STORAGE_KEY = eventMode ? 'holodori.event-combination.v1' : 'holodori.combination.v1';
|
||||
const PRESET_STORAGE_KEY = eventMode ? 'holodori.event-combination.presets.v1' : 'holodori.combination.presets.v1';
|
||||
const ANALYSIS_STORAGE_KEY = 'holodori.analysis.v2';
|
||||
const REDUCTION_LOCK_OPTIONS = [null, 0, 4, 8, 12] as const;
|
||||
const fiveStarCards = cardSummary.cards.filter((card) => card.rarity === 5);
|
||||
const cardById = new Map(fiveStarCards.map((card) => [card.id, card]));
|
||||
const allCards = cardSummary.cards;
|
||||
const fiveStarCards = allCards.filter((card) => card.rarity === 5);
|
||||
const eventBonusCharacters = new Set<string>(events.flatMap((event) => [...event.bonusCharacterIds]));
|
||||
const eligibleCards = eventMode
|
||||
? allCards.filter((card) => card.rarity === 5 || eventBonusCharacters.has(card.characterId))
|
||||
: fiveStarCards;
|
||||
const cardById = new Map(allCards.map((card) => [card.id, card]));
|
||||
const allCharts = exactChartCatalog.charts;
|
||||
const initialReleaseClock = createInitialReleaseClock($page.data?.serverNow);
|
||||
let currentReleaseClock = initialReleaseClock;
|
||||
@@ -111,9 +127,9 @@
|
||||
let selectedSongId = songs[0]?.id ?? '';
|
||||
let difficulty: DifficultyKey = 'expert';
|
||||
let fixedCardIds = ['', '', '', ''];
|
||||
let candidateAwakening = 5;
|
||||
let candidateAwakening = eventMode ? 0 : 5;
|
||||
let candidateGenerationBonus: number | null = null;
|
||||
let fixedAwakenings = [5, 5, 5, 5];
|
||||
let fixedAwakenings = eventMode ? [0, 0, 0, 0] : [5, 5, 5, 5];
|
||||
let fixedCooldownReductions: (CooldownReduction | null)[] = [null, null, null, null];
|
||||
let fixedAllBlueNodes = [false, false, false, false];
|
||||
let fixedGenerationBonusOverrides: (number | null)[] = [null, null, null, null];
|
||||
@@ -148,6 +164,9 @@
|
||||
let ready = false;
|
||||
|
||||
$: charts = releasedSongCharts(allCharts, releaseNow);
|
||||
$: currentEvent = eventMode
|
||||
? events.find((event) => Date.parse(event.startsAt) <= releaseNow && releaseNow <= Date.parse(event.endsAt)) ?? null
|
||||
: null;
|
||||
$: 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 ?? '';
|
||||
$: selectedChart = charts.find((chart) => chart.songId === selectedSongId && chart.difficulty === difficulty);
|
||||
@@ -155,7 +174,7 @@
|
||||
$: baselineChartMetadata = baselineState
|
||||
? charts.find((chart) => chart.songId === baselineState!.songId && chart.difficulty === baselineState!.difficulty)
|
||||
: undefined;
|
||||
$: baselineMatchesConditions = Boolean(baselineState?.songId === selectedSongId && baselineState?.difficulty === difficulty);
|
||||
$: baselineMatchesConditions = !eventMode && Boolean(baselineState?.songId === selectedSongId && baselineState?.difficulty === difficulty);
|
||||
$: comparableToBaseline = Boolean(baselineResult && baselineMatchesConditions);
|
||||
$: displayedRankings = buildDisplayedRankings(
|
||||
rankedResults,
|
||||
@@ -170,8 +189,8 @@
|
||||
$: if (!result && rankedResults.length > 0) rankedResults = [];
|
||||
$: fixedCards = fixedCardIds.map((id) => cardById.get(id)).filter((card): card is CardRow => Boolean(card));
|
||||
$: candidateCards = ownedCardsOnly
|
||||
? fiveStarCards.filter((card) => collection[card.id]?.owned === true)
|
||||
: fiveStarCards;
|
||||
? eligibleCards.filter((card) => collection[card.id]?.owned === true)
|
||||
: eligibleCards;
|
||||
$: leaderMember = leaderMembers.find((card) => card.characterId === leaderCharacterId);
|
||||
$: leaderOutfitOptions = leaderMember
|
||||
? fiveStarCards.filter((card) => card.characterId === leaderCharacterId && card.details.costume)
|
||||
@@ -502,7 +521,10 @@
|
||||
function roughCardScore(card: CardRow): number {
|
||||
const statTotal = Object.values(card.power.statsAtMaxLevel).reduce((sum, value) => sum + value, 0);
|
||||
const { activeValue, specialValue } = roughSkillValues(card);
|
||||
return statTotal * (1 + activeValue / 100 + specialValue / 500);
|
||||
const score = statTotal * (1 + activeValue / 100 + specialValue / 500);
|
||||
if (!currentEvent) return score;
|
||||
const bonus = eventBonusBreakdown(currentEvent, [card], (candidate) => awakeningForCard(candidate as CardRow));
|
||||
return score * (1 + bonus.totalPercent / 100);
|
||||
}
|
||||
|
||||
function roughStatScore(card: CardRow): number {
|
||||
@@ -534,7 +556,30 @@
|
||||
effectSum + effect.percent * (effect.target === 'all' ? cards.length : effect.targetCount ?? 1), 0);
|
||||
return sum + activeValue + specialValue + activationValue + passiveValue;
|
||||
}, 0);
|
||||
return statTotal * (1 + skillPotential / 500);
|
||||
const score = statTotal * (1 + skillPotential / 500);
|
||||
if (!currentEvent) return score;
|
||||
const bonus = eventBonusBreakdown(currentEvent, cards, (card) => awakeningForCard(card as CardRow));
|
||||
const songBonus = eventChallengeSongScoreBonusPercent(currentEvent, selectedSongId, cards.map((card) => card.id));
|
||||
return score * (1 + bonus.totalPercent / 100) * (1 + songBonus / 100);
|
||||
}
|
||||
|
||||
function roughEventBonusPriority(cards: readonly CardRow[]): number {
|
||||
if (!currentEvent) return roughCombinationScore(cards);
|
||||
const bonus = eventBonusBreakdown(currentEvent, cards, (card) => awakeningForCard(card as CardRow));
|
||||
// 최대 보너스 편성이 순수 스코어 후보에 밀려 선별 단계에서 사라지지 않도록 별도 층을 보존한다.
|
||||
return bonus.totalPercent * 1_000_000_000_000 + roughCombinationScore(cards);
|
||||
}
|
||||
|
||||
function rankingValue(ranking: RankedResult): number {
|
||||
return eventMode
|
||||
? eventPointIndex(ranking.result.expectedScore, ranking.eventBonusPercent ?? 0)
|
||||
: ranking.result.theoreticalMaxScore;
|
||||
}
|
||||
|
||||
function compareRankedResults(left: RankedResult, right: RankedResult): number {
|
||||
return rankingValue(right) - rankingValue(left) ||
|
||||
right.result.expectedScore - left.result.expectedScore ||
|
||||
right.result.theoreticalMaxScore - left.result.theoreticalMaxScore;
|
||||
}
|
||||
|
||||
function combinationKey(cards: readonly CardRow[] | readonly OptimizerCard[]): string {
|
||||
@@ -553,19 +598,26 @@
|
||||
}
|
||||
|
||||
function screeningCombinations(): CardRow[][] {
|
||||
const { screeningLimit } = combinationSearchLimits(fixedCards.length);
|
||||
const limits = combinationSearchLimits(fixedCards.length);
|
||||
const screeningLimit = eventMode ? Math.min(limits.screeningLimit, 9_000) : limits.screeningLimit;
|
||||
if (fixedCards.length === 4) {
|
||||
return generateCombinationCandidates(fixedCards, candidateCards, 5, roughCardScore, screeningLimit);
|
||||
}
|
||||
const globalBudget = Math.ceil(screeningLimit * 0.8);
|
||||
const globalBudget = eventMode ? screeningLimit : Math.ceil(screeningLimit * 0.8);
|
||||
const anchoredBudget = screeningLimit - globalBudget;
|
||||
const perStrategy = Math.ceil(globalBudget / 4);
|
||||
const strategies = [
|
||||
{ scoreCard: roughCardScore },
|
||||
{ scoreCard: roughSkillScore },
|
||||
{ scoreCard: roughStatScore },
|
||||
{ scoreCard: roughStatScore, scoreCombination: roughCombinationScore }
|
||||
];
|
||||
const strategies = eventMode
|
||||
? [
|
||||
{ scoreCard: roughCardScore },
|
||||
{ scoreCard: roughStatScore, scoreCombination: roughCombinationScore },
|
||||
{ scoreCard: roughCardScore, scoreCombination: roughEventBonusPriority }
|
||||
]
|
||||
: [
|
||||
{ scoreCard: roughCardScore },
|
||||
{ scoreCard: roughSkillScore },
|
||||
{ scoreCard: roughStatScore },
|
||||
{ scoreCard: roughStatScore, scoreCombination: roughCombinationScore }
|
||||
];
|
||||
const perStrategy = Math.ceil(globalBudget / strategies.length);
|
||||
const combined = new Map<string, CardRow[]>();
|
||||
for (const strategy of strategies) {
|
||||
for (const cards of generateCombinationCandidates(
|
||||
@@ -579,6 +631,7 @@
|
||||
combined.set(combinationKey(cards), cards);
|
||||
}
|
||||
}
|
||||
if (eventMode) return Array.from(combined.values()).slice(0, screeningLimit);
|
||||
const fixedCharacters = new Set(fixedCards.map((card) => card.characterId));
|
||||
const anchorCount = candidateCards.filter((card) => !fixedCharacters.has(card.characterId)).length;
|
||||
const perAnchorLimit = Math.max(1, Math.ceil(anchoredBudget / Math.max(1, anchorCount)));
|
||||
@@ -597,6 +650,7 @@
|
||||
function selectExactFinalists(prepared: PreparedCandidate[]): PreparedCandidate[] {
|
||||
if (fixedCards.length === 4) return prepared;
|
||||
const { baseExactFinalistLimit } = combinationSearchLimits(fixedCards.length);
|
||||
const finalistLimit = eventMode ? Math.min(baseExactFinalistLimit, 96) : baseExactFinalistLimit;
|
||||
const selected = new Map<string, PreparedCandidate>();
|
||||
const add = (candidates: PreparedCandidate[], count: number) => {
|
||||
for (const candidate of candidates.slice(0, count)) selected.set(combinationKey(candidate.cards), candidate);
|
||||
@@ -606,23 +660,23 @@
|
||||
(candidate) => candidate.cards,
|
||||
(candidate) => candidate.heuristic,
|
||||
fixedCardIds,
|
||||
Math.ceil(baseExactFinalistLimit / 2)
|
||||
Math.ceil(finalistLimit / 2)
|
||||
);
|
||||
add(diverse, diverse.length);
|
||||
const remainingLimit = Math.max(0, baseExactFinalistLimit - selected.size);
|
||||
const remainingLimit = Math.max(0, finalistLimit - selected.size);
|
||||
const heuristicLimit = Math.ceil(remainingLimit / 2);
|
||||
const skillLimit = Math.ceil(remainingLimit / 4);
|
||||
const powerLimit = remainingLimit - heuristicLimit - skillLimit;
|
||||
add([...prepared].sort((left, right) => right.heuristic - left.heuristic), heuristicLimit);
|
||||
add([...prepared].sort((left, right) => right.skillPotential - left.skillPotential), skillLimit);
|
||||
add([...prepared].sort((left, right) => right.overallPower - left.overallPower), powerLimit);
|
||||
if (selected.size < baseExactFinalistLimit) {
|
||||
if (selected.size < finalistLimit) {
|
||||
for (const candidate of [...prepared].sort((left, right) => right.heuristic - left.heuristic)) {
|
||||
selected.set(combinationKey(candidate.cards), candidate);
|
||||
if (selected.size >= baseExactFinalistLimit) break;
|
||||
if (selected.size >= finalistLimit) break;
|
||||
}
|
||||
}
|
||||
return Array.from(selected.values()).slice(0, baseExactFinalistLimit);
|
||||
return Array.from(selected.values()).slice(0, finalistLimit);
|
||||
}
|
||||
|
||||
function buildRefinementFinalists(rankings: RankedResult[]): PreparedCandidate[] {
|
||||
@@ -658,12 +712,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
const refinementLimit = Math.max(alternativeExactFinalistLimit, topResultNeighbors.size);
|
||||
const refinementLimit = eventMode
|
||||
? Math.min(alternativeExactFinalistLimit, 48)
|
||||
: Math.max(alternativeExactFinalistLimit, topResultNeighbors.size);
|
||||
const rankedAlternatives = Array.from(alternatives.values()).sort((left, right) =>
|
||||
right.roughScore - left.roughScore || combinationKey(left.cards).localeCompare(combinationKey(right.cards)));
|
||||
const chosenAlternatives = new Map<string, { cards: CardRow[]; roughScore: number }>();
|
||||
for (const candidate of topResultNeighbors.values()) {
|
||||
const rankedTopNeighbors = [...topResultNeighbors.values()].sort((left, right) => right.roughScore - left.roughScore);
|
||||
for (const candidate of rankedTopNeighbors) {
|
||||
chosenAlternatives.set(combinationKey(candidate.cards), candidate);
|
||||
if (chosenAlternatives.size >= refinementLimit) break;
|
||||
}
|
||||
for (const candidate of secondaryPriority.values()) {
|
||||
chosenAlternatives.set(combinationKey(candidate.cards), candidate);
|
||||
@@ -721,13 +779,26 @@
|
||||
card.special.scoreUpPercent * card.special.durationSeconds / 100 +
|
||||
(card.scoreSupportPercent ?? 0) +
|
||||
(card.passiveScoreSupportEffects ?? []).reduce((effectSum, effect) => effectSum + effect.percent * (effect.target === 'all' ? 5 : effect.targetCount ?? 1), 0), 0);
|
||||
const eventBonus = currentEvent
|
||||
? eventBonusBreakdown(currentEvent, cards, (card) => awakeningForCard(card as CardRow))
|
||||
: null;
|
||||
const eventSongScoreBonus = currentEvent
|
||||
? eventChallengeSongScoreBonusPercent(currentEvent, selectedSongId, cards.map((card) => card.id))
|
||||
: 0;
|
||||
const directSongScoreBonusPercent = boardEffects.directSongScoreBonusPercent + eventSongScoreBonus;
|
||||
const rankingMultiplier = eventBonus ? 1 + eventBonus.totalPercent / 100 : undefined;
|
||||
return {
|
||||
cards: optimizerCards,
|
||||
overallPower: power.overallPower,
|
||||
perfectNoteScore: perfectNoteScoreFromPower(power.overallPower, selectedChart?.scoreRatio ?? null),
|
||||
directSongScoreBonusPercent: boardEffects.directSongScoreBonusPercent,
|
||||
heuristic: power.overallPower * (1 + skillPotential / 500),
|
||||
skillPotential
|
||||
directSongScoreBonusPercent,
|
||||
heuristic: power.overallPower * (1 + skillPotential / 500) * (rankingMultiplier ?? 1) * (1 + eventSongScoreBonus / 100),
|
||||
skillPotential,
|
||||
...(eventBonus ? {
|
||||
eventBonusPercent: eventBonus.totalPercent,
|
||||
rankingMultiplier,
|
||||
optimizationObjective: 'expected' as const
|
||||
} : {})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -827,13 +898,11 @@
|
||||
for (const ranking of rankings) {
|
||||
const key = [...ranking.result.order].sort().join('|');
|
||||
const previous = bestByCombination.get(key);
|
||||
if (!previous || ranking.result.theoreticalMaxScore > previous.result.theoreticalMaxScore ||
|
||||
(ranking.result.theoreticalMaxScore === previous.result.theoreticalMaxScore && ranking.result.expectedScore > previous.result.expectedScore)) {
|
||||
if (!previous || compareRankedResults(ranking, previous) < 0) {
|
||||
bestByCombination.set(key, ranking);
|
||||
}
|
||||
}
|
||||
rankedResults = Array.from(bestByCombination.values()).sort((left, right) =>
|
||||
right.result.theoreticalMaxScore - left.result.theoreticalMaxScore || right.result.expectedScore - left.result.expectedScore).slice(0, 10);
|
||||
rankedResults = Array.from(bestByCombination.values()).sort(compareRankedResults).slice(0, 10);
|
||||
result = rankedResults[0]?.result ?? null;
|
||||
if (rankedResults.length > 0 && activeSearchChart && worker && rankedResults.every((ranking) => ranking.cards)) {
|
||||
searchPhase = 'probability';
|
||||
@@ -908,8 +977,7 @@
|
||||
completedWorkers += 1;
|
||||
parallelWorker.terminate();
|
||||
if (completedWorkers !== workerCount) return;
|
||||
completedRankings.sort((left, right) => right.result.theoreticalMaxScore - left.result.theoreticalMaxScore ||
|
||||
right.result.expectedScore - left.result.expectedScore);
|
||||
completedRankings.sort(compareRankedResults);
|
||||
parallelWorkers = [];
|
||||
completeExactPass(completedRankings.slice(0, 10));
|
||||
}
|
||||
@@ -1022,22 +1090,27 @@
|
||||
unique.set(displayedCompositionKey(baseline), baseline);
|
||||
}
|
||||
return Array.from(unique.values())
|
||||
.sort((left, right) => right.result.theoreticalMaxScore - left.result.theoreticalMaxScore ||
|
||||
right.result.expectedScore - left.result.expectedScore || Number(right.isBaseline) - Number(left.isBaseline))
|
||||
.sort((left, right) => compareRankedResults(left, right) || Number(right.isBaseline) - Number(left.isBaseline))
|
||||
.slice(0, 10);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head><title>조합 탐색 · 홀로도리 계산기</title></svelte:head>
|
||||
<svelte:head><title>{eventMode ? '이벤트 Pt 최적화' : '조합 탐색'} · 홀로도리 계산기</title></svelte:head>
|
||||
|
||||
<main>
|
||||
<header>
|
||||
<h1>조합 탐색</h1>
|
||||
<p class="page-description">악곡과 고정 멤버 조건을 바탕으로 최고 점수 조합과 스페셜 발동 순서를 함께 찾습니다.</p>
|
||||
<h1>{eventMode ? '이벤트 Pt 최적화' : '조합 탐색'}</h1>
|
||||
<p class="page-description">{eventMode ? '진행 중인 이벤트의 편성·신규 카드·개화 보너스를 반영해 예상 이벤트 Pt가 높은 조합과 스페셜 순서를 찾습니다.' : '악곡과 고정 멤버 조건을 바탕으로 최고 점수 조합과 스페셜 발동 순서를 함께 찾습니다.'}</p>
|
||||
{#if eventMode && currentEvent}
|
||||
<div class="event-summary" role="status">
|
||||
<strong>{currentEvent.name}</strong>
|
||||
<span>대상 멤버 1명당 +{currentEvent.formationBonusPercent}% · 신규 카드 추가 +{currentEvent.featuredCardBonusPercent}% · 과제곡 대응 카드 스코어 +{currentEvent.challengeSongScoreBonusPercent}%</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if selectedChart && selectedChart.scoreCalibrationSource !== 'measured'}
|
||||
<p class="calibration-warning" role="status"><strong>추정 점수 안내</strong> 이 악곡의 점수 계수는 실측값이 아니므로 실제 점수와 오차가 크게 발생할 수 있습니다.</p>
|
||||
{/if}
|
||||
<p class="baseline-guide"><strong>기준 조합</strong>은 배치 분석 결과가 저장되어 있고, 저장된 악곡과 난이도가 현재 탐색 조건과 같을 때 표시됩니다.</p>
|
||||
{#if !eventMode}<p class="baseline-guide"><strong>기준 조합</strong>은 배치 분석 결과가 저장되어 있고, 저장된 악곡과 난이도가 현재 탐색 조건과 같을 때 표시됩니다.</p>{/if}
|
||||
</header>
|
||||
{#if baselineState && baselineMatchesConditions}
|
||||
<section class="panel baseline-panel" aria-labelledby="baseline-title">
|
||||
@@ -1066,7 +1139,7 @@
|
||||
</section>
|
||||
{/if}
|
||||
<section class="panel" aria-labelledby="conditions-title">
|
||||
<div class="section-title"><div><span>01</span><h2 id="conditions-title">탐색 조건</h2></div><strong>{ownedCardsOnly ? '보유 5성 대상' : '5성 전체 대상'}</strong></div>
|
||||
<div class="section-title"><div><span>01</span><h2 id="conditions-title">탐색 조건</h2></div><strong>{ownedCardsOnly ? (eventMode ? '보유 이벤트 후보 대상' : '보유 5성 대상') : (eventMode ? '5성 + 대상 멤버 전 레어리티' : '5성 전체 대상')}</strong></div>
|
||||
<div class="song-fields">
|
||||
<div class="song-picker-field"><SongPicker {charts} {difficulty} bind:value={selectedSongId} on:change={() => result = null} /></div>
|
||||
<label>난이도<select bind:value={difficulty} on:change={() => result = null}><option value="expert">Expert</option><option value="hard">Hard</option></select></label>
|
||||
@@ -1090,7 +1163,7 @@
|
||||
<strong>내가 보유한 카드만 사용하기</strong>
|
||||
</label>
|
||||
<p class="candidate-guide">
|
||||
‘내가 보유한 카드만 사용하기’를 켜면 고정 카드와 자동 후보 모두 보유 5성 카드로 제한됩니다.<br>
|
||||
‘내가 보유한 카드만 사용하기’를 켜면 고정 카드와 자동 후보 모두 {eventMode ? '보유한 이벤트 후보 카드' : '보유 5성 카드'}로 제한됩니다.<br>
|
||||
보유 카드의 개화가 자동 후보 개화보다 높으면 실제 보유 개화를 우선 적용합니다.<br>
|
||||
기수 보너스는 녹색 노드를 활성화해 얻을 수 있는 보너스를 의미합니다.
|
||||
</p>
|
||||
@@ -1177,8 +1250,9 @@
|
||||
{/each}
|
||||
</span>
|
||||
<span class="rank-metric">
|
||||
<small>최고 점수</small>
|
||||
<strong>{ranking.result.theoreticalMaxScore.toLocaleString('ko-KR')}</strong>
|
||||
<small>{eventMode ? '예상 이벤트 Pt 지수' : '최고 점수'}</small>
|
||||
<strong>{eventMode ? eventPointIndex(ranking.result.expectedScore, ranking.eventBonusPercent ?? 0).toLocaleString('ko-KR') : ranking.result.theoreticalMaxScore.toLocaleString('ko-KR')}</strong>
|
||||
{#if eventMode}<i>획득량 +{ranking.eventBonusPercent ?? 0}% · 예상 점수 {ranking.result.expectedScore.toLocaleString('ko-KR')}</i>{/if}
|
||||
{#if comparableToBaseline && !ranking.isBaseline}<i>{scoreDelta(ranking.result.theoreticalMaxScore, baselineResult!.theoreticalMaxScore)}</i>{/if}
|
||||
<b class:complete={displayedAdditionalAwakenings(ranking) === 0}>추가 개화 {displayedAdditionalAwakenings(ranking)}회</b>
|
||||
</span>
|
||||
@@ -1203,6 +1277,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
<p class="awakening-guide">추가 개화는 현재 카드 데이터와 조합의 목표 개화를 비교한 값입니다. 미보유 카드를 처음 얻는 과정도 1회로 계산합니다.</p>
|
||||
{#if eventMode}<p class="event-point-guide">이벤트 Pt 지수는 예상 점수에 편성·신규 카드·개화 획득 배율을 적용한 비교값입니다. 게임 서버의 최종 반올림과 멀티 라이브 보정에 따라 실제 획득 Pt는 달라질 수 있습니다.</p>{/if}
|
||||
<p class="probability-guide">달성 확률은 모든 노트를 PERFECT로 처리한다고 가정하고 액티브 발동 확률만 반영합니다. 100%는 정확 계산, 99%·95% 이상은 25,000회 반복 계산값이며 최고점에 영향을 주지 않는 발동 실패는 100% 달성 확률을 낮추지 않습니다.</p>
|
||||
{:else}
|
||||
<div class="empty">고정할 카드와 악곡을 선택한 뒤 조합 탐색을 시작해 주세요.</div>
|
||||
@@ -1219,6 +1294,9 @@
|
||||
.calibration-warning strong { margin-right: 5px; color: #f1a3c4; }
|
||||
.baseline-guide { width: fit-content; margin: 10px 0 0; padding: 8px 11px; border-radius: 8px; color: #8d96a5; background: #1b2029; font-size: 13px; line-height: 1.55; }
|
||||
.baseline-guide strong { color: #dba0b9; }
|
||||
.event-summary { display: grid; gap: 5px; width: fit-content; margin-top: 12px; padding: 11px 13px; border: 1px solid #604653; border-radius: 10px; background: #261d23; }
|
||||
.event-summary strong { color: #f2a6c6; font-size: 14px; }
|
||||
.event-summary span { color: #c9b4bd; font-size: 12px; line-height: 1.5; }
|
||||
.panel { margin-top: 16px; padding: 24px; border: 1px solid #2a303a; border-radius: 16px; background: #171b22; }
|
||||
.section-title, .section-title > div { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||||
.section-title > div { justify-content: flex-start; }
|
||||
@@ -1324,6 +1402,7 @@
|
||||
.rank-probabilities strong { color: #dce1e8; font-size: 14px; line-height: 1.2; }
|
||||
.rank-probabilities i { color: #7f8998; font-size: 10px; font-style: normal; font-weight: 700; }
|
||||
.awakening-guide, .probability-guide { margin: 11px 2px 0; color: #7f8998; font-size: 12px; line-height: 1.55; }
|
||||
.event-point-guide { margin: 5px 2px 0; color: #d1a5b8; font-size: 12px; line-height: 1.55; }
|
||||
.awakening-guide { color: #aa8797; }
|
||||
.awakening-guide + .probability-guide { margin-top: 5px; }
|
||||
.ranking-detail { padding: 0 16px 18px; border-top: 1px solid #2b313c; }
|
||||
|
||||
Reference in New Issue
Block a user