diff --git a/UPDATES.md b/UPDATES.md index 79f094b..acf4ece 100644 --- a/UPDATES.md +++ b/UPDATES.md @@ -1,9 +1,13 @@ --- -updatedAt: 2026-09-04T10:39:24+09:00 +updatedAt: 2026-09-04T23:39:07+09:00 --- ## 2026년 9월 4일 +### ID 기수 보너스 판정 수정 + +`FIXED` ID 1~3기생에게 같은 번호의 JP 기수 보너스가 잘못 적용되던 문제를 수정했습니다. 초록 노드 효율 분석에서 잘못 누적된 보너스가 상한을 채워 ID 기수 노드의 효율이 0으로 계산되던 현상도 해결했습니다. + ### 스페셜 구간 노트 비중 표시 `NEW` 배치 분석과 조합 탐색 결과에서 멤버별 스페셜 구간의 노트 수와 비중, 스킬 미발동 기준 점수 비중을 확인할 수 있습니다. diff --git a/src/lib/engine/board-effects.test.ts b/src/lib/engine/board-effects.test.ts index 82cfadc..3aa65b4 100644 --- a/src/lib/engine/board-effects.test.ts +++ b/src/lib/engine/board-effects.test.ts @@ -27,6 +27,20 @@ const cards = [ { 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'], diff --git a/src/lib/engine/skill-condition.test.ts b/src/lib/engine/skill-condition.test.ts index c241e0b..3d3cfa2 100644 --- a/src/lib/engine/skill-condition.test.ts +++ b/src/lib/engine/skill-condition.test.ts @@ -28,3 +28,23 @@ test('복수 소속 멤버는 표기된 각 기수에 모두 속한다', () => { assert.equal(skillTargetBelongsTo(fubuki, 'GAMERS'), true); assert.equal(skillConditionSatisfied('1기생(이)가 1명 이상이면', [fubuki]), true); }); + +test('ID 기수는 같은 번호의 JP 기수와 구분한다', () => { + for (const number of [1, 2, 3]) { + const member = { type: '퓨어', generation: `hololive ID ${number}기생` }; + assert.equal(skillTargetBelongsTo(member, `ID ${number}기생`), true); + assert.equal(skillTargetBelongsTo(member, `${number}기생`), false); + assert.equal(skillConditionSatisfied(`${number}기생(이)가 1명 이상이면`, [member]), false); + } +}); + +test('EN과 DEV_IS 소속 표기를 제거해 기수명을 판정한다', () => { + for (const [generation, group] of [ + ['hololive EN Myth', 'Myth'], + ['hololive EN Promise', 'Promise'], + ['hololive EN Advent', 'Advent'], + ['DEV_IS ReGLOSS', 'ReGLOSS'] + ]) { + assert.equal(skillTargetBelongsTo({ type: '퓨어', generation }, group), true); + } +}); diff --git a/src/lib/engine/skill-condition.ts b/src/lib/engine/skill-condition.ts index 90022a5..0ab4f1d 100644 --- a/src/lib/engine/skill-condition.ts +++ b/src/lib/engine/skill-condition.ts @@ -10,6 +10,7 @@ export function plainSkillText(value: string): string { function normalizedGroup(value: string): string { return value .replace(/^hololive\s+/i, '') + .replace(/^(?:EN|DEV_IS)\s+/i, '') .replace(/\(이\)가$/, '') .replace(/\s*타입$/, '') .trim() @@ -19,15 +20,12 @@ function normalizedGroup(value: string): string { function memberBelongsTo(member: SkillConditionMember, group: string): boolean { const normalized = normalizedGroup(group); const type = normalizedGroup(member.type); - const generation = normalizedGroup(member.generation); const generations = member.generation .split('/') .map((value) => normalizedGroup(value)) .filter(Boolean); return type === normalized - || generation === normalized - || generation.endsWith(normalized) - || generations.some((value) => value === normalized || value.endsWith(normalized)); + || generations.includes(normalized); } export function skillConditionSatisfied(