feat: add alternate outfit exclusion to combination search
Publish GitHub Pages output / publish (push) Successful in 44s
Publish GitHub Pages output / publish (push) Successful in 44s
This commit is contained in:
+7
-1
@@ -1,7 +1,13 @@
|
|||||||
---
|
---
|
||||||
updatedAt: 2026-10-06T03:11:36.779Z
|
updatedAt: 2026-10-07T00:01:58.945Z
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026년 10월 7일
|
||||||
|
|
||||||
|
### 조합 탐색에서 이격 제외
|
||||||
|
|
||||||
|
`NEW` 보유 카드 제한을 해제한 조합 탐색에 ‘이격 제외’ 옵션을 추가했습니다. 수영복 등 다른 의상을 제외하고 일반 카드만 선택할 수 있습니다. 고정 카드와 자동 후보에 함께 적용되며 탐색 조건·프리셋에 저장됩니다. 리더 의상과 연결 카드는 제한하지 않으며, 실제 모집·천장 획득 가능 여부를 보장하는 필터는 아닙니다.
|
||||||
|
|
||||||
## 2026년 10월 6일
|
## 2026년 10월 6일
|
||||||
|
|
||||||
### 파란 보드 개방 가정으로 연결 카드 추천
|
### 파란 보드 개방 가정으로 연결 카드 추천
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { combinationCardCandidates } from './combination-card-filter.ts';
|
||||||
|
|
||||||
|
const cards = [
|
||||||
|
{ id: 'regular', versionName: '' },
|
||||||
|
{ id: 'swim', versionName: '수영복' },
|
||||||
|
{ id: 'future', versionName: '다른 의상' }
|
||||||
|
];
|
||||||
|
const collection = { swim: { owned: true } };
|
||||||
|
|
||||||
|
test('전체 후보는 기존대로 유지하고 일반 옵션은 일반 카드만 허용한다', () => {
|
||||||
|
assert.deepEqual(combinationCardCandidates(cards, collection, false, false), cards);
|
||||||
|
assert.deepEqual(combinationCardCandidates(cards, collection, false, true), [cards[0]]);
|
||||||
|
assert.equal(cards.length, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('보유 제한에서는 일반 옵션을 무시해 보유 수영복도 사용할 수 있다', () => {
|
||||||
|
assert.deepEqual(combinationCardCandidates(cards, collection, true, true), [cards[1]]);
|
||||||
|
assert.deepEqual(combinationCardCandidates(cards, {}, true, false), []);
|
||||||
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
/** Matches the card catalog's 일반 category, not current gacha availability. */
|
||||||
|
export function combinationCardCandidates<T extends { id: string; versionName: string }>(
|
||||||
|
cards: readonly T[],
|
||||||
|
collection: Record<string, { owned?: boolean }>,
|
||||||
|
ownedOnly: boolean,
|
||||||
|
regularOnly: boolean
|
||||||
|
): T[] {
|
||||||
|
return cards.filter((card) => ownedOnly
|
||||||
|
? collection[card.id]?.owned === true
|
||||||
|
: !regularOnly || card.versionName === '');
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import cardSummary from '$lib/data/cards-summary.json';
|
import cardSummary from '$lib/data/cards-summary.json';
|
||||||
import { leaderOutfitCards } from '$lib/engine/leader-outfits.ts';
|
import { leaderOutfitCards } from '$lib/engine/leader-outfits.ts';
|
||||||
|
import { combinationCardCandidates } from '$lib/engine/combination-card-filter.ts';
|
||||||
import boardSummary from '$lib/data/boards-summary.json';
|
import boardSummary from '$lib/data/boards-summary.json';
|
||||||
import memorySummary from '$lib/data/memory-summary.json';
|
import memorySummary from '$lib/data/memory-summary.json';
|
||||||
import exactChartCatalog from '$lib/data/exact-chart-catalog.json';
|
import exactChartCatalog from '$lib/data/exact-chart-catalog.json';
|
||||||
@@ -87,6 +88,7 @@
|
|||||||
fixedGenerationBonusOverrides?: (number | null)[];
|
fixedGenerationBonusOverrides?: (number | null)[];
|
||||||
songScoreBonusOverride?: number | null;
|
songScoreBonusOverride?: number | null;
|
||||||
ownedCardsOnly?: boolean;
|
ownedCardsOnly?: boolean;
|
||||||
|
regularCardsOnly?: boolean;
|
||||||
maxMediumProbabilityCards?: number | null;
|
maxMediumProbabilityCards?: number | null;
|
||||||
maximizeCompute?: boolean;
|
maximizeCompute?: boolean;
|
||||||
resultLimit?: number;
|
resultLimit?: number;
|
||||||
@@ -145,6 +147,7 @@
|
|||||||
let fixedGenerationBonusOverrides: (number | null)[] = [null, null, null, null];
|
let fixedGenerationBonusOverrides: (number | null)[] = [null, null, null, null];
|
||||||
let songScoreBonusOverride: number | null = 10;
|
let songScoreBonusOverride: number | null = 10;
|
||||||
let ownedCardsOnly = false;
|
let ownedCardsOnly = false;
|
||||||
|
let regularCardsOnly = false;
|
||||||
let maxMediumProbabilityCards: number | null = null;
|
let maxMediumProbabilityCards: number | null = null;
|
||||||
let resultLimit = 10;
|
let resultLimit = 10;
|
||||||
let activeResultLimit = 10;
|
let activeResultLimit = 10;
|
||||||
@@ -283,9 +286,7 @@
|
|||||||
$: if (ready && connectionCards && baselineState && baselineMatchesConditions && !baselineResult && !baselineRunning && !baselineError) void analyzeBaseline();
|
$: if (ready && connectionCards && baselineState && baselineMatchesConditions && !baselineResult && !baselineRunning && !baselineError) void analyzeBaseline();
|
||||||
$: if (!result && rankedResults.length > 0) rankedResults = [];
|
$: if (!result && rankedResults.length > 0) rankedResults = [];
|
||||||
$: fixedCards = fixedCardIds.map((id) => cardById.get(id)).filter((card): card is CardRow => Boolean(card));
|
$: fixedCards = fixedCardIds.map((id) => cardById.get(id)).filter((card): card is CardRow => Boolean(card));
|
||||||
$: candidateCards = ownedCardsOnly
|
$: candidateCards = combinationCardCandidates(eligibleCards, collection, ownedCardsOnly, regularCardsOnly);
|
||||||
? eligibleCards.filter((card) => collection[card.id]?.owned === true)
|
|
||||||
: eligibleCards;
|
|
||||||
$: leaderMember = leaderMembers.find((card) => card.characterId === leaderCharacterId);
|
$: leaderMember = leaderMembers.find((card) => card.characterId === leaderCharacterId);
|
||||||
$: leaderOutfitOptions = leaderMember
|
$: leaderOutfitOptions = leaderMember
|
||||||
? leaderOutfitCards(allCards, leaderCharacterId)
|
? leaderOutfitCards(allCards, leaderCharacterId)
|
||||||
@@ -313,6 +314,7 @@
|
|||||||
fixedGenerationBonusOverrides,
|
fixedGenerationBonusOverrides,
|
||||||
songScoreBonusOverride,
|
songScoreBonusOverride,
|
||||||
ownedCardsOnly,
|
ownedCardsOnly,
|
||||||
|
regularCardsOnly,
|
||||||
maxMediumProbabilityCards,
|
maxMediumProbabilityCards,
|
||||||
resultLimit,
|
resultLimit,
|
||||||
maximizeCompute,
|
maximizeCompute,
|
||||||
@@ -332,6 +334,7 @@
|
|||||||
fixedGenerationBonusOverrides,
|
fixedGenerationBonusOverrides,
|
||||||
songScoreBonusOverride,
|
songScoreBonusOverride,
|
||||||
ownedCardsOnly,
|
ownedCardsOnly,
|
||||||
|
regularCardsOnly,
|
||||||
maxMediumProbabilityCards,
|
maxMediumProbabilityCards,
|
||||||
resultLimit,
|
resultLimit,
|
||||||
maximizeCompute,
|
maximizeCompute,
|
||||||
@@ -475,11 +478,12 @@
|
|||||||
? null
|
? null
|
||||||
: normalizeSongScoreBonusOverride(state.songScoreBonusOverride) ?? 10;
|
: normalizeSongScoreBonusOverride(state.songScoreBonusOverride) ?? 10;
|
||||||
ownedCardsOnly = state.ownedCardsOnly === true;
|
ownedCardsOnly = state.ownedCardsOnly === true;
|
||||||
|
regularCardsOnly = state.regularCardsOnly === true;
|
||||||
resultLimit = [10, 20, 30, 50].includes(state.resultLimit ?? 10) ? state.resultLimit ?? 10 : 10;
|
resultLimit = [10, 20, 30, 50].includes(state.resultLimit ?? 10) ? state.resultLimit ?? 10 : 10;
|
||||||
maxMediumProbabilityCards = typeof state.maxMediumProbabilityCards === 'number' &&
|
maxMediumProbabilityCards = typeof state.maxMediumProbabilityCards === 'number' &&
|
||||||
Number.isInteger(state.maxMediumProbabilityCards) && state.maxMediumProbabilityCards >= 0 && state.maxMediumProbabilityCards < 5
|
Number.isInteger(state.maxMediumProbabilityCards) && state.maxMediumProbabilityCards >= 0 && state.maxMediumProbabilityCards < 5
|
||||||
? state.maxMediumProbabilityCards : null;
|
? state.maxMediumProbabilityCards : null;
|
||||||
if (ownedCardsOnly) toggleOwnedCardsOnly(true);
|
toggleOwnedCardsOnly(ownedCardsOnly);
|
||||||
maximizeCompute = state.maximizeCompute === true;
|
maximizeCompute = state.maximizeCompute === true;
|
||||||
if (typeof state.leaderCharacterId === 'string' && leaderMembers.some((member) => member.characterId === state.leaderCharacterId)) {
|
if (typeof state.leaderCharacterId === 'string' && leaderMembers.some((member) => member.characterId === state.leaderCharacterId)) {
|
||||||
leaderCharacterId = state.leaderCharacterId;
|
leaderCharacterId = state.leaderCharacterId;
|
||||||
@@ -585,8 +589,9 @@
|
|||||||
|
|
||||||
function toggleOwnedCardsOnly(enabled: boolean): void {
|
function toggleOwnedCardsOnly(enabled: boolean): void {
|
||||||
ownedCardsOnly = enabled;
|
ownedCardsOnly = enabled;
|
||||||
if (enabled) {
|
if (enabled || regularCardsOnly) {
|
||||||
const removedSlots = fixedCardIds.map((cardId) => Boolean(cardId && !collection[cardId]?.owned));
|
const allowedIds = new Set(combinationCardCandidates(allCards, collection, enabled, regularCardsOnly).map((card) => card.id));
|
||||||
|
const removedSlots = fixedCardIds.map((cardId) => Boolean(cardId && !allowedIds.has(cardId)));
|
||||||
fixedCardIds = fixedCardIds.map((cardId, slot) => removedSlots[slot] ? '' : cardId);
|
fixedCardIds = fixedCardIds.map((cardId, slot) => removedSlots[slot] ? '' : cardId);
|
||||||
fixedCooldownReductions = fixedCooldownReductions.map((value, slot) => removedSlots[slot] ? null : value);
|
fixedCooldownReductions = fixedCooldownReductions.map((value, slot) => removedSlots[slot] ? null : value);
|
||||||
fixedAllBlueNodes = fixedAllBlueNodes.map((value, slot) => removedSlots[slot] ? false : value);
|
fixedAllBlueNodes = fixedAllBlueNodes.map((value, slot) => removedSlots[slot] ? false : value);
|
||||||
@@ -1315,6 +1320,14 @@
|
|||||||
<span aria-hidden="true"></span>
|
<span aria-hidden="true"></span>
|
||||||
<strong>내가 보유한 카드만 사용하기</strong>
|
<strong>내가 보유한 카드만 사용하기</strong>
|
||||||
</label>
|
</label>
|
||||||
|
{#if !ownedCardsOnly}
|
||||||
|
<label class="owned-cards-option">
|
||||||
|
<input type="checkbox" checked={regularCardsOnly} disabled={running} on:change={(event) => { regularCardsOnly = event.currentTarget.checked; toggleOwnedCardsOnly(false); }}>
|
||||||
|
<span aria-hidden="true"></span>
|
||||||
|
<strong>이격 제외</strong>
|
||||||
|
</label>
|
||||||
|
<p class="candidate-guide">수영복 등 다른 의상의 이격 카드를 제외하고, 카드 목록의 ‘일반’ 분류만 고정 카드·자동 후보로 사용합니다. 제외되는 고정 카드는 해제됩니다. 현재 모집·천장 획득 가능 여부를 보장하지 않으며, 리더 의상과 연결 카드는 제한하지 않습니다.</p>
|
||||||
|
{/if}
|
||||||
<p class="candidate-guide">
|
<p class="candidate-guide">
|
||||||
‘내가 보유한 카드만 사용하기’를 켜면 고정 카드와 자동 후보 모두 {eventMode ? '보유한 이벤트 후보 카드' : '보유 5성 카드'}로 제한됩니다.<br>
|
‘내가 보유한 카드만 사용하기’를 켜면 고정 카드와 자동 후보 모두 {eventMode ? '보유한 이벤트 후보 카드' : '보유 5성 카드'}로 제한됩니다.<br>
|
||||||
보유 카드의 개화가 자동 후보 개화보다 높으면 실제 보유 개화를 우선 적용합니다.<br>
|
보유 카드의 개화가 자동 후보 개화보다 높으면 실제 보유 개화를 우선 적용합니다.<br>
|
||||||
|
|||||||
Reference in New Issue
Block a user