feat: 가창 보너스 덮어쓰기 추가

This commit is contained in:
2026-08-31 11:39:48 +09:00
parent 06e7edcceb
commit 2998278353
2 changed files with 66 additions and 11 deletions
+32 -4
View File
@@ -63,6 +63,7 @@
fixedAllBlueNodes?: boolean[];
fixedMaxGenerationBonuses?: boolean[];
fixedGenerationBonusOverrides?: (number | null)[];
songScoreBonusOverride?: number | null;
ownedCardsOnly?: boolean;
maximizeCompute?: boolean;
leaderCharacterId: string;
@@ -77,6 +78,7 @@
fixedCooldownReductions: (CooldownReduction | null)[];
allBlueNodes: boolean[];
generationBonusOverrides: (number | null)[];
songScoreBonusOverride?: number | null;
leaderCharacterId: string;
leaderOutfitCardId: string;
};
@@ -115,6 +117,7 @@
let fixedCooldownReductions: (CooldownReduction | null)[] = [null, null, null, null];
let fixedAllBlueNodes = [false, false, false, false];
let fixedGenerationBonusOverrides: (number | null)[] = [null, null, null, null];
let songScoreBonusOverride: number | null = 10;
let ownedCardsOnly = false;
let maximizeCompute = false;
let leaderCharacterId = leaderMembers[0]?.characterId ?? '';
@@ -195,6 +198,7 @@
fixedCooldownReductions,
fixedAllBlueNodes,
fixedGenerationBonusOverrides,
songScoreBonusOverride,
ownedCardsOnly,
maximizeCompute,
leaderCharacterId,
@@ -211,6 +215,7 @@
fixedCooldownReductions,
fixedAllBlueNodes,
fixedGenerationBonusOverrides,
songScoreBonusOverride,
ownedCardsOnly,
maximizeCompute,
leaderCharacterId,
@@ -306,6 +311,11 @@
return Math.max(0, Math.min(900, Number.isFinite(value) ? Math.round(Number(value)) : 0));
}
function normalizeSongScoreBonusOverride(value: unknown): number | null {
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) return null;
return Math.min(10, value);
}
function sanitizeGenerationBonusInput(input: HTMLInputElement, previousValue: number | null): string {
if (input.value === '') {
input.value = '';
@@ -348,6 +358,9 @@
if (typeof savedValue === 'number' && Number.isFinite(savedValue)) return normalizeGenerationBonus(savedValue);
return state.fixedMaxGenerationBonuses?.[index] === true ? 900 : null;
});
songScoreBonusOverride = state.songScoreBonusOverride === null
? null
: normalizeSongScoreBonusOverride(state.songScoreBonusOverride) ?? 10;
ownedCardsOnly = state.ownedCardsOnly === true;
if (ownedCardsOnly) toggleOwnedCardsOnly(true);
maximizeCompute = state.maximizeCompute === true;
@@ -389,6 +402,7 @@
const value = state.generationBonusOverrides?.[index];
return typeof value === 'number' && Number.isFinite(value) ? normalizeGenerationBonus(value) : null;
}),
songScoreBonusOverride: normalizeSongScoreBonusOverride(state.songScoreBonusOverride),
leaderCharacterId: typeof state.leaderCharacterId === 'string' ? state.leaderCharacterId : '',
leaderOutfitCardId: typeof state.leaderOutfitCardId === 'string' ? state.leaderOutfitCardId : ''
};
@@ -444,6 +458,14 @@
result = null;
}
function updateSongScoreBonusOverride(input: HTMLInputElement): void {
const parsed = Number(input.value);
if (input.value.trim() === '' || !Number.isFinite(parsed) || parsed < 0) input.value = '';
if (parsed > 10) input.value = '10';
songScoreBonusOverride = input.value === '' ? null : Math.min(10, parsed);
result = null;
}
function toggleOwnedCardsOnly(enabled: boolean): void {
ownedCardsOnly = enabled;
if (enabled) {
@@ -669,7 +691,7 @@
cards: connectionCards
}, {
generationBonusByCardId,
directSongScoreBonusPercent: 10
...(songScoreBonusOverride === null ? {} : { directSongScoreBonusPercent: songScoreBonusOverride })
});
const power = calculateUnitPower(
cards.map((card) => ({ card, awakening: awakeningForCard(card) })),
@@ -732,7 +754,12 @@
const boardEffects = calculateBoardEffects(boardSummary, baselineBoards, cards, state.leaderCharacterId, chartMetadata, {
placements: boardCards,
cards: connectionCards
}, { generationBonusByCardId });
}, {
generationBonusByCardId,
...(state.songScoreBonusOverride === null || state.songScoreBonusOverride === undefined
? {}
: { directSongScoreBonusPercent: state.songScoreBonusOverride })
});
const power = calculateUnitPower(
cards.map((card, index) => ({ card, awakening: state.awakenings[index] })),
outfit,
@@ -1055,6 +1082,7 @@
</div>
</div>
<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>
<label>가창 보너스 덮어쓰기 (0~10%)<input class="number-input" type="number" min="0" max="10" step="any" value={songScoreBonusOverride ?? ''} placeholder="실제 보드값" on:input={(event) => updateSongScoreBonusOverride(event.currentTarget)}></label>
</div>
<label class="owned-cards-option">
<input type="checkbox" checked={ownedCardsOnly} disabled={running} on:change={(event) => toggleOwnedCardsOnly(event.currentTarget.checked)}>
@@ -1112,7 +1140,7 @@
<span>리더 의상 효과</span>
<p>{leaderOutfitDescription || '적용할 수 있는 5성 의상이 없습니다.'}</p>
</div>
<p class="hint">기수 보너스 덮어쓰기는 빈칸이면 저장된 실제 보드를 사용하고, 숫자를 입력하면 해당 값으로 대체합니다. 자동 후보는 파란 노드 전체 활성화·기수 보너스 {candidateGenerationBonus === null ? '저장값' : candidateGenerationBonus}·악곡 보너스 10% 조건으로 비교합니다.</p>
<p class="hint">기수 보너스와 가창 보너스 덮어쓰기는 빈칸이면 저장된 실제 보드를 사용하고, 숫자를 입력하면 해당 값으로 대체합니다. 자동 후보는 파란 노드 전체 활성화·기수 보너스 {candidateGenerationBonus === null ? '저장값' : candidateGenerationBonus}·가창 보너스 {songScoreBonusOverride === null ? '실제 보드값' : `${songScoreBonusOverride}%`} 조건으로 비교합니다.</p>
<PresetManager storageKey={PRESET_STORAGE_KEY} snapshot={presetSnapshot} disabled={running} on:apply={applyPreset} />
{#if errorMessage}<p class="error" role="alert">{errorMessage}</p>{/if}
<button class="search" type="button" disabled={running} on:click={search}>{running ? searchPhase === 'screening' ? `후보 선별 중 ${Math.round(screeningProgress * 100)}%` : searchPhase === 'probability' ? `달성 확률 계산 중 ${Math.round(progress.evaluatedCandidates / Math.max(1, progress.totalCandidates) * 100)}%` : `정밀 분석 중 ${Math.round(progress.evaluatedCandidates / Math.max(1, progress.totalCandidates) * 100)}%` : '최적 조합 탐색'}</button>
@@ -1211,7 +1239,7 @@
.song-picker-field { min-width: 0; }
.chart-meta { display: flex; grid-column: 1 / -1; flex-wrap: wrap; align-items: center; gap: 8px; min-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-fields { display: grid; grid-template-columns: clamp(130px, 12vw, 180px) repeat(2, clamp(230px, 21vw, 340px)); gap: 9px; margin-top: 18px; }
.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; }
.fixed-fields { display: grid; gap: 9px; margin-top: 16px; }