feat: release version 1.0.4
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "holodream-tools",
|
"name": "holodream-tools",
|
||||||
"version": "1.0.3",
|
"version": "1.0.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "holodream-tools",
|
"name": "holodream-tools",
|
||||||
"version": "1.0.3",
|
"version": "1.0.4",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=22.12.0"
|
"node": ">=22.12.0"
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "holodream-tools",
|
"name": "holodream-tools",
|
||||||
"version": "1.0.3",
|
"version": "1.0.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# 홀로도리 계산기 1.0.4
|
||||||
|
|
||||||
|
카드 목록에서 액티브 정보를 더 쉽게 비교하고, 배치 분석과 조합 탐색의 입력 화면을 일관되게 개선한 업데이트입니다.
|
||||||
|
|
||||||
|
## 주요 변경 사항
|
||||||
|
|
||||||
|
- 카드 목록의 각 행에 액티브 주기, 확률, 지속 시간과 효과를 표시합니다.
|
||||||
|
- 액티브 주기, 확률, 지속 시간과 효과를 기준으로 오름차순·내림차순 정렬할 수 있습니다.
|
||||||
|
- 카드 목록 전체를 명함 또는 5개화 상태로 전환해 표시와 정렬 결과를 비교할 수 있습니다.
|
||||||
|
- 카드 목록 출력 조건에 `모두 취소` 기능을 추가했습니다.
|
||||||
|
- 액티브 또는 일부 값이 없는 카드는 `—`로 표시하고 정렬 시 항상 뒤에 배치합니다.
|
||||||
|
- 배치 분석의 카드 선택, 개화와 빈도 감소 입력을 열 헤더 중심의 표 형태로 정리했습니다.
|
||||||
|
- 조합 탐색의 탐색 조건 UI를 배치 분석과 같은 입력 구조와 스타일로 통일했습니다.
|
||||||
|
- 조합 탐색의 기수 보너스 입력을 숫자 `0~900` 범위로 제한했습니다.
|
||||||
|
- 보드 데이터의 활성 노드 수에서 항상 활성화된 가운데 연결 노드를 제외했습니다.
|
||||||
|
|
||||||
|
## 참고 사항
|
||||||
|
|
||||||
|
카드 목록의 정렬 기준, 정렬 방향과 명함·5개화 선택은 사용자 데이터에 저장됩니다. 이전 버전의 사용자 데이터와 백업 파일은 기본값을 자동으로 적용해 그대로 사용할 수 있습니다.
|
||||||
|
|
||||||
|
## 다운로드
|
||||||
|
|
||||||
|
Release의 Assets에서 `HolodoriCalc-offline-v1.0.4.zip`을 내려받아 완전히 압축 해제한 뒤 `index.html`을 실행해 주세요. GitHub가 자동으로 제공하는 Source code 압축파일은 실행용 배포본이 아닙니다.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
import { activeEffectPercent, sortCardsByActive, type CardSummary, type SkillLevel } from '../models/card-list.ts';
|
||||||
|
|
||||||
|
function skill(level: number, values: Partial<SkillLevel> = {}): SkillLevel {
|
||||||
|
return {
|
||||||
|
level,
|
||||||
|
description: '',
|
||||||
|
coolTimeSeconds: null,
|
||||||
|
probabilityPercent: null,
|
||||||
|
durationSeconds: null,
|
||||||
|
...values
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function card(id: string, active?: SkillLevel[]): CardSummary {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
rarity: 5,
|
||||||
|
type: '큐트',
|
||||||
|
name: id,
|
||||||
|
versionName: '',
|
||||||
|
generation: '',
|
||||||
|
highestStat: '퍼포먼스',
|
||||||
|
details: { title: '', passive: [], active, special: [], costume: null }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test('sorts numeric active values in both directions and keeps missing values last', () => {
|
||||||
|
const cards = [
|
||||||
|
card('missing'),
|
||||||
|
card('slow', [skill(1, { coolTimeSeconds: 24 })]),
|
||||||
|
card('fast', [skill(1, { coolTimeSeconds: 18 })])
|
||||||
|
];
|
||||||
|
|
||||||
|
assert.deepEqual(sortCardsByActive(cards, 'coolTime', 'asc', 1).map(({ id }) => id), ['fast', 'slow', 'missing']);
|
||||||
|
assert.deepEqual(sortCardsByActive(cards, 'coolTime', 'desc', 1).map(({ id }) => id), ['slow', 'fast', 'missing']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses the selected skill level and preserves source order for ties', () => {
|
||||||
|
const cards = [
|
||||||
|
card('first', [skill(1, { effect: '스코어가 80% UP' }), skill(2, { effect: '스코어가 120% UP' })]),
|
||||||
|
card('second', [skill(1, { effect: '스코어가 90% UP' }), skill(2, { effect: '스코어가 100% UP' })]),
|
||||||
|
card('same', [skill(1, { effect: '스코어가 90% UP' }), skill(2, { effect: '스코어가 100% UP' })])
|
||||||
|
];
|
||||||
|
|
||||||
|
assert.deepEqual(sortCardsByActive(cards, 'effect', 'desc', 1).map(({ id }) => id), ['second', 'same', 'first']);
|
||||||
|
assert.deepEqual(sortCardsByActive(cards, 'effect', 'desc', 2).map(({ id }) => id), ['first', 'second', 'same']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('falls back to the active description when the separated effect is unavailable', () => {
|
||||||
|
assert.equal(activeEffectPercent(skill(1, { description: '10초 동안 스코어가 85% UP' })), 85);
|
||||||
|
assert.equal(activeEffectPercent(skill(1, { effect: '라이프가 회복됩니다.' })), null);
|
||||||
|
});
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import type { CardListSortKey, SortDirection } from './user-data.ts';
|
||||||
|
|
||||||
|
export type CardType = '큐트' | '퓨어' | '해피';
|
||||||
|
export type HighestStat = '퍼포먼스' | '테크닉' | '센스';
|
||||||
|
|
||||||
|
export type SkillLevel = {
|
||||||
|
level: number;
|
||||||
|
description: string;
|
||||||
|
coolTimeSeconds: number | null;
|
||||||
|
probabilityPercent: number | null;
|
||||||
|
durationSeconds: number | null;
|
||||||
|
effect?: string;
|
||||||
|
condition?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CardDetails = {
|
||||||
|
title: string;
|
||||||
|
passive: SkillLevel[];
|
||||||
|
active?: SkillLevel[];
|
||||||
|
special: SkillLevel[];
|
||||||
|
costume: { name: string; description: string } | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CardSummary = {
|
||||||
|
id: string;
|
||||||
|
rarity: number;
|
||||||
|
type: CardType;
|
||||||
|
name: string;
|
||||||
|
versionName: string;
|
||||||
|
generation: string;
|
||||||
|
highestStat: HighestStat;
|
||||||
|
details: CardDetails;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function skillAtLevel(
|
||||||
|
levels: readonly SkillLevel[] | undefined,
|
||||||
|
selectedLevel: number
|
||||||
|
): SkillLevel | undefined {
|
||||||
|
return levels?.find((level) => level.level === selectedLevel) ?? levels?.at(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function activeEffectPercent(skill: SkillLevel | undefined): number | null {
|
||||||
|
const match = (skill?.effect || skill?.description)?.match(/스코어(?:가|\s+서포트\s+효과)\s*(\d+(?:\.\d+)?)%/);
|
||||||
|
return match ? Number(match[1]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function activeSortValue(
|
||||||
|
card: CardSummary,
|
||||||
|
sortKey: CardListSortKey,
|
||||||
|
selectedLevel: number
|
||||||
|
): number | null {
|
||||||
|
const active = skillAtLevel(card.details.active, selectedLevel);
|
||||||
|
if (!active) return null;
|
||||||
|
|
||||||
|
const value = {
|
||||||
|
coolTime: active.coolTimeSeconds,
|
||||||
|
probability: active.probabilityPercent,
|
||||||
|
duration: active.durationSeconds,
|
||||||
|
effect: activeEffectPercent(active)
|
||||||
|
}[sortKey];
|
||||||
|
|
||||||
|
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortCardsByActive(
|
||||||
|
cards: readonly CardSummary[],
|
||||||
|
sortKey: CardListSortKey | null,
|
||||||
|
sortDirection: SortDirection,
|
||||||
|
selectedLevel: number
|
||||||
|
): CardSummary[] {
|
||||||
|
if (!sortKey) return [...cards];
|
||||||
|
|
||||||
|
const direction = sortDirection === 'asc' ? 1 : -1;
|
||||||
|
return cards
|
||||||
|
.map((card, index) => ({ card, index, value: activeSortValue(card, sortKey, selectedLevel) }))
|
||||||
|
.sort((left, right) => {
|
||||||
|
if (left.value === null) return right.value === null ? left.index - right.index : 1;
|
||||||
|
if (right.value === null) return -1;
|
||||||
|
return (left.value - right.value) * direction || left.index - right.index;
|
||||||
|
})
|
||||||
|
.map(({ card }) => card);
|
||||||
|
}
|
||||||
@@ -12,6 +12,10 @@ export type SavedDeck = {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type CardListSortKey = 'coolTime' | 'probability' | 'duration' | 'effect';
|
||||||
|
export type SortDirection = 'asc' | 'desc';
|
||||||
|
export type CardSkillLevel = 1 | 2;
|
||||||
|
|
||||||
export type CardListPreferences = {
|
export type CardListPreferences = {
|
||||||
types: string[];
|
types: string[];
|
||||||
stats: string[];
|
stats: string[];
|
||||||
@@ -20,6 +24,9 @@ export type CardListPreferences = {
|
|||||||
activeProbabilities: string[];
|
activeProbabilities: string[];
|
||||||
cardVariants: string[];
|
cardVariants: string[];
|
||||||
generations: string[];
|
generations: string[];
|
||||||
|
sortKey: CardListSortKey | null;
|
||||||
|
sortDirection: SortDirection;
|
||||||
|
skillLevel: CardSkillLevel;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BoardCardPlacements = Record<string, Record<string, string>>;
|
export type BoardCardPlacements = Record<string, Record<string, string>>;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CardListPreferences, CollectionCardState, UserData } from '$lib/models/user-data';
|
import type { CardListPreferences, CardListSortKey, CardSkillLevel, CollectionCardState, SortDirection, UserData } from '$lib/models/user-data';
|
||||||
import { createUserData } from '$lib/models/user-data';
|
import { createUserData } from '$lib/models/user-data';
|
||||||
|
|
||||||
export const USER_DATA_STORAGE_KEY = 'holodori:user-data:v1';
|
export const USER_DATA_STORAGE_KEY = 'holodori:user-data:v1';
|
||||||
@@ -17,6 +17,20 @@ function finiteNumber(value: unknown, fallback = 0): number {
|
|||||||
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function cardListSortKey(value: unknown, fallback: CardListSortKey | null): CardListSortKey | null {
|
||||||
|
return value === 'coolTime' || value === 'probability' || value === 'duration' || value === 'effect'
|
||||||
|
? value
|
||||||
|
: fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortDirection(value: unknown, fallback: SortDirection): SortDirection {
|
||||||
|
return value === 'asc' || value === 'desc' ? value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cardSkillLevel(value: unknown, fallback: CardSkillLevel): CardSkillLevel {
|
||||||
|
return value === 1 || value === 2 ? value : fallback;
|
||||||
|
}
|
||||||
|
|
||||||
function parseCollection(value: Record<string, unknown>): UserData['collection'] {
|
function parseCollection(value: Record<string, unknown>): UserData['collection'] {
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(value).flatMap(([cardId, state]) => {
|
Object.entries(value).flatMap(([cardId, state]) => {
|
||||||
@@ -79,7 +93,10 @@ export function parseUserData(text: string, fallbackPreferences: CardListPrefere
|
|||||||
specialConditions: stringArray(preferences.specialConditions, fallbackPreferences.specialConditions),
|
specialConditions: stringArray(preferences.specialConditions, fallbackPreferences.specialConditions),
|
||||||
activeProbabilities: stringArray(preferences.activeProbabilities, fallbackPreferences.activeProbabilities),
|
activeProbabilities: stringArray(preferences.activeProbabilities, fallbackPreferences.activeProbabilities),
|
||||||
cardVariants: stringArray(preferences.cardVariants, fallbackPreferences.cardVariants),
|
cardVariants: stringArray(preferences.cardVariants, fallbackPreferences.cardVariants),
|
||||||
generations: stringArray(preferences.generations, fallbackPreferences.generations)
|
generations: stringArray(preferences.generations, fallbackPreferences.generations),
|
||||||
|
sortKey: cardListSortKey(preferences.sortKey, fallbackPreferences.sortKey),
|
||||||
|
sortDirection: sortDirection(preferences.sortDirection, fallbackPreferences.sortDirection),
|
||||||
|
skillLevel: cardSkillLevel(preferences.skillLevel, fallbackPreferences.skillLevel)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -474,6 +474,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-fields">
|
<div class="card-fields">
|
||||||
|
<div class="card-field-header" aria-hidden="true">
|
||||||
|
<span>카드 선택</span>
|
||||||
|
<span>개화</span>
|
||||||
|
<span>빈도 감소 고정</span>
|
||||||
|
</div>
|
||||||
{#each selectedCardIds as selectedId, slot}
|
{#each selectedCardIds as selectedId, slot}
|
||||||
<div class="card-field">
|
<div class="card-field">
|
||||||
<label><span>{slot + 1}번 선택</span>
|
<label><span>{slot + 1}번 선택</span>
|
||||||
@@ -484,7 +489,6 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<div class="awakening">
|
<div class="awakening">
|
||||||
<span>개화</span>
|
|
||||||
<div class="awakening-stepper">
|
<div class="awakening-stepper">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -502,7 +506,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="cooldown-lock">
|
<div class="cooldown-lock">
|
||||||
<span>빈도 감소 고정</span>
|
|
||||||
<div class="reduction-options">
|
<div class="reduction-options">
|
||||||
{#each REDUCTION_LOCK_OPTIONS as reduction}
|
{#each REDUCTION_LOCK_OPTIONS as reduction}
|
||||||
<button
|
<button
|
||||||
@@ -664,16 +667,18 @@
|
|||||||
.chart-meta { display: flex; gap: 8px; height: 42px; align-items: center; }
|
.chart-meta { display: flex; gap: 8px; height: 42px; align-items: center; }
|
||||||
.chart-meta span { padding: 7px 9px; border-radius: 7px; color: #adb5c2; background: #202631; font-size: 13px; white-space: nowrap; }
|
.chart-meta span { padding: 7px 9px; border-radius: 7px; color: #adb5c2; background: #202631; font-size: 13px; white-space: nowrap; }
|
||||||
.card-fields { display: grid; gap: 9px; margin-top: 18px; }
|
.card-fields { display: grid; gap: 9px; margin-top: 18px; }
|
||||||
.card-field { display: grid; grid-template-columns: minmax(320px, 1fr) clamp(130px, 12vw, 180px) clamp(304px, 27vw, 400px); gap: 9px; }
|
.card-field-header, .card-field { display: grid; grid-template-columns: minmax(320px, 1fr) clamp(130px, 12vw, 180px) clamp(304px, 27vw, 400px); gap: 9px; }
|
||||||
|
.card-field-header { padding: 0 2px; color: #8f98a8; font-size: 13px; font-weight: 800; }
|
||||||
|
.card-field-header span { padding-left: 2px; }
|
||||||
.card-field > label { grid-template-columns: 74px minmax(0, 1fr); align-items: center; }
|
.card-field > label { grid-template-columns: 74px minmax(0, 1fr); align-items: center; }
|
||||||
.card-field > .awakening { display: grid; grid-template-columns: 32px minmax(0, 1fr); align-items: center; gap: 7px; color: #8f98a8; font-size: 14px; font-weight: 750; }
|
.card-field > .awakening { min-width: 0; }
|
||||||
.awakening-stepper { display: grid; grid-template-columns: 32px minmax(0, 1fr) 32px; align-items: center; height: 42px; overflow: hidden; border: 1px solid #343c49; border-radius: 9px; background: #11151b; }
|
.awakening-stepper { display: grid; grid-template-columns: 32px minmax(0, 1fr) 32px; align-items: center; height: 42px; overflow: hidden; border: 1px solid #343c49; border-radius: 9px; background: #11151b; }
|
||||||
.awakening-stepper button { align-self: stretch; padding: 0; border: 0; color: #c1c8d3; background: #202630; font-size: 24px; line-height: 1; cursor: pointer; }
|
.awakening-stepper button { align-self: stretch; padding: 0; border: 0; color: #c1c8d3; background: #202630; font-size: 24px; line-height: 1; cursor: pointer; }
|
||||||
.awakening-stepper button:hover:not(:disabled) { color: #f5afcc; background: #2d2530; }
|
.awakening-stepper button:hover:not(:disabled) { color: #f5afcc; background: #2d2530; }
|
||||||
.awakening-stepper button:focus-visible { position: relative; z-index: 1; outline: 2px solid #df74a2; outline-offset: -2px; }
|
.awakening-stepper button:focus-visible { position: relative; z-index: 1; outline: 2px solid #df74a2; outline-offset: -2px; }
|
||||||
.awakening-stepper button:disabled { color: #525b69; background: #171c24; cursor: default; }
|
.awakening-stepper button:disabled { color: #525b69; background: #171c24; cursor: default; }
|
||||||
.awakening-stepper strong { color: #e8ebf1; font-size: 14px; text-align: center; white-space: nowrap; }
|
.awakening-stepper strong { color: #e8ebf1; font-size: 14px; text-align: center; white-space: nowrap; }
|
||||||
.cooldown-lock { display: grid; grid-template-columns: 88px minmax(0, 1fr); align-items: center; gap: 7px; color: #8f98a8; font-size: 14px; font-weight: 750; }
|
.cooldown-lock { min-width: 0; }
|
||||||
.reduction-options { display: grid; grid-template-columns: 1.35fr repeat(4, 1fr); height: 42px; overflow: hidden; border: 1px solid #343c49; border-radius: 9px; background: #11151b; }
|
.reduction-options { display: grid; grid-template-columns: 1.35fr repeat(4, 1fr); height: 42px; overflow: hidden; border: 1px solid #343c49; border-radius: 9px; background: #11151b; }
|
||||||
.reduction-options button { min-width: 0; padding: 0 6px; border: 0; border-left: 1px solid #2d3440; color: #8f98a8; background: transparent; font-size: 13px; font-weight: 750; cursor: pointer; }
|
.reduction-options button { min-width: 0; padding: 0 6px; border: 0; border-left: 1px solid #2d3440; color: #8f98a8; background: transparent; font-size: 13px; font-weight: 750; cursor: pointer; }
|
||||||
.reduction-options button:first-child { border-left: 0; }
|
.reduction-options button:first-child { border-left: 0; }
|
||||||
@@ -755,6 +760,7 @@
|
|||||||
.power-summary { grid-column: 1 / -1; }
|
.power-summary { grid-column: 1 / -1; }
|
||||||
.chart-meta { grid-column: 1 / -1; height: auto; }
|
.chart-meta { grid-column: 1 / -1; height: auto; }
|
||||||
.card-field { grid-template-columns: minmax(0, 1fr) 102px; }
|
.card-field { grid-template-columns: minmax(0, 1fr) 102px; }
|
||||||
|
.card-field-header { display: none; }
|
||||||
.card-field > label { grid-template-columns: 58px minmax(0, 1fr); }
|
.card-field > label { grid-template-columns: 58px minmax(0, 1fr); }
|
||||||
.card-field > .awakening { grid-template-columns: 1fr; }
|
.card-field > .awakening { grid-template-columns: 1fr; }
|
||||||
.awakening-stepper { grid-template-columns: 28px minmax(0, 1fr) 28px; }
|
.awakening-stepper { grid-template-columns: 28px minmax(0, 1fr) 28px; }
|
||||||
|
|||||||
+244
-68
@@ -1,43 +1,13 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import cardSummary from '$lib/data/cards-summary.json';
|
import cardSummary from '$lib/data/cards-summary.json';
|
||||||
import type { CardListPreferences, UserData } from '$lib/models/user-data';
|
import { skillAtLevel, sortCardsByActive, type CardSummary, type CardType, type HighestStat, type SkillLevel } from '$lib/models/card-list';
|
||||||
|
import type { CardListPreferences, CardListSortKey, CardSkillLevel, SortDirection, UserData } from '$lib/models/user-data';
|
||||||
import { loadBrowserData, saveBrowserData } from '$lib/storage/browser-storage';
|
import { loadBrowserData, saveBrowserData } from '$lib/storage/browser-storage';
|
||||||
|
|
||||||
type CardType = '큐트' | '퓨어' | '해피';
|
|
||||||
type HighestStat = '퍼포먼스' | '테크닉' | '센스';
|
|
||||||
type SkillConditionStatus = '일반' | '조건부';
|
type SkillConditionStatus = '일반' | '조건부';
|
||||||
type ActiveProbability = '높음' | '중간';
|
type ActiveProbability = '높음' | '중간';
|
||||||
|
|
||||||
type SkillLevel = {
|
|
||||||
level: number;
|
|
||||||
description: string;
|
|
||||||
coolTimeSeconds: number | null;
|
|
||||||
probabilityPercent: number | null;
|
|
||||||
durationSeconds: number | null;
|
|
||||||
effect?: string;
|
|
||||||
condition?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CardDetails = {
|
|
||||||
title: string;
|
|
||||||
passive: SkillLevel[];
|
|
||||||
active: SkillLevel[];
|
|
||||||
special: SkillLevel[];
|
|
||||||
costume: { name: string; description: string } | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
type CardSummary = {
|
|
||||||
id: string;
|
|
||||||
rarity: number;
|
|
||||||
type: CardType;
|
|
||||||
name: string;
|
|
||||||
versionName: string;
|
|
||||||
generation: string;
|
|
||||||
highestStat: HighestStat;
|
|
||||||
details: CardDetails;
|
|
||||||
};
|
|
||||||
|
|
||||||
const allCards = cardSummary.cards as CardSummary[];
|
const allCards = cardSummary.cards as CardSummary[];
|
||||||
const cards = allCards.filter((card) => card.rarity === 5);
|
const cards = allCards.filter((card) => card.rarity === 5);
|
||||||
const typeOptions: CardType[] = ['큐트', '퓨어', '해피'];
|
const typeOptions: CardType[] = ['큐트', '퓨어', '해피'];
|
||||||
@@ -59,8 +29,10 @@
|
|||||||
let selectedActiveProbabilities = [...activeProbabilityOptions];
|
let selectedActiveProbabilities = [...activeProbabilityOptions];
|
||||||
let selectedCardVariants = [...cardVariantOptions];
|
let selectedCardVariants = [...cardVariantOptions];
|
||||||
let selectedGenerations = [...generationOptions];
|
let selectedGenerations = [...generationOptions];
|
||||||
|
let sortKey: CardListSortKey | null = null;
|
||||||
|
let sortDirection: SortDirection = 'asc';
|
||||||
let expandedCardId: string | null = null;
|
let expandedCardId: string | null = null;
|
||||||
let selectedSkillLevel = 1;
|
let selectedSkillLevel: CardSkillLevel = 1;
|
||||||
let collection: UserData['collection'] = {};
|
let collection: UserData['collection'] = {};
|
||||||
let memberBoards: UserData['memberBoards'] = {};
|
let memberBoards: UserData['memberBoards'] = {};
|
||||||
let boardCards: UserData['boardCards'] = {};
|
let boardCards: UserData['boardCards'] = {};
|
||||||
@@ -95,14 +67,19 @@
|
|||||||
} catch {}
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
$: filteredCards = cards.filter((card) =>
|
$: filteredCards = sortCardsByActive(
|
||||||
|
cards.filter((card) =>
|
||||||
selectedTypes.includes(card.type)
|
selectedTypes.includes(card.type)
|
||||||
&& selectedStats.includes(card.highestStat)
|
&& selectedStats.includes(card.highestStat)
|
||||||
&& selectedActiveConditions.includes(skillConditionStatus(card.details.active))
|
&& matchesSkillCondition(selectedActiveConditions, card.details.active ?? [])
|
||||||
&& selectedSpecialConditions.includes(skillConditionStatus(card.details.special))
|
&& selectedSpecialConditions.includes(skillConditionStatus(card.details.special))
|
||||||
&& selectedActiveProbabilities.includes(activeProbabilityStatus(card.details.active))
|
&& matchesActiveProbability(card.details.active ?? [])
|
||||||
&& selectedCardVariants.includes(cardVariantStatus(card))
|
&& selectedCardVariants.includes(cardVariantStatus(card))
|
||||||
&& generationMemberships(card.generation).some((generation) => selectedGenerations.includes(generation))
|
&& generationMemberships(card.generation).some((generation) => selectedGenerations.includes(generation))
|
||||||
|
),
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
selectedSkillLevel
|
||||||
);
|
);
|
||||||
|
|
||||||
$: allSelected = selectedTypes.length === typeOptions.length
|
$: allSelected = selectedTypes.length === typeOptions.length
|
||||||
@@ -113,6 +90,14 @@
|
|||||||
&& selectedCardVariants.length === cardVariantOptions.length
|
&& selectedCardVariants.length === cardVariantOptions.length
|
||||||
&& selectedGenerations.length === generationOptions.length;
|
&& selectedGenerations.length === generationOptions.length;
|
||||||
|
|
||||||
|
$: allDeselected = selectedTypes.length === 0
|
||||||
|
&& selectedStats.length === 0
|
||||||
|
&& selectedActiveConditions.length === 0
|
||||||
|
&& selectedSpecialConditions.length === 0
|
||||||
|
&& selectedActiveProbabilities.length === 0
|
||||||
|
&& selectedCardVariants.length === 0
|
||||||
|
&& selectedGenerations.length === 0;
|
||||||
|
|
||||||
$: if (expandedCardId && !filteredCards.some((card) => card.id === expandedCardId)) {
|
$: if (expandedCardId && !filteredCards.some((card) => card.id === expandedCardId)) {
|
||||||
expandedCardId = null;
|
expandedCardId = null;
|
||||||
}
|
}
|
||||||
@@ -127,6 +112,16 @@
|
|||||||
selectedGenerations = [...generationOptions];
|
selectedGenerations = [...generationOptions];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectNone() {
|
||||||
|
selectedTypes = [];
|
||||||
|
selectedStats = [];
|
||||||
|
selectedActiveConditions = [];
|
||||||
|
selectedSpecialConditions = [];
|
||||||
|
selectedActiveProbabilities = [];
|
||||||
|
selectedCardVariants = [];
|
||||||
|
selectedGenerations = [];
|
||||||
|
}
|
||||||
|
|
||||||
function defaultPreferences(): CardListPreferences {
|
function defaultPreferences(): CardListPreferences {
|
||||||
return {
|
return {
|
||||||
types: [...typeOptions],
|
types: [...typeOptions],
|
||||||
@@ -135,7 +130,10 @@
|
|||||||
specialConditions: [...skillConditionOptions],
|
specialConditions: [...skillConditionOptions],
|
||||||
activeProbabilities: [...activeProbabilityOptions],
|
activeProbabilities: [...activeProbabilityOptions],
|
||||||
cardVariants: [...cardVariantOptions],
|
cardVariants: [...cardVariantOptions],
|
||||||
generations: [...generationOptions]
|
generations: [...generationOptions],
|
||||||
|
sortKey: null,
|
||||||
|
sortDirection: 'asc',
|
||||||
|
skillLevel: 1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +145,10 @@
|
|||||||
specialConditions: [...selectedSpecialConditions],
|
specialConditions: [...selectedSpecialConditions],
|
||||||
activeProbabilities: [...selectedActiveProbabilities],
|
activeProbabilities: [...selectedActiveProbabilities],
|
||||||
cardVariants: [...selectedCardVariants],
|
cardVariants: [...selectedCardVariants],
|
||||||
generations: [...selectedGenerations]
|
generations: [...selectedGenerations],
|
||||||
|
sortKey,
|
||||||
|
sortDirection,
|
||||||
|
skillLevel: selectedSkillLevel
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +161,9 @@
|
|||||||
selectedCardVariants = data.preferences.cardVariants.filter((value) => cardVariantOptions.includes(value));
|
selectedCardVariants = data.preferences.cardVariants.filter((value) => cardVariantOptions.includes(value));
|
||||||
selectedGenerations = [...new Set(data.preferences.generations.flatMap(generationMemberships))]
|
selectedGenerations = [...new Set(data.preferences.generations.flatMap(generationMemberships))]
|
||||||
.filter((value) => generationOptions.includes(value));
|
.filter((value) => generationOptions.includes(value));
|
||||||
|
sortKey = data.preferences.sortKey;
|
||||||
|
sortDirection = data.preferences.sortDirection;
|
||||||
|
selectedSkillLevel = data.preferences.skillLevel;
|
||||||
collection = data.collection;
|
collection = data.collection;
|
||||||
memberBoards = data.memberBoards;
|
memberBoards = data.memberBoards;
|
||||||
boardCards = data.boardCards;
|
boardCards = data.boardCards;
|
||||||
@@ -189,23 +193,55 @@
|
|||||||
return levels.some((level) => Boolean(level.condition)) ? '조건부' : '일반';
|
return levels.some((level) => Boolean(level.condition)) ? '조건부' : '일반';
|
||||||
}
|
}
|
||||||
|
|
||||||
function activeProbabilityStatus(levels: SkillLevel[]): ActiveProbability {
|
function matchesSkillCondition(selected: SkillConditionStatus[], levels: SkillLevel[]): boolean {
|
||||||
|
return levels.length === 0
|
||||||
|
? selected.length === skillConditionOptions.length
|
||||||
|
: selected.includes(skillConditionStatus(levels));
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeProbabilityStatus(levels: SkillLevel[]): ActiveProbability | null {
|
||||||
|
if (levels.length === 0) return null;
|
||||||
return levels.some((level) => level.probabilityPercent === 55) ? '높음' : '중간';
|
return levels.some((level) => level.probabilityPercent === 55) ? '높음' : '중간';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function matchesActiveProbability(levels: SkillLevel[]): boolean {
|
||||||
|
const status = activeProbabilityStatus(levels);
|
||||||
|
return status === null
|
||||||
|
? selectedActiveProbabilities.length === activeProbabilityOptions.length
|
||||||
|
: selectedActiveProbabilities.includes(status);
|
||||||
|
}
|
||||||
|
|
||||||
function toggleCard(cardId: string) {
|
function toggleCard(cardId: string) {
|
||||||
expandedCardId = expandedCardId === cardId ? null : cardId;
|
expandedCardId = expandedCardId === cardId ? null : cardId;
|
||||||
selectedSkillLevel = 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function skillAtLevel(levels: SkillLevel[], selectedLevel: number) {
|
function toggleSort(nextSortKey: CardListSortKey) {
|
||||||
return levels.find((level) => level.level === selectedLevel) ?? levels.at(-1);
|
if (sortKey === nextSortKey) {
|
||||||
|
sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function probabilityLabel(probabilityPercent: number | null) {
|
sortKey = nextSortKey;
|
||||||
|
sortDirection = 'asc';
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortButtonLabel(key: CardListSortKey, label: string) {
|
||||||
|
if (sortKey !== key) return `${label} 오름차순 정렬`;
|
||||||
|
return `${label} ${sortDirection === 'asc' ? '내림차순' : '오름차순'}으로 변경`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function secondsLabel(seconds: number | null | undefined) {
|
||||||
|
return seconds == null ? '—' : `${seconds}초`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function probabilityLabel(probabilityPercent: number | null | undefined) {
|
||||||
if (probabilityPercent === 55) return '높음';
|
if (probabilityPercent === 55) return '높음';
|
||||||
if (probabilityPercent === 46) return '중간';
|
if (probabilityPercent === 46) return '중간';
|
||||||
return probabilityPercent === null ? '—' : `${probabilityPercent}%`;
|
return probabilityPercent == null ? '—' : `${probabilityPercent}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activeEffectLabel(active: SkillLevel | undefined) {
|
||||||
|
return active?.effect || active?.description || '—';
|
||||||
}
|
}
|
||||||
|
|
||||||
function descriptionSegments(description: string, highlightNumbers = false) {
|
function descriptionSegments(description: string, highlightNumbers = false) {
|
||||||
@@ -287,7 +323,10 @@
|
|||||||
<section class="filters" aria-labelledby="filter-heading">
|
<section class="filters" aria-labelledby="filter-heading">
|
||||||
<div class="filter-header">
|
<div class="filter-header">
|
||||||
<h2 id="filter-heading">출력 조건</h2>
|
<h2 id="filter-heading">출력 조건</h2>
|
||||||
<button class="select-all" type="button" onclick={selectAll} disabled={allSelected}>모두 선택</button>
|
<div class="filter-actions">
|
||||||
|
<button class="filter-action" type="button" onclick={selectNone} disabled={allDeselected}>모두 취소</button>
|
||||||
|
<button class="filter-action" type="button" onclick={selectAll} disabled={allSelected}>모두 선택</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="filter-groups">
|
<div class="filter-groups">
|
||||||
@@ -392,12 +431,30 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="table-panel" aria-label="카드 목록">
|
<section class="table-panel" aria-label="카드 목록">
|
||||||
<div class="list-header" aria-hidden="true">
|
<div class="list-header">
|
||||||
<span>타입</span>
|
<span>타입</span>
|
||||||
<span>주스탯</span>
|
<span>주스탯</span>
|
||||||
<span>기수</span>
|
<span>기수</span>
|
||||||
<span>이름</span>
|
<span>이름</span>
|
||||||
<span></span>
|
<button class="sort-button" type="button" class:active={sortKey === 'coolTime'} aria-label={sortButtonLabel('coolTime', '주기')} onclick={() => toggleSort('coolTime')}>
|
||||||
|
<span>주기</span><span class="sort-indicator" aria-hidden="true">{sortKey === 'coolTime' ? (sortDirection === 'asc' ? '↑' : '↓') : '↕'}</span>
|
||||||
|
</button>
|
||||||
|
<button class="sort-button" type="button" class:active={sortKey === 'probability'} aria-label={sortButtonLabel('probability', '확률')} onclick={() => toggleSort('probability')}>
|
||||||
|
<span>확률</span><span class="sort-indicator" aria-hidden="true">{sortKey === 'probability' ? (sortDirection === 'asc' ? '↑' : '↓') : '↕'}</span>
|
||||||
|
</button>
|
||||||
|
<button class="sort-button" type="button" class:active={sortKey === 'duration'} aria-label={sortButtonLabel('duration', '지속')} onclick={() => toggleSort('duration')}>
|
||||||
|
<span>지속</span><span class="sort-indicator" aria-hidden="true">{sortKey === 'duration' ? (sortDirection === 'asc' ? '↑' : '↓') : '↕'}</span>
|
||||||
|
</button>
|
||||||
|
<button class="sort-button" type="button" class:active={sortKey === 'effect'} aria-label={sortButtonLabel('effect', '효과')} onclick={() => toggleSort('effect')}>
|
||||||
|
<span>효과</span><span class="sort-indicator" aria-hidden="true">{sortKey === 'effect' ? (sortDirection === 'asc' ? '↑' : '↓') : '↕'}</span>
|
||||||
|
</button>
|
||||||
|
<div class="header-level-control">
|
||||||
|
<span>개화</span>
|
||||||
|
<div class="level-toggle header-level-toggle" aria-label="목록에 표시할 스킬 개화 상태">
|
||||||
|
<button type="button" class:active={selectedSkillLevel === 1} aria-pressed={selectedSkillLevel === 1} onclick={() => selectedSkillLevel = 1}>명함</button>
|
||||||
|
<button type="button" class:active={selectedSkillLevel === 2} aria-pressed={selectedSkillLevel === 2} onclick={() => selectedSkillLevel = 2}>5 개화</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<span></span>
|
<span></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -448,13 +505,24 @@
|
|||||||
{#if card.versionName}<span class="version-badge">{card.versionName}</span>{/if}
|
{#if card.versionName}<span class="version-badge">{card.versionName}</span>{/if}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
<span class="summary-cell active-cycle-cell active-summary-cell">
|
||||||
|
<span class="mobile-label">주기</span>
|
||||||
|
<span class="active-value">{secondsLabel(active?.coolTimeSeconds)}</span>
|
||||||
|
</span>
|
||||||
|
<span class="summary-cell active-probability-cell active-summary-cell">
|
||||||
|
<span class="mobile-label">확률</span>
|
||||||
|
<span class="active-value">{probabilityLabel(active?.probabilityPercent)}</span>
|
||||||
|
</span>
|
||||||
|
<span class="summary-cell active-duration-cell active-summary-cell">
|
||||||
|
<span class="mobile-label">지속</span>
|
||||||
|
<span class="active-value">{secondsLabel(active?.durationSeconds)}</span>
|
||||||
|
</span>
|
||||||
|
<span class="summary-cell active-effect-cell active-summary-cell">
|
||||||
|
<span class="mobile-label">효과</span>
|
||||||
|
<span class="active-effect" title={activeEffectLabel(active)}>{activeEffectLabel(active)}</span>
|
||||||
|
</span>
|
||||||
<span class="summary-cell level-cell">
|
<span class="summary-cell level-cell">
|
||||||
{#if expanded}
|
<span class="skill-level-label">{selectedSkillLevel === 1 ? '명함' : '5 개화'}</span>
|
||||||
<div class="level-toggle inline-level-toggle" aria-label="스킬 개화 상태">
|
|
||||||
<button type="button" class:active={selectedSkillLevel === 1} aria-pressed={selectedSkillLevel === 1} onclick={() => selectedSkillLevel = 1}>명함</button>
|
|
||||||
<button type="button" class:active={selectedSkillLevel === 2} aria-pressed={selectedSkillLevel === 2} onclick={() => selectedSkillLevel = 2}>5 개화</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</span>
|
</span>
|
||||||
<span class="chevron" aria-hidden="true"></span>
|
<span class="chevron" aria-hidden="true"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -704,7 +772,12 @@
|
|||||||
letter-spacing: -0.025em;
|
letter-spacing: -0.025em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-all {
|
.filter-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-action {
|
||||||
padding: 7px 12px;
|
padding: 7px 12px;
|
||||||
border: 1px solid #dce1ea;
|
border: 1px solid #dce1ea;
|
||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
@@ -717,13 +790,13 @@
|
|||||||
transition: border-color 150ms ease, color 150ms ease, background 150ms ease;
|
transition: border-color 150ms ease, color 150ms ease, background 150ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-all:hover:not(:disabled) {
|
.filter-action:hover:not(:disabled) {
|
||||||
border-color: #ef8bb5;
|
border-color: #ef8bb5;
|
||||||
color: #d83f82;
|
color: #d83f82;
|
||||||
background: #fff8fb;
|
background: #fff8fb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-all:disabled {
|
.filter-action:disabled {
|
||||||
opacity: 0.42;
|
opacity: 0.42;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
@@ -831,7 +904,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-panel {
|
.table-panel {
|
||||||
overflow: hidden;
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
border: 1px solid #e3e7ef;
|
border: 1px solid #e3e7ef;
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
background: rgb(255 255 255 / 90%);
|
background: rgb(255 255 255 / 90%);
|
||||||
@@ -842,11 +916,12 @@
|
|||||||
.list-header,
|
.list-header,
|
||||||
.card-summary {
|
.card-summary {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: clamp(72px, 5vw, 88px) clamp(84px, 6vw, 104px) clamp(150px, 11vw, 184px) minmax(280px, 1fr) clamp(116px, 9vw, 144px) 40px;
|
grid-template-columns: 62px 72px 110px minmax(160px, 1fr) 66px 66px 66px minmax(160px, 1.15fr) 190px 32px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-header {
|
.list-header {
|
||||||
|
min-width: 1260px;
|
||||||
min-height: 54px;
|
min-height: 54px;
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
border-bottom: 1px solid #e8ebf1;
|
border-bottom: 1px solid #e8ebf1;
|
||||||
@@ -856,7 +931,44 @@
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-header .sort-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 5px;
|
||||||
|
width: fit-content;
|
||||||
|
padding: 6px 7px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 7px;
|
||||||
|
color: inherit;
|
||||||
|
background: transparent;
|
||||||
|
font: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 150ms ease, background 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header .sort-button:hover,
|
||||||
|
.list-header .sort-button.active {
|
||||||
|
color: #d83f82;
|
||||||
|
background: #fff0f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header .sort-button:focus-visible {
|
||||||
|
outline: 3px solid rgb(73 134 231 / 25%);
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sort-indicator {
|
||||||
|
color: #a4adbb;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-header .sort-button.active .sort-indicator {
|
||||||
|
color: currentcolor;
|
||||||
|
}
|
||||||
|
|
||||||
.card-entry {
|
.card-entry {
|
||||||
|
min-width: 1260px;
|
||||||
border-bottom: 1px solid #edf0f5;
|
border-bottom: 1px solid #edf0f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -915,11 +1027,49 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.active-summary-cell {
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-value,
|
||||||
|
.active-effect {
|
||||||
|
color: #4d586d;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 750;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-effect {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.level-cell {
|
.level-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header-level-control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-level-toggle {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-level-label {
|
||||||
|
padding: 4px 9px;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #b43d72;
|
||||||
|
background: #fff0f7;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
.mobile-label {
|
.mobile-label {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -958,12 +1108,6 @@
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inline-level-toggle {
|
|
||||||
position: relative;
|
|
||||||
z-index: 3;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.level-toggle button {
|
.level-toggle button {
|
||||||
min-width: 50px;
|
min-width: 50px;
|
||||||
padding: 3px 7px;
|
padding: 3px 7px;
|
||||||
@@ -1395,13 +1539,13 @@
|
|||||||
box-shadow: 0 14px 42px rgb(0 0 0 / 24%);
|
box-shadow: 0 14px 42px rgb(0 0 0 / 24%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-all {
|
.filter-action {
|
||||||
border-color: #343c4b;
|
border-color: #343c4b;
|
||||||
color: #b5bdcb;
|
color: #b5bdcb;
|
||||||
background: #1d2430;
|
background: #1d2430;
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-all:hover:not(:disabled) {
|
.filter-action:hover:not(:disabled) {
|
||||||
border-color: #c85b8b;
|
border-color: #c85b8b;
|
||||||
color: #f08db7;
|
color: #f08db7;
|
||||||
background: #2a1c27;
|
background: #2a1c27;
|
||||||
@@ -1442,6 +1586,22 @@
|
|||||||
background: #131923;
|
background: #131923;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-header .sort-button:hover,
|
||||||
|
.list-header .sort-button.active {
|
||||||
|
color: #f08db7;
|
||||||
|
background: #2a1b26;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-value,
|
||||||
|
.active-effect {
|
||||||
|
color: #c2cad8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-level-label {
|
||||||
|
color: #f08db7;
|
||||||
|
background: #2a1b26;
|
||||||
|
}
|
||||||
|
|
||||||
.card-entry {
|
.card-entry {
|
||||||
border-color: #282f3d;
|
border-color: #282f3d;
|
||||||
}
|
}
|
||||||
@@ -1574,7 +1734,7 @@
|
|||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.list-header,
|
.list-header,
|
||||||
.card-summary {
|
.card-summary {
|
||||||
grid-template-columns: 72px 84px 150px minmax(0, 1fr) 116px 36px;
|
grid-template-columns: 62px 72px 110px minmax(160px, 1fr) 66px 66px 66px minmax(160px, 1.15fr) 190px 32px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1625,12 +1785,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.card-entry {
|
.card-entry {
|
||||||
|
min-width: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-summary {
|
.card-summary {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 72px 84px minmax(0, 1fr);
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
min-width: 0;
|
||||||
gap: 14px 12px;
|
gap: 14px 12px;
|
||||||
padding: 19px 0;
|
padding: 19px 0;
|
||||||
padding-right: 32px;
|
padding-right: 32px;
|
||||||
@@ -1661,6 +1823,20 @@
|
|||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.active-summary-cell {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-effect-cell {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.active-effect {
|
||||||
|
overflow: visible;
|
||||||
|
text-overflow: clip;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
.level-cell {
|
.level-cell {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|||||||
@@ -277,6 +277,27 @@
|
|||||||
return Math.max(0, Math.min(900, Number.isFinite(value) ? Math.round(Number(value)) : 0));
|
return Math.max(0, Math.min(900, Number.isFinite(value) ? Math.round(Number(value)) : 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sanitizeGenerationBonusInput(input: HTMLInputElement, previousValue: number | null): string {
|
||||||
|
if (input.value === '') {
|
||||||
|
input.value = '';
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^\d+$/.test(input.value)) {
|
||||||
|
input.value = previousValue === null ? '' : String(previousValue);
|
||||||
|
return input.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = Number(input.value);
|
||||||
|
if (value > 900) {
|
||||||
|
input.value = previousValue === null ? '' : String(previousValue);
|
||||||
|
return input.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.value = String(value);
|
||||||
|
return input.value;
|
||||||
|
}
|
||||||
|
|
||||||
function saveState(state: SavedState): void {
|
function saveState(state: SavedState): void {
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
|
||||||
}
|
}
|
||||||
@@ -913,6 +934,7 @@
|
|||||||
<div class="song-fields">
|
<div class="song-fields">
|
||||||
<label>악곡<select bind:value={selectedSongId} on:change={() => result = null}>{#each songs as song}<option value={song.id}>{song.title}</option>{/each}</select></label>
|
<label>악곡<select bind:value={selectedSongId} on:change={() => result = null}>{#each songs as song}<option value={song.id}>{song.title}</option>{/each}</select></label>
|
||||||
<label>난이도<select bind:value={difficulty} on:change={() => result = null}><option value="expert">Expert</option><option value="hard">Hard</option></select></label>
|
<label>난이도<select bind:value={difficulty} on:change={() => result = null}><option value="expert">Expert</option><option value="hard">Hard</option></select></label>
|
||||||
|
<div class="chart-meta"><span>길이 {selectedChart?.durationSeconds ?? 0}초</span><span>노트 {selectedChart?.totalNotes?.toLocaleString('ko-KR') ?? 0}개</span></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="candidate-fields">
|
<div class="candidate-fields">
|
||||||
<div class="candidate-awakening">
|
<div class="candidate-awakening">
|
||||||
@@ -923,20 +945,20 @@
|
|||||||
<button type="button" disabled={candidateAwakening >= 5} aria-label="자동 후보 개화 높이기" on:click={() => updateCandidateAwakening(candidateAwakening + 1)}>›</button>
|
<button type="button" disabled={candidateAwakening >= 5} aria-label="자동 후보 개화 높이기" on:click={() => updateCandidateAwakening(candidateAwakening + 1)}>›</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<label>자동 후보 기수 보너스 덮어쓰기<input class="number-input" type="text" inputmode="numeric" pattern="[0-9]*" value={candidateGenerationBonus ?? ''} placeholder="미사용" on:change={(event) => updateCandidateGenerationBonus(event.currentTarget.value)}></label>
|
<label>자동 후보 기수 보너스 덮어쓰기 (0~900)<input class="number-input" type="text" inputmode="numeric" pattern="[0-9]*" maxlength="3" value={candidateGenerationBonus ?? ''} placeholder="미사용" on:input={(event) => updateCandidateGenerationBonus(sanitizeGenerationBonusInput(event.currentTarget, candidateGenerationBonus))}></label>
|
||||||
</div>
|
</div>
|
||||||
<p class="candidate-guide">
|
<p class="candidate-guide">
|
||||||
보유 카드의 개화가 자동 후보 개화보다 높으면 실제 보유 개화를 우선 적용합니다.<br>
|
보유 카드의 개화가 자동 후보 개화보다 높으면 실제 보유 개화를 우선 적용합니다.<br>
|
||||||
기수 보너스는 녹색 노드를 활성화해 얻을 수 있는 보너스를 의미합니다.
|
기수 보너스는 녹색 노드를 활성화해 얻을 수 있는 보너스를 의미합니다.
|
||||||
</p>
|
</p>
|
||||||
<div class="fixed-fields">
|
<div class="fixed-fields">
|
||||||
<div class="fixed-column-headings">
|
<div class="fixed-column-headings" aria-hidden="true">
|
||||||
<span>고정 멤버</span><span>개화</span><span>빈도 감소 고정</span>
|
<span>고정 카드</span><span>개화</span><span>빈도 감소 고정</span>
|
||||||
<div><span>파란 노드 전체</span><span class="generation-heading">기수 보너스<br>덮어쓰기</span></div>
|
<div><span>파란 노드 전체</span><span class="generation-heading">기수 보너스<br>덮어쓰기 (0~900)</span></div>
|
||||||
</div>
|
</div>
|
||||||
{#each fixedCardIds as selectedId, slot}
|
{#each fixedCardIds as selectedId, slot}
|
||||||
<div class="fixed-field">
|
<div class="fixed-field">
|
||||||
<label>{slot + 1}번 고정
|
<label><span>{slot + 1}번 고정</span>
|
||||||
<select value={selectedId} on:change={(event) => updateFixedCard(slot, event.currentTarget.value)}>
|
<select value={selectedId} on:change={(event) => updateFixedCard(slot, event.currentTarget.value)}>
|
||||||
<option value="">고정하지 않음</option>
|
<option value="">고정하지 않음</option>
|
||||||
{#each fiveStarCards as card}<option value={card.id} disabled={selectedInAnotherSlot(card, slot)}>{cardLabel(card)}</option>{/each}
|
{#each fiveStarCards as card}<option value={card.id} disabled={selectedInAnotherSlot(card, slot)}>{cardLabel(card)}</option>{/each}
|
||||||
@@ -961,7 +983,7 @@
|
|||||||
<input type="checkbox" disabled={!selectedId} checked={fixedAllBlueNodes[slot]} aria-label={`${slot + 1}번 고정 카드 파란 노드 전체 활성화`} on:change={() => toggleFixedAllBlueNodes(slot)}>
|
<input type="checkbox" disabled={!selectedId} checked={fixedAllBlueNodes[slot]} aria-label={`${slot + 1}번 고정 카드 파란 노드 전체 활성화`} on:change={() => toggleFixedAllBlueNodes(slot)}>
|
||||||
<span aria-hidden="true"></span>
|
<span aria-hidden="true"></span>
|
||||||
</label>
|
</label>
|
||||||
<input class="generation-input" type="text" inputmode="numeric" pattern="[0-9]*" disabled={!selectedId} value={fixedGenerationBonusOverrides[slot] ?? ''} placeholder="미사용" aria-label={`${slot + 1}번 고정 카드 기수 보너스 덮어쓰기`} on:change={(event) => updateFixedGenerationBonus(slot, event.currentTarget.value)}>
|
<input class="generation-input" type="text" inputmode="numeric" pattern="[0-9]*" maxlength="3" disabled={!selectedId} value={fixedGenerationBonusOverrides[slot] ?? ''} placeholder="미사용" aria-label={`${slot + 1}번 고정 카드 기수 보너스 덮어쓰기 (0~900)`} on:input={(event) => updateFixedGenerationBonus(slot, sanitizeGenerationBonusInput(event.currentTarget, fixedGenerationBonusOverrides[slot] ?? null))}>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -1056,26 +1078,32 @@
|
|||||||
label { display: grid; gap: 7px; color: #8f98a8; font-size: 14px; font-weight: 750; }
|
label { display: grid; gap: 7px; color: #8f98a8; font-size: 14px; font-weight: 750; }
|
||||||
select, .number-input { width: 100%; min-width: 0; height: 42px; padding: 0 12px; border: 1px solid #343c49; border-radius: 9px; color: #e8ebf1; background: #11151b; font: inherit; }
|
select, .number-input { width: 100%; min-width: 0; height: 42px; padding: 0 12px; border: 1px solid #343c49; border-radius: 9px; color: #e8ebf1; background: #11151b; font: inherit; }
|
||||||
select:focus, .number-input:focus, .generation-input:focus { border-color: #d86f9c; outline: 3px solid rgb(216 111 156 / 14%); }
|
select:focus, .number-input:focus, .generation-input:focus { border-color: #d86f9c; outline: 3px solid rgb(216 111 156 / 14%); }
|
||||||
.song-fields { display: grid; grid-template-columns: minmax(0, 1fr) clamp(130px, 11vw, 180px); gap: 10px; margin-top: 22px; }
|
.song-fields { display: grid; grid-template-columns: minmax(0, 2fr) clamp(110px, 11vw, 170px) minmax(220px, .8fr); align-items: end; gap: 12px; margin-top: 22px; }
|
||||||
.candidate-fields { display: grid; grid-template-columns: clamp(130px, 11vw, 180px) clamp(260px, 24vw, 380px); gap: 10px; margin-top: 14px; padding-top: 14px; border-top: 1px solid #292f39; }
|
.chart-meta { display: flex; align-items: center; gap: 8px; height: 42px; }
|
||||||
|
.chart-meta span { padding: 7px 9px; border-radius: 7px; color: #adb5c2; background: #202631; font-size: 13px; white-space: nowrap; }
|
||||||
|
.candidate-fields { display: grid; grid-template-columns: clamp(130px, 12vw, 180px) clamp(260px, 24vw, 380px); gap: 9px; margin-top: 18px; }
|
||||||
.candidate-awakening { display: grid; gap: 7px; color: #8f98a8; font-size: 14px; font-weight: 750; }
|
.candidate-awakening { display: grid; gap: 7px; color: #8f98a8; font-size: 14px; font-weight: 750; }
|
||||||
.candidate-guide { margin: 7px 0 0; color: #737d8d; font-size: 12px; line-height: 1.5; }
|
.candidate-guide { margin: 7px 0 0; color: #737d8d; font-size: 12px; line-height: 1.5; }
|
||||||
.fixed-fields { display: grid; gap: 9px; margin-top: 16px; }
|
.fixed-fields { display: grid; gap: 9px; margin-top: 16px; }
|
||||||
.fixed-guide { margin: 8px 0 0; color: #737d8d; font-size: 12px; line-height: 1.5; }
|
.fixed-guide { margin: 8px 0 0; color: #737d8d; font-size: 12px; line-height: 1.5; }
|
||||||
.fixed-column-headings { display: grid; grid-template-columns: minmax(320px, 1fr) clamp(120px, 11vw, 170px) clamp(304px, 26vw, 390px) clamp(180px, 16vw, 240px); gap: 9px; margin-bottom: -3px; }
|
.fixed-column-headings { display: grid; grid-template-columns: minmax(320px, 1fr) clamp(130px, 12vw, 180px) clamp(304px, 27vw, 400px) clamp(180px, 16vw, 240px); gap: 9px; padding: 0 2px; }
|
||||||
.fixed-column-headings > span { align-self: center; color: #8f98a8; font-size: 12px; font-weight: 800; text-align: center; }
|
.fixed-column-headings > span { align-self: center; padding-left: 2px; color: #8f98a8; font-size: 13px; font-weight: 800; }
|
||||||
.fixed-column-headings > span:first-child { text-align: left; }
|
|
||||||
.fixed-column-headings > div { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; color: #8f98a8; font-size: 12px; font-weight: 800; text-align: center; }
|
.fixed-column-headings > div { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; color: #8f98a8; font-size: 12px; font-weight: 800; text-align: center; }
|
||||||
.generation-heading { line-height: 1.25; white-space: nowrap; }
|
.generation-heading { line-height: 1.25; white-space: nowrap; }
|
||||||
.fixed-field { display: grid; grid-template-columns: minmax(320px, 1fr) clamp(120px, 11vw, 170px) clamp(304px, 26vw, 390px) clamp(180px, 16vw, 240px); gap: 9px; }
|
.fixed-field { display: grid; grid-template-columns: minmax(320px, 1fr) clamp(130px, 12vw, 180px) clamp(304px, 27vw, 400px) clamp(180px, 16vw, 240px); gap: 9px; }
|
||||||
.fixed-awakening, .fixed-reduction { display: grid; align-self: end; align-items: center; height: 42px; }
|
.fixed-field > label { grid-template-columns: 74px minmax(0, 1fr); align-items: center; }
|
||||||
.awakening-stepper { display: grid; grid-template-columns: 30px minmax(0, 1fr) 30px; align-items: center; height: 42px; overflow: hidden; border: 1px solid #343c49; border-radius: 9px; background: #11151b; }
|
.fixed-awakening, .fixed-reduction { min-width: 0; }
|
||||||
.awakening-stepper button { align-self: stretch; padding: 0; border: 0; color: #c1c8d3; background: #202630; font-size: 22px; cursor: pointer; }
|
.awakening-stepper { display: grid; grid-template-columns: 32px minmax(0, 1fr) 32px; align-items: center; height: 42px; overflow: hidden; border: 1px solid #343c49; border-radius: 9px; background: #11151b; }
|
||||||
.awakening-stepper strong { color: #e8ebf1; font-size: 14px; text-align: center; }
|
.awakening-stepper button { align-self: stretch; padding: 0; border: 0; color: #c1c8d3; background: #202630; font-size: 24px; line-height: 1; cursor: pointer; }
|
||||||
|
.awakening-stepper button:hover:not(:disabled) { color: #f5afcc; background: #2d2530; }
|
||||||
|
.awakening-stepper button:focus-visible { position: relative; z-index: 1; outline: 2px solid #df74a2; outline-offset: -2px; }
|
||||||
|
.awakening-stepper strong { color: #e8ebf1; font-size: 14px; text-align: center; white-space: nowrap; }
|
||||||
.reduction-options { display: grid; grid-template-columns: 1.35fr repeat(4, 1fr); height: 42px; overflow: hidden; border: 1px solid #343c49; border-radius: 9px; background: #11151b; }
|
.reduction-options { display: grid; grid-template-columns: 1.35fr repeat(4, 1fr); height: 42px; overflow: hidden; border: 1px solid #343c49; border-radius: 9px; background: #11151b; }
|
||||||
.reduction-options button { min-width: 0; padding: 0 5px; border: 0; border-left: 1px solid #2d3440; color: #8f98a8; background: transparent; font-size: 12px; font-weight: 750; cursor: pointer; }
|
.reduction-options button { min-width: 0; padding: 0 6px; border: 0; border-left: 1px solid #2d3440; color: #8f98a8; background: transparent; font-size: 13px; font-weight: 750; cursor: pointer; }
|
||||||
.reduction-options button:first-child { border-left: 0; }
|
.reduction-options button:first-child { border-left: 0; }
|
||||||
|
.reduction-options button:hover:not(:disabled) { color: #e9edf3; background: #202630; }
|
||||||
.reduction-options button.active { color: #171019; background: #ed8bb5; }
|
.reduction-options button.active { color: #171019; background: #ed8bb5; }
|
||||||
|
.reduction-options button:focus-visible { position: relative; z-index: 1; outline: 2px solid #f1a0c2; outline-offset: -2px; }
|
||||||
.fixed-overrides { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; align-self: end; height: 42px; }
|
.fixed-overrides { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; align-self: end; height: 42px; }
|
||||||
.override-checkbox { display: grid; place-items: center; cursor: pointer; }
|
.override-checkbox { display: grid; place-items: center; cursor: pointer; }
|
||||||
.override-checkbox input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
.override-checkbox input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
||||||
@@ -1088,13 +1116,14 @@
|
|||||||
.generation-input::placeholder { color: #68717f; opacity: 1; }
|
.generation-input::placeholder { color: #68717f; opacity: 1; }
|
||||||
.generation-input:disabled { opacity: .35; }
|
.generation-input:disabled { opacity: .35; }
|
||||||
.awakening-stepper button:disabled, .reduction-options button:disabled { opacity: .35; cursor: default; }
|
.awakening-stepper button:disabled, .reduction-options button:disabled { opacity: .35; cursor: default; }
|
||||||
.leader-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 26px; padding-top: 20px; border-top: 1px solid #303641; }
|
.leader-fields { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; margin-top: 16px; }
|
||||||
.outfit-effect { display: grid; grid-template-columns: 100px minmax(0, 1fr); gap: 12px; margin-top: 9px; padding: 11px 13px; border: 1px solid #2d3440; border-radius: 9px; background: #12161c; }
|
.outfit-effect { display: grid; grid-template-columns: 100px minmax(0, 1fr); gap: 12px; margin-top: 9px; padding: 11px 13px; border: 1px solid #2d3440; border-radius: 9px; background: #12161c; }
|
||||||
.outfit-effect span { color: #d993b1; font-size: 13px; font-weight: 850; }
|
.outfit-effect span { color: #d993b1; font-size: 13px; font-weight: 850; }
|
||||||
.outfit-effect p { margin: 0; color: #b9c1cd; font-size: 14px; line-height: 1.55; }
|
.outfit-effect p { margin: 0; color: #b9c1cd; font-size: 14px; line-height: 1.55; }
|
||||||
.hint { margin: 13px 0 0; color: #858f9f; font-size: 13px; line-height: 1.6; }
|
.hint { margin: 13px 0 0; color: #858f9f; font-size: 13px; line-height: 1.6; }
|
||||||
.error { margin: 14px 0 0; color: #ff9f9f; font-size: 13px; }
|
.error { margin: 14px 0 0; color: #ff9f9f; font-size: 13px; }
|
||||||
.search { width: 100%; height: 46px; margin-top: 18px; border: 0; border-radius: 10px; color: #171019; background: #ed8bb5; font-weight: 900; cursor: pointer; }
|
.search { width: 100%; height: 46px; margin-top: 20px; border: 0; border-radius: 10px; color: #171019; background: #ed8bb5; font-weight: 900; cursor: pointer; }
|
||||||
|
.search:hover:not(:disabled) { background: #f2a0c2; }
|
||||||
.search:disabled { opacity: .55; cursor: default; }
|
.search:disabled { opacity: .55; cursor: default; }
|
||||||
.compute-option { display: flex; align-items: center; gap: 9px; width: fit-content; margin-top: 12px; cursor: pointer; }
|
.compute-option { display: flex; align-items: center; gap: 9px; width: fit-content; margin-top: 12px; cursor: pointer; }
|
||||||
.compute-option input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
.compute-option input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
||||||
@@ -1150,6 +1179,7 @@
|
|||||||
main { width: calc(100% - 24px); padding-top: var(--page-content-top); }
|
main { width: calc(100% - 24px); padding-top: var(--page-content-top); }
|
||||||
.panel { padding: 17px; }
|
.panel { padding: 17px; }
|
||||||
.song-fields { grid-template-columns: 1fr 100px; }
|
.song-fields { grid-template-columns: 1fr 100px; }
|
||||||
|
.chart-meta { grid-column: 1 / -1; height: auto; }
|
||||||
.candidate-fields { grid-template-columns: 1fr; }
|
.candidate-fields { grid-template-columns: 1fr; }
|
||||||
.fixed-column-headings { display: none; }
|
.fixed-column-headings { display: none; }
|
||||||
.fixed-field { grid-template-columns: minmax(0, 1fr) 102px; }
|
.fixed-field { grid-template-columns: minmax(0, 1fr) 102px; }
|
||||||
|
|||||||
@@ -92,7 +92,10 @@
|
|||||||
specialConditions: ['일반', '조건부'],
|
specialConditions: ['일반', '조건부'],
|
||||||
activeProbabilities: ['높음', '중간'],
|
activeProbabilities: ['높음', '중간'],
|
||||||
cardVariants: ['일반', ...new Set(fiveStarCards.map((card) => card.versionName).filter(Boolean))],
|
cardVariants: ['일반', ...new Set(fiveStarCards.map((card) => card.versionName).filter(Boolean))],
|
||||||
generations: [...new Set(fiveStarCards.map((card) => card.generation).filter(Boolean))]
|
generations: [...new Set(fiveStarCards.map((card) => card.generation).filter(Boolean))],
|
||||||
|
sortKey: null,
|
||||||
|
sortDirection: 'asc',
|
||||||
|
skillLevel: 1
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,7 +134,10 @@
|
|||||||
$: filteredFourStarCards = filteredCards.filter((card) => card.rarity === 4);
|
$: filteredFourStarCards = filteredCards.filter((card) => card.rarity === 4);
|
||||||
$: selectedBoardMember = boardMembers.find((member) => member.id === selectedMemberId) ?? null;
|
$: selectedBoardMember = boardMembers.find((member) => member.id === selectedMemberId) ?? null;
|
||||||
$: selectedActiveBoardNodes = userData.memberBoards[selectedMemberId] ?? [];
|
$: selectedActiveBoardNodes = userData.memberBoards[selectedMemberId] ?? [];
|
||||||
$: selectedActiveBoardNodeCount = new Set([centerNodeId(), ...selectedActiveBoardNodes].filter(Boolean)).size;
|
$: selectedActiveBoardNodeCount = new Set(selectedActiveBoardNodes.filter((nodeId) => nodeId !== centerNodeId())).size;
|
||||||
|
$: selectedBoardNodeCount = selectedBoardMember
|
||||||
|
? boardModels[selectedBoardMember.positionGroupId].filter((node) => node.groupId !== centerNodeId()).length
|
||||||
|
: 0;
|
||||||
$: selectedBoardEdges = selectedBoardMember ? boardEdges(boardModels[selectedBoardMember.positionGroupId]) : [];
|
$: selectedBoardEdges = selectedBoardMember ? boardEdges(boardModels[selectedBoardMember.positionGroupId]) : [];
|
||||||
$: selectedBoardCardPlacements = userData.boardCards[selectedMemberId] ?? {};
|
$: selectedBoardCardPlacements = userData.boardCards[selectedMemberId] ?? {};
|
||||||
$: selectedConnectionBoosts = connectionBoostsForMember(
|
$: selectedConnectionBoosts = connectionBoostsForMember(
|
||||||
@@ -1291,7 +1297,7 @@
|
|||||||
|
|
||||||
{#if selectedBoardMember}
|
{#if selectedBoardMember}
|
||||||
<div class="board-toolbar">
|
<div class="board-toolbar">
|
||||||
<p><strong>{selectedActiveBoardNodeCount}</strong> / {boardModels[selectedBoardMember.positionGroupId].length}개 활성화</p>
|
<p><strong>{selectedActiveBoardNodeCount}</strong> / {selectedBoardNodeCount}개 활성화</p>
|
||||||
<div><button type="button" on:click={activateAllBlueNodes}>모든 파란 노드 활성화</button><button type="button" on:click={() => setAllBoardNodes(true)}>모두 활성화</button><button type="button" on:click={() => setAllBoardNodes(false)}>모두 해제</button></div>
|
<div><button type="button" on:click={activateAllBlueNodes}>모든 파란 노드 활성화</button><button type="button" on:click={() => setAllBoardNodes(true)}>모두 활성화</button><button type="button" on:click={() => setAllBoardNodes(false)}>모두 해제</button></div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
Reference in New Issue
Block a user