Sending position only every 200ms while moving
[libreadventure.git] / front / src / Phaser / Game / GameScene.ts
1 import {GameManager, gameManager, HasMovedEvent} from "./GameManager";
2 import {
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";
16
17 export enum Textures {
18 Player = "male1"
19 }
20
21 interface GameSceneInitInterface {
22 initPosition: PointInterface|null
23 }
24
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>;
34 map: ITiledMap;
35 groups: Map<string, Sprite>;
36 startX = 704;// 22 case
37 startY = 32; // 1 case
38 circleTexture: CanvasTexture;
39 initPosition: PositionInterface;
40
41 MapKey: string;
42 MapUrlFile: string;
43 RoomId: string;
44 instance: string;
45
46 currentTick: number;
47 lastSentTick: number; // The last tick at which a position was sent.
48 lastMoveEventSent: HasMovedEvent = {
49 direction: '',
50 moving: false,
51 x: -1000,
52 y: -1000
53 }
54
55 PositionNextScene: Array<any> = new Array<any>();
56
57 static createFromUrl(mapUrlFile: string, instance: string): GameScene {
58 let key = GameScene.getMapKeyByUrl(mapUrlFile);
59 return new GameScene(key, mapUrlFile, instance);
60 }
61
62 constructor(MapKey : string, MapUrlFile: string, instance: string) {
63 super({
64 key: MapKey
65 });
66
67 this.GameManager = gameManager;
68 this.Terrains = [];
69 this.groups = new Map<string, Sprite>();
70 this.instance = instance;
71
72 this.MapKey = MapKey;
73 this.MapUrlFile = MapUrlFile;
74 this.RoomId = this.instance + '__' + this.MapKey;
75 }
76
77 //hook preload scene
78 preload(): void {
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
83 this.map = data.data;
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)
88 return;
89 }
90 //TODO strategy to add access token
91 this.load.image(tileset.name, `${url}/${tileset.image}`);
92 })
93 });
94 //TODO strategy to add access token
95 this.load.tilemapTiledJSON(this.MapKey, this.MapUrlFile);
96
97 //add player png
98 PLAYER_RESOURCES.forEach((playerResource: any) => {
99 this.load.spritesheet(
100 playerResource.name,
101 playerResource.img,
102 {frameWidth: 32, frameHeight: 32}
103 );
104 });
105
106 this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
107 }
108
109 //hook initialisation
110 init(initData : GameSceneInitInterface) {
111 this.initPosition = initData.initPosition;
112 }
113
114 //hook create scene
115 create(): void {
116 //initalise map
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));
120 });
121
122 //permit to set bound collision
123 this.physics.world.setBounds(0,0, this.Map.widthInPixels, this.Map.heightInPixels);
124
125 //add layer on map
126 this.Layers = new Array<Phaser.Tilemaps.StaticTilemapLayer>();
127 let depth = -2;
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));
131 }
132 if (layer.type === 'tilelayer' && this.getExitSceneUrl(layer) !== undefined) {
133 this.loadNextGame(layer, this.map.width, this.map.tilewidth, this.map.tileheight);
134 }
135 if (layer.type === 'tilelayer' && layer.name === "start") {
136 this.startUser(layer);
137 }
138 if (layer.type === 'objectgroup' && layer.name === 'floorLayer') {
139 depth = 10000;
140 }
141 });
142
143 if (depth === -2) {
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.');
145 }
146
147 //add entities
148 this.Objects = new Array<Phaser.Physics.Arcade.Sprite>();
149
150 //init event click
151 this.EventToClickOnTile();
152
153 //initialise list of other player
154 this.MapPlayers = this.physics.add.group({ immovable: true });
155
156 //notify game manager can to create currentUser in map
157 this.createCurrentPlayer();
158
159 //initialise camera
160 this.initCamera();
161
162
163 // Let's generate the circle for the group delimiter
164 let circleElement = Object.values(this.textures.list).find((object: Texture) => object.key === 'circleSprite');
165 if(circleElement) {
166 this.textures.remove('circleSprite');
167 }
168 this.circleTexture = this.textures.createCanvas('circleSprite', 96, 96);
169 let context = this.circleTexture.context;
170 context.beginPath();
171 context.arc(48, 48, 48, 0, 2 * Math.PI, false);
172 // context.lineWidth = 5;
173 context.strokeStyle = '#ffffff';
174 context.stroke();
175 this.circleTexture.refresh();
176
177 // Let's alter browser history
178 let url = new URL(this.MapUrlFile);
179 let path = '/_/'+this.instance+'/'+url.host+url.pathname;
180 if (url.hash) {
181 // FIXME: entry should be dictated by a property passed to init()
182 path += '#'+url.hash;
183 }
184 window.history.pushState({}, null, path);
185 }
186
187 private getExitSceneUrl(layer: ITiledMapLayer): string|undefined {
188 let properties : any = layer.properties;
189 if (!properties) {
190 return undefined;
191 }
192 let obj = properties.find((property:any) => property.name === "exitSceneUrl");
193 if (obj === undefined) {
194 return undefined;
195 }
196 return obj.value;
197 }
198
199 private getExitSceneInstance(layer: ITiledMapLayer): string|undefined {
200 let properties : any = layer.properties;
201 if (!properties) {
202 return undefined;
203 }
204 let obj = properties.find((property:any) => property.name === "exitInstance");
205 if (obj === undefined) {
206 return undefined;
207 }
208 return obj.value;
209 }
210
211 /**
212 *
213 * @param layer
214 * @param mapWidth
215 * @param tileWidth
216 * @param tileHeight
217 */
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;
223 }
224
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);
228
229 let tiles : any = layer.data;
230 tiles.forEach((objectKey : number, key: number) => {
231 if(objectKey === 0){
232 return;
233 }
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),
244 key: exitSceneKey
245 })
246 });
247 }
248
249 /**
250 * @param layer
251 */
252 private startUser(layer: ITiledMapLayer): void {
253 if (this.initPosition !== undefined) {
254 this.startX = this.initPosition.x;
255 this.startY = this.initPosition.y;
256 return;
257 }
258
259 let tiles : any = layer.data;
260 tiles.forEach((objectKey : number, key: number) => {
261 if(objectKey === 0){
262 return;
263 }
264 let y = Math.floor(key / layer.width);
265 let x = key % layer.width;
266
267 this.startX = (x * 32);
268 this.startY = (y * 32);
269 });
270 }
271
272 //todo: in a dedicated class/function?
273 initCamera() {
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);
277 }
278
279 addLayer(Layer : Phaser.Tilemaps.StaticTilemapLayer){
280 this.Layers.push(Layer);
281 }
282
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)
288 });
289 Layer.setCollisionByProperty({collides: true});
290 if (DEBUG_MODE) {
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
296 });
297 }
298 });
299 }
300
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)
305 });
306 })
307 }
308
309 createCurrentPlayer(){
310 //initialise player
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.
314 this,
315 this.startX,
316 this.startY,
317 this.GameManager.getPlayerName(),
318 this.GameManager.getCharacterSelected(),
319 PlayerAnimationNames.WalkDown,
320 false
321 );
322
323 //create collision
324 this.createCollisionWithPlayer();
325 this.createCollisionObject();
326
327 //join room
328 this.GameManager.joinRoom(this.RoomId, this.startX, this.startY, PlayerAnimationNames.WalkDown, false);
329
330 //listen event to share position of user
331 this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this))
332 }
333
334 pushPlayerPosition(event: HasMovedEvent) {
335 if (this.lastMoveEventSent === event) {
336 return;
337 }
338
339 // If the player is not moving, let's send the info right now.
340 if (event.moving === false) {
341 this.doPushPlayerPosition(event);
342 return;
343 }
344
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);
348 return;
349 }
350
351 // If more than 200ms happened since last event sent
352 if (this.currentTick - this.lastSentTick >= POSITION_DELAY) {
353 this.doPushPlayerPosition(event);
354 return;
355 }
356
357 // Otherwise, do nothing.
358 }
359
360 private doPushPlayerPosition(event: HasMovedEvent): void {
361 this.lastMoveEventSent = event;
362 this.lastSentTick = this.currentTick;
363 this.GameManager.pushPlayerPosition(event);
364 }
365
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));
371 if(tile){
372 this.CurrentPlayer.say("Your touch " + tile.layer.name);
373 }
374 });
375 }
376
377 /**
378 * @param time
379 * @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate.
380 */
381 update(time: number, delta: number) : void {
382 this.currentTick = time;
383 this.CurrentPlayer.moveUser(delta);
384 let nextSceneKey = this.checkToExit();
385 if(nextSceneKey){
386 this.scene.start(nextSceneKey.key);
387 }
388 }
389
390 /**
391 *
392 */
393 checkToExit(){
394 if(this.PositionNextScene.length === 0){
395 return null;
396 }
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
400 })
401 }
402
403 public initUsersPosition(usersPosition: MessageUserPositionInterface[]): void {
404 if(!this.CurrentPlayer){
405 console.error('Cannot initiate users list because map is not loaded yet')
406 return;
407 }
408
409 let currentPlayerId = this.GameManager.getPlayerId();
410
411 // clean map
412 this.MapPlayersByKey.forEach((player: GamerInterface) => {
413 player.destroy();
414 this.MapPlayers.remove(player);
415 });
416 this.MapPlayersByKey = new Map<string, GamerInterface>();
417
418 // load map
419 usersPosition.forEach((userPosition : MessageUserPositionInterface) => {
420 if(userPosition.userId === currentPlayerId){
421 return;
422 }
423 this.addPlayer(userPosition);
424 });
425 }
426
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);
430 if(!player){
431 return null;
432 }
433 return (player as GamerInterface);*/
434 }
435
436 /**
437 * Create new player
438 */
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
445 });
446 return;
447 }
448 //initialise player
449 let player = new Player(
450 addPlayerData.userId,
451 this,
452 addPlayerData.position.x,
453 addPlayerData.position.y,
454 addPlayerData.name,
455 addPlayerData.character,
456 addPlayerData.position.direction,
457 addPlayerData.position.moving
458 );
459 this.MapPlayers.add(player);
460 this.MapPlayersByKey.set(player.userId, player);
461 player.updatePosition(addPlayerData.position);
462
463 //init collision
464 /*this.physics.add.collider(this.CurrentPlayer, player, (CurrentPlayer: CurrentGamerInterface, MapPlayer: GamerInterface) => {
465 CurrentPlayer.say("Hello, how are you ? ");
466 });*/
467 }
468
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);
474 }
475 player.destroy();
476 this.MapPlayers.remove(player);
477 this.MapPlayersByKey.delete(userId);
478 }
479
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 +'"');
484 }
485 player.updatePosition(message.position);
486 }
487
488 shareGroupPosition(groupPositionMessage: GroupCreatedUpdatedMessageInterface) {
489 let groupId = groupPositionMessage.groupId;
490
491 if (this.groups.has(groupId)) {
492 this.groups.get(groupId).setPosition(Math.round(groupPositionMessage.position.x), Math.round(groupPositionMessage.position.y));
493 } else {
494 // TODO: circle radius should not be hard stored
495 let sprite = new Sprite(
496 this,
497 Math.round(groupPositionMessage.position.x),
498 Math.round(groupPositionMessage.position.y),
499 'circleSprite');
500 sprite.setDisplayOrigin(48, 48);
501 this.add.existing(sprite);
502 this.groups.set(groupId, sprite);
503 }
504 }
505
506 deleteGroup(groupId: string): void {
507 if(!this.groups.get(groupId)){
508 return;
509 }
510 this.groups.get(groupId).destroy();
511 this.groups.delete(groupId);
512 }
513
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);
519
520
521 let tab = mapUrlStart.split("/");
522 return tab[tab.length -1].substr(0, tab[tab.length -1].indexOf(".json"));
523 }
524 }