diff --git a/back/src/Assets/Maps/Floor0/floor0.json b/back/src/Assets/Maps/Floor0/floor0.json index 987004e6..94a215b3 100644 --- a/back/src/Assets/Maps/Floor0/floor0.json +++ b/back/src/Assets/Maps/Floor0/floor0.json @@ -199,7 +199,19 @@ "draworder":"topdown", "id":3, "name":"floorLayer", - "objects":[], + "objects":[ + { + "height":0, + "id":1, + "name":"computer", + "point":true, + "rotation":0, + "type":"computer", + "visible":true, + "width":0, + "x":431, + "y":142 + }], "opacity":1, "type":"objectgroup", "visible":true, @@ -225,7 +237,7 @@ "y":0 }], "nextlayerid":18, - "nextobjectid":1, + "nextobjectid":2, "orientation":"orthogonal", "renderorder":"right-down", "tiledversion":"1.3.3", diff --git a/docker-compose.yaml b/docker-compose.yaml index 74bbafbf..53208c76 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -38,6 +38,28 @@ services: - "traefik.http.routers.front-ssl.tls=true" - "traefik.http.routers.front-ssl.service=front" + maps: + image: thecodingmachine/nodejs:12-apache + environment: + DEBUG_MODE: "$DEBUG_MODE" + HOST: "0.0.0.0" + NODE_ENV: development + APACHE_DOCUMENT_ROOT: dist/ + #APACHE_EXTENSIONS: headers + #APACHE_EXTENSION_HEADERS: 1 + STARTUP_COMMAND_0: sudo a2enmod headers + STARTUP_COMMAND_1: yarn install + STARTUP_COMMAND_2: yarn run dev & + volumes: + - ./maps:/var/www/html + labels: + - "traefik.http.routers.maps.rule=Host(`maps.workadventure.localhost`)" + - "traefik.http.routers.maps.entryPoints=web,traefik" + - "traefik.http.services.maps.loadbalancer.server.port=80" + - "traefik.http.routers.maps-ssl.rule=Host(`maps.workadventure.localhost`)" + - "traefik.http.routers.maps-ssl.entryPoints=websecure" + - "traefik.http.routers.maps-ssl.tls=true" + - "traefik.http.routers.maps-ssl.service=maps" back: image: thecodingmachine/nodejs:12 diff --git a/front/package.json b/front/package.json index b0c5502b..e5ea5b66 100644 --- a/front/package.json +++ b/front/package.json @@ -25,7 +25,8 @@ "phaser": "^3.22.0", "queue-typescript": "^1.0.1", "simple-peer": "^9.6.2", - "socket.io-client": "^2.3.0" + "socket.io-client": "^2.3.0", + "webpack-require-http": "^0.4.3" }, "scripts": { "start": "webpack-dev-server --open", diff --git a/front/src/Phaser/Game/GameScene.ts b/front/src/Phaser/Game/GameScene.ts index a46672ee..6d311e33 100644 --- a/front/src/Phaser/Game/GameScene.ts +++ b/front/src/Phaser/Game/GameScene.ts @@ -10,7 +10,7 @@ import { DEBUG_MODE, ZOOM_LEVEL, POSITION_DELAY } from "../../Enum/EnvironmentVa import { ITiledMap, ITiledMapLayer, - ITiledMapLayerProperty, + ITiledMapLayerProperty, ITiledMapObject, ITiledTileSet } from "../Map/ITiledMap"; import {PLAYER_RESOURCES, PlayerResourceDescriptionInterface} from "../Entity/Character"; @@ -28,6 +28,8 @@ import {SimplePeer} from "../../WebRtc/SimplePeer"; import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR; import {FourOFourSceneName} from "../Reconnecting/FourOFourScene"; +import {ItemFactoryInterface} from "../Items/ItemFactoryInterface"; +import {ActionableItem} from "../Items/ActionableItem"; export enum Textures { @@ -89,6 +91,9 @@ export class GameScene extends Phaser.Scene { private connection: Connection; private simplePeer : SimplePeer; private connectionPromise: Promise + // A promise that will resolve when the "create" method is called (signaling loading is ended) + private createPromise: Promise; + private createPromiseResolve: (value?: void | PromiseLike) => void; MapKey: string; MapUrlFile: string; @@ -106,6 +111,9 @@ export class GameScene extends Phaser.Scene { private PositionNextScene: Array> = new Array>(); private startLayerName: string|undefined; + private actionableItems: Array = new Array(); + // The item that can be selected by pressing the space key. + private outlinedItem: ActionableItem|null = null; static createFromUrl(mapUrlFile: string, instance: string, key: string|null = null): GameScene { const mapKey = GameScene.getMapKeyByUrl(mapUrlFile); @@ -128,6 +136,10 @@ export class GameScene extends Phaser.Scene { this.MapKey = MapKey; this.MapUrlFile = MapUrlFile; this.RoomId = this.instance + '__' + MapKey; + + this.createPromise = new Promise((resolve, reject): void => { + this.createPromiseResolve = resolve; + }) } //hook preload scene @@ -225,7 +237,7 @@ export class GameScene extends Phaser.Scene { // FIXME: we need to put a "unknown" instead of a "any" and validate the structure of the JSON we are receiving. // eslint-disable-next-line @typescript-eslint/no-explicit-any - private onMapLoad(data: any): void { + private async onMapLoad(data: any): Promise { // Triggered when the map is loaded // Load tiles attached to the map recursively this.mapFile = data.data; @@ -238,6 +250,85 @@ export class GameScene extends Phaser.Scene { //TODO strategy to add access token this.load.image(`${url}/${tileset.image}`, `${url}/${tileset.image}`); }) + + // Scan the object layers for objects to load and load them. + let objects = new Map(); + + for (let layer of this.mapFile.layers) { + if (layer.type === 'objectgroup') { + for (let object of layer.objects) { + let objectsOfType: ITiledMapObject[]|undefined; + if (!objects.has(object.type)) { + objectsOfType = new Array(); + } else { + objectsOfType = objects.get(object.type); + if (objectsOfType === undefined) { + throw new Error('Unexpected object type not found'); + } + } + objectsOfType.push(object); + objects.set(object.type, objectsOfType); + } + } + } + + for (let [itemType, objectsOfType] of objects) { + // FIXME: we would ideally need for the loader to WAIT for the import to be performed, which means writing our own loader plugin. + + let itemFactory: ItemFactoryInterface; + + switch (itemType) { + case 'computer': + let module = await import('../Items/Computer/computer'); + itemFactory = module.default as ItemFactoryInterface; + break; + default: + throw new Error('Unsupported object type: "'+ itemType +'"'); + } + + itemFactory.preload(this.load); + this.load.start(); // Let's manually start the loader because the import might be over AFTER the loading ends. + + this.load.on('complete', () => { + // FIXME: the factory might fail because the resources might not be loaded yet... + // We would need to add a loader ended event in addition to the createPromise + this.createPromise.then(() => { + itemFactory.create(this); + + for (let object of objectsOfType) { + // TODO: we should pass here a factory to create sprites (maybe?) + let actionableItem = itemFactory.factory(this, object); + this.actionableItems.push(actionableItem); + } + }); + }); + + // import(/* webpackIgnore: true */ scriptUrl).then(result => { + // + // result.default.preload(this.load); + // + // this.load.start(); // Let's manually start the loader because the import might be over AFTER the loading ends. + // this.load.on('complete', () => { + // // FIXME: the factory might fail because the resources might not be loaded yet... + // // We would need to add a loader ended event in addition to the createPromise + // this.createPromise.then(() => { + // result.default.create(this); + // + // for (let object of objectsOfType) { + // // TODO: we should pass here a factory to create sprites (maybe?) + // let objectSprite = result.default.factory(this, object); + // } + // }); + // }); + // }); + } + + // TEST: let's load a module dynamically! + /*let foo = "http://maps.workadventure.localhost/computer.js"; + import(/* webpackIgnore: true * / foo).then(result => { + console.log(result); + + });*/ } //hook initialisation @@ -361,6 +452,8 @@ export class GameScene extends Phaser.Scene { } }, 500); } + + this.createPromiseResolve(); } private getExitSceneUrl(layer: ITiledMapLayer): string|undefined { @@ -526,6 +619,7 @@ export class GameScene extends Phaser.Scene { //listen event to share position of user this.CurrentPlayer.on(hasMovedEventName, this.pushPlayerPosition.bind(this)) + this.CurrentPlayer.on(hasMovedEventName, this.outlineItem.bind(this)) }); } @@ -555,6 +649,49 @@ export class GameScene extends Phaser.Scene { // Otherwise, do nothing. } + /** + * Finds the correct item to outline and outline it (if there is an item to be outlined) + * @param event + */ + private outlineItem(event: HasMovedEvent): void { + let x = event.x; + let y = event.y; + switch (event.direction) { + case PlayerAnimationNames.WalkUp: + y -= 32; + break; + case PlayerAnimationNames.WalkDown: + y += 32; + break; + case PlayerAnimationNames.WalkLeft: + x -= 32; + break; + case PlayerAnimationNames.WalkRight: + x += 32; + break; + default: + throw new Error('Unexpected direction "' + event.direction + '"'); + } + + let shortestDistance: number = Infinity; + let selectedItem: ActionableItem|null = null; + for (let item of this.actionableItems) { + let distance = item.actionableDistance(x, y); + if (distance !== null && distance < shortestDistance) { + shortestDistance = distance; + selectedItem = item; + } + } + + if (this.outlinedItem === selectedItem) { + return; + } + + this.outlinedItem?.notSelectable(); + this.outlinedItem = selectedItem; + this.outlinedItem?.selectable(); + } + private doPushPlayerPosition(event: HasMovedEvent): void { this.lastMoveEventSent = event; this.lastSentTick = this.currentTick; diff --git a/front/src/Phaser/Items/ActionableItem.ts b/front/src/Phaser/Items/ActionableItem.ts index e69de29b..229b0888 100644 --- a/front/src/Phaser/Items/ActionableItem.ts +++ b/front/src/Phaser/Items/ActionableItem.ts @@ -0,0 +1,60 @@ +/** + * An actionable item represents an in-game object that can be activated using the space-bar. + * It has coordinates and an "activation radius" + */ +import Sprite = Phaser.GameObjects.Sprite; +import {OutlinePipeline} from "../Shaders/OutlinePipeline"; + +export class ActionableItem { + private readonly activationRadiusSquared : number; + private isSelectable: boolean = false; + + public constructor(private sprite: Sprite, private activationRadius: number) { + this.activationRadiusSquared = activationRadius * activationRadius; + } + + /** + * Returns the square of the distance to the object center IF we are in item action range + * OR null if we are out of range. + */ + public actionableDistance(x: number, y: number): number|null { + let distanceSquared = (x - this.sprite.x)*(x - this.sprite.x) + (y - this.sprite.y)*(y - this.sprite.y); + if (distanceSquared < this.activationRadiusSquared) { + return distanceSquared; + } else { + return null; + } + } + + /** + * Show the outline of the sprite. + */ + public selectable(): void { + if (this.isSelectable) { + return; + } + this.isSelectable = true; + this.sprite.setPipeline(OutlinePipeline.KEY); + this.sprite.pipeline.setFloat2('uTextureSize', + this.sprite.texture.getSourceImage().width, this.sprite.texture.getSourceImage().height); + } + + /** + * Hide the outline of the sprite + */ + public notSelectable(): void { + if (!this.isSelectable) { + return; + } + this.isSelectable = false; + this.sprite.resetPipeline(); + } + + /** + * Triggered when the "space" key is pressed and the object is in range of being activated. + */ + public activate(): void { + + } +} + diff --git a/front/src/Phaser/Items/ActionableSprite.ts b/front/src/Phaser/Items/ActionableSprite.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/front/src/Phaser/Items/Computer/computer.ts b/front/src/Phaser/Items/Computer/computer.ts new file mode 100644 index 00000000..99eddbb7 --- /dev/null +++ b/front/src/Phaser/Items/Computer/computer.ts @@ -0,0 +1,24 @@ +import * as Phaser from 'phaser'; +import {Scene} from "phaser"; +import Sprite = Phaser.GameObjects.Sprite; +import {ITiledMapObject} from "../../Map/ITiledMap"; +import {ItemFactoryInterface} from "../ItemFactoryInterface"; +import {GameScene} from "../../Game/GameScene"; +import {ActionableItem} from "../ActionableItem"; + +export default { + preload: (loader: Phaser.Loader.LoaderPlugin): void => { + loader.atlas('computer', 'http://maps.workadventure.localhost/computer/computer.png', 'http://maps.workadventure.localhost/computer/computer_atlas.json'); + }, + create: (scene: GameScene): void => { + + }, + factory: (scene: GameScene, object: ITiledMapObject): ActionableItem => { + // Idée: ESSAYER WebPack? https://paultavares.wordpress.com/2018/07/02/webpack-how-to-generate-an-es-module-bundle/ + let foo = new Sprite(scene, object.x, object.y, 'computer'); + scene.add.existing(foo); + + return new ActionableItem(foo, 32); + //scene.add.sprite(object.x, object.y, 'computer'); + } +} as ItemFactoryInterface; diff --git a/front/src/Phaser/Items/ItemFactoryInterface.ts b/front/src/Phaser/Items/ItemFactoryInterface.ts new file mode 100644 index 00000000..0f88f76b --- /dev/null +++ b/front/src/Phaser/Items/ItemFactoryInterface.ts @@ -0,0 +1,10 @@ +import LoaderPlugin = Phaser.Loader.LoaderPlugin; +import {GameScene} from "../Game/GameScene"; +import {ITiledMapObject} from "../Map/ITiledMap"; +import {ActionableItem} from "./ActionableItem"; + +export interface ItemFactoryInterface { + preload: (loader: LoaderPlugin) => void; + create: (scene: GameScene) => void; + factory: (scene: GameScene, object: ITiledMapObject) => ActionableItem; +} diff --git a/front/src/Phaser/Player/Player.ts b/front/src/Phaser/Player/Player.ts index 1a3a3a03..64adc246 100644 --- a/front/src/Phaser/Player/Player.ts +++ b/front/src/Phaser/Player/Player.ts @@ -33,10 +33,6 @@ export class Player extends Character implements CurrentGamerInterface { //the current player model should be push away by other players to prevent conflict this.setImmovable(false); - - this.setPipeline(OutlinePipeline.KEY); - this.pipeline.setFloat2('uTextureSize', - this.texture.getSourceImage().width, this.texture.getSourceImage().height); } moveUser(delta: number): void { diff --git a/front/src/Phaser/Shaders/OutlinePipeline.ts b/front/src/Phaser/Shaders/OutlinePipeline.ts index f65a66d2..6b416b8a 100644 --- a/front/src/Phaser/Shaders/OutlinePipeline.ts +++ b/front/src/Phaser/Shaders/OutlinePipeline.ts @@ -50,7 +50,7 @@ export class OutlinePipeline extends Phaser.Renderer.WebGL.Pipelines.TextureTint if (texture.a == 0.0 && max(max(upAlpha, downAlpha), max(leftAlpha, rightAlpha)) == 1.0) { - color = vec4(1.0, 1.0, 1.0, 1.0); + color = vec4(1.0, 1.0, 0.0, 1.0); } gl_FragColor = color; diff --git a/front/src/Phaser/UserInput/UserInputManager.ts b/front/src/Phaser/UserInput/UserInputManager.ts index eddbbf74..d40d149e 100644 --- a/front/src/Phaser/UserInput/UserInputManager.ts +++ b/front/src/Phaser/UserInput/UserInputManager.ts @@ -47,6 +47,7 @@ export class UserInputManager { {event: UserInputEvent.SpeedUp, keyInstance: Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SHIFT) }, {event: UserInputEvent.Interact, keyInstance: Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.E) }, + {event: UserInputEvent.Interact, keyInstance: Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE) }, {event: UserInputEvent.Shout, keyInstance: Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.F) }, ]; } diff --git a/front/tsconfig.json b/front/tsconfig.json index 1661efa2..9a140744 100644 --- a/front/tsconfig.json +++ b/front/tsconfig.json @@ -3,9 +3,8 @@ "outDir": "./dist/", "sourceMap": true, "moduleResolution": "node", - "noImplicitAny": true, - "module": "CommonJS", - "target": "es5", + "module": "ESNext", + "target": "ES2015", "downlevelIteration": true, "jsx": "react", "allowJs": true, diff --git a/front/webpack.config.js b/front/webpack.config.js index e162b4f8..68b1bc7e 100644 --- a/front/webpack.config.js +++ b/front/webpack.config.js @@ -33,6 +33,9 @@ module.exports = { path: path.resolve(__dirname, 'dist'), publicPath: '/' }, + externals:[ + require('webpack-require-http') + ], plugins: [ new HtmlWebpackPlugin( { diff --git a/front/yarn.lock b/front/yarn.lock index 05e9b368..b943ebf0 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -801,6 +801,11 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +charenc@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= + chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -1063,6 +1068,11 @@ cross-spawn@6.0.5, cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" +crypt@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= + crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -2350,7 +2360,7 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" -is-buffer@^1.1.5: +is-buffer@^1.1.5, is-buffer@~1.1.1: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -2691,6 +2701,15 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" +md5@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" + integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk= + dependencies: + charenc "~0.0.1" + crypt "~0.0.1" + is-buffer "~1.1.1" + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -4501,6 +4520,14 @@ webpack-merge@^4.2.2: dependencies: lodash "^4.17.15" +webpack-require-http@^0.4.3: + version "0.4.3" + resolved "https://registry.yarnpkg.com/webpack-require-http/-/webpack-require-http-0.4.3.tgz#5690d8cc57246a53a81f1ccffd20d0394d70261c" + integrity sha1-VpDYzFckalOoHxzP/SDQOU1wJhw= + dependencies: + md5 "^2.0.0" + url "^0.11.0" + webpack-sources@^1.4.0, webpack-sources@^1.4.1: version "1.4.3" resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.4.3.tgz#eedd8ec0b928fbf1cbfe994e22d2d890f330a933" diff --git a/maps/.gitignore b/maps/.gitignore new file mode 100644 index 00000000..2ccbe465 --- /dev/null +++ b/maps/.gitignore @@ -0,0 +1 @@ +/node_modules/ diff --git a/maps/dist/.htaccess b/maps/dist/.htaccess new file mode 100644 index 00000000..f2895509 --- /dev/null +++ b/maps/dist/.htaccess @@ -0,0 +1 @@ +Header set Access-Control-Allow-Origin "*" diff --git a/maps/objects/computer.ts b/maps/objects/computer.ts index e69de29b..145d19df 100644 --- a/maps/objects/computer.ts +++ b/maps/objects/computer.ts @@ -0,0 +1,56 @@ +import * as Phaser from 'phaser'; +import {Scene} from "phaser"; +import Sprite = Phaser.GameObjects.Sprite; + +interface ITiledMapObject { + id: number; + + /** + * Tile object id + */ + gid: number; + height: number; + name: string; + properties: {[key: string]: string}; + rotation: number; + type: string; + visible: boolean; + width: number; + x: number; + y: number; + + /** + * Whether or not object is an ellipse + */ + ellipse: boolean; + + /** + * Polygon points + */ + polygon: {x: number, y: number}[]; + + /** + * Polyline points + */ + polyline: {x: number, y: number}[]; +} + +class MySprite extends Sprite { + +} + + +export default { + preload: (loader: Phaser.Loader.LoaderPlugin) => { + loader.atlas('computer', 'http://maps.workadventure.localhost/computer/computer.png', 'http://maps.workadventure.localhost/computer/computer_atlas.json'); + }, + create: (scene: Scene) => { + + }, + factory: (scene: Scene, object: ITiledMapObject) => { + // Idée: ESSAYER WebPack? https://paultavares.wordpress.com/2018/07/02/webpack-how-to-generate-an-es-module-bundle/ + let foo = new MySprite(scene, object.x, object.y, 'computer'); + scene.add.existing(foo); + //scene.add.sprite(object.x, object.y, 'computer'); + } +}; diff --git a/maps/package.json b/maps/package.json index a20c876f..3623c205 100644 --- a/maps/package.json +++ b/maps/package.json @@ -1,13 +1,12 @@ { - "name": "workadventureback", + "name": "workadventuremaps", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "tsc": "tsc", - "dev": "ts-node-dev --respawn --transpileOnly ./server.ts", - "prod": "tsc && node ./dist/server.js", - "profile": "tsc && node --prof ./dist/server.js", + "dev": "tsc -w", + "prod": "tsc", "test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json", "lint": "node_modules/.bin/eslint src/ . --ext .ts", "fix": "node_modules/.bin/eslint --fix src/ . --ext .ts" @@ -36,22 +35,9 @@ }, "homepage": "https://github.com/thecodingmachine/workadventure#readme", "dependencies": { - "@types/express": "^4.17.4", - "@types/http-status-codes": "^1.2.0", - "@types/jsonwebtoken": "^8.3.8", - "@types/socket.io": "^2.1.4", - "@types/uuidv4": "^5.0.0", - "body-parser": "^1.19.0", - "express": "^4.17.1", - "generic-type-guard": "^3.2.0", - "http-status-codes": "^1.4.0", - "jsonwebtoken": "^8.5.1", - "prom-client": "^12.0.0", - "socket.io": "^2.3.0", - "systeminformation": "^4.26.5", + "phaser": "^3.24.1", "ts-node-dev": "^1.0.0-pre.44", - "typescript": "^3.8.3", - "uuidv4": "^6.0.7" + "typescript": "^3.8.3" }, "devDependencies": { "@types/jasmine": "^3.5.10",