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
+34 -7
View File
@@ -46,6 +46,7 @@
fixedCooldownReductions?: (CooldownReduction | null)[];
allBlueNodes?: boolean[];
generationBonusOverrides?: (number | null)[];
songScoreBonusOverride?: number | null;
leaderCharacterId: string;
leaderOutfitCardId: string;
};
@@ -96,6 +97,7 @@
let fixedCooldownReductions: (CooldownReduction | null)[] = selectedCardIds.map(() => null);
let allBlueNodes = selectedCardIds.map(() => false);
let generationBonusOverrides: (number | null)[] = selectedCardIds.map(() => null);
let songScoreBonusOverride: number | null = null;
let leaderCharacterId = leaderMembers[0]?.characterId ?? '';
let leaderOutfitCardId = leaderMembers[0]?.id ?? '';
let worker: Worker | null = null;
@@ -141,7 +143,10 @@
$: boardEffects = calculateBoardEffects(boardSummary, effectiveMemberBoards, selectedSourceCards, leaderCharacterId, selectedChart, {
placements: boardCards,
cards: connectionCards
}, { generationBonusByCardId });
}, {
generationBonusByCardId,
...(songScoreBonusOverride === null ? {} : { directSongScoreBonusPercent: songScoreBonusOverride })
});
$: memoryPermil = memoryParameterPermil(memoryCount, memorySummary.effects);
$: unitPower = (() => {
try {
@@ -180,6 +185,7 @@
fixedCooldownReductions,
allBlueNodes,
generationBonusOverrides,
songScoreBonusOverride,
leaderCharacterId,
leaderOutfitCardId
} satisfies SavedAnalysisState;
@@ -192,6 +198,7 @@
fixedCooldownReductions,
allBlueNodes,
generationBonusOverrides,
songScoreBonusOverride,
leaderCharacterId,
leaderOutfitCardId
});
@@ -301,6 +308,7 @@
if (!legacy && state.schemaVersion !== 2) return;
if (typeof state.songId === 'string' && songs.some((song) => song.id === state.songId)) selectedSongId = state.songId;
if (state.difficulty === 'hard' || state.difficulty === 'expert') difficulty = state.difficulty;
songScoreBonusOverride = normalizeSongScoreBonusOverride(state.songScoreBonusOverride);
if (Array.isArray(state.cardIds) && state.cardIds.length === 5) {
const restoredCards = state.cardIds.map((id) => cardById.get(id)).filter((card): card is CardRow => Boolean(card));
@@ -380,6 +388,19 @@
result = null;
}
function normalizeSongScoreBonusOverride(value: unknown): number | null {
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) return null;
return Math.min(10, value);
}
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 boardsForSelectedCards(
storedBoards: MemberBoards,
cardIds: readonly string[],
@@ -439,7 +460,10 @@
const localBoardEffects = calculateBoardEffects(boardSummary, effectiveMemberBoards, selectedSourceCards, leaderCharacterId, chartSummary, {
placements,
cards: connectionCards
}, { generationBonusByCardId });
}, {
generationBonusByCardId,
...(songScoreBonusOverride === null ? {} : { directSongScoreBonusPercent: songScoreBonusOverride })
});
const localUnitPower = calculateUnitPower(selectedCardIds.map((id, index) => ({
card: cardById.get(id)!, awakening: selectedAwakenings[index]
})), leaderOutfit, localBoardEffects, memoryPermil, memberPowerUpPercent);
@@ -776,8 +800,11 @@
<option value="hard">Hard</option>
</select>
</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 class="power-summary"><span>자동 계산 종합력</span><strong>{overallPower.toLocaleString('ko-KR')}</strong></div>
<div class="chart-meta"><span>길이 {selectedChart?.durationSeconds ?? 0}초</span><span>노트 {selectedChart?.totalNotes?.toLocaleString('ko-KR') ?? 0}개</span><span>1노트 {Math.ceil(perfectNoteScore).toLocaleString('ko-KR')}점 · {scoreCalibrationLabel}</span>{#if boardEffects.directSongScoreBonusPercent > 0}<span>가창 보너스 {boardEffects.directSongScoreBonusPercent}%</span>{/if}</div>
<div class="chart-meta"><span>길이 {selectedChart?.durationSeconds ?? 0}초</span><span>노트 {selectedChart?.totalNotes?.toLocaleString('ko-KR') ?? 0}개</span><span>1노트 {Math.ceil(perfectNoteScore).toLocaleString('ko-KR')}점 · {scoreCalibrationLabel}</span>{#if boardEffects.directSongScoreBonusPercent > 0 || songScoreBonusOverride !== null}<span>가창 보너스 {boardEffects.directSongScoreBonusPercent}%{songScoreBonusOverride === null ? '' : ' · 덮어쓰기'}</span>{/if}</div>
</div>
<div class="card-fields">
@@ -835,7 +862,7 @@
</div>
{/each}
</div>
<p class="board-guide">‘파란 노드 전체’를 사용하지 않으면 사용자 데이터에 저장된 해당 멤버의 보드를 사용합니다. 기수 보너스는 빈칸이면 실제 보드 수치를 사용하고, 숫자를 입력하면 해당 값으로 대체합니다.</p>
<p class="board-guide">‘파란 노드 전체’를 사용하지 않으면 사용자 데이터에 저장된 해당 멤버의 보드를 사용합니다. 기수 보너스와 가창 보너스 덮어쓰기는 빈칸이면 실제 보드 수치를 사용하고, 숫자를 입력하면 해당 값으로 대체합니다.</p>
<div class="leader-fields">
<label>리더
<select value={leaderCharacterId} on:change={(event) => updateLeader(event.currentTarget.value)}>
@@ -1010,9 +1037,9 @@
h2 { margin: 0; font-size: 18px; }
.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, .generation-input:focus { border-color: #d86f9c; outline: 3px solid rgb(216 111 156 / 14%); }
.song-fields { display: grid; grid-template-columns: minmax(0, 4fr) clamp(110px, 11vw, 170px) clamp(140px, 14vw, 220px); align-items: start; gap: 12px; margin-top: 22px; }
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%); }
.song-fields { display: grid; grid-template-columns: minmax(0, 4fr) clamp(110px, 11vw, 170px) clamp(190px, 17vw, 250px) clamp(140px, 14vw, 220px); align-items: start; gap: 12px; margin-top: 22px; }
.song-picker-field { min-width: 0; }
.power-summary { display: grid; gap: 4px; min-height: 42px; margin-top: 25px; padding: 7px 11px; border-radius: 9px; background: #22202a; }
.power-summary span { color: #929cab; font-size: 12px; font-weight: 750; }