1 import {GameManager, gameManager, HasMovedEvent} from "./GameManager";
3 GroupCreatedUpdatedMessageInterface,
4 MessageUserMovedInterface,
5 MessageUserPositionInterface, PointInterface, PositionInterface
6 } from "../../Connection";
7 import {CurrentGamerInterface, GamerInterface, hasMovedEventName, Player} from "../Player/Player";
8 import { DEBUG_MODE, ZOOM_LEVEL, POSITION_DELAY } from "../../Enum/EnvironmentVariable";
9 import {ITiledMap, ITiledMapLayer, ITiledTileSet} from "../Map/ITiledMap";
10 import {PLAYER_RESOURCES} from "../Entity/PlayableCaracter";
11 import Texture = Phaser.Textures.Texture;
12 import Sprite = Phaser.GameObjects.Sprite;
13 import CanvasTexture = Phaser.Textures.CanvasTexture;
14 import {AddPlayerInterface} from "./AddPlayerInterface";
15 import {PlayerAnimationNames} from "../Player/Animation";
17 export enum Textures {
21 interface GameSceneInitInterface {
22 initPosition: PointInterface|null
25 export class GameScene extends Phaser.Scene {
26 GameManager : GameManager;
27 Terrains : Array<Phaser.Tilemaps.Tileset>;
28 CurrentPlayer: CurrentGamerInterface;
29 MapPlayers : Phaser.Physics.Arcade.Group;
30 MapPlayersByKey : Map<string, GamerInterface> = new Map<string, GamerInterface>();
31 Map: Phaser.Tilemaps.Tilemap;
32 Layers : Array<Phaser.Tilemaps.StaticTilemapLayer>;
33 Objects : Array<Phaser.Physics.Arcade.Sprite>;
35 groups: Map<string, Sprite>;
36 startX = 704;// 22 case
37 startY = 32; // 1 case
38 circleTexture: CanvasTexture;
39 initPosition: PositionInterface;
47 lastSentTick: number; // The last tick at which a position was sent.
48 lastMoveEventSent: HasMovedEvent = {
55 PositionNextScene: Array<any> = new Array<any>();
57 static createFromUrl(mapUrlFile: string, instance: string): GameScene {
58 let key = GameScene.getMapKeyByUrl(mapUrlFile);
59 return new GameScene(key, mapUrlFile, instance);
62 constructor(MapKey : string, MapUrlFile: string, instance: string) {
67 this.GameManager = gameManager;
69 this.groups = new Map<string, Sprite>();
70 this.instance = instance;
73 this.MapUrlFile = MapUrlFile;
74 this.RoomId = this.instance + '__' + this.MapKey;
79 this.GameManager.setCurrentGameScene(this);
80 this.load.on('filecomplete-tilemapJSON-'+this.MapKey, (key: string, type: string, data: any) => {
81 // Triggered when the map is loaded
82 // Load tiles attached to the map recursively
84 let url = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/'));
85 this.map.tilesets.forEach((tileset) => {
86 if (typeof tileset.name === 'undefined' || typeof tileset.image === 'undefined') {
87 console.warn("Don't know how to handle tileset ", tileset)
90 //TODO strategy to add access token
91 this.load.image(tileset.name, `${url}/${tileset.image}`);
94 //TODO strategy to add access token
95 this.load.tilemapTiledJSON(this.MapKey, this.MapUrlFile);
98 PLAYER_RESOURCES.forEach((playerResource: any) => {
99 this.load.spritesheet(
102 {frameWidth: 32, frameHeight: 32}
106 this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
109 //hook initialisation
110 init(initData : GameSceneInitInterface) {
111 this.initPosition = initData.initPosition;
117 this.Map = this.add.tilemap(this.MapKey);
118 this.map.tilesets.forEach((tileset: ITiledTileSet) => {
119 this.Terrains.push(this.Map.addTilesetImage(tileset.name, tileset.name));
122 //permit to set bound collision
123 this.physics.world.setBounds(0,0, this.Map.widthInPixels, this.Map.heightInPixels);
126 this.Layers = new Array<Phaser.Tilemaps.StaticTilemapLayer>();
128 this.map.layers.forEach((layer : ITiledMapLayer) => {
129 if (layer.type === 'tilelayer') {
130 this.addLayer(this.Map.createStaticLayer(layer.name, this.Terrains, 0, 0).setDepth(depth));
132 if (layer.type === 'tilelayer' && this.getExitSceneUrl(layer) !== undefined) {
133 this.loadNextGame(layer, this.map.width, this.map.tilewidth, this.map.tileheight);
135 if (layer.type === 'tilelayer' && layer.name === "start") {
136 this.startUser(layer);
138 if (layer.type === 'objectgroup' && layer.name === 'floorLayer') {
144 throw new Error('Your map MUST contain a layer of type "objectgroup" whose name is "floorLayer" that represents the layer characters are drawn at.');
148 this.Objects = new Array<Phaser.Physics.Arcade.Sprite>();
151 this.EventToClickOnTile();
153 //initialise list of other player
154 this.MapPlayers = this.physics.add.group({ immovable: true });
156 //notify game manager can to create currentUser in map
157 this.createCurrentPlayer();
163 // Let's generate the circle for the group delimiter
164 let circleElement = Object.values(this.textures.list).find((object: Texture) => object.key === 'circleSprite');
166 this.textures.remove('circleSprite');
168 this.circleTexture = this.textures.createCanvas('circleSprite', 96, 96);
169 let context = this.circleTexture.context;
171 context.arc(48, 48, 48, 0, 2 * Math.PI, false);
172 // context.lineWidth = 5;
173 context.strokeStyle = '#ffffff';
175 this.circleTexture.refresh();
177 // Let's alter browser history
178 let url = new URL(this.MapUrlFile);
179 let path = '/_/'+this.instance+'/'+url.host+url.pathname;
181 // FIXME: entry should be dictated by a property passed to init()
182 path += '#'+url.hash;
184 window.history.pushState({}, null, path);
187 private getExitSceneUrl(layer: ITiledMapLayer): string|undefined {
188 let properties : any = layer.properties;
192 let obj = properties.find((property:any) => property.name === "exitSceneUrl");
193 if (obj === undefined) {
199 private getExitSceneInstance(layer: ITiledMapLayer): string|undefined {
200 let properties : any = layer.properties;
204 let obj = properties.find((property:any) => property.name === "exitInstance");
205 if (obj === undefined) {
218 private loadNextGame(layer: ITiledMapLayer, mapWidth: number, tileWidth: number, tileHeight: number){
219 let exitSceneUrl = this.getExitSceneUrl(layer);
220 let instance = this.getExitSceneInstance(layer);
221 if (instance === undefined) {
222 instance = this.instance;
225 // TODO: eventually compute a relative URL
226 let absoluteExitSceneUrl = new URL(exitSceneUrl, this.MapUrlFile).href;
227 let exitSceneKey = gameManager.loadMap(absoluteExitSceneUrl, this.scene, instance);
229 let tiles : any = layer.data;
230 tiles.forEach((objectKey : number, key: number) => {
234 //key + 1 because the start x = 0;
235 let y : number = parseInt(((key + 1) / mapWidth).toString());
236 let x : number = key - (y * mapWidth);
237 //push and save switching case
238 // TODO: this is not efficient. We should refactor that to enable a search by key. For instance: this.PositionNextScene[y][x] = exitSceneKey
239 this.PositionNextScene.push({
240 xStart: (x * tileWidth),
241 yStart: (y * tileWidth),
242 xEnd: ((x +1) * tileHeight),
243 yEnd: ((y + 1) * tileHeight),
252 private startUser(layer: ITiledMapLayer): void {
253 if (this.initPosition !== undefined) {
254 this.startX = this.initPosition.x;
255 this.startY = this.initPosition.y;
259 let tiles : any = layer.data;
260 tiles.forEach((objectKey : number, key: number) => {
264 let y = Math.floor(key / layer.width);
265 let x = key % layer.width;
267 this.startX = (x * 32);
268 this.startY = (y * 32);
272 //todo: in a dedicated class/function?
274 this.cameras.main.setBounds(0,0, this.Map.widthInPixels, this.Map.heightInPixels);
275 this.cameras.main.startFollow(this.CurrentPlayer);
276 this.cameras.main.setZoom(ZOOM_LEVEL);
279 addLayer(Layer : Phaser.Tilemaps.StaticTilemapLayer){
280 this.Layers.push(Layer);
283 createCollisionWithPlayer() {
284 //add collision layer
285 this.Layers.forEach((Layer: Phaser.Tilemaps.StaticTilemapLayer) => {
286 this.physics.add.collider(this.CurrentPlayer, Layer, (object1: any, object2: any) => {
287 //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name)
289 Layer.setCollisionByProperty({collides: true});
291 //debug code to see the collision hitbox of the object in the top layer
292 Layer.renderDebug(this.add.graphics(), {
293 tileColor: null, //non-colliding tiles
294 collidingTileColor: new Phaser.Display.Color(243, 134, 48, 200), // Colliding tiles,
295 faceColor: new Phaser.Display.Color(40, 39, 37, 255) // Colliding face edges
301 createCollisionObject(){
302 this.Objects.forEach((Object : Phaser.Physics.Arcade.Sprite) => {
303 this.physics.add.collider(this.CurrentPlayer, Object, (object1: any, object2: any) => {
304 //this.CurrentPlayer.say("Collision with object : " + (object2 as Phaser.Physics.Arcade.Sprite).texture.key)
309 createCurrentPlayer(){
311 //TODO create animation moving between exit and start
312 this.CurrentPlayer = new Player(
313 null, // The current player is not has no id (because the id can change if connection is lost and we should check that id using the GameManager.
317 this.GameManager.getPlayerName(),
318 this.GameManager.getCharacterSelected(),
319 PlayerAnimationNames.WalkDown,
324 this.createCollisionWithPlayer();
325 this.createCollisionObject();
328 this.GameManager.joinRoom(this.RoomId, this.startX, this.startY, PlayerAnimationNames.WalkDown, false);
330 //listen event to share position of user
331 this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this))
334 pushPlayerPosition(event: HasMovedEvent) {
335 if (this.lastMoveEventSent === event) {
339 // If the player is not moving, let's send the info right now.
340 if (event.moving === false) {
341 this.doPushPlayerPosition(event);
345 // If the player is moving, and if it changed direction, let's send an event
346 if (event.direction !== this.lastMoveEventSent.direction) {
347 this.doPushPlayerPosition(event);
351 // If more than 200ms happened since last event sent
352 if (this.currentTick - this.lastSentTick >= POSITION_DELAY) {
353 this.doPushPlayerPosition(event);
357 // Otherwise, do nothing.
360 private doPushPlayerPosition(event: HasMovedEvent): void {
361 this.lastMoveEventSent = event;
362 this.lastSentTick = this.currentTick;
363 this.GameManager.pushPlayerPosition(event);
366 EventToClickOnTile(){
367 // debug code to get a tile properties by clicking on it
368 this.input.on("pointerdown", (pointer: Phaser.Input.Pointer)=>{
369 //pixel position toz tile position
370 let tile = this.Map.getTileAt(this.Map.worldToTileX(pointer.worldX), this.Map.worldToTileY(pointer.worldY));
372 this.CurrentPlayer.say("Your touch " + tile.layer.name);
379 * @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate.
381 update(time: number, delta: number) : void {
382 this.currentTick = time;
383 this.CurrentPlayer.moveUser(delta);
384 let nextSceneKey = this.checkToExit();
386 this.scene.start(nextSceneKey.key);
394 if(this.PositionNextScene.length === 0){
397 return this.PositionNextScene.find((position : any) => {
398 return position.xStart <= this.CurrentPlayer.x && this.CurrentPlayer.x <= position.xEnd
399 && position.yStart <= this.CurrentPlayer.y && this.CurrentPlayer.y <= position.yEnd
403 public initUsersPosition(usersPosition: MessageUserPositionInterface[]): void {
404 if(!this.CurrentPlayer){
405 console.error('Cannot initiate users list because map is not loaded yet')
409 let currentPlayerId = this.GameManager.getPlayerId();
412 this.MapPlayersByKey.forEach((player: GamerInterface) => {
414 this.MapPlayers.remove(player);
416 this.MapPlayersByKey = new Map<string, GamerInterface>();
419 usersPosition.forEach((userPosition : MessageUserPositionInterface) => {
420 if(userPosition.userId === currentPlayerId){
423 this.addPlayer(userPosition);
427 private findPlayerInMap(UserId : string) : GamerInterface | null{
428 return this.MapPlayersByKey.get(UserId);
429 /*let player = this.MapPlayers.getChildren().find((player: Player) => UserId === player.userId);
433 return (player as GamerInterface);*/
439 public addPlayer(addPlayerData : AddPlayerInterface) : void{
440 //check if exist player, if exist, move position
441 if(this.MapPlayersByKey.has(addPlayerData.userId)){
442 this.updatePlayerPosition({
443 userId: addPlayerData.userId,
444 position: addPlayerData.position
449 let player = new Player(
450 addPlayerData.userId,
452 addPlayerData.position.x,
453 addPlayerData.position.y,
455 addPlayerData.character,
456 addPlayerData.position.direction,
457 addPlayerData.position.moving
459 this.MapPlayers.add(player);
460 this.MapPlayersByKey.set(player.userId, player);
461 player.updatePosition(addPlayerData.position);
464 /*this.physics.add.collider(this.CurrentPlayer, player, (CurrentPlayer: CurrentGamerInterface, MapPlayer: GamerInterface) => {
465 CurrentPlayer.say("Hello, how are you ? ");
469 public removePlayer(userId: string) {
470 console.log('Removing player ', userId)
471 let player = this.MapPlayersByKey.get(userId);
472 if (player === undefined) {
473 console.error('Cannot find user with id ', userId);
476 this.MapPlayers.remove(player);
477 this.MapPlayersByKey.delete(userId);
480 updatePlayerPosition(message: MessageUserMovedInterface): void {
481 let player : GamerInterface | undefined = this.MapPlayersByKey.get(message.userId);
482 if (player === undefined) {
483 throw new Error('Cannot find player with ID "' + message.userId +'"');
485 player.updatePosition(message.position);
488 shareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface) {
489 let groupId = groupPositionMessage.groupId;
491 if (this.groups.has(groupId)) {
492 this.groups.get(groupId).setPosition(Math.round(groupPositionMessage.position.x), Math.round(groupPositionMessage.position.y));
494 // TODO: circle radius should not be hard stored
495 let sprite = new Sprite(
497 Math.round(groupPositionMessage.position.x),
498 Math.round(groupPositionMessage.position.y),
500 sprite.setDisplayOrigin(48, 48);
501 this.add.existing(sprite);
502 this.groups.set(groupId, sprite);
506 deleteGroup(groupId: string): void {
507 if(!this.groups.get(groupId)){
510 this.groups.get(groupId).destroy();
511 this.groups.delete(groupId);
514 public static getMapKeyByUrl(mapUrlStart: string) : string {
515 // FIXME: the key should be computed from the full URL of the map.
516 let startPos = mapUrlStart.indexOf('://')+3;
517 let endPos = mapUrlStart.indexOf(".json");
518 return mapUrlStart.substring(startPos, endPos);
521 let tab = mapUrlStart.split("/");
522 return tab[tab.length -1].substr(0, tab[tab.length -1].indexOf(".json"));