diff --git a/back/src/Model/Group.ts b/back/src/Model/Group.ts index 099e871b..39128c96 100644 --- a/back/src/Model/Group.ts +++ b/back/src/Model/Group.ts @@ -1,6 +1,6 @@ -import {MessageUserPosition} from "./Websocket/MessageUserPosition"; import { World } from "./World"; import { UserInterface } from "./UserInterface"; +import {PositionInterface} from "_Model/PositionInterface"; import {uuid} from "uuidv4"; export class Group { @@ -34,17 +34,39 @@ export class Group { return this.id; } + /** + * Returns the barycenter of all users (i.e. the center of the group) + */ + getPosition(): PositionInterface { + let x = 0; + let y = 0; + // Let's compute the barycenter of all users. + this.users.forEach((user: UserInterface) => { + x += user.position.x; + y += user.position.y; + }); + x /= this.users.length; + y /= this.users.length; + return { + x, + y + }; + } + isFull(): boolean { return this.users.length >= Group.MAX_PER_GROUP; } + isEmpty(): boolean { + return this.users.length <= 1; + } + join(user: UserInterface): void { // Broadcast on the right event - for(let i = 0; i < this.users.length; i++){ - let groupUser : UserInterface = this.users[i]; + this.users.forEach((groupUser: UserInterface) => { this.connectCallback(user.id, groupUser.id, this); - } + }); this.users.push(user); user.group = this; } @@ -71,7 +93,7 @@ export class Group { return stillIn; } - removeFromGroup(users: UserInterface[]): void + /*removeFromGroup(users: UserInterface[]): void { for(let i = 0; i < users.length; i++){ let user = users[i]; @@ -80,5 +102,32 @@ export class Group { this.users.splice(index, 1); } } + }*/ + + leave(user: UserInterface): void + { + const index = this.users.indexOf(user, 0); + if (index === -1) { + throw new Error("Could not find user in the group"); + } + + this.users.splice(index, 1); + user.group = undefined; + + // Broadcast on the right event + this.users.forEach((groupUser: UserInterface) => { + this.disconnectCallback(user.id, groupUser.id); + }); + } + + /** + * Let's kick everybody out. + * Usually used when there is only one user left. + */ + destroy(): void + { + this.users.forEach((user: UserInterface) => { + this.leave(user); + }) } } diff --git a/back/src/Model/PositionInterface.ts b/back/src/Model/PositionInterface.ts new file mode 100644 index 00000000..d3b0dd47 --- /dev/null +++ b/back/src/Model/PositionInterface.ts @@ -0,0 +1,4 @@ +export interface PositionInterface { + x: number, + y: number +} diff --git a/back/src/Model/World.ts b/back/src/Model/World.ts index 02391ec8..7f4786a7 100644 --- a/back/src/Model/World.ts +++ b/back/src/Model/World.ts @@ -4,6 +4,7 @@ import {Group} from "./Group"; import {Distance} from "./Distance"; import {UserInterface} from "./UserInterface"; import {ExSocketInterface} from "_Model/Websocket/ExSocketInterface"; +import {PositionInterface} from "_Model/PositionInterface"; export class World { static readonly MIN_DISTANCE = 160; @@ -21,17 +22,22 @@ export class World { this.groups = []; this.connectCallback = connectCallback; this.disconnectCallback = disconnectCallback; - } + } public join(userPosition: MessageUserPosition): void { this.users.set(userPosition.userId, { id: userPosition.userId, position: userPosition.position }); + // Let's call update position to trigger the join / leave room + this.updatePosition(userPosition); } public leave(user : ExSocketInterface){ - /*TODO leaver user in group*/ + let userObj = this.users.get(user.id); + if (userObj !== undefined && typeof userObj.group !== 'undefined') { + this.leaveGroup(user); + } this.users.delete(user.userId); } @@ -47,20 +53,52 @@ export class World { if (typeof user.group === 'undefined') { // If the user is not part of a group: // should he join a group? - let closestUser: UserInterface|null = this.searchClosestAvailableUser(user); - if (closestUser !== null) { - // Is the closest user part of a group? - if (typeof closestUser.group === 'undefined') { + let closestItem: UserInterface|Group|null = this.searchClosestAvailableUserOrGroup(user); + + if (closestItem !== null) { + if (closestItem instanceof Group) { + // Let's join the group! + closestItem.join(user); + } else { + let closestUser : UserInterface = closestItem; let group: Group = new Group([ user, closestUser ], this.connectCallback, this.disconnectCallback); - } else { - closestUser.group.join(user); + this.groups.push(group); } } + + } else { + // If the user is part of a group: + // should he leave the group? + let distance = World.computeDistanceBetweenPositions(user.position, user.group.getPosition()); + if (distance > World.MIN_DISTANCE) { + this.leaveGroup(user); + } + } + } + + /** + * Makes a user leave a group and closes and destroy the group if the group contains only one remaining person. + * + * @param user + */ + private leaveGroup(user: UserInterface): void { + let group = user.group; + if (typeof group === 'undefined') { + throw new Error("The user is part of no group"); + } + group.leave(user); + + if (group.isEmpty()) { + group.destroy(); + const index = this.groups.indexOf(group, 0); + if (index === -1) { + throw new Error("Could not find group"); + } + this.groups.splice(index, 1); } - // TODO : vérifier qu'ils ne sont pas déja dans un groupe plein } /** @@ -68,53 +106,37 @@ export class World { * - close enough (distance <= MIN_DISTANCE) * - not in a group OR in a group that is not full */ - private searchClosestAvailableUser(user: UserInterface): UserInterface|null + private searchClosestAvailableUserOrGroup(user: UserInterface): UserInterface|Group|null { -/* - let sortedUsersByDistance: UserInteface[] = Array.from(this.users.values()).sort((user1: UserInteface, user2: UserInteface): number => { - let distance1 = World.computeDistance(user, user1); - let distance2 = World.computeDistance(user, user2); - return distance1 - distance2; - }); - - // The first element should be the current user (distance 0). Let's remove it. - if (sortedUsersByDistance[0] === user) { - sortedUsersByDistance.shift(); - } - - for(let i = 0; i < sortedUsersByDistance.length; i++) { - let currentUser = sortedUsersByDistance[i]; - let distance = World.computeDistance(currentUser, user); - if(distance > World.MIN_DISTANCE) { - return; - } - } -*/ let usersToBeGroupedWith: Distance[] = []; let minimumDistanceFound: number = World.MIN_DISTANCE; - let matchingUser: UserInterface | null = null; + let matchingItem: UserInterface | Group | null = null; this.users.forEach(function(currentUser, userId) { + // Let's only check users that are not part of a group + if (typeof currentUser.group !== 'undefined') { + return; + } if(currentUser === user) { return; } let distance = World.computeDistance(user, currentUser); // compute distance between peers. - - if(distance <= minimumDistanceFound) { - if (typeof currentUser.group === 'undefined' || !currentUser.group.isFull()) { + if(distance <= minimumDistanceFound) { + minimumDistanceFound = distance; + matchingItem = currentUser; + } + /*if (typeof currentUser.group === 'undefined' || !currentUser.group.isFull()) { // We found a user we can bind to. - minimumDistanceFound = distance; - matchingUser = currentUser; return; - } + }*/ /* if(context.groups.length > 0) { - + context.groups.forEach(group => { if(group.isPartOfGroup(userPosition)) { // Is the user in a group ? if(group.isStillIn(userPosition)) { // Is the user leaving the group ? (is the user at more than max distance of each player) - + // Should we split the group? (is each player reachable from the current player?) // This is needed if // A <==> B <==> C <===> D @@ -138,11 +160,20 @@ export class World { usersToBeGroupedWith.push(dist); } */ - } - - }, this.users); + }); - return matchingUser; + this.groups.forEach(function(group: Group) { + if (group.isFull()) { + return; + } + let distance = World.computeDistanceBetweenPositions(user.position, group.getPosition()); + if(distance <= minimumDistanceFound) { + minimumDistanceFound = distance; + matchingItem = group; + } + }); + + return matchingItem; } public static computeDistance(user1: UserInterface, user2: UserInterface): number @@ -150,6 +181,11 @@ export class World { return Math.sqrt(Math.pow(user2.position.x - user1.position.x, 2) + Math.pow(user2.position.y - user1.position.y, 2)); } + public static computeDistanceBetweenPositions(position1: PositionInterface, position2: PositionInterface): number + { + return Math.sqrt(Math.pow(position2.x - position1.x, 2) + Math.pow(position2.y - position1.y, 2)); + } + /*getDistancesBetweenGroupUsers(group: Group): Distance[] { let i = 0; @@ -167,7 +203,7 @@ export class World { } }); }); - + distances.sort(World.compareDistances); return distances; @@ -193,7 +229,7 @@ export class World { // Detecte le ou les users qui se sont fait sortir du groupe let difference = users.filter(x => !groupTmp.includes(x)); - // TODO : Notify users un difference that they have left the group + // TODO : Notify users un difference that they have left the group } let newgroup = new Group(groupTmp); @@ -210,4 +246,4 @@ export class World { } return 0; }*/ -} \ No newline at end of file +} diff --git a/back/tests/WorldTest.ts b/back/tests/WorldTest.ts index 1f5affc8..1d499727 100644 --- a/back/tests/WorldTest.ts +++ b/back/tests/WorldTest.ts @@ -12,7 +12,7 @@ describe("World", () => { connectCalled = true; } let disconnect = (user1: string, user2: string): void => { - + } let world = new World(connect, disconnect); @@ -53,14 +53,103 @@ describe("World", () => { })); expect(connectCalled).toBe(false); }); - /** + + it("should connect 3 users", () => { + let connectCalled: boolean = false; + let connect = (user1: string, user2: string): void => { + connectCalled = true; + } + let disconnect = (user1: string, user2: string): void => { + + } + + let world = new World(connect, disconnect); + + world.join(new MessageUserPosition({ + userId: "foo", + roomId: 1, + position: new Point(100, 100) + })); + + world.join(new MessageUserPosition({ + userId: "bar", + roomId: 1, + position: new Point(200, 100) + })); + + expect(connectCalled).toBe(true); + connectCalled = false; + + // baz joins at the outer limit of the group + world.join(new MessageUserPosition({ + userId: "baz", + roomId: 1, + position: new Point(311, 100) + })); + + expect(connectCalled).toBe(false); + + world.updatePosition(new MessageUserPosition({ + userId: "baz", + roomId: 1, + position: new Point(309, 100) + })); + + expect(connectCalled).toBe(true); + }); + + it("should disconnect user1 and user2", () => { + let connectCalled: boolean = false; + let disconnectCalled: boolean = false; + let connect = (user1: string, user2: string): void => { + connectCalled = true; + } + let disconnect = (user1: string, user2: string): void => { + disconnectCalled = true; + } + + let world = new World(connect, disconnect); + + world.join(new MessageUserPosition({ + userId: "foo", + roomId: 1, + position: new Point(100, 100) + })); + + world.join(new MessageUserPosition({ + userId: "bar", + roomId: 1, + position: new Point(259, 100) + })); + + expect(connectCalled).toBe(true); + expect(disconnectCalled).toBe(false); + + world.updatePosition(new MessageUserPosition({ + userId: "bar", + roomId: 1, + position: new Point(100+160+160+1, 100) + })); + + expect(disconnectCalled).toBe(true); + + disconnectCalled = false; + world.updatePosition(new MessageUserPosition({ + userId: "bar", + roomId: 1, + position: new Point(262, 100) + })); + expect(disconnectCalled).toBe(false); + }); + + /** it('Should return the distances between all users', () => { let connectCalled: boolean = false; let connect = (user1: string, user2: string): void => { connectCalled = true; } let disconnect = (user1: string, user2: string): void => { - + } let world = new World(connect, disconnect); @@ -100,4 +189,4 @@ describe("World", () => { //expect(distances).toBe([]); }) **/ -}) \ No newline at end of file +}) diff --git a/front/src/Enum/EnvironmentVariable.ts b/front/src/Enum/EnvironmentVariable.ts index 6c0226a2..f44f717b 100644 --- a/front/src/Enum/EnvironmentVariable.ts +++ b/front/src/Enum/EnvironmentVariable.ts @@ -1,4 +1,4 @@ -const DEBUG_MODE: boolean = !!process.env.DEBUG_MODE || false; +const DEBUG_MODE: boolean = process.env.DEBUG_MODE as any === true; const API_URL = process.env.API_URL || "http://api.workadventure.localhost"; const ROOM = [process.env.ROOM || "THECODINGMACHINE"]; const RESOLUTION = 4; diff --git a/front/src/Phaser/Entity/PlayableCaracter.ts b/front/src/Phaser/Entity/PlayableCaracter.ts index b84d2dd8..987d6bd3 100644 --- a/front/src/Phaser/Entity/PlayableCaracter.ts +++ b/front/src/Phaser/Entity/PlayableCaracter.ts @@ -22,18 +22,15 @@ export class PlayableCaracter extends Phaser.Physics.Arcade.Sprite { this.setVelocity(x, y); - //todo improve animations to better account for diagonal movement - if (this.body.velocity.x > 0) { //moving right - this.play(PlayerAnimationNames.WalkRight, true); - } - if (this.body.velocity.x < 0) { //moving left - this.anims.playReverse(PlayerAnimationNames.WalkLeft, true); - } + //up or down animationss are prioritized over left and right if (this.body.velocity.y < 0) { //moving up this.play(PlayerAnimationNames.WalkUp, true); - } - if (this.body.velocity.y > 0) { //moving down + } else if (this.body.velocity.y > 0) { //moving down this.play(PlayerAnimationNames.WalkDown, true); + } else if (this.body.velocity.x > 0) { //moving right + this.play(PlayerAnimationNames.WalkRight, true); + } else if (this.body.velocity.x < 0) { //moving left + this.anims.playReverse(PlayerAnimationNames.WalkLeft, true); } if(this.bubble) { diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index e36fe809..f7fbd7c3 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -6,8 +6,8 @@ import Tile = Phaser.Tilemaps.Tile; import {ITiledMap, ITiledTileSet} from "../Map/ITiledMap"; import {cypressAsserter} from "../../Cypress/CypressAsserter"; +export const GameSceneName = "GameScene"; export enum Textures { - Rock = 'rock', Player = 'playerModel', Map = 'map' } @@ -32,7 +32,7 @@ export class GameScene extends Phaser.Scene implements GameSceneInterface{ constructor() { super({ - key: "GameScene" + key: GameSceneName }); this.GameManager = gameManager; this.Terrains = []; diff --git a/front/src/Phaser/Login/LogincScene.ts b/front/src/Phaser/Login/LogincScene.ts index 1aa1e0af..15ead519 100644 --- a/front/src/Phaser/Login/LogincScene.ts +++ b/front/src/Phaser/Login/LogincScene.ts @@ -2,8 +2,7 @@ import {gameManager} from "../Game/GameManager"; import {TextField} from "../Components/TextField"; import {TextInput} from "../Components/TextInput"; import {ClickButton} from "../Components/ClickButton"; -import {GameSceneInterface} from "../Game/GameScene"; -import {MessageUserPositionInterface} from "../../Connexion"; +import {GameSceneName} from "../Game/GameScene"; //todo: put this constants in a dedicated file export const LoginSceneName = "LoginScene"; @@ -11,7 +10,7 @@ enum LoginTextures { playButton = "play_button", } -export class LogincScene extends Phaser.Scene implements GameSceneInterface { +export class LogincScene extends Phaser.Scene { private emailInput: TextInput; private textField: TextField; private playButton: ClickButton; @@ -47,16 +46,7 @@ export class LogincScene extends Phaser.Scene implements GameSceneInterface { let email = this.emailInput.text; if (!email) return; gameManager.connect(email).then(() => { - this.scene.start("GameScene"); + this.scene.start(GameSceneName); }); } - - Map: Phaser.Tilemaps.Tilemap; - RoomId: string; - - createCurrentPlayer(UserId: string): void { - } - - shareUserPosition(UsersPosition: Array): void { - } } \ No newline at end of file diff --git a/front/src/Phaser/Player/Player.ts b/front/src/Phaser/Player/Player.ts index 7eaf4dfb..59c1f96e 100644 --- a/front/src/Phaser/Player/Player.ts +++ b/front/src/Phaser/Player/Player.ts @@ -67,27 +67,25 @@ export class Player extends PlayableCaracter implements CurrentGamerInterface, G let speedMultiplier = activeEvents.get(UserInputEvent.SpeedUp) ? 25 : 9; let moveAmount = speedMultiplier * delta; + let x = 0; + let y = 0; if (activeEvents.get(UserInputEvent.MoveUp)) { - this.move(0, -moveAmount); - haveMove = true; + y = - moveAmount; direction = PlayerAnimationNames.WalkUp; - } - if (activeEvents.get(UserInputEvent.MoveLeft)) { - this.move(-moveAmount, 0); - haveMove = true; - direction = PlayerAnimationNames.WalkLeft; - } - if (activeEvents.get(UserInputEvent.MoveDown)) { - this.move(0, moveAmount); - haveMove = true; + } else if (activeEvents.get(UserInputEvent.MoveDown)) { + y = moveAmount; direction = PlayerAnimationNames.WalkDown; } - if (activeEvents.get(UserInputEvent.MoveRight)) { - this.move(moveAmount, 0); - haveMove = true; + if (activeEvents.get(UserInputEvent.MoveLeft)) { + x = -moveAmount; + direction = PlayerAnimationNames.WalkLeft; + } else if (activeEvents.get(UserInputEvent.MoveRight)) { + x = moveAmount; direction = PlayerAnimationNames.WalkRight; } - if (!haveMove) { + if (x !== 0 || y !== 0) { + this.move(x, y); + } else { direction = PlayerAnimationNames.None; this.stop(); }