diff --git a/UPDATES.md b/UPDATES.md index b2749ac..a9a8722 100644 --- a/UPDATES.md +++ b/UPDATES.md @@ -4,6 +4,10 @@ updatedAt: 2026-09-09T12:00:00+09:00 ## 2026년 9월 9일 +### FUWAMOCO 솔로 악곡 보너스 수정 + +`FIXED` 후와와와 모코코 둘만 가창하는 FUWAMOCO 악곡을 솔로 악곡으로 판정하여, 두 멤버의 솔로 악곡 보너스가 함께 적용되도록 수정했습니다. + ### 연결 카드 최적화의 편성 대상 수정 `FIXED` 배치 분석의 연결 카드 최적화에서 편성에 없는 멤버의 파란 보드에도 카드를 필수 배치하던 문제를 수정했습니다. 큰 발동률 노드를 포함하는 필수 배치 조건은 편성 멤버에게만 적용하며, 편성 밖 보드의 전체 능력치·가창 보너스 등 점수에 기여하는 효과는 계속 반영합니다. diff --git a/src/lib/engine/board-effects.test.ts b/src/lib/engine/board-effects.test.ts index 3aa65b4..48403c4 100644 --- a/src/lib/engine/board-effects.test.ts +++ b/src/lib/engine/board-effects.test.ts @@ -27,6 +27,38 @@ const cards = [ { id: 'card-b', characterId: 'b', type: '퓨어', generation: '1기생' } ]; +test('FUWAMOCO 악곡은 두 멤버의 솔로 보너스를 합산하고 단체 보너스를 제외한다', () => { + const sisters = ['chr-04016', 'chr-04017']; + const songSummary: BoardSummarySource = { + nodeGroups: Object.fromEntries(['solo', 'solo-fuwamoko', 'group'].map((mode) => [ + `Y-${mode}`, + [{ characterIds: [], effect: { + id: `music_singer_type-${mode}`, + type: 'live_score_bonus_add_permil_up_by_music_skill_tree_character_and_music_singer_type', + value: 5 + } }] + ])) + }; + const bonus = (singerCharacterIds: string[], singerType: string, mode: string, owners = sisters) => + calculateBoardEffects(songSummary, Object.fromEntries(owners.map((id) => [id, [`Y-${mode}`]])), + [], '', { singerType, singerCharacterIds }).directSongScoreBonusPercent; + + for (const singerType of ['solo', 'group']) { + for (const mode of ['solo', 'solo-fuwamoko']) { + assert.equal(bonus(sisters, singerType, mode), 1); + for (const sister of sisters) assert.equal(bonus(sisters, singerType, mode, [sister]), 0.5); + assert.equal(bonus([...sisters, 'other'], 'group', mode), 0); + assert.equal(bonus(sisters, singerType, mode, ['other']), 0); + } + assert.equal(bonus(sisters, singerType, 'group'), 0); + } + for (const sister of sisters) { + assert.equal(bonus([sister], 'solo', 'solo'), 0.5); + assert.equal(bonus([sister], 'solo', 'solo-fuwamoko'), 0); + } + assert.equal(bonus([...sisters, 'other'], 'group', 'group'), 1); +}); + test('JP 3기생 보너스가 코보의 ID 3기생 보너스 상한을 소모하지 않는다', () => { const summary: BoardSummarySource = { nodeGroups: { diff --git a/src/lib/engine/board-effects.ts b/src/lib/engine/board-effects.ts index bdf9f6b..41b51d0 100644 --- a/src/lib/engine/board-effects.ts +++ b/src/lib/engine/board-effects.ts @@ -75,10 +75,14 @@ function songBonusApplies(effect: BoardEffect, ownerId: string, song?: BoardEffe if (!effect.id) return false; const expectedType = effect.id.match(/music_singer_type-(all|group|solo)/)?.[1]; if (expectedType === 'all') return true; - if (!song || expectedType !== song.singerType) return false; + if (!song) return false; const singers = song.singerCharacterIds ?? []; + // FUWAMOCO-only songs count as solo songs for both sisters. + const isFuwamoco = singers.length === 2 && singers.includes('chr-04016') && singers.includes('chr-04017'); + const singerType = isFuwamoco ? 'solo' : song.singerType; + if (expectedType !== singerType) return false; if (effect.id.endsWith('-fuwamoko')) { - return singers.length === 2 && singers.includes('chr-04016') && singers.includes('chr-04017'); + return isFuwamoco && singers.includes(ownerId); } return singers.includes(ownerId); }