workadventure/back/src/Model/Group.ts

180 lines
4.9 KiB
TypeScript

import { ConnectCallback, DisconnectCallback, GameRoom } from "./GameRoom";
import { User } from "./User";
import { PositionInterface } from "_Model/PositionInterface";
import { Movable } from "_Model/Movable";
import { PositionNotifier } from "_Model/PositionNotifier";
import { MAX_PER_GROUP } from "../Enum/EnvironmentVariable";
import type { Zone } from "../Model/Zone";
export class Group implements Movable {
private static nextId: number = 1;
private id: number;
private users: Set<User>;
private x!: number;
private y!: number;
private wasDestroyed: boolean = false;
private roomId: string;
private currentZone: Zone | null = null;
/**
* When outOfBounds = true, a user if out of the bounds of the group BUT still considered inside it (because we are in following mode)
*/
private outOfBounds = false;
constructor(
roomId: string,
users: User[],
private groupRadius: number,
private connectCallback: ConnectCallback,
private disconnectCallback: DisconnectCallback,
private positionNotifier: PositionNotifier
) {
this.roomId = roomId;
this.users = new Set<User>();
this.id = Group.nextId;
Group.nextId++;
users.forEach((user: User) => {
this.join(user);
});
this.updatePosition();
}
getUsers(): User[] {
return Array.from(this.users.values());
}
getId(): number {
return this.id;
}
/**
* Returns the barycenter of all users (i.e. the center of the group)
*/
getPosition(): PositionInterface {
return {
x: this.x,
y: this.y,
};
}
/**
* Computes the barycenter of all users (i.e. the center of the group)
*/
updatePosition(): void {
const oldX = this.x;
const oldY = this.y;
let x = 0;
let y = 0;
// Let's compute the barycenter of all users.
this.users.forEach((user: User) => {
const position = user.getPosition();
x += position.x;
y += position.y;
});
x /= this.users.size;
y /= this.users.size;
if (this.users.size === 0) {
throw new Error("EMPTY GROUP FOUND!!!");
}
this.x = x;
this.y = y;
if (this.outOfBounds) {
return;
}
if (oldX === undefined) {
this.currentZone = this.positionNotifier.enter(this);
} else {
this.currentZone = this.positionNotifier.updatePosition(this, { x, y }, { x: oldX, y: oldY });
}
}
searchForNearbyUsers(): void {
if (!this.currentZone) return;
for (const user of this.positionNotifier.getAllUsersInSquareAroundZone(this.currentZone)) {
if (user.group || this.isFull()) return; //we ignore users that are already in a group.
const distance = GameRoom.computeDistanceBetweenPositions(user.getPosition(), this.getPosition());
if (distance < this.groupRadius) {
this.join(user);
this.updatePosition();
}
}
}
isFull(): boolean {
return this.users.size >= MAX_PER_GROUP;
}
isEmpty(): boolean {
return this.users.size <= 1;
}
join(user: User): void {
// Broadcast on the right event
this.connectCallback(user, this);
this.users.add(user);
user.group = this;
}
leave(user: User): void {
const success = this.users.delete(user);
if (success === false) {
throw new Error(`Could not find user ${user.id} in the group ${this.id}`);
}
user.group = undefined;
if (this.users.size !== 0) {
this.updatePosition();
}
// Broadcast on the right event
this.disconnectCallback(user, this);
}
/**
* Let's kick everybody out.
* Usually used when there is only one user left.
*/
destroy(): void {
if (!this.outOfBounds) {
this.positionNotifier.leave(this);
}
for (const user of this.users) {
this.leave(user);
}
this.wasDestroyed = true;
}
get getSize() {
return this.users.size;
}
/**
* A group can have at most one person leading the way in it.
*/
get leader(): User | undefined {
for (const user of this.users) {
if (user.hasFollowers()) {
return user;
}
}
return undefined;
}
setOutOfBounds(outOfBounds: boolean): void {
if (this.outOfBounds === true && outOfBounds === false) {
this.positionNotifier.enter(this);
this.outOfBounds = false;
} else if (this.outOfBounds === false && outOfBounds === true) {
this.positionNotifier.leave(this);
this.outOfBounds = true;
}
}
}