import test from 'node:test'; import assert from 'node:assert/strict'; import { calculateBoardEffects, type BoardSummarySource } from './board-effects.ts'; const summary: BoardSummarySource = { members: [{ id: 'a', positionGroupId: 'model-a' }], models: { 'model-a': [{ groupId: 'S-1', x: 0, y: 0 }, { groupId: 'B-1', x: 1, y: 0 }] }, nodeGroups: { 'B-1': [{ characterIds: [], effect: { type: 'performance_up', value: 100, targetId: 'skill_tree_effect_target-skill_tree_character', trigger: 'always' } }], 'B-2': [{ characterIds: [], effect: { type: 'live_active_skill_cool_time_shorten_permil_up', value: 40, targetId: 'skill_tree_effect_target-skill_tree_character', trigger: 'always' } }], 'R-1': [{ characterIds: [], effect: { type: 'all_parameter_up_permil_up', value: 30, targetId: 'skill_tree_effect_target-all', trigger: 'set_live_leader' } }], 'R-2': [{ characterIds: [], effect: { id: 'skill_tree_effect-live_active_skill_effect_up_permil_up-100-skill_tree_effect_target-all-skill_tree_effect_passive_trigger-music_skill_tree_character-set_live_leader', type: 'live_active_skill_effect_up_permil_up', value: 100, targetId: 'skill_tree_effect_target-all', trigger: 'set_live_leader' } }], 'G-1': [{ characterIds: [], effect: { type: 'all_parameter_up', value: 5, targetId: 'skill_tree_effect_target-all', trigger: 'always' } }], 'G-2': [{ characterIds: [], effect: { type: 'all_parameter_up_for_character_grouping', value: 50, targetId: 'skill_tree_effect_target-character_grouping-grp-gen_0', trigger: 'always' } }], 'Y-1': [{ characterIds: [], effect: { id: 'skill_tree_effect-live_score_bonus_add_permil_up_by_music_skill_tree_character_and_music_singer_type-5-skill_tree_effect_passive_trigger-music_skill_tree_character_and_music_singer_type-solo-always-solo', type: 'live_score_bonus_add_permil_up_by_music_skill_tree_character_and_music_singer_type', value: 5, trigger: 'always' } }] } }; const cards = [ { id: 'card-a', characterId: 'a', type: '큐트', generation: '0기생' }, { id: 'card-b', characterId: 'b', type: '퓨어', generation: '1기생' } ]; test('JP 3기생 보너스가 코보의 ID 3기생 보너스 상한을 소모하지 않는다', () => { const summary: BoardSummarySource = { nodeGroups: { 'G-jp': [{ characterIds: [], effect: { type: 'all_parameter_up_for_character_grouping', value: 900, targetId: 'skill_tree_effect_target-character_grouping-grp-gen_3' } }], 'G-id': [{ characterIds: [], effect: { type: 'all_parameter_up_for_character_grouping', value: 150, targetId: 'skill_tree_effect_target-character_grouping-grp-indonesia-gen_3' } }] } }; const kobo = [{ id: 'kobo', characterId: 'chr-03009', type: '퓨어', generation: 'hololive ID 3기생' }]; const before = calculateBoardEffects(summary, { account: ['G-jp'] }, kobo, ''); const after = calculateBoardEffects(summary, { account: ['G-jp', 'G-id'] }, kobo, ''); assert.deepEqual(before.flatByCardId.kobo, { performance: 0, technique: 0, sense: 0 }); assert.deepEqual(after.flatByCardId.kobo, { performance: 150, technique: 150, sense: 150 }); }); test('편성 멤버의 파란 보드, 리더의 빨간 보드와 공통 초록 보드를 합산한다', () => { const result = calculateBoardEffects(summary, { a: ['B-1', 'B-2', 'Y-1'], leader: ['R-1', 'R-2'], account: ['G-1'] }, cards, 'leader', { singerType: 'solo', singerCharacterIds: ['a'] }); assert.equal(result.flatByCardId['card-a'].performance, 105); assert.equal(result.flatByCardId['card-b'].performance, 5); assert.equal(result.rateByCardId['card-a'].sense, 3); assert.deepEqual(result.allowedCooldownReductionsByCardId['card-a'], [0, 4, 8, 12]); assert.deepEqual(result.allowedCooldownReductionsByCardId['card-b'], [0, 4, 8, 12]); assert.equal(result.directSongScoreBonusPercent, 0.5); assert.equal(result.scoreSupportPercent, 0); }); test('리더가 악곡 가창자이면 조건부 빨간 스코어 서포트를 적용한다', () => { const result = calculateBoardEffects(summary, { leader: ['R-2'] }, cards, 'leader', { singerType: 'group', singerCharacterIds: ['leader'] }); assert.equal(result.scoreSupportPercent, 10); }); test('연결 카드가 범위 안의 활성 노드 효과를 증폭한다', () => { const result = calculateBoardEffects(summary, { a: ['B-1'] }, cards, 'a', null, { placements: { a: { 'S-1': 'connect-card' } }, cards: [{ id: 'connect-card', awakening: 0, connect: [{ level: 1, effectPermilUp: 2000, extent: [{ x: 1, y: 0 }] }] }] }); assert.equal(result.flatByCardId['card-a'].performance, 300); }); test('조합 탐색용 기수 보너스와 악곡 보너스는 실제 노드 합계를 대체한다', () => { const result = calculateBoardEffects(summary, { account: ['G-2', 'Y-1'] }, cards, 'a', { singerType: 'solo', singerCharacterIds: ['a'] }, null, { generationBonusByCardId: { 'card-a': 900 }, directSongScoreBonusPercent: 10 }); assert.equal(result.flatByCardId['card-a'].performance, 900); assert.equal(result.flatByCardId['card-a'].technique, 900); assert.equal(result.directSongScoreBonusPercent, 10); const zeroOverride = calculateBoardEffects(summary, { account: ['G-2'] }, cards, 'a', null, null, { generationBonusByCardId: { 'card-a': 0 } }); const customOverride = calculateBoardEffects(summary, { account: ['G-2'] }, cards, 'a', null, null, { generationBonusByCardId: { 'card-a': 450 } }); assert.equal(zeroOverride.flatByCardId['card-a'].performance, 0); assert.equal(customOverride.flatByCardId['card-a'].performance, 450); }); test('같은 기수의 보드 보너스는 카드별로 최대 900까지만 적용한다', () => { const cappedSummary: BoardSummarySource = { nodeGroups: { 'G-10': [{ characterIds: [], effect: { type: 'all_parameter_up_for_character_grouping', value: 600, targetId: 'skill_tree_effect_target-character_grouping-grp-gen_0' } }], 'G-11': [{ characterIds: [], effect: { type: 'all_parameter_up_for_character_grouping', value: 500, targetId: 'skill_tree_effect_target-character_grouping-grp-gen_0' } }] } }; const result = calculateBoardEffects(cappedSummary, { account: ['G-10', 'G-11'] }, cards, 'a'); assert.equal(result.flatByCardId['card-a'].performance, 900); assert.equal(result.flatByCardId['card-a'].technique, 900); assert.equal(result.flatByCardId['card-a'].sense, 900); assert.equal(result.flatByCardId['card-b'].performance, 0); }); test('복수 소속 카드도 모든 기수 보너스를 합쳐 카드별 900까지만 적용한다', () => { const multiGroupingSummary: BoardSummarySource = { nodeGroups: { 'G-20': [{ characterIds: [], effect: { type: 'all_parameter_up_for_character_grouping', value: 900, targetId: 'skill_tree_effect_target-character_grouping-grp-gen_1' } }], 'G-21': [{ characterIds: [], effect: { type: 'all_parameter_up_for_character_grouping', value: 900, targetId: 'skill_tree_effect_target-character_grouping-grp-gamers' } }], 'G-22': [{ characterIds: [], effect: { type: 'all_parameter_up_for_character_grouping', value: 100, targetId: 'skill_tree_effect_target-character_grouping-grp-gen_1' } }] } }; const multiGroupingCards = [{ id: 'fubuki', characterId: 'fubuki', type: '퓨어', generation: 'hololive 1기생 / GAMERS' }]; const firstGenerationMaxed = calculateBoardEffects(multiGroupingSummary, { account: ['G-20'] }, multiGroupingCards, 'fubuki'); const gamersAdded = calculateBoardEffects(multiGroupingSummary, { account: ['G-20', 'G-21'] }, multiGroupingCards, 'fubuki'); const result = calculateBoardEffects(multiGroupingSummary, { account: ['G-20', 'G-21', 'G-22'] }, multiGroupingCards, 'fubuki'); assert.equal(firstGenerationMaxed.flatByCardId.fubuki.performance, 900); assert.equal(gamersAdded.flatByCardId.fubuki.performance, 900); assert.equal(result.flatByCardId.fubuki.performance, 900); });