From 8df20e7191dd1ee887fe03066d57536e8ec542ca Mon Sep 17 00:00:00 2001 From: Chris Merry Date: Mon, 30 Mar 2026 23:18:36 +0100 Subject: [PATCH] Fix lust assignment bugs causing teams to be silently dropped When a healer captain was present (including stale captain flags persisted in session storage), the lust fallback could assign a same-role healer provider, giving the team 6 members instead of 5 and causing it to fail the length check silently. Additionally, multi-spec lust players could appear on multiple teams due to stale entries in the providers list, and their DPS entry being selected could deplete the healer pool. - Remove lust fallback that forced conflicting same-role providers - Deduplicate lust providers by player, preferring healer entries to prevent healer pool depletion with multi-spec selections - Enable captain logging to surface stale captain flags from session storage Co-Authored-By: Claude Opus 4.6 (1M context) --- src/stores/__tests__/members.store.spec.ts | 202 +++++++++++++++++++++ src/stores/members.store.ts | 30 +-- 2 files changed, 219 insertions(+), 13 deletions(-) diff --git a/src/stores/__tests__/members.store.spec.ts b/src/stores/__tests__/members.store.spec.ts index 7ef1d7e..607267f 100644 --- a/src/stores/__tests__/members.store.spec.ts +++ b/src/stores/__tests__/members.store.spec.ts @@ -520,4 +520,206 @@ describe('members store', () => { } } }); + + test('it skips lust when all providers conflict with captain role', { repeats: 10 }, async () => { + const store = useMembersStore(); + const config = useConfigStore(); + const teams = useTeamsStore(); + + config.spreadLust = true; + config.autoPug = false; + config.fancy = false; + + // Healer captain with ONLY healer lust providers available + const members: Member[] = [ + { + character: { + name: 'CaptainHealer', + class: 'Priest', + active_spec_name: 'Holy', + active_spec_role: 'HEALING', + realm: 'Test' + }, + captain: true, + rank: 1 + }, + { + character: { + name: 'LustHealer1', + class: 'Shaman', + active_spec_name: 'Restoration', + active_spec_role: 'HEALING', + realm: 'Test' + }, + rank: 1 + }, + { + character: { + name: 'Tank1', + class: 'Warrior', + active_spec_name: 'Protection', + active_spec_role: 'TANK', + realm: 'Test' + }, + rank: 1 + }, + { + character: { + name: 'Tank2', + class: 'Warrior', + active_spec_name: 'Protection', + active_spec_role: 'TANK', + realm: 'Test' + }, + rank: 1 + }, + ...Array.from({ length: 6 }, (_, i) => ({ + character: { + name: `DPS${i + 1}`, + class: 'Rogue' as const, + active_spec_name: 'Outlaw' as const, + active_spec_role: 'DPS' as const, + realm: 'Test' + }, + rank: 1 + })) + ]; + + store.selectedMembers = members; + + await store.randomise(); + + // Must produce 2 teams (previously the healer captain team got 6 members and was dropped) + expect(teams.teams.length).toBe(2); + for (const team of teams.teams) { + expect(team.members.length).toBe(5); + expect( + team.members.filter((m) => m.character.active_spec_role === 'TANK').length + ).toBe(1); + expect( + team.members.filter((m) => m.character.active_spec_role === 'HEALING').length + ).toBe(1); + expect( + team.members.filter((m) => m.character.active_spec_role === 'DPS').length + ).toBe(3); + } + }); + + test('it handles multi-spec lust players without depleting healer pool', { repeats: 25 }, async () => { + const store = useMembersStore(); + const config = useConfigStore(); + const teams = useTeamsStore(); + + config.spreadLust = true; + config.autoPug = false; + config.fancy = false; + + // Shaman selected with both Resto (HEALING) and Enhance (DPS) specs + // If the DPS entry is used as lust, pruneWorkingMembers removes the healer entry too, + // depleting the healer pool and leaving another team without a healer + const members: Member[] = [ + { + character: { + name: 'FlexShaman', + class: 'Shaman', + active_spec_name: 'Restoration', + active_spec_role: 'HEALING', + realm: 'Test' + }, + rank: 1 + }, + { + character: { + name: 'FlexShaman', + class: 'Shaman', + active_spec_name: 'Enhancement', + active_spec_role: 'DPS', + realm: 'Test' + }, + rank: 1 + }, + { + character: { + name: 'Healer2', + class: 'Paladin', + active_spec_name: 'Holy', + active_spec_role: 'HEALING', + realm: 'Test' + }, + rank: 1 + }, + { + character: { + name: 'Tank1', + class: 'Warrior', + active_spec_name: 'Protection', + active_spec_role: 'TANK', + realm: 'Test' + }, + rank: 1 + }, + { + character: { + name: 'Tank2', + class: 'Death Knight', + active_spec_name: 'Blood', + active_spec_role: 'TANK', + realm: 'Test' + }, + rank: 1 + }, + { + character: { + name: 'LustDPS', + class: 'Mage', + active_spec_name: 'Fire', + active_spec_role: 'DPS', + realm: 'Test' + }, + rank: 1 + }, + ...Array.from({ length: 5 }, (_, i) => ({ + character: { + name: `DPS${i + 1}`, + class: 'Rogue' as const, + active_spec_name: 'Outlaw' as const, + active_spec_role: 'DPS' as const, + realm: 'Test' + }, + rank: 1 + })) + ]; + + store.selectedMembers = members; + + await store.randomise(); + + // 10 unique players = 2 teams (FlexShaman counted once) + expect(teams.teams.length).toBe(2); + for (const team of teams.teams) { + expect(team.members.length).toBe(5); + expect( + team.members.filter((m) => m.character.active_spec_role === 'TANK').length + ).toBe(1); + expect( + team.members.filter((m) => m.character.active_spec_role === 'HEALING').length + ).toBe(1); + expect( + team.members.filter((m) => m.character.active_spec_role === 'DPS').length + ).toBe(3); + + // No duplicate players within a team + const unique = new Set( + team.members.map((m) => `${m.character.name}-${m.character.realm}`) + ); + expect(unique.size).toBe(5); + } + + // No duplicate players across teams + const allPlayers = teams.teams.flatMap((t) => + t.members.map((m) => `${m.character.name}-${m.character.realm}`) + ); + const allUnique = new Set(allPlayers); + expect(allUnique.size).toBe(allPlayers.length); + }); }); diff --git a/src/stores/members.store.ts b/src/stores/members.store.ts index 0cb0273..7702548 100644 --- a/src/stores/members.store.ts +++ b/src/stores/members.store.ts @@ -351,9 +351,9 @@ export const useMembersStore = defineStore('members', () => { error.value = 'Too many team captains'; return; } - // if (captains.length) { - // console.log(`captains: ${captains.map((m) => m?.character.name).join(', ')}`); - // } + if (captains.length) { + console.log(`captains: ${captains.map((m) => m?.character.name).join(', ')}`); + } /* prune picked characters */ function pruneWorkingMembers(member: Member) { @@ -367,10 +367,19 @@ export const useMembersStore = defineStore('members', () => { const isHealer = (member: Member) => member.character.active_spec_role === 'HEALING'; const isDamageDealer = (member: Member) => member.character.active_spec_role === 'DPS'; - // lust allocation logic - const availableLustProviders = workingMembers.filter( - (member) => !member.captain && classSpecLust[member.character.class] - ); + // lust allocation logic - deduplicate by player, preferring healer entries + // Using a healer entry as lust fills both the lust and healer slot, + // preventing healer pool depletion when multi-spec players are selected + const lustProviderMap = new Map(); + for (const member of workingMembers) { + if (member.captain || !classSpecLust[member.character.class]) continue; + const key = `${member.character.name}-${member.character.realm}`; + const existing = lustProviderMap.get(key); + if (!existing || (isHealer(member) && !isHealer(existing))) { + lustProviderMap.set(key, member); + } + } + const availableLustProviders = [...lustProviderMap.values()]; shuffle(availableLustProviders); for (const team of workingTeams) { @@ -387,7 +396,7 @@ export const useMembersStore = defineStore('members', () => { if (!captainHasLust) { // pick a lust provider // Try to find one that doesn't conflict with captain's role (specifically Healer/Tank) - let providerIndex = availableLustProviders.findIndex((m) => { + const providerIndex = availableLustProviders.findIndex((m) => { // If team has Healer, avoid Healer Lust if (team.members.find(isHealer) && isHealer(m)) return false; // If team has Tank, avoid Tank Lust (though none exist currently) @@ -395,11 +404,6 @@ export const useMembersStore = defineStore('members', () => { return true; }); - // If no optimal provider found, take the first one (fallback) - if (providerIndex === -1 && availableLustProviders.length > 0) { - providerIndex = 0; - } - if (providerIndex !== -1) { const lust = availableLustProviders[providerIndex]; pruneWorkingMembers(lust);