/** * GroupLink - Manages bidirectional links between actors and groups * * This class handles bidirectional relationships between: * - Characters and their groups/encounters * - Groups and their members/encounters * * @author Vermine2047 System */ export class GroupLink { /** * Updates actors when a group is modified. * @param {Actor} group - The modified group * @param {Object} changes - The changes made */ static async updateActorsOnGroupChange(group, changes) { if (group?.type !== 'group') return; const groupData = group.system; const members = [...(groupData.members || [])]; const encounters = [...(groupData.encounters || [])]; // Update group members and encounters if (changes?.members !== undefined || changes?.encounters !== undefined) { await this._updateMembersInGroup(group, members); await this._updateEncountersInGroup(group, encounters); } // Sync data to member actors await this._syncGroupToMembers(group, members); await this._syncGroupToEncounters(group, encounters); } /** * Updates groups when an actor is modified. * @param {Actor} actor - The modified actor * @param {Object} changes - The changes made */ static async updateGroupsOnActorChange(actor, changes) { if (actor?.type === 'group') return; const actorData = actor.system; const encounters = [...(actorData.encounters || [])]; // If actor's encounters have changed if (changes?.encounters !== undefined) { // Update each group in encounters for (const groupId of encounters) { const group = game.actors.get(groupId); if (group && group.type === 'group') { await this._updateActorInGroupMembers(group, actor.id); } } } } /** * Syncs group data to member actors. * @param {Actor} group - The group * @param {Array} memberIds - List of member IDs */ static async _syncGroupToMembers(group, memberIds) { const groupId = group.id; for (const memberId of memberIds) { const member = game.actors.get(memberId); if (!member) continue; const memberEncounters = [...(member.system.encounters || [])]; if (!memberEncounters.includes(groupId)) { memberEncounters.push(groupId); await member.update({ 'system.encounters': memberEncounters }); } } } /** * Syncs group data to encounter actors. * @param {Actor} group - The group * @param {Array} encounterIds - List of encounter IDs */ static async _syncGroupToEncounters(group, encounterIds) { const groupId = group.id; for (const encounterId of encounterIds) { const encounter = game.actors.get(encounterId); if (!encounter) continue; const encounterGroups = [...(encounter.system.encounters || [])]; if (!encounterGroups.includes(groupId)) { encounterGroups.push(groupId); await encounter.update({ 'system.encounters': encounterGroups }); } } } /** * Updates members in a group. * @param {Actor} group - The group * @param {Array} memberIds - List of member IDs */ static async _updateMembersInGroup(group, memberIds) { const groupId = group.id; const currentMembers = [...(group.system.members || [])]; // Convert to Sets for O(1) lookups const currentMembersSet = new Set(currentMembers); const memberIdsSet = new Set(memberIds); // Find members to remove and add const membersToRemove = [...currentMembersSet].filter(id => !memberIdsSet.has(id)); const membersToAdd = [...memberIdsSet].filter(id => !currentMembersSet.has(id)); // Update actors that were removed for (const memberId of membersToRemove) { const member = game.actors.get(memberId); if (member) { const memberEncounters = (member.system.encounters || []).filter(id => id !== groupId); await member.update({ 'system.encounters': memberEncounters }); } } // Update new members for (const memberId of membersToAdd) { const member = game.actors.get(memberId); if (member) { const memberEncounters = [...(member.system.encounters || [])]; if (!memberEncounters.includes(groupId)) { memberEncounters.push(groupId); await member.update({ 'system.encounters': memberEncounters }); } } } } /** * Updates encounters in a group. * @param {Actor} group - The group * @param {Array} encounterIds - List of encounter IDs */ static async _updateEncountersInGroup(group, encounterIds) { const groupId = group.id; const currentEncounters = [...(group.system.encounters || [])]; // Convert to Sets for O(1) lookups const currentEncountersSet = new Set(currentEncounters); const encounterIdsSet = new Set(encounterIds); // Find encounters to remove and add const encountersToRemove = [...currentEncountersSet].filter(id => !encounterIdsSet.has(id)); const encountersToAdd = [...encounterIdsSet].filter(id => !currentEncountersSet.has(id)); // Update actors that were removed from encounters for (const encounterId of encountersToRemove) { const encounter = game.actors.get(encounterId); if (encounter) { const encounterGroups = (encounter.system.encounters || []).filter(id => id !== groupId); await encounter.update({ 'system.encounters': encounterGroups }); } } // Update new encounters for (const encounterId of encountersToAdd) { const encounter = game.actors.get(encounterId); if (encounter) { const encounterGroups = [...(encounter.system.encounters || [])]; if (!encounterGroups.includes(groupId)) { encounterGroups.push(groupId); await encounter.update({ 'system.encounters': encounterGroups }); } } } } /** * Updates an actor in group members. * @param {Actor} group - The group * @param {string} actorId - The actor ID */ static async _updateActorInGroupMembers(group, actorId) { const groupMembers = [...(group.system.members || [])]; if (!groupMembers.includes(actorId)) { groupMembers.push(actorId); await group.update({ 'system.members': groupMembers }); } } /** * Updates an actor in group encounters. * @param {Actor} group - The group * @param {string} actorId - The actor ID */ static async _updateActorInGroupEncounters(group, actorId) { const groupEncounters = [...(group.system.encounters || [])]; if (!groupEncounters.includes(actorId)) { groupEncounters.push(actorId); await group.update({ 'system.encounters': groupEncounters }); } } /** * Returns Actor objects for a list of IDs. * @param {Array} actorIds - List of actor IDs * @returns {Array} List of Actor objects */ static getActorObjects(actorIds) { if (!Array.isArray(actorIds)) return []; return actorIds .map(id => game.actors.get(id)) .filter(actor => actor !== undefined); } /** * Returns Actor objects for group members. * @param {Actor} group - The group * @returns {Array} List of Actor objects */ static getGroupMembers(group) { const memberIds = group?.system?.members || []; return this.getActorObjects(memberIds); } /** * Returns Actor objects for group encounters * @param {Actor} group - The group * @returns {Array} - List of Actor objects */ static getGroupEncounters(group) { const encounterIds = group.system.encounters || []; return this.getActorObjects(encounterIds); } /** * Returns groups that an actor belongs to * @param {Actor} actor - The actor * @returns {Array} - List of Actor objects (groups) */ static getActorGroups(actor) { const groupIds = actor.system.encounters || []; return this.getActorObjects(groupIds).filter(a => a.type === 'group'); } /** * Returns encounters (NPC/Creatures) for an actor * @param {Actor} actor - The actor * @returns {Array} - List of Actor objects (NPC/Creatures) */ static getActorEncounters(actor) { const encounterIds = actor.system.encounters || []; return this.getActorObjects(encounterIds).filter(a => a.type !== 'group'); } /** * Removes an actor from all groups. * @param {string} actorId - The actor ID to remove */ static async removeActorFromAllGroups(actorId) { const allGroups = game.actors.filter(a => a.type === 'group'); for (const group of allGroups) { const members = [...(group.system.members || [])]; const encounters = [...(group.system.encounters || [])]; // Use Set for O(1) lookups const membersSet = new Set(members); const encountersSet = new Set(encounters); const hasActorInMembers = membersSet.has(actorId); const hasActorInEncounters = encountersSet.has(actorId); if (hasActorInMembers || hasActorInEncounters) { const newMembers = hasActorInMembers ? members.filter(id => id !== actorId) : members; const newEncounters = hasActorInEncounters ? encounters.filter(id => id !== actorId) : encounters; await group.update({ 'system.members': newMembers, 'system.encounters': newEncounters }); } } // Remove groups from actor encounters const actor = game.actors.get(actorId); if (actor) { await actor.update({ 'system.encounters': [] }); } } /** * Adds an actor to a group. * @param {string} actorId - The actor ID * @param {string} groupId - The group ID */ static async addActorToGroup(actorId, groupId) { const actor = game.actors.get(actorId); const group = game.actors.get(groupId); if (!actor || !group || group.type !== 'group') return; // Add actor to group members using spread operator const groupMembers = [...(group.system.members || [])]; if (!groupMembers.includes(actorId)) { groupMembers.push(actorId); await group.update({ 'system.members': groupMembers }); } // Add group to actor encounters using spread operator const actorEncounters = [...(actor.system.encounters || [])]; if (!actorEncounters.includes(groupId)) { actorEncounters.push(groupId); await actor.update({ 'system.encounters': actorEncounters }); } } /** * Removes an actor from a group. * @param {string} actorId - The actor ID * @param {string} groupId - The group ID */ static async removeActorFromGroup(actorId, groupId) { const actor = game.actors.get(actorId); const group = game.actors.get(groupId); if (!actor || !group || group.type !== 'group') return; // Remove actor from group members using spread operator and filter const groupMembers = [...(group.system.members || [])].filter(id => id !== actorId); await group.update({ 'system.members': groupMembers }); // Remove group from actor encounters using spread operator and filter const actorEncounters = [...(actor.system.encounters || [])].filter(id => id !== groupId); await actor.update({ 'system.encounters': actorEncounters }); } /** * Initializes hooks for automatic synchronization between actors and groups. * Sets up event listeners for actor creation, updates, and deletion. */ static registerHooks() { // Hook on actor update - synchronize group memberships Hooks.on('updateActor', async (actor, changes, options, userId) => { if (!game.user.isGM && userId !== game.userId) return; // If it is a group being updated, sync its members if (actor.type === 'group') { await this.updateActorsOnGroupChange(actor, changes); } // If it is another actor being updated, sync its groups else { await this.updateGroupsOnActorChange(actor, changes); } }); // Hook on actor creation - clean up invalid group references Hooks.on('createActor', async (actor, options, userId) => { if (!game.user.isGM && userId !== game.userId) return; // If a character is created, check for invalid group references if (actor.type !== 'group') { const encounters = [...(actor.system.encounters || [])]; const validGroups = new Set( encounters.filter(id => game.actors.get(id)) ); // Only update if there are invalid references if (validGroups.size !== encounters.length) { await actor.update({ 'system.encounters': [...validGroups] }); } } }); // Hook on actor deletion - clean up references Hooks.on('deleteActor', async (actor, options, userId) => { if (!game.user.isGM && userId !== game.userId) return; if (actor.type === 'group') { // If a group is deleted, clean up references in its members and encounters const memberIds = [...(actor.system.members || [])]; const encounterIds = [...(actor.system.encounters || [])]; for (const id of [...memberIds, ...encounterIds]) { const a = game.actors.get(id); if (a) { const encounters = (a.system.encounters || []).filter(eid => eid !== actor.id); await a.update({ 'system.encounters': encounters }); } } } else { // If an actor is deleted, remove it from all groups await this.removeActorFromAllGroups(actor.id); } }); } } // Exporter pour utilisation globale export default GroupLink;