fix: round base card stats up to match in-game power
Publish GitHub Pages output / publish (push) Successful in 44s

This commit is contained in:
2026-09-06 22:31:56 +09:00
parent e688036d11
commit c6368a0304
4 changed files with 303 additions and 253 deletions
+5 -1
View File
@@ -1,9 +1,13 @@
--- ---
updatedAt: 2026-09-06T21:34:36+09:00 updatedAt: 2026-09-06T22:30:18+09:00
--- ---
## 2026년 9월 6일 ## 2026년 9월 6일
### 종합력 계산의 기본 능력치 보정
`FIXED` 카드 기본 능력치의 소수점 처리를 올림으로 수정했습니다. 일부 편성에서 종합력과 패시브·의상·보드·메모리·멤버 강화 효과가 실제 게임보다 소폭 낮게 계산되던 문제를 해결했습니다.
### 악곡별 점수 계산 정확도 개선 ### 악곡별 점수 계산 정확도 개선
`IMPROVED` 388개 채보의 기본 점수를 악곡·난이도별 공식으로 계산하도록 개선했습니다. 비가시 채보 지점을 포함한 점수 환산 기준을 반영했으며, 배치 분석과 조합 탐색에서 적용 여부를 ‘곡별 공식’으로 표시합니다. 코로네 곡 Expert도 원본 채보의 비가시 지점을 직접 집계해 반영했습니다. `IMPROVED` 388개 채보의 기본 점수를 악곡·난이도별 공식으로 계산하도록 개선했습니다. 비가시 채보 지점을 포함한 점수 환산 기준을 반영했으며, 배치 분석과 조합 탐색에서 적용 여부를 ‘곡별 공식’으로 표시합니다. 코로네 곡 Expert도 원본 채보의 비가시 지점을 직접 집계해 반영했습니다.
+5 -3
View File
@@ -604,10 +604,12 @@ const cardSummary = {
.sort(([left], [right]) => Number(card[right]) - Number(card[left]))[0][1]; .sort(([left], [right]) => Number(card[right]) - Number(card[left]))[0][1];
const maxLevel = cardRecord.progression.levels.at(-1); const maxLevel = cardRecord.progression.levels.at(-1);
const parameterBaseValue = Number(maxLevel?.parameterBaseValue ?? 0); const parameterBaseValue = Number(maxLevel?.parameterBaseValue ?? 0);
// Round each base stat up before applying awakening and other bonuses.
// Verified against the in-game unit power breakdown (2026-09-06).
const statsAtMaxLevel = { const statsAtMaxLevel = {
performance: Math.round(parameterBaseValue * Number(card.performancePermilMultiply ?? 0) / 1000), performance: Math.ceil(parameterBaseValue * Number(card.performancePermilMultiply ?? 0) / 1000),
technique: Math.round(parameterBaseValue * Number(card.techniquePermilMultiply ?? 0) / 1000), technique: Math.ceil(parameterBaseValue * Number(card.techniquePermilMultiply ?? 0) / 1000),
sense: Math.round(parameterBaseValue * Number(card.sensePermilMultiply ?? 0) / 1000) sense: Math.ceil(parameterBaseValue * Number(card.sensePermilMultiply ?? 0) / 1000)
}; };
return { return {
File diff suppressed because it is too large Load Diff
+44
View File
@@ -1,7 +1,51 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { calculateUnitPower, type UnitPowerCard } from './unit-power.ts'; import { calculateUnitPower, type UnitPowerCard } from './unit-power.ts';
test('실제 카드 데이터로 2026-09-06 인게임 종합력의 모든 항목을 재현한다', () => {
const catalog = JSON.parse(readFileSync(new URL('../data/cards-summary.json', import.meta.url), 'utf8')) as {
cards: (UnitPowerCard & { details: { costume: { name: string; description: string } } })[];
};
const findCard = (id: string) => {
const found = catalog.cards.find((entry) => entry.id === id);
assert.ok(found, id);
return found;
};
// Screenshot formation: Fubuki, Matsuri, Botan, Kobo, Flare; Baelz costume.
// Only the effective board stats are retained from the supplied backup.
const ids = [
'card-00006-5-uniq-0007-00', 'card-00007-5-uniq-0008-00',
'card-00032-5-uniq-0026-00', 'card-03009-5-uniq-0041-00',
'card-00021-5-uniq-0017-00'
];
const awakenings = [3, 3, 5, 3, 2];
const flats = [[4914, 5109, 5721], [4797, 5240, 5604], [4794, 4989, 5811], [4964, 5159, 5771], [4597, 5040, 5404]];
const result = calculateUnitPower(
ids.map((id, index) => ({ card: findCard(id), awakening: awakenings[index] })),
findCard('card-04012-5-uniq-0051-00').details.costume,
{
flatByCardId: Object.fromEntries(ids.map((id, index) => [id, {
performance: flats[index][0], technique: flats[index][1], sense: flats[index][2]
}])),
rateByCardId: Object.fromEntries(ids.map((id) => [id, { performance: 28, technique: 28, sense: 28 }]))
},
58,
4.13
);
assert.deepEqual(result, {
overallPower: 371616,
baseParameterTotal: 117966,
potentialTotal: 11805,
passiveTotal: 34087,
outfitTotal: 71524,
boardTotal: 114256,
memoryTotal: 7536,
memberPowerUpTotal: 14442,
outfitScoreSupportPercent: 0
});
});
function card(id: string, passive: string, awakenedPassive = passive): UnitPowerCard { function card(id: string, passive: string, awakenedPassive = passive): UnitPowerCard {
return { return {
id, id,