feat: release version 1.0.6
This commit is contained in:
@@ -32,6 +32,8 @@
|
||||
cardIds: string[];
|
||||
awakenings: number[];
|
||||
fixedCooldownReductions?: (CooldownReduction | null)[];
|
||||
allBlueNodes?: boolean[];
|
||||
generationBonusOverrides?: (number | null)[];
|
||||
leaderCharacterId: string;
|
||||
leaderOutfitCardId: string;
|
||||
};
|
||||
@@ -50,6 +52,12 @@
|
||||
let charts = releasedSongCharts(allCharts, releaseNow);
|
||||
let songs = Array.from(new Map(charts.map((chart) => [chart.songId, { id: chart.songId, title: chart.title }])).values());
|
||||
const cardById = new Map(fiveStarCards.map((card) => [card.id, card]));
|
||||
const blueGroupsByCharacter = Object.fromEntries(boardSummary.members.map((member) => [
|
||||
member.id,
|
||||
(boardSummary.models as Record<string, { groupId: string }[]>)[member.positionGroupId]
|
||||
?.filter((position) => position.groupId.startsWith('B'))
|
||||
.map((position) => position.groupId) ?? []
|
||||
]));
|
||||
|
||||
function uniqueMemberCards(cards: readonly CardRow[], limit = Number.POSITIVE_INFINITY): CardRow[] {
|
||||
const memberIds = new Set<string>();
|
||||
@@ -72,6 +80,8 @@
|
||||
let selectedCardIds = uniqueMemberCards(fiveStarCards, 5).map((card) => card.id);
|
||||
let selectedAwakenings = selectedCardIds.map(() => 5);
|
||||
let fixedCooldownReductions: (CooldownReduction | null)[] = selectedCardIds.map(() => null);
|
||||
let allBlueNodes = selectedCardIds.map(() => false);
|
||||
let generationBonusOverrides: (number | null)[] = selectedCardIds.map(() => null);
|
||||
let leaderCharacterId = leaderMembers[0]?.characterId ?? '';
|
||||
let leaderOutfitCardId = leaderMembers[0]?.id ?? '';
|
||||
let worker: Worker | null = null;
|
||||
@@ -105,10 +115,14 @@
|
||||
awakening: collection[card.id]?.awakening ?? 0,
|
||||
connect: card.details.connect
|
||||
}));
|
||||
$: boardEffects = calculateBoardEffects(boardSummary, memberBoards, selectedSourceCards, leaderCharacterId, selectedChart, {
|
||||
$: effectiveMemberBoards = boardsForSelectedCards(memberBoards, selectedCardIds, allBlueNodes);
|
||||
$: generationBonusByCardId = Object.fromEntries(selectedCardIds
|
||||
.map((cardId, index) => [cardId, generationBonusOverrides[index]] as const)
|
||||
.filter((entry): entry is readonly [string, number] => entry[1] !== null));
|
||||
$: boardEffects = calculateBoardEffects(boardSummary, effectiveMemberBoards, selectedSourceCards, leaderCharacterId, selectedChart, {
|
||||
placements: boardCards,
|
||||
cards: connectionCards
|
||||
});
|
||||
}, { generationBonusByCardId });
|
||||
$: memoryPermil = memoryParameterPermil(memoryCount, memorySummary.effects);
|
||||
$: unitPower = (() => {
|
||||
try {
|
||||
@@ -146,6 +160,8 @@
|
||||
cardIds: selectedCardIds,
|
||||
awakenings: selectedAwakenings,
|
||||
fixedCooldownReductions,
|
||||
allBlueNodes,
|
||||
generationBonusOverrides,
|
||||
leaderCharacterId,
|
||||
leaderOutfitCardId
|
||||
} satisfies SavedAnalysisState;
|
||||
@@ -156,6 +172,8 @@
|
||||
cardIds: selectedCardIds,
|
||||
awakenings: selectedAwakenings,
|
||||
fixedCooldownReductions,
|
||||
allBlueNodes,
|
||||
generationBonusOverrides,
|
||||
leaderCharacterId,
|
||||
leaderOutfitCardId
|
||||
});
|
||||
@@ -275,6 +293,8 @@
|
||||
return normalizeAwakening(state.awakenings?.[index]);
|
||||
});
|
||||
fixedCooldownReductions = restoredCards.map((_, index) => normalizeFixedReduction(state.fixedCooldownReductions?.[index]));
|
||||
allBlueNodes = restoredCards.map((_, index) => state.allBlueNodes?.[index] === true);
|
||||
generationBonusOverrides = restoredCards.map((_, index) => normalizeGenerationBonusOverride(state.generationBonusOverrides?.[index]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,8 +335,47 @@
|
||||
result = null;
|
||||
}
|
||||
|
||||
function memberSelectedInAnotherSlot(card: CardRow, slot: number): boolean {
|
||||
return selectedCardIds.some((id, index) => index !== slot && cardById.get(id)?.characterId === card.characterId);
|
||||
function normalizeGenerationBonusOverride(value: unknown): number | null {
|
||||
if (typeof value !== 'number' || !Number.isFinite(value)) return null;
|
||||
return Math.max(0, Math.min(900, Math.round(value)));
|
||||
}
|
||||
|
||||
function sanitizeGenerationBonusInput(input: HTMLInputElement, previousValue: number | null): string {
|
||||
if (input.value === '') return '';
|
||||
if (!/^\d+$/.test(input.value) || Number(input.value) > 900) {
|
||||
input.value = previousValue === null ? '' : String(previousValue);
|
||||
return input.value;
|
||||
}
|
||||
input.value = String(Number(input.value));
|
||||
return input.value;
|
||||
}
|
||||
|
||||
function toggleAllBlueNodes(slot: number): void {
|
||||
allBlueNodes = allBlueNodes.map((enabled, index) => index === slot ? !enabled : enabled);
|
||||
result = null;
|
||||
}
|
||||
|
||||
function updateGenerationBonus(slot: number, rawValue: string): void {
|
||||
const parsed = Number(rawValue);
|
||||
const value = rawValue.trim() === '' || !Number.isFinite(parsed) ? null : normalizeGenerationBonusOverride(parsed);
|
||||
generationBonusOverrides = generationBonusOverrides.map((bonus, index) => index === slot ? value : bonus);
|
||||
result = null;
|
||||
}
|
||||
|
||||
function boardsForSelectedCards(
|
||||
storedBoards: MemberBoards,
|
||||
cardIds: readonly string[],
|
||||
blueNodeOverrides: readonly boolean[]
|
||||
): MemberBoards {
|
||||
const boards = Object.fromEntries(Object.entries(storedBoards).map(([characterId, groupIds]) => [characterId, [...groupIds]]));
|
||||
cardIds.forEach((cardId, index) => {
|
||||
if (!blueNodeOverrides[index]) return;
|
||||
const characterId = cardById.get(cardId)?.characterId;
|
||||
if (!characterId) return;
|
||||
const nonBlueGroups = (boards[characterId] ?? []).filter((groupId) => !groupId.startsWith('B'));
|
||||
boards[characterId] = [...nonBlueGroups, ...(blueGroupsByCharacter[characterId] ?? [])];
|
||||
});
|
||||
return boards;
|
||||
}
|
||||
|
||||
function updateCard(slot: number, cardId: string): void {
|
||||
@@ -522,13 +581,14 @@
|
||||
<span>카드 선택</span>
|
||||
<span>개화</span>
|
||||
<span>빈도 감소 고정</span>
|
||||
<div><span>파란 노드 전체</span><span>기수 보너스<br>덮어쓰기 (0~900)</span></div>
|
||||
</div>
|
||||
{#each selectedCardIds as selectedId, slot}
|
||||
<div class="card-field">
|
||||
<label><span>{slot + 1}번 선택</span>
|
||||
<select value={selectedId} on:change={(event) => updateCard(slot, event.currentTarget.value)}>
|
||||
{#each fiveStarCards as card}
|
||||
<option value={card.id} disabled={memberSelectedInAnotherSlot(card, slot)}>{collection[card.id]?.owned ? '● ' : ''}{cardLabel(card)}</option>
|
||||
<option value={card.id} disabled={selectedCardIds.some((id, index) => index !== slot && cardById.get(id)?.characterId === card.characterId)}>{collection[card.id]?.owned ? '● ' : ''}{cardLabel(card)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
@@ -561,9 +621,17 @@
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="board-overrides">
|
||||
<label class="override-checkbox">
|
||||
<input type="checkbox" checked={allBlueNodes[slot]} aria-label={`${slot + 1}번 카드 파란 노드 전체 활성화`} on:change={() => toggleAllBlueNodes(slot)}>
|
||||
<span aria-hidden="true"></span>
|
||||
</label>
|
||||
<input class="generation-input" type="text" inputmode="numeric" pattern="[0-9]*" maxlength="3" value={generationBonusOverrides[slot] ?? ''} placeholder="미사용" aria-label={`${slot + 1}번 카드 기수 보너스 덮어쓰기 (0~900)`} on:input={(event) => updateGenerationBonus(slot, sanitizeGenerationBonusInput(event.currentTarget, generationBonusOverrides[slot] ?? null))}>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<p class="board-guide">‘파란 노드 전체’를 사용하지 않으면 사용자 데이터에 저장된 해당 멤버의 보드를 사용합니다. 기수 보너스는 빈칸이면 실제 보드 수치를 사용하고, 숫자를 입력하면 해당 값으로 대체합니다.</p>
|
||||
<div class="leader-fields">
|
||||
<label>리더
|
||||
<select value={leaderCharacterId} on:change={(event) => updateLeader(event.currentTarget.value)}>
|
||||
@@ -705,7 +773,7 @@
|
||||
.section-title > strong { color: #aeb6c4; font-size: 14px; }
|
||||
label { display: grid; gap: 7px; color: #8f98a8; font-size: 14px; font-weight: 750; }
|
||||
select { 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 { border-color: #d86f9c; outline: 3px solid rgb(216 111 156 / 14%); }
|
||||
select:focus, .generation-input:focus { border-color: #d86f9c; outline: 3px solid rgb(216 111 156 / 14%); }
|
||||
.song-fields { display: grid; grid-template-columns: minmax(0, 2fr) clamp(110px, 11vw, 170px) clamp(140px, 14vw, 220px); align-items: end; gap: 12px; margin-top: 22px; }
|
||||
.power-summary { display: grid; gap: 4px; min-height: 42px; padding: 7px 11px; border-radius: 9px; background: #22202a; }
|
||||
.power-summary span { color: #929cab; font-size: 12px; font-weight: 750; }
|
||||
@@ -713,9 +781,10 @@
|
||||
.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; }
|
||||
.card-fields { display: grid; gap: 9px; margin-top: 18px; }
|
||||
.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, .card-field { display: grid; grid-template-columns: minmax(280px, 1fr) clamp(120px, 10vw, 160px) clamp(280px, 24vw, 360px) clamp(180px, 16vw, 240px); 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-header > div { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; color: #8f98a8; font-size: 12px; text-align: center; }
|
||||
.card-field > label { grid-template-columns: 74px minmax(0, 1fr); align-items: center; }
|
||||
.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; }
|
||||
@@ -731,6 +800,16 @@
|
||||
.reduction-options button:hover { color: #e9edf3; background: #202630; }
|
||||
.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; }
|
||||
.board-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 input { position: absolute; width: 1px; height: 1px; opacity: 0; }
|
||||
.override-checkbox span { display: grid; place-items: center; width: 22px; height: 22px; border: 1px solid #4a5361; border-radius: 6px; background: #11151b; }
|
||||
.override-checkbox input:checked + span { border-color: #ed8bb5; background: #ed8bb5; }
|
||||
.override-checkbox input:checked + span::after { content: '✓'; color: #171019; font-size: 14px; font-weight: 950; }
|
||||
.override-checkbox input:focus-visible + span { outline: 3px solid rgb(216 111 156 / 20%); }
|
||||
.generation-input { align-self: center; width: 100%; min-width: 0; height: 32px; padding: 0 7px; border: 1px solid #414957; border-radius: 7px; color: #e8ebf1; background: #11151b; font-size: 12px; font-weight: 800; text-align: center; }
|
||||
.generation-input::placeholder { color: #68717f; opacity: 1; }
|
||||
.board-guide { margin: 8px 0 0; color: #737d8d; font-size: 12px; line-height: 1.5; }
|
||||
.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 span { color: #d993b1; font-size: 13px; font-weight: 850; }
|
||||
@@ -794,7 +873,7 @@
|
||||
.time-axis span:last-child { transform: translateX(-100%); }
|
||||
.empty { display: grid; place-items: center; min-height: 150px; margin-top: 18px; border: 1px dashed #303744; border-radius: 12px; padding: 28px; text-align: center; }
|
||||
@media (max-width: 1100px) {
|
||||
.card-field { grid-template-columns: minmax(0, 1fr) 130px 304px; }
|
||||
.card-field-header, .card-field { grid-template-columns: minmax(0, 1fr) 120px 280px 180px; }
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
@@ -811,6 +890,7 @@
|
||||
.card-field > .awakening { grid-template-columns: 1fr; }
|
||||
.awakening-stepper { grid-template-columns: 28px minmax(0, 1fr) 28px; }
|
||||
.cooldown-lock { grid-column: 1 / -1; grid-template-columns: 88px minmax(0, 1fr); }
|
||||
.board-overrides { grid-column: 1 / -1; }
|
||||
.leader-fields { grid-template-columns: 1fr; }
|
||||
.outfit-effect { grid-template-columns: 1fr; gap: 5px; }
|
||||
.order-list li { grid-template-columns: 30px minmax(0, 1fr); }
|
||||
|
||||
Reference in New Issue
Block a user