feat: compare top five cooldown combinations in analysis
Publish GitHub Pages output / publish (push) Successful in 42s
Publish GitHub Pages output / publish (push) Successful in 42s
This commit is contained in:
+5
-1
@@ -1,9 +1,13 @@
|
|||||||
---
|
---
|
||||||
updatedAt: 2026-09-25T11:16:00Z
|
updatedAt: 2026-09-25T14:08:52Z
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2026년 9월 25일
|
## 2026년 9월 25일
|
||||||
|
|
||||||
|
### 배치 분석 빈도 조합 비교
|
||||||
|
|
||||||
|
`IMPROVED` 서로 다른 빈도 감소 조합을 최고점 내림차순으로 최대 5개 표시합니다. 각 조합의 예상 점수와 고점 달성 확률을 비교하고, 원하는 결과를 선택해 상세 점수와 그래프를 확인할 수 있습니다. 점수·확률 열의 최댓값은 공동 최댓값을 포함해 굵게 표시합니다.
|
||||||
|
|
||||||
### 64. 악곡과 이벤트 과제곡 선택
|
### 64. 악곡과 이벤트 과제곡 선택
|
||||||
|
|
||||||
`DATA` 아냐 멜핏사의 솔로곡 64.를 추가했습니다. Easy·Normal·Hard·Expert의 정확한 채보와 곡별 점수 계산을 지원합니다.
|
`DATA` 아냐 멜핏사의 솔로곡 64.를 추가했습니다. Easy·Normal·Hard·Expert의 정확한 채보와 곡별 점수 계산을 지원합니다.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { optimizeExactOrderAndCooldowns } from './exact-optimizer.ts';
|
import { optimizeExactOrderAndCooldowns, rankExactCooldownCombinations } from './exact-optimizer.ts';
|
||||||
import { passiveScoreSupportByCard } from './passive-support.ts';
|
import { passiveScoreSupportByCard } from './passive-support.ts';
|
||||||
import type { ExactSongChart, OptimizerCard } from './types.ts';
|
import type { ExactSongChart, OptimizerCard } from './types.ts';
|
||||||
|
|
||||||
@@ -21,6 +21,27 @@ const chart: ExactSongChart = {
|
|||||||
specialStartSeconds: [1, 2, 3, 4, 5]
|
specialStartSeconds: [1, 2, 3, 4, 5]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
test('빈도 조합별 최적 순서를 최고점 순으로 최대 5개 반환한다', () => {
|
||||||
|
const flexible = cards.map((card, index) => ({ ...card, allowedCooldownReductions: index < 3 ? [0, 4] as const : [0] as const }));
|
||||||
|
const exhaustive = [0, 4].flatMap((a) => [0, 4].flatMap((b) => [0, 4].map((c) =>
|
||||||
|
optimizeExactOrderAndCooldowns(cards.map((card, index) => ({
|
||||||
|
...card, allowedCooldownReductions: [[a, b, c, 0, 0][index] as 0 | 4]
|
||||||
|
})), chart, { perfectNoteScore: 100 })
|
||||||
|
))).sort((left, right) => right.theoreticalMaxScore - left.theoreticalMaxScore ||
|
||||||
|
right.expectedScore - left.expectedScore ||
|
||||||
|
Object.values(left.cooldownReductions).reduce<number>((a, b) => a + b, 0) - Object.values(right.cooldownReductions).reduce<number>((a, b) => a + b, 0));
|
||||||
|
const ranked = rankExactCooldownCombinations(flexible, chart, { perfectNoteScore: 100 });
|
||||||
|
assert.equal(ranked.length, 5);
|
||||||
|
assert.equal(new Set(ranked.map((result) => JSON.stringify(result.cooldownReductions))).size, 5);
|
||||||
|
ranked.forEach((result, index) => {
|
||||||
|
assert.equal(result.theoreticalMaxScore, exhaustive[index].theoreticalMaxScore);
|
||||||
|
assert.deepEqual(result.cooldownReductions, exhaustive[index].cooldownReductions);
|
||||||
|
assert.deepEqual(result.order, exhaustive[index].order);
|
||||||
|
assert.equal(result.evaluatedCandidates, 120 * 8);
|
||||||
|
});
|
||||||
|
assert.equal(rankExactCooldownCombinations(cards, chart, { perfectNoteScore: 100 }).length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
test('정확 분석 결과에 최적 조합의 액티브와 스페셜 지속 구간을 포함한다', () => {
|
test('정확 분석 결과에 최적 조합의 액티브와 스페셜 지속 구간을 포함한다', () => {
|
||||||
const result = optimizeExactOrderAndCooldowns(cards, chart, { perfectNoteScore: 100 });
|
const result = optimizeExactOrderAndCooldowns(cards, chart, { perfectNoteScore: 100 });
|
||||||
|
|
||||||
|
|||||||
@@ -226,6 +226,16 @@ export function optimizeExactOrderAndCooldowns(
|
|||||||
chart: ExactSongChart,
|
chart: ExactSongChart,
|
||||||
options: ExactOptimizeOptions
|
options: ExactOptimizeOptions
|
||||||
): ExactOptimizationResult {
|
): ExactOptimizationResult {
|
||||||
|
return rankExactCooldownCombinations(cards, chart, options, 1)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Keep the best special order for each distinct per-card cooldown assignment. */
|
||||||
|
export function rankExactCooldownCombinations(
|
||||||
|
cards: readonly OptimizerCard[],
|
||||||
|
chart: ExactSongChart,
|
||||||
|
options: ExactOptimizeOptions,
|
||||||
|
limit = 5
|
||||||
|
): ExactOptimizationResult[] {
|
||||||
if (cards.length !== 5 || new Set(cards.map((card) => card.id)).size !== 5) {
|
if (cards.length !== 5 || new Set(cards.map((card) => card.id)).size !== 5) {
|
||||||
throw new Error('서로 다른 카드 5장이 필요합니다.');
|
throw new Error('서로 다른 카드 5장이 필요합니다.');
|
||||||
}
|
}
|
||||||
@@ -234,46 +244,39 @@ export function optimizeExactOrderAndCooldowns(
|
|||||||
const orderCount = cards.reduce((value, _, index) => value * (index + 1), 1);
|
const orderCount = cards.reduce((value, _, index) => value * (index + 1), 1);
|
||||||
const totalCandidates = orderCount * reductionAssignments.length;
|
const totalCandidates = orderCount * reductionAssignments.length;
|
||||||
let evaluatedCandidates = 0;
|
let evaluatedCandidates = 0;
|
||||||
let bestTheoreticalMaxScore = Number.NEGATIVE_INFINITY;
|
type Candidate = {
|
||||||
let bestExpectedScore = Number.NEGATIVE_INFINITY;
|
order: OptimizerCard[];
|
||||||
let bestOrder: OptimizerCard[] | null = null;
|
reductions: Record<string, CooldownReduction>;
|
||||||
let bestReductions: Record<string, CooldownReduction> | null = null;
|
theoreticalMaxScore: number;
|
||||||
let bestReductionTotal = Number.POSITIVE_INFINITY;
|
expectedScore: number;
|
||||||
|
reductionTotal: number;
|
||||||
|
};
|
||||||
|
const bestByAssignment = new Map<Record<string, CooldownReduction>, Candidate>();
|
||||||
|
const compare = (left: Candidate, right: Candidate): number =>
|
||||||
|
(options.objective === 'expected'
|
||||||
|
? right.expectedScore - left.expectedScore || right.theoreticalMaxScore - left.theoreticalMaxScore
|
||||||
|
: right.theoreticalMaxScore - left.theoreticalMaxScore || right.expectedScore - left.expectedScore) ||
|
||||||
|
left.reductionTotal - right.reductionTotal;
|
||||||
|
|
||||||
for (const order of permutations(cards)) {
|
for (const order of permutations(cards)) {
|
||||||
const profiles = makeProfiles(order, chart);
|
const profiles = makeProfiles(order, chart);
|
||||||
for (const reductions of reductionAssignments) {
|
for (const reductions of reductionAssignments) {
|
||||||
const score = scoreAssignment(order, reductions, profiles, base, options.directSongScoreBonusPercent ?? 0);
|
const score = scoreAssignment(order, reductions, profiles, base, options.directSongScoreBonusPercent ?? 0);
|
||||||
const reductionTotal = Object.values(reductions).reduce<number>((sum, value) => sum + value, 0);
|
const reductionTotal = Object.values(reductions).reduce<number>((sum, value) => sum + value, 0);
|
||||||
const expectedFirst = options.objective === 'expected';
|
const candidate = { ...score, order, reductions, reductionTotal };
|
||||||
const isBetterPrimary = expectedFirst
|
const previous = bestByAssignment.get(reductions);
|
||||||
? score.expectedScore > bestExpectedScore
|
if (!previous || compare(candidate, previous) < 0) bestByAssignment.set(reductions, candidate);
|
||||||
: score.theoreticalMaxScore > bestTheoreticalMaxScore;
|
|
||||||
const isBetterSecondary = expectedFirst
|
|
||||||
? score.expectedScore === bestExpectedScore && score.theoreticalMaxScore > bestTheoreticalMaxScore
|
|
||||||
: score.theoreticalMaxScore === bestTheoreticalMaxScore && score.expectedScore > bestExpectedScore;
|
|
||||||
const isCheaperTie = score.theoreticalMaxScore === bestTheoreticalMaxScore &&
|
|
||||||
score.expectedScore === bestExpectedScore && reductionTotal < bestReductionTotal;
|
|
||||||
if (isBetterPrimary || isBetterSecondary || isCheaperTie) {
|
|
||||||
bestTheoreticalMaxScore = score.theoreticalMaxScore;
|
|
||||||
bestExpectedScore = score.expectedScore;
|
|
||||||
bestOrder = order;
|
|
||||||
bestReductions = reductions;
|
|
||||||
bestReductionTotal = reductionTotal;
|
|
||||||
}
|
|
||||||
evaluatedCandidates += 1;
|
evaluatedCandidates += 1;
|
||||||
}
|
}
|
||||||
options.onProgress?.({ evaluatedCandidates, totalCandidates });
|
options.onProgress?.({ evaluatedCandidates, totalCandidates });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!bestOrder || !bestReductions) throw new Error('분석 결과를 만들지 못했습니다.');
|
return [...bestByAssignment.values()].sort(compare).slice(0, Math.max(1, Math.floor(limit))).map((candidate) => ({
|
||||||
const windows = buildExactSkillWindows(bestOrder, bestReductions, chart);
|
...calculateExactScore(candidate.order, candidate.reductions, chart, options),
|
||||||
return {
|
order: candidate.order.map((card) => card.id),
|
||||||
...calculateExactScore(bestOrder, bestReductions, chart, options),
|
|
||||||
order: bestOrder.map((card) => card.id),
|
|
||||||
formationOrder: cards.map((card) => card.id),
|
formationOrder: cards.map((card) => card.id),
|
||||||
cooldownReductions: { ...bestReductions },
|
cooldownReductions: { ...candidate.reductions },
|
||||||
...windows,
|
...buildExactSkillWindows(candidate.order, candidate.reductions, chart),
|
||||||
evaluatedCandidates
|
evaluatedCandidates
|
||||||
};
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/// <reference lib="webworker" />
|
/// <reference lib="webworker" />
|
||||||
|
|
||||||
import { countOptimizationCandidates, estimateScoreAchievementProbabilities, optimizeExactOrderAndCooldowns } from '../engine/index.ts';
|
import { countOptimizationCandidates, estimateScoreAchievementProbabilities, optimizeExactOrderAndCooldowns } from '../engine/index.ts';
|
||||||
|
import { rankExactCooldownCombinations } from '../engine/exact-optimizer.ts';
|
||||||
import type { ExactOptimizationResult, ExactOptimizeOptions, ExactSongChart, OptimizerCard, ScoreAchievementProbabilities } from '../engine/index.ts';
|
import type { ExactOptimizationResult, ExactOptimizeOptions, ExactSongChart, OptimizerCard, ScoreAchievementProbabilities } from '../engine/index.ts';
|
||||||
|
|
||||||
type OptimizeRequest = {
|
type OptimizeRequest = {
|
||||||
@@ -10,6 +11,7 @@ type OptimizeRequest = {
|
|||||||
perfectNoteScore: number;
|
perfectNoteScore: number;
|
||||||
directSongScoreBonusPercent?: number;
|
directSongScoreBonusPercent?: number;
|
||||||
includeAchievementProbabilities?: boolean;
|
includeAchievementProbabilities?: boolean;
|
||||||
|
cooldownResultLimit?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type CombinationCandidate = {
|
type CombinationCandidate = {
|
||||||
@@ -77,19 +79,19 @@ self.onmessage = (event: MessageEvent<OptimizeRequest | CombinationRequest | Ach
|
|||||||
directSongScoreBonusPercent: event.data.directSongScoreBonusPercent,
|
directSongScoreBonusPercent: event.data.directSongScoreBonusPercent,
|
||||||
onProgress: (progress) => self.postMessage({ type: 'progress', progress })
|
onProgress: (progress) => self.postMessage({ type: 'progress', progress })
|
||||||
};
|
};
|
||||||
const result = optimizeExactOrderAndCooldowns(event.data.cards, event.data.chart, options);
|
const request = event.data;
|
||||||
let achievementProbabilities: ScoreAchievementProbabilities | undefined;
|
const results = rankExactCooldownCombinations(request.cards, request.chart, options, request.cooldownResultLimit === 5 ? 5 : 1);
|
||||||
if (event.data.includeAchievementProbabilities) {
|
if (request.includeAchievementProbabilities) self.postMessage({ type: 'probability-start' });
|
||||||
self.postMessage({ type: 'probability-start' });
|
const cooldownRankings = results.map((result) => {
|
||||||
const orderedCards = cardsForResult(event.data.cards, result);
|
const achievementProbabilities = request.includeAchievementProbabilities ? estimateScoreAchievementProbabilities(
|
||||||
achievementProbabilities = estimateScoreAchievementProbabilities(
|
cardsForResult(request.cards, result),
|
||||||
orderedCards,
|
|
||||||
result.cooldownReductions,
|
result.cooldownReductions,
|
||||||
event.data.chart,
|
request.chart,
|
||||||
{ ...options, sampleCount: 25_000 }
|
{ ...options, sampleCount: 25_000 }
|
||||||
);
|
) : undefined;
|
||||||
}
|
return { result, achievementProbabilities };
|
||||||
self.postMessage({ type: 'result', result, achievementProbabilities });
|
});
|
||||||
|
self.postMessage({ type: 'result', ...cooldownRankings[0], cooldownRankings });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -118,6 +118,23 @@
|
|||||||
let running = false;
|
let running = false;
|
||||||
let progress: OptimizationProgress = { evaluatedCandidates: 0, totalCandidates: 0 };
|
let progress: OptimizationProgress = { evaluatedCandidates: 0, totalCandidates: 0 };
|
||||||
let result: ExactOptimizationResult | null = null;
|
let result: ExactOptimizationResult | null = null;
|
||||||
|
let cooldownRankings: { result: ExactOptimizationResult; achievementProbabilities?: ScoreAchievementProbabilities }[] = [];
|
||||||
|
$: cooldownColumnMaxima = {
|
||||||
|
score: Math.max(...cooldownRankings.map(({ result }) => result.theoreticalMaxScore)),
|
||||||
|
expected: Math.max(...cooldownRankings.map(({ result }) => result.expectedScore)),
|
||||||
|
maximum: Math.max(...cooldownRankings.map(({ achievementProbabilities: p }) => p?.maximum ?? -Infinity)),
|
||||||
|
atLeast995Percent: Math.max(...cooldownRankings.map(({ achievementProbabilities: p }) => p?.atLeast995Percent ?? -Infinity)),
|
||||||
|
atLeast99Percent: Math.max(...cooldownRankings.map(({ achievementProbabilities: p }) => p?.atLeast99Percent ?? -Infinity)),
|
||||||
|
atLeast95Percent: Math.max(...cooldownRankings.map(({ achievementProbabilities: p }) => p?.atLeast95Percent ?? -Infinity))
|
||||||
|
};
|
||||||
|
|
||||||
|
function selectCooldownRanking(index: number): void {
|
||||||
|
const ranking = cooldownRankings[index];
|
||||||
|
if (!ranking || running) return;
|
||||||
|
result = ranking.result;
|
||||||
|
achievementProbabilities = ranking.achievementProbabilities;
|
||||||
|
clearConnectionOptimizationProposal();
|
||||||
|
}
|
||||||
type SimulationContext = { cards: OptimizerCard[]; chart: ExactSongChart; options: ExactScoreOptions };
|
type SimulationContext = { cards: OptimizerCard[]; chart: ExactSongChart; options: ExactScoreOptions };
|
||||||
let simulationContext: SimulationContext | null = null;
|
let simulationContext: SimulationContext | null = null;
|
||||||
let pendingSimulationContext: SimulationContext | null = null;
|
let pendingSimulationContext: SimulationContext | null = null;
|
||||||
@@ -393,6 +410,7 @@
|
|||||||
if (event.data.type === 'progress') progress = event.data.progress;
|
if (event.data.type === 'progress') progress = event.data.progress;
|
||||||
if (event.data.type === 'result') {
|
if (event.data.type === 'result') {
|
||||||
result = event.data.result;
|
result = event.data.result;
|
||||||
|
cooldownRankings = event.data.cooldownRankings ?? [];
|
||||||
transferredResult = false;
|
transferredResult = false;
|
||||||
resultCapture = pendingCapture;
|
resultCapture = pendingCapture;
|
||||||
resultInputKey = pendingInputKey;
|
resultInputKey = pendingInputKey;
|
||||||
@@ -864,7 +882,8 @@
|
|||||||
chart,
|
chart,
|
||||||
perfectNoteScore: noteScore,
|
perfectNoteScore: noteScore,
|
||||||
directSongScoreBonusPercent: songBonus,
|
directSongScoreBonusPercent: songBonus,
|
||||||
includeAchievementProbabilities: true
|
includeAchievementProbabilities: true,
|
||||||
|
cooldownResultLimit: mode === 'analysis' ? 5 : 1
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
running = false;
|
running = false;
|
||||||
@@ -1142,6 +1161,31 @@
|
|||||||
{#if mode === 'analysis' && resultCapture}<p class="board-guide"><strong>{resultCapture.title} · {difficultyLabel(resultCapture.settings.difficulty)}</strong> · 분석 당시 종합력 {resultCapture.overallPower.toLocaleString('ko-KR')}</p>{/if}
|
{#if mode === 'analysis' && resultCapture}<p class="board-guide"><strong>{resultCapture.title} · {difficultyLabel(resultCapture.settings.difficulty)}</strong> · 분석 당시 종합력 {resultCapture.overallPower.toLocaleString('ko-KR')}</p>{/if}
|
||||||
{#if mode === 'rating' && result}<p class="board-guide"><strong>{selectedChart?.title} · {difficultyLabel(difficulty)}</strong> · 종합력 {overallPower.toLocaleString('ko-KR')} · 가창 보너스 10%</p>{/if}
|
{#if mode === 'rating' && result}<p class="board-guide"><strong>{selectedChart?.title} · {difficultyLabel(difficulty)}</strong> · 종합력 {overallPower.toLocaleString('ko-KR')} · 가창 보너스 10%</p>{/if}
|
||||||
{#if result}
|
{#if result}
|
||||||
|
{#if mode === 'analysis' && cooldownRankings.length > 0}
|
||||||
|
<section class="cooldown-rankings" aria-labelledby="cooldown-rankings-title">
|
||||||
|
<h3 id="cooldown-rankings-title">빈도 조합 상위 {cooldownRankings.length}개</h3>
|
||||||
|
<p class="probability-guide">빈도 감소 조합마다 최적의 스페셜 순서를 구해 최고점 내림차순으로 표시합니다. 확률은 각 조합의 최고점 기준이며, 결과를 선택하면 아래 상세 점수와 그래프에 반영됩니다.</p>
|
||||||
|
<div class="cooldown-table-scroll">
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>순위</th><th>카드별 빈도 감소</th><th>최고점</th><th>예상 점수</th><th>100% 달성</th><th>99.5% 이상</th><th>99% 이상</th><th>95% 이상</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{#each cooldownRankings as ranking, index}
|
||||||
|
<tr class:selected={result === ranking.result}>
|
||||||
|
<td><button class="cooldown-select" type="button" aria-label={`${index + 1}위 빈도 조합 선택`} aria-pressed={result === ranking.result} disabled={running} on:click={() => selectCooldownRanking(index)}><span class="cooldown-rank">{index + 1}위</span><span class="cooldown-selection"><span class="cooldown-selection-dot" aria-hidden="true"></span>{result === ranking.result ? '선택됨' : '선택'}</span></button></td>
|
||||||
|
<td>{#each ranking.result.formationOrder as cardId}<div>{resultCardName(cardId)} <strong>{ranking.result.cooldownReductions[cardId]}%</strong></div>{/each}</td>
|
||||||
|
<td><span class:column-maximum={ranking.result.theoreticalMaxScore === cooldownColumnMaxima.score}>{ranking.result.theoreticalMaxScore.toLocaleString('ko-KR')}</span></td>
|
||||||
|
<td><span class:column-maximum={ranking.result.expectedScore === cooldownColumnMaxima.expected}>{ranking.result.expectedScore.toLocaleString('ko-KR')}</span></td>
|
||||||
|
<td><span class:column-maximum={ranking.achievementProbabilities?.maximum === cooldownColumnMaxima.maximum}>{probabilityLabel(ranking.achievementProbabilities?.maximum)}</span></td>
|
||||||
|
<td><span class:column-maximum={ranking.achievementProbabilities?.atLeast995Percent === cooldownColumnMaxima.atLeast995Percent}>{probabilityLabel(ranking.achievementProbabilities?.atLeast995Percent)}</span><small>{scoreThreshold(ranking.result.theoreticalMaxScore, 0.995)}점 이상</small></td>
|
||||||
|
<td><span class:column-maximum={ranking.achievementProbabilities?.atLeast99Percent === cooldownColumnMaxima.atLeast99Percent}>{probabilityLabel(ranking.achievementProbabilities?.atLeast99Percent)}</span><small>{scoreThreshold(ranking.result.theoreticalMaxScore, 0.99)}점 이상</small></td>
|
||||||
|
<td><span class:column-maximum={ranking.achievementProbabilities?.atLeast95Percent === cooldownColumnMaxima.atLeast95Percent}>{probabilityLabel(ranking.achievementProbabilities?.atLeast95Percent)}</span><small>{scoreThreshold(ranking.result.theoreticalMaxScore, 0.95)}점 이상</small></td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
<div class="order-sections">
|
<div class="order-sections">
|
||||||
<section class="order-section">
|
<section class="order-section">
|
||||||
<h3>추천 스페셜 발동 순서</h3>
|
<h3>추천 스페셜 발동 순서</h3>
|
||||||
@@ -1333,6 +1377,22 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.cooldown-rankings { margin-bottom: 24px; min-width: 0; }
|
||||||
|
.cooldown-rankings .column-maximum { font-weight: 800; }
|
||||||
|
.cooldown-table-scroll { overflow-x: auto; margin-top: 12px; }
|
||||||
|
.cooldown-rankings table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||||
|
.cooldown-rankings th, .cooldown-rankings td { padding: 12px; text-align: left; border-bottom: 1px solid #334155; white-space: nowrap; }
|
||||||
|
.cooldown-rankings tr.selected { background: #2d2530; }
|
||||||
|
.cooldown-rankings small { display: block; color: #9caaba; margin-top: 4px; }
|
||||||
|
.cooldown-select { display: grid; justify-items: center; gap: 6px; min-width: 76px; min-height: 64px; padding: 10px 12px; border: 1px solid #414b5b; border-radius: 8px; background: #202630; color: #c1c8d3; font: inherit; cursor: pointer; }
|
||||||
|
.cooldown-rank { font-size: 15px; font-weight: 800; }
|
||||||
|
.cooldown-selection { display: flex; align-items: center; gap: 5px; font-size: 11px; }
|
||||||
|
.cooldown-selection-dot { width: 10px; height: 10px; border: 1px solid currentColor; border-radius: 50%; box-sizing: border-box; }
|
||||||
|
.cooldown-select:hover:not(:disabled) { border-color: #df74a2; color: #f5afcc; background: #2d2530; }
|
||||||
|
.cooldown-select[aria-pressed='true'] { border-color: #ed8bb5; color: #f5afcc; background: #37232e; }
|
||||||
|
.cooldown-select[aria-pressed='true'] .cooldown-selection-dot { background: #ed8bb5; border-color: #ed8bb5; box-shadow: inset 0 0 0 2px #37232e; }
|
||||||
|
.cooldown-select:focus-visible { outline: 2px solid #f1a0c2; outline-offset: 3px; }
|
||||||
|
.cooldown-select:disabled { opacity: .55; cursor: default; }
|
||||||
.mobile-field-label, .selected-card-name { display: none; }
|
.mobile-field-label, .selected-card-name { display: none; }
|
||||||
.active-window-list { margin: 12px 0; padding: 12px; border: 1px solid #343c49; border-radius: 10px; }
|
.active-window-list { margin: 12px 0; padding: 12px; border: 1px solid #343c49; border-radius: 10px; }
|
||||||
.active-window-list summary { min-height: 44px; align-content: center; cursor: pointer; font-size: 14px; }
|
.active-window-list summary { min-height: 44px; align-content: center; cursor: pointer; font-size: 14px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user