219 lines
8.7 KiB
TypeScript
219 lines
8.7 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
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 {
|
|
return {
|
|
id,
|
|
characterId: id,
|
|
name: id,
|
|
type: '큐트',
|
|
generation: '0기생',
|
|
power: { maxLevel: 80, statsAtMaxLevel: { performance: 100, technique: 100, sense: 100 } },
|
|
details: { passive: [{ level: 1, description: passive }, { level: 2, description: awakenedPassive }] }
|
|
};
|
|
}
|
|
|
|
test('최대 레벨 기본 스탯, 개화 보너스와 패시브로 종합력을 계산한다', () => {
|
|
const result = calculateUnitPower([
|
|
{ card: card('a', '0기생(이)가 2명 이상이면 자신의 전체 파라미터가 20% UP'), awakening: 5 },
|
|
...['b', 'c', 'd', 'e'].map((id) => ({ card: card(id, ''), awakening: 5 }))
|
|
]);
|
|
|
|
assert.equal(result.baseParameterTotal, 1500);
|
|
assert.equal(result.potentialTotal, 150);
|
|
assert.equal(result.passiveTotal, 66);
|
|
assert.equal(result.overallPower, 1716);
|
|
});
|
|
|
|
test('패시브 2레벨은 4개화부터 적용한다', () => {
|
|
const passiveCard = card('a', '자신의 전체 파라미터가 10% UP', '자신의 전체 파라미터가 20% UP');
|
|
const otherCards = ['b', 'c', 'd', 'e'].map((id) => ({ card: card(id, ''), awakening: 0 }));
|
|
|
|
const before = calculateUnitPower([{ card: passiveCard, awakening: 3 }, ...otherCards]);
|
|
const after = calculateUnitPower([{ card: passiveCard, awakening: 4 }, ...otherCards]);
|
|
|
|
assert.equal(before.passiveTotal, 33);
|
|
assert.equal(after.passiveTotal, 66);
|
|
});
|
|
|
|
test('편성 조건을 만족한 리더 의상 효과를 종합력에 더한다', () => {
|
|
const result = calculateUnitPower(
|
|
['a', 'b', 'c', 'd', 'e'].map((id) => ({ card: card(id, ''), awakening: 0 })),
|
|
{ name: '테스트 의상', description: '0기생(이)가 2명 이상이면 전원의 전체 파라미터가 50% UP' }
|
|
);
|
|
|
|
assert.equal(result.outfitTotal, 750);
|
|
assert.equal(result.overallPower, 2250);
|
|
});
|
|
|
|
test('보유 메모리 효과는 개화 적용 후 각 멤버의 능력치마다 올림한다', () => {
|
|
const result = calculateUnitPower(
|
|
['a', 'b', 'c', 'd', 'e'].map((id) => ({ card: card(id, ''), awakening: 2 })),
|
|
null,
|
|
null,
|
|
46
|
|
);
|
|
|
|
assert.equal(result.potentialTotal, 150);
|
|
assert.equal(result.memoryTotal, 90);
|
|
assert.equal(result.overallPower, 1740);
|
|
});
|
|
|
|
test('보드 고정 증가분에는 보드 퍼센트 증가율을 다시 곱하지 않는다', () => {
|
|
const result = calculateUnitPower(
|
|
['a', 'b', 'c', 'd', 'e'].map((id) => ({ card: card(id, ''), awakening: 0 })),
|
|
null,
|
|
{
|
|
flatByCardId: { a: { performance: 355, technique: 0, sense: 0 } },
|
|
rateByCardId: { a: { performance: 5, technique: 0, sense: 0 } }
|
|
}
|
|
);
|
|
assert.equal(result.boardTotal, 5 + 355);
|
|
assert.equal(result.overallPower, 1500 + 5 + 355);
|
|
});
|
|
|
|
test('멤버 강화는 메모리를 제외한 파라미터·패시브·의상·보드 합계에 적용한다', () => {
|
|
const result = calculateUnitPower(
|
|
['a', 'b', 'c', 'd', 'e'].map((id) => ({ card: card(id, ''), awakening: 0 })),
|
|
null,
|
|
null,
|
|
100,
|
|
10
|
|
);
|
|
|
|
assert.equal(result.memoryTotal, 150);
|
|
assert.equal(result.memberPowerUpTotal, 150);
|
|
assert.equal(result.overallPower, 1800);
|
|
});
|
|
|
|
test('조건부 기수 패시브는 복수 소속 멤버도 대상으로 삼는다', () => {
|
|
const fubuki = {
|
|
...card('fubuki', '1기생(이)가 2명 이상이면 1기생2명의 퍼포먼스가 10% UP'),
|
|
generation: 'hololive 1기생 / GAMERS'
|
|
};
|
|
const matsuri = {
|
|
...card('matsuri', ''),
|
|
generation: 'hololive 1기생'
|
|
};
|
|
const result = calculateUnitPower([
|
|
{ card: fubuki, awakening: 0 },
|
|
{ card: matsuri, awakening: 0 },
|
|
...['c', 'd', 'e'].map((id) => ({ card: card(id, ''), awakening: 0 }))
|
|
]);
|
|
|
|
assert.equal(result.passiveTotal, 20);
|
|
});
|
|
|
|
test('지정 인원 퍼포먼스 패시브는 전체 파라미터가 아닌 기본 퍼포먼스 상위 멤버에게 적용한다', () => {
|
|
const source = {
|
|
...card('source', '퓨어 타입 2명의 퍼포먼스가 10% UP'),
|
|
type: '해피'
|
|
};
|
|
const targets = [
|
|
{ id: 'performance-high', performance: 200, technique: 0, sense: 0 },
|
|
{ id: 'total-high', performance: 100, technique: 200, sense: 0 },
|
|
{ id: 'total-second', performance: 150, technique: 100, sense: 0 }
|
|
].map(({ id, performance, technique, sense }) => ({
|
|
card: {
|
|
...card(id, ''),
|
|
type: '퓨어',
|
|
power: { maxLevel: 80, statsAtMaxLevel: { performance, technique, sense } }
|
|
},
|
|
awakening: 0
|
|
}));
|
|
|
|
const result = calculateUnitPower([
|
|
{ card: source, awakening: 0 },
|
|
...targets,
|
|
{ card: card('other', ''), awakening: 0 }
|
|
]);
|
|
|
|
assert.equal(result.passiveTotal, 35);
|
|
});
|
|
|
|
test('2026-09-30 실측 편성은 보드 보정 전 개화 반영 퍼포먼스로 대상을 선정한다', () => {
|
|
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;
|
|
};
|
|
// Effective board stats only; no account backup is retained in this fixture.
|
|
const commonIds = [
|
|
'card-00006-5-uniq-0007-00', 'card-00007-5-uniq-0008-00',
|
|
'card-00032-5-uniq-0026-00', 'card-03005-5-uniq-0084-00'
|
|
];
|
|
const commonFlats = [[5460, 5863, 6076], [5572, 5772, 6235], [5572, 5772, 6195], [5592, 5792, 6005]];
|
|
for (const scenario of [
|
|
{ id: 'card-03009-5-uniq-0041-00', awakening: 4, flat: [5492, 5692, 6205],
|
|
expected: { overallPower: 401743, baseParameterTotal: 117966, potentialTotal: 11805,
|
|
passiveTotal: 47017, outfitTotal: 71524, boardTotal: 123637, memoryTotal: 8442, memberPowerUpTotal: 21352 } },
|
|
// Flare outranks Fubuki before board bonuses, but trails her after bonuses.
|
|
{ id: 'card-00021-5-uniq-0017-00', awakening: 2, flat: [4295, 4495, 4873],
|
|
expected: { overallPower: 393801, baseParameterTotal: 117817, potentialTotal: 11790,
|
|
passiveTotal: 42530, outfitTotal: 72445, boardTotal: 119865, memoryTotal: 8432, memberPowerUpTotal: 20922 } }
|
|
]) {
|
|
const ids = [...commonIds, scenario.id];
|
|
const awakenings = [5, 4, 5, 5, scenario.awakening];
|
|
const flats = [...commonFlats, scenario.flat];
|
|
const members = ids.map((id, index) => ({ card: findCard(id), awakening: awakenings[index] }));
|
|
const board = {
|
|
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 }]))
|
|
};
|
|
for (const order of [members, [...members].reverse()]) {
|
|
assert.deepEqual(calculateUnitPower(order, findCard('card-04012-5-uniq-0051-00').details.costume, board, 65, 5.74),
|
|
{ ...scenario.expected, outfitScoreSupportPercent: 0 }, scenario.id);
|
|
}
|
|
}
|
|
});
|