Files
vermine2047/module/system/group-link.mjs
T
uberwald 386d80639c code review: fix critical issues and improve code quality
- Fix constructor in rollDialog.mjs (spread operator for options)
- Remove all console.log statements from production code
- Add comprehensive JSDoc comments for all public APIs
- Convert French comments to English for consistency
- Use parseInt with radix parameter (10) throughout
- Replace let with const where appropriate
- Use Set for O(1) lookups in group-link.mjs methods
- Use spread operators for array cloning
- Optimize removeActorFromAllGroups with Set lookups
- Improve registerHooks with better comments and Set usage
- Simplify roll-message.hbs template logic
- Fix duplicate VERMINE key in lang/fr.json
- Add missing error translations
- Add .eslintrc.js with FoundryVTT-compatible linting config

Compatibility: FoundryVTT v11-v14

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-06-04 13:33:58 +02:00

430 lines
14 KiB
JavaScript

/**
* 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<string>} 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<string>} 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<string>} 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<string>} 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<string>} actorIds - List of actor IDs
* @returns {Array<Actor>} 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<Actor>} 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;