feat: release version 1.0.4
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
||||
export type CardListSortKey = 'coolTime' | 'probability' | 'duration' | 'effect';
|
||||
export type SortDirection = 'asc' | 'desc';
|
||||
export type CardSkillLevel = 1 | 2;
|
||||
|
||||
export type CardListPreferences = {
|
||||
types: string[];
|
||||
stats: string[];
|
||||
@@ -20,6 +24,9 @@ export type CardListPreferences = {
|
||||
activeProbabilities: string[];
|
||||
cardVariants: string[];
|
||||
generations: string[];
|
||||
sortKey: CardListSortKey | null;
|
||||
sortDirection: SortDirection;
|
||||
skillLevel: CardSkillLevel;
|
||||
};
|
||||
|
||||
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';
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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'] {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).flatMap(([cardId, state]) => {
|
||||
@@ -79,7 +93,10 @@ export function parseUserData(text: string, fallbackPreferences: CardListPrefere
|
||||
specialConditions: stringArray(preferences.specialConditions, fallbackPreferences.specialConditions),
|
||||
activeProbabilities: stringArray(preferences.activeProbabilities, fallbackPreferences.activeProbabilities),
|
||||
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)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user