diff --git a/front/.gitignore b/front/.gitignore index 048e02ca..a74d68d5 100644 --- a/front/.gitignore +++ b/front/.gitignore @@ -1,3 +1,6 @@ /node_modules/ /dist/bundle.js /yarn-error.log +/dist/webpack.config.js +/dist/webpack.config.js.map +/dist/src diff --git a/front/src/Phaser/Components/TextInput.ts b/front/src/Phaser/Components/TextInput.ts index b92da1ff..92ddcb56 100644 --- a/front/src/Phaser/Components/TextInput.ts +++ b/front/src/Phaser/Components/TextInput.ts @@ -2,8 +2,9 @@ export class TextInput extends Phaser.GameObjects.BitmapText { private underLineLength = 10; private underLine: Phaser.GameObjects.Text; - constructor(scene: Phaser.Scene, x: number, y: number, maxLength: number) { - super(scene, x, y, 'main_font', '', 32); + + constructor(scene: Phaser.Scene, x: number, y: number, maxLength: number, text: string, onChange: (text: string) => void) { + super(scene, x, y, 'main_font', text, 32); this.scene.add.existing(this); this.underLine = this.scene.add.text(x, y+1, '_______', { fontFamily: 'Arial', fontSize: "32px", color: '#ffffff'}) @@ -17,6 +18,7 @@ export class TextInput extends Phaser.GameObjects.BitmapText { } else if ((event.keyCode === 32 || (event.keyCode >= 48 && event.keyCode <= 90)) && this.text.length < maxLength) { this.addLetter(event.key); } + onChange(this.text); }); } @@ -38,6 +40,4 @@ export class TextInput extends Phaser.GameObjects.BitmapText { getText(): string { return this.text; } - - } diff --git a/front/src/Phaser/Entity/PlayableCaracter.ts b/front/src/Phaser/Entity/PlayableCaracter.ts index 826bfc6a..e1b774ef 100644 --- a/front/src/Phaser/Entity/PlayableCaracter.ts +++ b/front/src/Phaser/Entity/PlayableCaracter.ts @@ -3,25 +3,25 @@ import {SpeechBubble} from "./SpeechBubble"; import BitmapText = Phaser.GameObjects.BitmapText; export const PLAYER_RESOURCES: Array = [ - {name: "male1", img: "resources/characters/pipoya/Male 01-1.png", x: 32, y: 32}, - {name: "male2", img: "resources/characters/pipoya/Male 02-2.png", x: 64, y: 32}, - {name: "male3", img: "resources/characters/pipoya/Male 03-4.png", x: 96, y: 32}, - {name: "male4", img: "resources/characters/pipoya/Male 09-1.png", x: 128, y: 32}, + {name: "male1", img: "resources/characters/pipoya/Male 01-1.png" /*, x: 32, y: 32*/}, + {name: "male2", img: "resources/characters/pipoya/Male 02-2.png"/*, x: 64, y: 32*/}, + {name: "male3", img: "resources/characters/pipoya/Male 03-4.png"/*, x: 96, y: 32*/}, + {name: "male4", img: "resources/characters/pipoya/Male 09-1.png"/*, x: 128, y: 32*/}, - {name: "male5", img: "resources/characters/pipoya/Male 10-3.png", x: 32, y: 64}, - {name: "male6", img: "resources/characters/pipoya/Male 17-2.png", x: 64, y: 64}, - {name: "male7", img: "resources/characters/pipoya/Male 18-1.png", x: 96, y: 64}, - {name: "male8", img: "resources/characters/pipoya/Male 16-4.png", x: 128, y: 64}, + {name: "male5", img: "resources/characters/pipoya/Male 10-3.png"/*, x: 32, y: 64*/}, + {name: "male6", img: "resources/characters/pipoya/Male 17-2.png"/*, x: 64, y: 64*/}, + {name: "male7", img: "resources/characters/pipoya/Male 18-1.png"/*, x: 96, y: 64*/}, + {name: "male8", img: "resources/characters/pipoya/Male 16-4.png"/*, x: 128, y: 64*/}, - {name: "Female1", img: "resources/characters/pipoya/Female 01-1.png", x: 32, y: 96}, - {name: "Female2", img: "resources/characters/pipoya/Female 02-2.png", x: 64, y: 96}, - {name: "Female3", img: "resources/characters/pipoya/Female 03-4.png", x: 96, y: 96}, - {name: "Female4", img: "resources/characters/pipoya/Female 09-1.png", x: 128, y: 96}, + {name: "Female1", img: "resources/characters/pipoya/Female 01-1.png"/*, x: 32, y: 96*/}, + {name: "Female2", img: "resources/characters/pipoya/Female 02-2.png"/*, x: 64, y: 96*/}, + {name: "Female3", img: "resources/characters/pipoya/Female 03-4.png"/*, x: 96, y: 96*/}, + {name: "Female4", img: "resources/characters/pipoya/Female 09-1.png"/*, x: 128, y: 96*/}, - {name: "Female5", img: "resources/characters/pipoya/Female 10-3.png", x: 32, y: 128}, - {name: "Female6", img: "resources/characters/pipoya/Female 17-2.png", x: 64, y: 128}, - {name: "Female7", img: "resources/characters/pipoya/Female 18-1.png", x: 96, y: 128}, - {name: "Female8", img: "resources/characters/pipoya/Female 16-4.png", x: 128, y: 128} + {name: "Female5", img: "resources/characters/pipoya/Female 10-3.png"/*, x: 32, y: 128*/}, + {name: "Female6", img: "resources/characters/pipoya/Female 17-2.png"/*, x: 64, y: 128*/}, + {name: "Female7", img: "resources/characters/pipoya/Female 18-1.png"/*, x: 96, y: 128*/}, + {name: "Female8", img: "resources/characters/pipoya/Female 16-4.png"/*, x: 128, y: 128*/} ]; export class PlayableCaracter extends Phaser.Physics.Arcade.Sprite { diff --git a/front/src/Phaser/Login/LoginScene.ts b/front/src/Phaser/Login/LoginScene.ts index 4b42c6a9..da684da2 100644 --- a/front/src/Phaser/Login/LoginScene.ts +++ b/front/src/Phaser/Login/LoginScene.ts @@ -6,12 +6,11 @@ import Image = Phaser.GameObjects.Image; import Rectangle = Phaser.GameObjects.Rectangle; import {PLAYER_RESOURCES} from "../Entity/PlayableCaracter"; import {cypressAsserter} from "../../Cypress/CypressAsserter"; -import {GroupCreatedUpdatedMessageInterface, MessageUserJoined, MessageUserPositionInterface} from "../../Connection"; +import {SelectCharacterSceneInitDataInterface, SelectCharacterSceneName} from "./SelectCharacterScene"; //todo: put this constants in a dedicated file export const LoginSceneName = "LoginScene"; enum LoginTextures { - //playButton = "play_button", icon = "icon", mainFont = "main_font" } @@ -19,19 +18,18 @@ enum LoginTextures { export class LoginScene extends Phaser.Scene { private nameInput: TextInput; private textField: TextField; - private playButton: ClickButton; private infoTextField: TextField; private pressReturnField: TextField; private logo: Image; - - private selectedRectangle: Rectangle; - private selectedPlayer: Phaser.Physics.Arcade.Sprite; - private players: Array = new Array(); + private name: string; constructor() { super({ key: LoginSceneName }); + if (window.localStorage) { + this.name = window.localStorage.getItem('playerName') ?? ''; + } } preload() { @@ -56,13 +54,16 @@ export class LoginScene extends Phaser.Scene { this.textField = new TextField(this, this.game.renderer.width / 2, 50, 'Enter your name:'); this.textField.setOrigin(0.5).setCenterAlign() - this.nameInput = new TextInput(this, this.game.renderer.width / 2 - 64, 70, 4); + this.nameInput = new TextInput(this, this.game.renderer.width / 2 - 64, 70, 4, this.name,(text: string) => { + this.name = text; + if (window.localStorage) { + window.localStorage.setItem('playerName', text); + } + }); this.pressReturnField = new TextField(this, this.game.renderer.width / 2, 130, 'Press enter to start'); this.pressReturnField.setOrigin(0.5).setCenterAlign() - this.selectedRectangle = this.add.rectangle(32, 32, 32, 32).setStrokeStyle(2, 0xFFFFFF); - this.logo = new Image(this, this.game.renderer.width - 30, this.game.renderer.height - 20, LoginTextures.icon); this.add.existing(this.logo); @@ -70,100 +71,24 @@ export class LoginScene extends Phaser.Scene { this.infoTextField = new TextField(this, 10, this.game.renderer.height - 35, infoText); this.input.keyboard.on('keyup-ENTER', () => { - let name = this.nameInput.getText(); - if (name === '') { + if (this.name === '') { return } - return this.login(name); + this.login(this.name); }); - /*create user*/ - this.createCurrentPlayer(); cypressAsserter.initFinished(); } update(time: number, delta: number): void { - if (this.nameInput.getText() == '') { + if (this.name == '') { this.pressReturnField.setVisible(false); } else { this.pressReturnField.setVisible(!!(Math.floor(time / 500) % 2)); } } - private async login(name: string) { - return gameManager.connect(name, this.selectedPlayer.texture.key).then(() => { - // Do we have a start URL in the address bar? If so, let's redirect to this address - let instanceAndMapUrl = this.findMapUrl(); - if (instanceAndMapUrl !== null) { - let [mapUrl, instance] = instanceAndMapUrl; - let key = gameManager.loadMap(mapUrl, this.scene, instance); - this.scene.start(key); - return mapUrl; - } else { - // If we do not have a map address in the URL, let's ask the server for a start map. - return gameManager.loadStartMap().then((scene : any) => { - if (!scene) { - return; - } - let key = gameManager.loadMap(window.location.protocol + "//" + scene.mapUrlStart, this.scene, scene.startInstance); - this.scene.start(key); - return scene; - }).catch((err) => { - console.error(err); - throw err; - }); - } - }).catch((err) => { - console.error(err); - throw err; - }); - } - - /** - * Returns the map URL and the instance from the current URL - */ - private findMapUrl(): [string, string]|null { - let path = window.location.pathname; - if (!path.startsWith('/_/')) { - return null; - } - let instanceAndMap = path.substr(3); - let firstSlash = instanceAndMap.indexOf('/'); - if (firstSlash === -1) { - return null; - } - let instance = instanceAndMap.substr(0, firstSlash); - return [window.location.protocol+'//'+instanceAndMap.substr(firstSlash+1), instance]; - } - - Map: Phaser.Tilemaps.Tilemap; - - initAnimation(): void { - throw new Error("Method not implemented."); - } - - createCurrentPlayer(): void { - for (let i = 0; i { - this.selectedPlayer.anims.pause(); - this.selectedRectangle.setY(player.y); - this.selectedRectangle.setX(player.x); - player.play(playerResource.name); - this.selectedPlayer = player; - }); - this.players.push(player); - } - this.selectedPlayer = this.players[0]; - this.selectedPlayer.play(PLAYER_RESOURCES[0].name); + private login(name: string): void { + this.scene.start(SelectCharacterSceneName, { name } as SelectCharacterSceneInitDataInterface); } } diff --git a/front/src/Phaser/Login/SelectCharacterScene.ts b/front/src/Phaser/Login/SelectCharacterScene.ts new file mode 100644 index 00000000..98b5262e --- /dev/null +++ b/front/src/Phaser/Login/SelectCharacterScene.ts @@ -0,0 +1,214 @@ +import {gameManager} from "../Game/GameManager"; +import {TextField} from "../Components/TextField"; +import {ClickButton} from "../Components/ClickButton"; +import Image = Phaser.GameObjects.Image; +import Rectangle = Phaser.GameObjects.Rectangle; +import {PLAYER_RESOURCES} from "../Entity/PlayableCaracter"; + +//todo: put this constants in a dedicated file +export const SelectCharacterSceneName = "SelectCharacterScene"; +enum LoginTextures { + playButton = "play_button", + icon = "icon", + mainFont = "main_font" +} + +export interface SelectCharacterSceneInitDataInterface { + name: string +} + +export class SelectCharacterScene extends Phaser.Scene { + private readonly nbCharactersPerRow = 4; + private textField: TextField; + private pressReturnField: TextField; + private logo: Image; + private loginName: string; + + private selectedRectangle: Rectangle; + private selectedRectangleXPos = 0; // Number of the character selected in the rows + private selectedRectangleYPos = 0; // Number of the character selected in the columns + private selectedPlayer: Phaser.Physics.Arcade.Sprite; + private players: Array = new Array(); + + constructor() { + super({ + key: SelectCharacterSceneName + }); + } + + init({ name }: SelectCharacterSceneInitDataInterface) { + this.loginName = name; + } + + preload() { + this.load.image(LoginTextures.playButton, "resources/objects/play_button.png"); + this.load.image(LoginTextures.icon, "resources/logos/tcm_full.png"); + // Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap + this.load.bitmapFont(LoginTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml'); + //add player png + PLAYER_RESOURCES.forEach((playerResource: any) => { + this.load.spritesheet( + playerResource.name, + playerResource.img, + {frameWidth: 32, frameHeight: 32} + ); + }); + } + + create() { + this.textField = new TextField(this, this.game.renderer.width / 2, 50, 'Select your character'); + this.textField.setOrigin(0.5).setCenterAlign() + + this.pressReturnField = new TextField(this, this.game.renderer.width / 2, 230, 'Press enter to start'); + this.pressReturnField.setOrigin(0.5).setCenterAlign() + + let rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16; + + this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xFFFFFF); + + this.logo = new Image(this, this.game.renderer.width - 30, this.game.renderer.height - 20, LoginTextures.icon); + this.add.existing(this.logo); + + this.input.keyboard.on('keyup-ENTER', () => { + return this.login(this.loginName); + }); + + this.input.keyboard.on('keydown-RIGHT', () => { + if (this.selectedRectangleXPos < this.nbCharactersPerRow - 1) { + this.selectedRectangleXPos++; + } + this.updateSelectedPlayer(); + }); + this.input.keyboard.on('keydown-LEFT', () => { + if (this.selectedRectangleXPos > 0) { + this.selectedRectangleXPos--; + } + this.updateSelectedPlayer(); + }); + this.input.keyboard.on('keydown-DOWN', () => { + if (this.selectedRectangleYPos < Math.ceil(PLAYER_RESOURCES.length / this.nbCharactersPerRow) - 1) { + this.selectedRectangleYPos++; + } + this.updateSelectedPlayer(); + }); + this.input.keyboard.on('keydown-UP', () => { + if (this.selectedRectangleYPos > 0) { + this.selectedRectangleYPos--; + } + this.updateSelectedPlayer(); + }); + + /*create user*/ + this.createCurrentPlayer(); + + if (window.localStorage) { + let playerNumberStr: string = window.localStorage.getItem('selectedPlayer') ?? '0'; + let playerNumber: number = Number(playerNumberStr); + this.selectedRectangleXPos = playerNumber % this.nbCharactersPerRow; + this.selectedRectangleYPos = Math.floor(playerNumber / this.nbCharactersPerRow); + this.updateSelectedPlayer(); + } + } + + update(time: number, delta: number): void { + this.pressReturnField.setVisible(!!(Math.floor(time / 500) % 2)); + } + + private async login(name: string) { + return gameManager.connect(name, this.selectedPlayer.texture.key).then(() => { + // Do we have a start URL in the address bar? If so, let's redirect to this address + let instanceAndMapUrl = this.findMapUrl(); + if (instanceAndMapUrl !== null) { + let [mapUrl, instance] = instanceAndMapUrl; + let key = gameManager.loadMap(mapUrl, this.scene, instance); + this.scene.start(key); + return mapUrl; + } else { + // If we do not have a map address in the URL, let's ask the server for a start map. + return gameManager.loadStartMap().then((scene : any) => { + if (!scene) { + return; + } + let key = gameManager.loadMap(window.location.protocol + "//" + scene.mapUrlStart, this.scene, scene.startInstance); + this.scene.start(key); + return scene; + }).catch((err) => { + console.error(err); + throw err; + }); + } + }).catch((err) => { + console.error(err); + throw err; + }); + } + + /** + * Returns the map URL and the instance from the current URL + */ + private findMapUrl(): [string, string]|null { + let path = window.location.pathname; + if (!path.startsWith('/_/')) { + return null; + } + let instanceAndMap = path.substr(3); + let firstSlash = instanceAndMap.indexOf('/'); + if (firstSlash === -1) { + return null; + } + let instance = instanceAndMap.substr(0, firstSlash); + return [window.location.protocol+'//'+instanceAndMap.substr(firstSlash+1), instance]; + } + + createCurrentPlayer(): void { + for (let i = 0; i { + this.selectedRectangleXPos = col; + this.selectedRectangleYPos = row; + this.updateSelectedPlayer(); + }); + this.players.push(player); + } + this.selectedPlayer = this.players[0]; + this.selectedPlayer.play(PLAYER_RESOURCES[0].name); + } + + /** + * Returns pixel position by on column and row number + */ + private getCharacterPosition(x: number, y: number): [number, number] { + return [ + this.game.renderer.width / 2 + 16 + (x - this.nbCharactersPerRow / 2) * 32, + y * 32 + 90 + ]; + } + + private updateSelectedPlayer(): void { + this.selectedPlayer.anims.pause(); + let [x, y] = this.getCharacterPosition(this.selectedRectangleXPos, this.selectedRectangleYPos); + this.selectedRectangle.setX(x); + this.selectedRectangle.setY(y); + let playerNumber = this.selectedRectangleXPos + this.selectedRectangleYPos * this.nbCharactersPerRow; + let player = this.players[playerNumber]; + player.play(PLAYER_RESOURCES[playerNumber].name); + this.selectedPlayer = player; + if (window.localStorage) { + window.localStorage.setItem('selectedPlayer', String(playerNumber)); + } + } +} diff --git a/front/src/Phaser/Player/Animation.ts b/front/src/Phaser/Player/Animation.ts index 10cee1e8..8ca7d671 100644 --- a/front/src/Phaser/Player/Animation.ts +++ b/front/src/Phaser/Player/Animation.ts @@ -1,14 +1,5 @@ import {Textures} from "../Game/GameScene"; -interface AnimationData { - key: string; - frameRate: number; - repeat: number; - frameModel: string; //todo use an enum - frameStart: number; - frameEnd: number; -} - export enum PlayerAnimationNames { WalkDown = 'down', WalkLeft = 'left', @@ -16,34 +7,4 @@ export enum PlayerAnimationNames { WalkRight = 'right', } -export const getPlayerAnimations = (name: string = Textures.Player): AnimationData[] => { - return [{ - key: `${name}-${PlayerAnimationNames.WalkDown}`, - frameModel: name, - frameStart: 0, - frameEnd: 2, - frameRate: 10, - repeat: -1 - }, { - key: `${name}-${PlayerAnimationNames.WalkLeft}`, - frameModel: name, - frameStart: 3, - frameEnd: 5, - frameRate: 10, - repeat: -1 - }, { - key: `${name}-${PlayerAnimationNames.WalkRight}`, - frameModel: name, - frameStart: 6, - frameEnd: 8, - frameRate: 10, - repeat: -1 - }, { - key: `${name}-${PlayerAnimationNames.WalkUp}`, - frameModel: name, - frameStart: 9, - frameEnd: 11, - frameRate: 10, - repeat: -1 - }]; -}; + diff --git a/front/src/Phaser/Player/Player.ts b/front/src/Phaser/Player/Player.ts index 2b5f8fea..1c8458c0 100644 --- a/front/src/Phaser/Player/Player.ts +++ b/front/src/Phaser/Player/Player.ts @@ -1,4 +1,4 @@ -import {getPlayerAnimations, PlayerAnimationNames} from "./Animation"; +import {PlayerAnimationNames} from "./Animation"; import {GameScene, Textures} from "../Game/GameScene"; import {MessageUserPositionInterface, PointInterface} from "../../Connection"; import {ActiveEventList, UserInputEvent, UserInputManager} from "../UserInput/UserInputManager"; @@ -17,6 +17,16 @@ export interface GamerInterface extends PlayableCaracter{ say(text : string) : void; } +interface AnimationData { + key: string; + frameRate: number; + repeat: number; + frameModel: string; //todo use an enum + frameStart: number; + frameEnd: number; +} + + export class Player extends PlayableCaracter implements CurrentGamerInterface, GamerInterface { userId: string; userInputManager: UserInputManager; @@ -49,7 +59,7 @@ export class Player extends PlayableCaracter implements CurrentGamerInterface, G } private initAnimation(): void { - getPlayerAnimations(this.PlayerTexture).forEach(d => { + this.getPlayerAnimations(this.PlayerTexture).forEach(d => { this.scene.anims.create({ key: d.key, frames: this.scene.anims.generateFrameNumbers(d.frameModel, {start: d.frameStart, end: d.frameEnd}), @@ -59,6 +69,38 @@ export class Player extends PlayableCaracter implements CurrentGamerInterface, G }) } + private getPlayerAnimations(name: string): AnimationData[] { + return [{ + key: `${name}-${PlayerAnimationNames.WalkDown}`, + frameModel: name, + frameStart: 0, + frameEnd: 2, + frameRate: 10, + repeat: -1 + }, { + key: `${name}-${PlayerAnimationNames.WalkLeft}`, + frameModel: name, + frameStart: 3, + frameEnd: 5, + frameRate: 10, + repeat: -1 + }, { + key: `${name}-${PlayerAnimationNames.WalkRight}`, + frameModel: name, + frameStart: 6, + frameEnd: 8, + frameRate: 10, + repeat: -1 + }, { + key: `${name}-${PlayerAnimationNames.WalkUp}`, + frameModel: name, + frameStart: 9, + frameEnd: 11, + frameRate: 10, + repeat: -1 + }]; + } + moveUser(delta: number): void { //if user client on shift, camera and player speed let direction = null; diff --git a/front/src/index.ts b/front/src/index.ts index 5ece7d56..843925ac 100644 --- a/front/src/index.ts +++ b/front/src/index.ts @@ -5,13 +5,14 @@ import {cypressAsserter} from "./Cypress/CypressAsserter"; import {LoginScene} from "./Phaser/Login/LoginScene"; import {ReconnectingScene} from "./Phaser/Reconnecting/ReconnectingScene"; import {gameManager} from "./Phaser/Game/GameManager"; +import {SelectCharacterScene} from "./Phaser/Login/SelectCharacterScene"; const config: GameConfig = { title: "Office game", width: window.innerWidth / RESOLUTION, height: window.innerHeight / RESOLUTION, parent: "game", - scene: [LoginScene, ReconnectingScene], + scene: [LoginScene, SelectCharacterScene, ReconnectingScene], zoom: RESOLUTION, physics: { default: "arcade",