Merge branch 'develop' into usersLimit

# Conflicts:
#	back/src/Controller/IoSocketController.ts
#	front/src/Connexion/RoomConnection.ts
#	front/src/Phaser/Game/GameScene.ts
This commit is contained in:
Gregoire Parant 2020-11-21 19:01:25 +01:00
commit 3c0bd9da6c
37 changed files with 1022 additions and 366 deletions

View file

@ -20,7 +20,7 @@ jobs:
# Create a slugified value of the branch # Create a slugified value of the branch
- uses: rlespinasse/github-slug-action@1.1.1 - uses: rlespinasse/github-slug-action@3.1.0
- name: "Build and push front image" - name: "Build and push front image"
uses: docker/build-push-action@v1 uses: docker/build-push-action@v1
@ -43,7 +43,7 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
# Create a slugified value of the branch # Create a slugified value of the branch
- uses: rlespinasse/github-slug-action@master - uses: rlespinasse/github-slug-action@3.1.0
- name: "Build and push back image" - name: "Build and push back image"
uses: docker/build-push-action@v1 uses: docker/build-push-action@v1
@ -66,7 +66,7 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
# Create a slugified value of the branch # Create a slugified value of the branch
- uses: rlespinasse/github-slug-action@master - uses: rlespinasse/github-slug-action@3.1.0
- name: "Build and push back image" - name: "Build and push back image"
uses: docker/build-push-action@v1 uses: docker/build-push-action@v1
@ -90,7 +90,7 @@ jobs:
# Create a slugified value of the branch # Create a slugified value of the branch
- uses: rlespinasse/github-slug-action@master - uses: rlespinasse/github-slug-action@3.1.0
- name: "Build and push front image" - name: "Build and push front image"
uses: docker/build-push-action@v1 uses: docker/build-push-action@v1
@ -114,7 +114,7 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
# Create a slugified value of the branch # Create a slugified value of the branch
- uses: rlespinasse/github-slug-action@1.1.0 - uses: rlespinasse/github-slug-action@3.1.0
- name: Deploy - name: Deploy
uses: thecodingmachine/deeployer@master uses: thecodingmachine/deeployer@master
@ -137,7 +137,7 @@ jobs:
check_for_duplicate_msg: true check_for_duplicate_msg: true
- name: Run Cypress tests - name: Run Cypress tests
uses: cypress-io/github-action@v1 uses: cypress-io/github-action@v2
if: ${{ env.GITHUB_REF_SLUG != 'master' }} if: ${{ env.GITHUB_REF_SLUG != 'master' }}
env: env:
CYPRESS_BASE_URL: https://play.${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com CYPRESS_BASE_URL: https://play.${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com
@ -148,7 +148,7 @@ jobs:
working-directory: e2e working-directory: e2e
- name: Run Cypress tests in prod - name: Run Cypress tests in prod
uses: cypress-io/github-action@v1 uses: cypress-io/github-action@v2
if: ${{ env.GITHUB_REF_SLUG == 'master' }} if: ${{ env.GITHUB_REF_SLUG == 'master' }}
env: env:
CYPRESS_BASE_URL: https://play.workadventu.re CYPRESS_BASE_URL: https://play.workadventu.re

View file

@ -20,9 +20,9 @@ import {parse} from "query-string";
import {jwtTokenManager} from "../Services/JWTTokenManager"; import {jwtTokenManager} from "../Services/JWTTokenManager";
import {adminApi, CharacterTexture, FetchMemberDataByUuidResponse} from "../Services/AdminApi"; import {adminApi, CharacterTexture, FetchMemberDataByUuidResponse} from "../Services/AdminApi";
import {SocketManager, socketManager} from "../Services/SocketManager"; import {SocketManager, socketManager} from "../Services/SocketManager";
import {emitInBatch, resetPing} from "../Services/IoSocketHelpers"; import {emitInBatch} from "../Services/IoSocketHelpers";
import {clientEventsEmitter} from "../Services/ClientEventsEmitter"; import {clientEventsEmitter} from "../Services/ClientEventsEmitter";
import {ADMIN_API_TOKEN, MAX_USERS_PER_ROOM} from "../Enum/EnvironmentVariable"; import {ADMIN_API_TOKEN, MAX_USERS_PER_ROOM, ADMIN_API_URL, SOCKET_IDLE_TIMER} from "../Enum/EnvironmentVariable";
export class IoSocketController { export class IoSocketController {
private nextUserId: number = 1; private nextUserId: number = 1;
@ -43,6 +43,7 @@ export class IoSocketController {
if (token !== ADMIN_API_TOKEN) { if (token !== ADMIN_API_TOKEN) {
console.log('Admin access refused for token: '+token) console.log('Admin access refused for token: '+token)
res.writeStatus("401 Unauthorized").end('Incorrect token'); res.writeStatus("401 Unauthorized").end('Incorrect token');
return;
} }
const roomId = query.roomId as string; const roomId = query.roomId as string;
@ -110,6 +111,7 @@ export class IoSocketController {
this.app.ws('/room', { this.app.ws('/room', {
/* Options */ /* Options */
//compression: uWS.SHARED_COMPRESSOR, //compression: uWS.SHARED_COMPRESSOR,
idleTimeout: SOCKET_IDLE_TIMER,
maxPayloadLength: 16 * 1024 * 1024, maxPayloadLength: 16 * 1024 * 1024,
maxBackpressure: 65536, // Maximum 64kB of data in the buffer. maxBackpressure: 65536, // Maximum 64kB of data in the buffer.
//idleTimeout: 10, //idleTimeout: 10,
@ -164,23 +166,24 @@ export class IoSocketController {
let memberTextures: CharacterTexture[] = []; let memberTextures: CharacterTexture[] = [];
const room = await socketManager.getOrCreateRoom(roomId); const room = await socketManager.getOrCreateRoom(roomId);
//TODO http return status if(room.isFull){
/*if (room.isFull) {
throw new Error('Room is full'); throw new Error('Room is full');
}*/ }
if (ADMIN_API_URL) {
try { try {
const userData = await adminApi.fetchMemberDataByUuid(userUuid); const userData = await adminApi.fetchMemberDataByUuid(userUuid);
//console.log('USERDATA', userData) //console.log('USERDATA', userData)
memberTags = userData.tags; memberTags = userData.tags;
memberTextures = userData.textures; memberTextures = userData.textures;
if (!room.anonymous && room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && !room.canAccess(memberTags)) { if (!room.anonymous && room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && !room.canAccess(memberTags)) {
throw new Error('No correct tags') throw new Error('No correct tags')
}
//console.log('access granted for user '+userUuid+' and room '+roomId);
} catch (e) {
console.log('access not granted for user '+userUuid+' and room '+roomId);
console.error(e);
throw new Error('Client cannot acces this ressource.')
} }
//console.log('access granted for user '+userUuid+' and room '+roomId);
} catch (e) {
console.log('access not granted for user '+userUuid+' and room '+roomId);
throw new Error('Client cannot acces this ressource.')
} }
// Generate characterLayers objects from characterLayers string[] // Generate characterLayers objects from characterLayers string[]
@ -236,21 +239,19 @@ export class IoSocketController {
}, },
/* Handlers */ /* Handlers */
open: (ws) => { open: (ws) => {
(async () => { // Let's join the room
// Let's join the room const client = this.initClient(ws); //todo: into the upgrade instead?
const client = this.initClient(ws); //todo: into the upgrade instead? const room = socketManager.getRoomById(client.roomId);
const room = socketManager.getRoomById(client.roomId); if (room && room.isFull) {
if (room && room.isFull) { socketManager.emitCloseMessage(client, 302);
socketManager.emitCloseMessage(client, 302); }else {
}else { socketManager.handleJoinRoom(client);
socketManager.handleJoinRoom(client); }
resetPing(client);
}
//get data information and shwo messages //get data information and show messages
try { if (ADMIN_API_URL) {
const res: FetchMemberDataByUuidResponse = await adminApi.fetchMemberDataByUuid(client.userUuid); adminApi.fetchMemberDataByUuid(client.userUuid).then((res: FetchMemberDataByUuidResponse) => {
if (!res.messages) { if (!res.messages) {
return; return;
} }
@ -262,10 +263,10 @@ export class IoSocketController {
message: messageToSend.message message: messageToSend.message
}) })
}); });
} catch (err) { }).catch((err) => {
console.error('fetchMemberDataByUuid => err', err); console.error('fetchMemberDataByUuid => err', err);
} });
})(); }
}, },
message: (ws, arrayBuffer, isBinary): void => { message: (ws, arrayBuffer, isBinary): void => {

View file

@ -3,7 +3,7 @@ import {BaseController} from "./BaseController";
import {parse} from "query-string"; import {parse} from "query-string";
import {adminApi} from "../Services/AdminApi"; import {adminApi} from "../Services/AdminApi";
//todo: delete this
export class MapController extends BaseController{ export class MapController extends BaseController{
constructor(private App : TemplatedApp) { constructor(private App : TemplatedApp) {
@ -34,18 +34,21 @@ export class MapController extends BaseController{
res.writeStatus("400 Bad request"); res.writeStatus("400 Bad request");
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end("Expected organizationSlug parameter"); res.end("Expected organizationSlug parameter");
return;
} }
if (typeof query.worldSlug !== 'string') { if (typeof query.worldSlug !== 'string') {
console.error('Expected worldSlug parameter'); console.error('Expected worldSlug parameter');
res.writeStatus("400 Bad request"); res.writeStatus("400 Bad request");
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end("Expected worldSlug parameter"); res.end("Expected worldSlug parameter");
return;
} }
if (typeof query.roomSlug !== 'string' && query.roomSlug !== undefined) { if (typeof query.roomSlug !== 'string' && query.roomSlug !== undefined) {
console.error('Expected only one roomSlug parameter'); console.error('Expected only one roomSlug parameter');
res.writeStatus("400 Bad request"); res.writeStatus("400 Bad request");
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end("Expected only one roomSlug parameter"); res.end("Expected only one roomSlug parameter");
return;
} }
(async () => { (async () => {

View file

@ -3,13 +3,14 @@ const URL_ROOM_STARTED = "/Floor0/floor0.json";
const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64; const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64;
const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48; const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48;
const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false; const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false;
const ADMIN_API_URL = process.env.ADMIN_API_URL || 'http://admin'; const ADMIN_API_URL = process.env.ADMIN_API_URL || '';
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken'; const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken';
const MAX_USERS_PER_ROOM = parseInt(process.env.MAX_USERS_PER_ROOM || '') || 600; const MAX_USERS_PER_ROOM = parseInt(process.env.MAX_USERS_PER_ROOM || '') || 600;
const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80; const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL;
const JITSI_ISS = process.env.JITSI_ISS || ''; const JITSI_ISS = process.env.JITSI_ISS || '';
const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || ''; const SECRET_JITSI_KEY = process.env.SECRET_JITSI_KEY || '';
export const SOCKET_IDLE_TIMER = parseInt(process.env.SOCKET_IDLE_TIMER as string) || 30; // maximum time (in second) without activity before a socket is closed
export { export {
SECRET_KEY, SECRET_KEY,

View file

@ -25,7 +25,6 @@ export interface ExSocketInterface extends WebSocket, Identificable {
emitInBatch: (payload: SubMessage) => void; emitInBatch: (payload: SubMessage) => void;
batchedMessages: BatchMessage; batchedMessages: BatchMessage;
batchTimeout: NodeJS.Timeout|null; batchTimeout: NodeJS.Timeout|null;
pingTimeout: NodeJS.Timeout|null;
disconnecting: boolean, disconnecting: boolean,
tags: string[], tags: string[],
textures: CharacterTexture[], textures: CharacterTexture[],

View file

@ -6,8 +6,13 @@ class GaugeManager {
private nbClientsPerRoomGauge: Gauge<string>; private nbClientsPerRoomGauge: Gauge<string>;
private nbGroupsPerRoomGauge: Gauge<string>; private nbGroupsPerRoomGauge: Gauge<string>;
private nbGroupsPerRoomCounter: Counter<string>; private nbGroupsPerRoomCounter: Counter<string>;
private nbRoomsGauge: Gauge<string>;
constructor() { constructor() {
this.nbRoomsGauge = new Gauge({
name: 'workadventure_nb_rooms',
help: 'Number of active rooms'
});
this.nbClientsGauge = new Gauge({ this.nbClientsGauge = new Gauge({
name: 'workadventure_nb_sockets', name: 'workadventure_nb_sockets',
help: 'Number of connected sockets', help: 'Number of connected sockets',
@ -31,6 +36,13 @@ class GaugeManager {
}); });
} }
incNbRoomGauge(): void {
this.nbRoomsGauge.inc();
}
decNbRoomGauge(): void {
this.nbRoomsGauge.dec();
}
incNbClientPerRoomGauge(roomId: string): void { incNbClientPerRoomGauge(roomId: string): void {
this.nbClientsGauge.inc(); this.nbClientsGauge.inc();
this.nbClientsPerRoomGauge.inc({ room: roomId }); this.nbClientsPerRoomGauge.inc({ room: roomId });

View file

@ -18,22 +18,6 @@ export function emitInBatch(socket: ExSocketInterface, payload: SubMessage): voi
socket.batchTimeout = null; socket.batchTimeout = null;
}, 100); }, 100);
} }
// If we send a message, we don't need to keep the connection alive
resetPing(socket);
}
export function resetPing(ws: ExSocketInterface): void {
if (ws.pingTimeout) {
clearTimeout(ws.pingTimeout);
}
ws.pingTimeout = setTimeout(() => {
if (ws.disconnecting) {
return;
}
ws.ping();
resetPing(ws);
}, 29000);
} }
export function emitError(Client: ExSocketInterface, message: string): void { export function emitError(Client: ExSocketInterface, message: string): void {
@ -47,4 +31,5 @@ export function emitError(Client: ExSocketInterface, message: string): void {
Client.send(serverToClientMessage.serializeBinary().buffer, true); Client.send(serverToClientMessage.serializeBinary().buffer, true);
} }
console.warn(message); console.warn(message);
} }

View file

@ -1,11 +1,11 @@
import {ALLOW_ARTILLERY, SECRET_KEY} from "../Enum/EnvironmentVariable"; import {ADMIN_API_URL, ALLOW_ARTILLERY, SECRET_KEY} from "../Enum/EnvironmentVariable";
import {uuid} from "uuidv4"; import {uuid} from "uuidv4";
import Jwt from "jsonwebtoken"; import Jwt from "jsonwebtoken";
import {TokenInterface} from "../Controller/AuthenticateController"; import {TokenInterface} from "../Controller/AuthenticateController";
import {adminApi, AdminApiData} from "../Services/AdminApi"; import {adminApi, AdminApiData} from "../Services/AdminApi";
class JWTTokenManager { class JWTTokenManager {
public createJWTToken(userUuid: string) { public createJWTToken(userUuid: string) {
return Jwt.sign({userUuid: userUuid}, SECRET_KEY, {expiresIn: '200d'}); //todo: add a mechanic to refresh or recreate token return Jwt.sign({userUuid: userUuid}, SECRET_KEY, {expiresIn: '200d'}); //todo: add a mechanic to refresh or recreate token
} }
@ -48,17 +48,21 @@ class JWTTokenManager {
return; return;
} }
//verify user in admin if (ADMIN_API_URL) {
adminApi.fetchCheckUserByToken(tokenInterface.userUuid).then(() => { //verify user in admin
resolve(tokenInterface.userUuid); adminApi.fetchCheckUserByToken(tokenInterface.userUuid).then(() => {
}).catch((err) => {
//anonymous user
if(err.response && err.response.status && err.response.status === 404){
resolve(tokenInterface.userUuid); resolve(tokenInterface.userUuid);
return; }).catch((err) => {
} //anonymous user
reject(new Error('Authentication error, invalid token structure. ' + err)); if(err.response && err.response.status && err.response.status === 404){
}); resolve(tokenInterface.userUuid);
return;
}
reject(err);
});
} else {
resolve(tokenInterface.userUuid);
}
}); });
}); });
} }
@ -66,7 +70,7 @@ class JWTTokenManager {
private isValidToken(token: object): token is TokenInterface { private isValidToken(token: object): token is TokenInterface {
return !(typeof((token as TokenInterface).userUuid) !== 'string'); return !(typeof((token as TokenInterface).userUuid) !== 'string');
} }
} }
export const jwtTokenManager = new JWTTokenManager(); export const jwtTokenManager = new JWTTokenManager();

View file

@ -353,6 +353,7 @@ export class SocketManager {
world.leave(Client); world.leave(Client);
if (world.isEmpty()) { if (world.isEmpty()) {
this.Worlds.delete(Client.roomId); this.Worlds.delete(Client.roomId);
gaugeManager.decNbRoomGauge();
} }
} }
//user leave previous room //user leave previous room
@ -385,6 +386,7 @@ export class SocketManager {
world.tags = data.tags world.tags = data.tags
world.policyType = Number(data.policy_type) world.policyType = Number(data.policy_type)
} }
gaugeManager.incNbRoomGauge();
this.Worlds.set(roomId, world); this.Worlds.set(roomId, world);
} }
return Promise.resolve(world) return Promise.resolve(world)

View file

@ -3,6 +3,7 @@
local namespace = env.GITHUB_REF_SLUG, local namespace = env.GITHUB_REF_SLUG,
local tag = namespace, local tag = namespace,
local url = if namespace == "master" then "workadventu.re" else namespace+".workadventure.test.thecodingmachine.com", local url = if namespace == "master" then "workadventu.re" else namespace+".workadventure.test.thecodingmachine.com",
local adminUrl = if namespace == "master" || namespace == "develop" || std.startsWith(namespace, "admin") then "https://admin."+url else null,
"$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json", "$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json",
"version": "1.0", "version": "1.0",
"containers": { "containers": {
@ -16,11 +17,12 @@
"env": { "env": {
"SECRET_KEY": "tempSecretKeyNeedsToChange", "SECRET_KEY": "tempSecretKeyNeedsToChange",
"ADMIN_API_TOKEN": env.ADMIN_API_TOKEN, "ADMIN_API_TOKEN": env.ADMIN_API_TOKEN,
"ADMIN_API_URL": "https://admin."+url,
"JITSI_ISS": env.JITSI_ISS, "JITSI_ISS": env.JITSI_ISS,
"JITSI_URL": env.JITSI_URL, "JITSI_URL": env.JITSI_URL,
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
} } + if adminUrl != null then {
"ADMIN_API_URL": adminUrl,
} else {}
}, },
"front": { "front": {
"image": "thecodingmachine/workadventure-front:"+tag, "image": "thecodingmachine/workadventure-front:"+tag,
@ -31,6 +33,7 @@
"ports": [80], "ports": [80],
"env": { "env": {
"API_URL": "api."+url, "API_URL": "api."+url,
"ADMIN_URL": "admin."+url,
"JITSI_URL": env.JITSI_URL, "JITSI_URL": env.JITSI_URL,
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY, "SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
"TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443", "TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443",

View file

@ -27,6 +27,7 @@ services:
HOST: "0.0.0.0" HOST: "0.0.0.0"
NODE_ENV: development NODE_ENV: development
API_URL: api.workadventure.localhost API_URL: api.workadventure.localhost
ADMIN_URL: admin.workadventure.localhost
STARTUP_COMMAND_1: yarn install STARTUP_COMMAND_1: yarn install
TURN_SERVER: "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443" TURN_SERVER: "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443"
TURN_USER: workadventure TURN_USER: workadventure

77
front/dist/resources/logos/boy.svg vendored Normal file
View file

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 188.149 188.149" style="enable-background:new 0 0 188.149 188.149;" xml:space="preserve">
<g>
<g>
<defs>
<circle id="SVGID_1_" cx="94.075" cy="94.075" r="94.074"/>
</defs>
<use xlink:href="#SVGID_1_" style="overflow:visible;fill:#4AC8EB;"/>
<clipPath id="SVGID_2_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<g style="clip-path:url(#SVGID_2_);">
<path style="fill:#A57561;" d="M148.572,197.629v0.01H39.507v-0.01c0-15.124,6.147-28.809,16.09-38.679
c0.463-0.463,0.926-0.905,1.408-1.347c1.43-1.326,2.931-2.57,4.493-3.732c1.028-0.771,2.098-1.491,3.177-2.189
c1.08-0.699,2.19-1.357,3.331-1.975c0.021-0.021,0.042-0.021,0.042-0.021c0.441-0.247,0.873-0.493,1.306-0.761
c1.295-0.761,2.519-1.624,3.69-2.56c4.966-3.948,8.812-9.254,11.001-15.34v-0.011c1.306-3.629,2.016-7.546,2.016-11.639
l16.121,0.072c0,4.04,0.688,7.927,1.964,11.525c2.169,6.117,5.994,11.433,10.96,15.401c0.339,0.277,0.688,0.545,1.038,0.802
c0.483,0.36,0.977,0.71,1.47,1.039c0.37,0.246,0.751,0.493,1.132,0.72c0.421,0.257,0.853,0.514,1.285,0.75
c0.041,0.011,0.071,0.021,0.103,0.041c0.03,0.02,0.062,0.041,0.093,0.061c1.265,0.699,2.498,1.439,3.69,2.221
c0.401,0.258,0.802,0.524,1.192,0.803c0.515,0.37,1.039,0.74,1.543,1.131h0.021C139.966,163.875,148.572,179.729,148.572,197.629
z"/>
<path style="fill:#EB6D4A;" d="M148.572,197.629H39.507c0-15.124,6.147-28.809,16.09-38.679c0.463-0.463,0.926-0.905,1.408-1.347
c1.43-1.326,2.931-2.581,4.493-3.742c1.028-0.762,2.098-1.491,3.177-2.18c1.08-0.699,2.19-1.357,3.331-1.975
c0.021-0.021,0.042-0.021,0.042-0.021c0.441-0.247,0.873-0.493,1.306-0.761c1.295-0.761,2.519-1.624,3.69-2.56
c5.347,5.469,12.79,8.852,21.046,8.852c8.226,0,15.669-3.393,21.016-8.842c0.339,0.277,0.688,0.545,1.038,0.802
c0.483,0.36,0.977,0.71,1.47,1.039c0.37,0.246,0.751,0.493,1.132,0.72c0.421,0.267,0.853,0.514,1.285,0.75
c0.041,0.011,0.071,0.021,0.103,0.041c0.03,0.011,0.062,0.031,0.093,0.052c1.265,0.699,2.498,1.439,3.69,2.23
c0.401,0.258,0.802,0.524,1.192,0.803c0.515,0.37,1.039,0.74,1.543,1.131h0.021C139.966,163.875,148.572,179.729,148.572,197.629
z"/>
<path style="fill:#A57561;" d="M52.183,46.81v34.117c0,28.977,25.437,52.466,41.857,52.466c16.421,0,41.858-23.489,41.858-52.466
V46.81H52.183z"/>
<path style="fill:#141720;" d="M52.183,76.823L52.183,76.823c2.063,0,3.734-1.671,3.734-3.733V49.356h-3.734V76.823z"/>
<path style="fill:#141720;" d="M135.899,76.823L135.899,76.823V49.356h-3.733V73.09
C132.165,75.152,133.836,76.823,135.899,76.823z"/>
<path style="fill:#141720;" d="M135.893,48.33c0,4.884-0.328,6.061-3.734,5.801c-0.137-2.367-17.141-4.296-38.111-4.296
c-20.985,0-37.989,1.929-38.126,4.296c-3.406,0.26-3.734-0.917-3.734-5.801c0-0.479,0.014-0.984,0.027-1.519
c0.52-12.052,7.318-34.582,41.833-34.582c34.5,0,41.299,22.53,41.818,34.582C135.879,47.346,135.893,47.852,135.893,48.33z"/>
</g>
<path style="clip-path:url(#SVGID_2_);fill:#FFFFFF;" d="M115.106,146.119c-3.517,10.826-10.601,21.539-21.036,30.299
c-10.436-8.76-17.509-19.473-21.025-30.299c0.052,0.02,0.113,0.041,0.165,0.061c5.531,4.955,12.852,7.979,20.86,7.979
c8.009,0,15.319-3.013,20.851-7.968C114.982,146.17,115.044,146.14,115.106,146.119z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -0,0 +1 @@
<svg height="682pt" viewBox="-21 -47 682.66669 682" width="682pt" xmlns="http://www.w3.org/2000/svg"><path d="m640 86.65625v283.972656c0 48.511719-39.472656 87.988282-87.988281 87.988282h-279.152344l-185.183594 128.863281v-128.863281c-48.375-.164063-87.675781-39.574219-87.675781-87.988282v-283.972656c0-48.515625 39.472656-87.988281 87.988281-87.988281h464.023438c48.515625 0 87.988281 39.472656 87.988281 87.988281zm0 0" fill="#ffdb2d"/><path d="m640 86.65625v283.972656c0 48.511719-39.472656 87.988282-87.988281 87.988282h-232.109375v-459.949219h232.109375c48.515625 0 87.988281 39.472656 87.988281 87.988281zm0 0" fill="#ffaa20"/><g fill="#fff"><path d="m171.296875 131.167969h297.40625v37.5h-297.40625zm0 0"/><path d="m171.296875 211.167969h297.40625v37.5h-297.40625zm0 0"/><path d="m171.296875 291.167969h297.40625v37.5h-297.40625zm0 0"/></g><path d="m319.902344 131.167969h148.800781v37.5h-148.800781zm0 0" fill="#e1e1e3"/><path d="m319.902344 211.167969h148.800781v37.5h-148.800781zm0 0" fill="#e1e1e3"/><path d="m319.902344 291.167969h148.800781v37.5h-148.800781zm0 0" fill="#e1e1e3"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,6 +1,14 @@
*{
font-family: 'Open Sans', sans-serif;
}
body{ body{
overflow: hidden; overflow: hidden;
} }
body button:focus,
body img:focus,
body input:focus {
outline: -webkit-focus-ring-color auto 0;
}
body .message-info{ body .message-info{
width: 20%; width: 20%;
height: auto; height: auto;
@ -72,14 +80,16 @@ body .message-info.warning{
#div-myCamVideo { #div-myCamVideo {
position: absolute; position: absolute;
right: 0; right: 15px;
bottom: 0; bottom: 15px;
border-radius: 15px 15px 15px 15px;
} }
video#myCamVideo{ video#myCamVideo{
width: 15vw; width: 15vw;
-webkit-transform: scaleX(-1); -webkit-transform: scaleX(-1);
transform: scaleX(-1); transform: scaleX(-1);
border-radius: 15px 15px 15px 15px;
/*width: 200px;*/ /*width: 200px;*/
/*height: 113px;*/ /*height: 113px;*/
} }
@ -372,6 +382,7 @@ body {
margin: 2%; margin: 2%;
transition: margin-left 0.2s, margin-right 0.2s, margin-bottom 0.2s, margin-top 0.2s, max-height 0.2s, max-width 0.2s; transition: margin-left 0.2s, margin-right 0.2s, margin-bottom 0.2s, margin-top 0.2s, max-height 0.2s, max-width 0.2s;
cursor: pointer; cursor: pointer;
border-radius: 15px 15px 15px 15px;
} }
.sidebar > div:hover { .sidebar > div:hover {
@ -384,6 +395,7 @@ body {
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
border-radius: 15px;
} }
.chat-mode { .chat-mode {
@ -435,7 +447,7 @@ body {
max-height: 80%; max-height: 80%;
top: -80%; top: -80%;
left: 10%; left: 10%;
background: #000000a6; background: #333333;
z-index: 200; z-index: 200;
transition: all 0.1s ease-out; transition: all 0.1s ease-out;
} }
@ -532,7 +544,7 @@ body {
border: 1px solid black; border: 1px solid black;
background-color: #00000000; background-color: #00000000;
color: #ffda01; color: #ffda01;
border-radius: 10px; border-radius: 15px;
padding: 10px 30px; padding: 10px 30px;
transition: all .2s ease; transition: all .2s ease;
} }
@ -627,6 +639,7 @@ div.modal-report-user{
left: calc(50% - 400px); left: calc(50% - 400px);
top: 100px; top: 100px;
background-color: #000000ad; background-color: #000000ad;
border-radius: 15px;
} }
.modal-report-user textarea{ .modal-report-user textarea{
@ -638,6 +651,7 @@ div.modal-report-user{
color: white; color: white;
width: calc(100% - 60px); width: calc(100% - 60px);
margin: 30px; margin: 30px;
border-radius: 15px;
} }
.modal-report-user img{ .modal-report-user img{
@ -669,7 +683,7 @@ div.modal-report-user{
border: 1px solid black; border: 1px solid black;
background-color: #00000000; background-color: #00000000;
color: #ffda01; color: #ffda01;
border-radius: 10px; border-radius: 15px;
padding: 10px 30px; padding: 10px 30px;
transition: all .2s ease; transition: all .2s ease;
} }
@ -701,3 +715,189 @@ div.modal-report-user{
max-width: calc(800px - 60px); /* size of modal - padding*/ max-width: calc(800px - 60px); /* size of modal - padding*/
} }
/*MESSAGE*/
.discussion{
position: fixed;
left: -300px;
top: 0px;
width: 220px;
height: 100%;
background-color: #333333;
padding: 20px;
transition: all 0.5s ease;
}
.discussion.active{
left: 0;
}
.discussion .active-btn{
display: none;
cursor: pointer;
height: 50px;
width: 50px;
background-color: #2d2d2dba;
position: absolute;
top: calc(50% - 25px);
margin-left: 315px;
border-radius: 50%;
border: none;
transition: all 0.5s ease;
}
.discussion .active-btn.active{
display: block;
}
.discussion .active-btn:hover {
transform: scale(1.1) rotateY(3.142rad);
}
.discussion .active-btn img{
width: 26px;
height: 26px;
margin: 13px 5px;
}
.discussion .close-btn{
position: absolute;
top: 0;
right: 10px;
background: none;
border: none;
cursor: pointer;
}
.discussion .close-btn img{
height: 15px;
right: 15px;
}
.discussion p{
color: white;
font-size: 22px;
padding-left: 10px;
margin: 0;
}
.discussion .participants{
height: 200px;
margin: 10px 0;
}
.discussion .participants .participant{
display: flex;
margin: 5px 10px;
background-color: #ffffff69;
padding: 5px;
border-radius: 15px;
cursor: pointer;
}
.discussion .participants .participant:hover{
background-color: #ffffff;
}
.discussion .participants .participant:hover p{
color: black;
}
.discussion .participants .participant:before {
content: '';
height: 10px;
width: 10px;
background-color: #1e7e34;
position: absolute;
margin-left: 18px;
border-radius: 50%;
margin-top: 18px;
}
.discussion .participants .participant img{
width: 26px;
height: 26px;
}
.discussion .participants .participant p{
font-size: 16px;
margin-left: 10px;
margin-top: 2px;
}
.discussion .participants .participant button.report-btn{
cursor: pointer;
position: absolute;
background-color: #2d2d2dba;
right: 34px;
margin: 0px;
padding: 6px 0px;
border-radius: 15px;
border: none;
color: white;
width: 0px;
overflow: hidden;
transition: all .5s ease;
}
.discussion .participants .participant:hover button.report-btn{
width: 70px;
}
.discussion .messages{
position: absolute;
height: calc(100% - 360px);
overflow-x: hidden;
overflow-y: auto;
max-width: calc(100% - 40px);
width: calc(100% - 40px);
}
.discussion .messages h2{
color: white;
}
.discussion .messages .message{
margin: 5px;
float: right;
text-align: right;
width: 100%;
}
.discussion .messages .message.me{
float: left;
text-align: left;
}
.discussion .messages .message p{
font-size: 12px;
}
.discussion .messages .message p.body{
font-size: 16px;
overflow: hidden;
white-space: pre-wrap;
word-wrap: break-word;
}
.discussion .send-message{
position: absolute;
bottom: 45px;
width: 220px;
height: 26px;
}
.discussion .send-message input{
position: absolute;
width: calc(100% - 10px);
height: 20px;
background-color: #171717;
color: white;
border-radius: 15px;
border: none;
padding: 6px;
}
.discussion .send-message img{
position: absolute;
margin-right: 10px;
width: 20px;
height: 20px;
background-color: #ffffff69;
}
.discussion .send-message img:hover{
background-color: #ffffff;
}

View file

@ -27,7 +27,9 @@ import {
SendJitsiJwtMessage, SendJitsiJwtMessage,
CharacterLayerMessage, CharacterLayerMessage,
SendUserMessage, SendUserMessage,
CloseMessage CloseMessage,
PingMessage,
SendUserMessage
} from "../Messages/generated/messages_pb" } from "../Messages/generated/messages_pb"
import {UserSimplePeerInterface} from "../WebRtc/SimplePeer"; import {UserSimplePeerInterface} from "../WebRtc/SimplePeer";
@ -43,6 +45,8 @@ import {
} from "./ConnexionModels"; } from "./ConnexionModels";
import {BodyResourceDescriptionInterface} from "../Phaser/Entity/body_character"; import {BodyResourceDescriptionInterface} from "../Phaser/Entity/body_character";
const manualPingDelay = 20000;
export class RoomConnection implements RoomConnection { export class RoomConnection implements RoomConnection {
private readonly socket: WebSocket; private readonly socket: WebSocket;
private userId: number|null = null; private userId: number|null = null;
@ -85,7 +89,9 @@ export class RoomConnection implements RoomConnection {
this.socket.binaryType = 'arraybuffer'; this.socket.binaryType = 'arraybuffer';
this.socket.onopen = (ev) => { this.socket.onopen = (ev) => {
//console.log('WS connected'); //we manually ping every 20s to not be logged out by the server, even when the game is in background.
const pingMessage = new PingMessage();
setInterval(() => this.socket.send(pingMessage.serializeBinary().buffer), manualPingDelay);
}; };
this.socket.onmessage = (messageEvent) => { this.socket.onmessage = (messageEvent) => {

View file

@ -1,12 +1,12 @@
const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true"; const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true";
const API_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.API_URL || "api.workadventure.localhost"); const API_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.API_URL || "api.workadventure.localhost");
const ADMIN_URL = API_URL.replace('api', 'admin'); const ADMIN_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.ADMIN_URL || "admin.workadventure.localhost");
const TURN_SERVER: string = process.env.TURN_SERVER || "turn:numb.viagenie.ca"; const TURN_SERVER: string = process.env.TURN_SERVER || "turn:numb.viagenie.ca";
const TURN_USER: string = process.env.TURN_USER || 'g.parant@thecodingmachine.com'; const TURN_USER: string = process.env.TURN_USER || 'g.parant@thecodingmachine.com';
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || 'itcugcOHxle9Acqi$'; const TURN_PASSWORD: string = process.env.TURN_PASSWORD || 'itcugcOHxle9Acqi$';
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL; const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL;
const JITSI_PRIVATE_MODE : boolean = process.env.JITSI_PRIVATE_MODE == "true"; const JITSI_PRIVATE_MODE : boolean = process.env.JITSI_PRIVATE_MODE == "true";
const RESOLUTION = 3; const RESOLUTION = 2;
const ZOOM_LEVEL = 1/*3/4*/; const ZOOM_LEVEL = 1/*3/4*/;
const POSITION_DELAY = 200; // Wait 200ms between sending position events const POSITION_DELAY = 200; // Wait 200ms between sending position events
const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player

View file

@ -350,51 +350,17 @@ export class GameScene extends ResizableScene implements CenterListener {
throw new Error('Your map MUST contain a layer of type "objectgroup" whose name is "floorLayer" that represents the layer characters are drawn at.'); throw new Error('Your map MUST contain a layer of type "objectgroup" whose name is "floorLayer" that represents the layer characters are drawn at.');
} }
// If there is an init position passed this.initStartXAndStartY();
if (this.initPosition !== null) {
this.startX = this.initPosition.x;
this.startY = this.initPosition.y;
} else {
// Now, let's find the start layer
if (this.room.hash) {
for (const layer of this.mapFile.layers) {
if (this.room.hash === layer.name && layer.type === 'tilelayer' && this.isStartLayer(layer)) {
const startPosition = this.startUser(layer);
this.startX = startPosition.x;
this.startY = startPosition.y;
}
}
}
if (this.startX === undefined) {
// If we have no start layer specified or if the hash passed does not exist, let's go with the default start position.
for (const layer of this.mapFile.layers) {
if (layer.type === 'tilelayer' && layer.name === "start") {
const startPosition = this.startUser(layer);
this.startX = startPosition.x;
this.startY = startPosition.y;
}
}
}
}
// Still no start position? Something is wrong with the map, we need a "start" layer.
if (this.startX === undefined) {
console.warn('This map is missing a layer named "start" that contains the available default start positions.');
// Let's start in the middle of the map
this.startX = this.mapFile.width * 16;
this.startY = this.mapFile.height * 16;
}
//add entities //add entities
this.Objects = new Array<Phaser.Physics.Arcade.Sprite>(); this.Objects = new Array<Phaser.Physics.Arcade.Sprite>();
//init event click
this.EventToClickOnTile();
//initialise list of other player //initialise list of other player
this.MapPlayers = this.physics.add.group({immovable: true}); this.MapPlayers = this.physics.add.group({immovable: true});
//create input to move //create input to move
this.userInputManager = new UserInputManager(this); this.userInputManager = new UserInputManager(this);
mediaManager.setUserInputManager(this.userInputManager);
//notify game manager can to create currentUser in map //notify game manager can to create currentUser in map
this.createCurrentPlayer(); this.createCurrentPlayer();
@ -474,35 +440,7 @@ export class GameScene extends ResizableScene implements CenterListener {
// From now, this game scene will be notified of reposition events // From now, this game scene will be notified of reposition events
layoutManager.setListener(this); layoutManager.setListener(this);
this.triggerOnMapLayerPropertyChange();
this.gameMap.onPropertyChange('openWebsite', (newValue, oldValue) => {
if (newValue === undefined) {
coWebsiteManager.closeCoWebsite();
} else {
coWebsiteManager.loadCoWebsite(newValue as string);
}
});
this.gameMap.onPropertyChange('jitsiRoom', (newValue, oldValue, allProps) => {
if (newValue === undefined) {
this.stopJitsi();
} else {
if (JITSI_PRIVATE_MODE) {
const adminTag = allProps.get("jitsiRoomAdminTag") as string|undefined;
this.connection.emitQueryJitsiJwtMessage(this.instance.replace('/', '-') + "-" + newValue, adminTag);
} else {
this.startJitsi(newValue as string);
}
}
})
this.gameMap.onPropertyChange('silent', (newValue, oldValue) => {
if (newValue === undefined || newValue === false || newValue === '') {
this.connection.setSilent(false);
} else {
this.connection.setSilent(true);
}
});
const camera = this.cameras.main; const camera = this.cameras.main;
@ -650,7 +588,7 @@ export class GameScene extends ResizableScene implements CenterListener {
}); });
// When connection is performed, let's connect SimplePeer // When connection is performed, let's connect SimplePeer
this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic); this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.GameManager.getPlayerName());
this.GlobalMessageManager = new GlobalMessageManager(this.connection); this.GlobalMessageManager = new GlobalMessageManager(this.connection);
this.UserMessageManager = new UserMessageManager(this.connection); this.UserMessageManager = new UserMessageManager(this.connection);
@ -675,10 +613,12 @@ export class GameScene extends ResizableScene implements CenterListener {
this.gameMap.setPosition(event.x, event.y); this.gameMap.setPosition(event.x, event.y);
}) })
this.scene.wake(); this.scene.wake();
this.scene.sleep(ReconnectingSceneName); this.scene.sleep(ReconnectingSceneName);
//init user position and play trigger to check layers properties
this.gameMap.setPosition(this.CurrentPlayer.x, this.CurrentPlayer.y);
return connection; return connection;
}).catch(error => { }).catch(error => {
if (error === connexionErrorTypes.tooManyUsers) { if (error === connexionErrorTypes.tooManyUsers) {
@ -688,7 +628,41 @@ export class GameScene extends ResizableScene implements CenterListener {
}); });
} }
private triggerOnMapLayerPropertyChange(){
this.gameMap.onPropertyChange('openWebsite', (newValue, oldValue) => {
if (newValue === undefined) {
coWebsiteManager.closeCoWebsite();
} else {
coWebsiteManager.loadCoWebsite(newValue as string);
}
});
this.gameMap.onPropertyChange('jitsiRoom', (newValue, oldValue, allProps) => {
if (newValue === undefined) {
this.stopJitsi();
} else {
if (JITSI_PRIVATE_MODE) {
const adminTag = allProps.get("jitsiRoomAdminTag") as string|undefined;
this.connection.emitQueryJitsiJwtMessage(this.instance.replace('/', '-') + "-" + newValue, adminTag);
} else {
this.startJitsi(newValue as string);
}
}
})
this.gameMap.onPropertyChange('silent', (newValue, oldValue) => {
if (newValue === undefined || newValue === false || newValue === '') {
this.connection.setSilent(false);
} else {
this.connection.setSilent(true);
}
});
}
private switchLayoutMode(): void { private switchLayoutMode(): void {
//if discussion is activated, this layout cannot be activated
if(mediaManager.activatedDiscussion){
return;
}
const mode = layoutManager.getLayoutMode(); const mode = layoutManager.getLayoutMode();
if (mode === LayoutMode.Presentation) { if (mode === LayoutMode.Presentation) {
layoutManager.switchLayoutMode(LayoutMode.VideoChat); layoutManager.switchLayoutMode(LayoutMode.VideoChat);
@ -701,6 +675,40 @@ export class GameScene extends ResizableScene implements CenterListener {
} }
} }
private initStartXAndStartY() {
// If there is an init position passed
if (this.initPosition !== null) {
this.startX = this.initPosition.x;
this.startY = this.initPosition.y;
} else {
// Now, let's find the start layer
if (this.room.hash) {
this.initPositionFromLayerName(this.room.hash);
}
if (this.startX === undefined) {
// If we have no start layer specified or if the hash passed does not exist, let's go with the default start position.
this.initPositionFromLayerName("start");
}
}
// Still no start position? Something is wrong with the map, we need a "start" layer.
if (this.startX === undefined) {
console.warn('This map is missing a layer named "start" that contains the available default start positions.');
// Let's start in the middle of the map
this.startX = this.mapFile.width * 16;
this.startY = this.mapFile.height * 16;
}
}
private initPositionFromLayerName(layerName: string) {
for (const layer of this.mapFile.layers) {
if (layerName === layer.name && layer.type === 'tilelayer' && (layerName === "start" || this.isStartLayer(layer))) {
const startPosition = this.startUser(layer);
this.startX = startPosition.x;
this.startY = startPosition.y;
}
}
}
private getExitUrl(layer: ITiledMapLayer): string|undefined { private getExitUrl(layer: ITiledMapLayer): string|undefined {
return this.getProperty(layer, "exitUrl") as string|undefined; return this.getProperty(layer, "exitUrl") as string|undefined;
} }
@ -739,9 +747,6 @@ export class GameScene extends ResizableScene implements CenterListener {
instance = this.instance; instance = this.instance;
} }
//console.log('existSceneUrl', exitSceneUrl);
//console.log('existSceneInstance', instance);
const absoluteExitSceneUrl = new URL(exitSceneUrl, this.MapUrlFile).href; const absoluteExitSceneUrl = new URL(exitSceneUrl, this.MapUrlFile).href;
const absoluteExitSceneUrlWithoutProtocol = absoluteExitSceneUrl.toString().substr(absoluteExitSceneUrl.toString().indexOf('://')+3); const absoluteExitSceneUrlWithoutProtocol = absoluteExitSceneUrl.toString().substr(absoluteExitSceneUrl.toString().indexOf('://')+3);
const roomId = '_/'+instance+'/'+absoluteExitSceneUrlWithoutProtocol; const roomId = '_/'+instance+'/'+absoluteExitSceneUrlWithoutProtocol;
@ -759,13 +764,9 @@ export class GameScene extends ResizableScene implements CenterListener {
this.loadNextGame(layer, mapWidth, fullPath); this.loadNextGame(layer, mapWidth, fullPath);
} }
/**
*
* @param layer
* @param mapWidth
*/
//todo: push that into the gameManager //todo: push that into the gameManager
private loadNextGame(layer: ITiledMapLayer, mapWidth: number, roomId: string){ private loadNextGame(layer: ITiledMapLayer, mapWidth: number, roomId: string){
const room = new Room(roomId); const room = new Room(roomId);
gameManager.loadMap(room, this.scene); gameManager.loadMap(room, this.scene);
const exitSceneKey = roomId; const exitSceneKey = roomId;
@ -798,9 +799,6 @@ export class GameScene extends ResizableScene implements CenterListener {
} }
} }
/**
* @param layer
*/
private startUser(layer: ITiledMapLayer): PositionInterface { private startUser(layer: ITiledMapLayer): PositionInterface {
const tiles = layer.data; const tiles = layer.data;
if (typeof(tiles) === 'string') { if (typeof(tiles) === 'string') {
@ -956,17 +954,6 @@ export class GameScene extends ResizableScene implements CenterListener {
}); });
} }
EventToClickOnTile(){
// debug code to get a tile properties by clicking on it
/*this.input.on("pointerdown", (pointer: Phaser.Input.Pointer)=>{
//pixel position toz tile position
const tile = this.Map.getTileAt(this.Map.worldToTileX(pointer.worldX), this.Map.worldToTileY(pointer.worldY));
if(tile){
this.CurrentPlayer.say("Your touch " + tile.layer.name);
}
});*/
}
/** /**
* @param time * @param time
* @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate. * @param delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate.
@ -1011,21 +998,22 @@ export class GameScene extends ResizableScene implements CenterListener {
}); });
const nextSceneKey = this.checkToExit(); const nextSceneKey = this.checkToExit();
if (nextSceneKey) { if (!nextSceneKey) return;
this.goToNextScene(nextSceneKey.key); if (nextSceneKey.key !== this.scene.key) {
// We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map.
this.connection.closeConnection();
this.simplePeer.unregister();
this.scene.stop();
this.scene.remove(this.scene.key);
this.scene.start(nextSceneKey.key);
} else {
//if the exit points to the current map, we simply teleport the user back to the startLayer
this.initPositionFromLayerName(this.room.hash || 'start');
this.CurrentPlayer.x = this.startX;
this.CurrentPlayer.y = this.startY;
} }
} }
private goToNextScene(nextSceneKey: string): void {
// We are completely destroying the current scene to avoid using a half-backed instance when coming back to the same map.
this.connection.closeConnection();
this.simplePeer.unregister();
this.scene.stop();
this.scene.remove(this.scene.key);
this.scene.start(nextSceneKey);
}
private checkToExit(): {key: string, hash: string} | null { private checkToExit(): {key: string, hash: string} | null {
const x = Math.floor(this.CurrentPlayer.x / 32); const x = Math.floor(this.CurrentPlayer.x / 32);
const y = Math.floor(this.CurrentPlayer.y / 32); const y = Math.floor(this.CurrentPlayer.y / 32);
@ -1124,11 +1112,6 @@ export class GameScene extends ResizableScene implements CenterListener {
await Promise.all(loadPromises); await Promise.all(loadPromises);
player.addTextures(characterLayerList, 1); player.addTextures(characterLayerList, 1);
//init collision
/*this.physics.add.collider(this.CurrentPlayer, player, (CurrentPlayer: CurrentGamerInterface, MapPlayer: GamerInterface) => {
CurrentPlayer.say("Hello, how are you ? ");
});*/
} }
/** /**
@ -1171,7 +1154,6 @@ export class GameScene extends ResizableScene implements CenterListener {
// We do not update the player position directly (because it is sent only every 200ms). // We do not update the player position directly (because it is sent only every 200ms).
// Instead we use the PlayersPositionInterpolator that will do a smooth animation over the next 200ms. // Instead we use the PlayersPositionInterpolator that will do a smooth animation over the next 200ms.
const playerMovement = new PlayerMovement({ x: player.x, y: player.y }, this.currentTick, message.position, this.currentTick + POSITION_DELAY); const playerMovement = new PlayerMovement({ x: player.x, y: player.y }, this.currentTick, message.position, this.currentTick + POSITION_DELAY);
//console.log('Target position: ', player.x, player.y);
this.playersPositionInterpolator.updatePlayerPosition(player.userId, playerMovement); this.playersPositionInterpolator.updatePlayerPosition(player.userId, playerMovement);
} }
@ -1220,11 +1202,6 @@ export class GameScene extends ResizableScene implements CenterListener {
/** /**
* Sends to the server an event emitted by one of the ActionableItems. * Sends to the server an event emitted by one of the ActionableItems.
*
* @param itemId
* @param eventName
* @param state
* @param parameters
*/ */
emitActionableEvent(itemId: number, eventName: string, state: unknown, parameters: unknown) { emitActionableEvent(itemId: number, eventName: string, state: unknown, parameters: unknown) {
this.connection.emitActionableEvent(itemId, eventName, state, parameters); this.connection.emitActionableEvent(itemId, eventName, state, parameters);

View file

@ -7,6 +7,7 @@ import {mediaManager} from "../../WebRtc/MediaManager";
import {RESOLUTION} from "../../Enum/EnvironmentVariable"; import {RESOLUTION} from "../../Enum/EnvironmentVariable";
import {SoundMeter} from "../Components/SoundMeter"; import {SoundMeter} from "../Components/SoundMeter";
import {SoundMeterSprite} from "../Components/SoundMeterSprite"; import {SoundMeterSprite} from "../Components/SoundMeterSprite";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
export const EnableCameraSceneName = "EnableCameraScene"; export const EnableCameraSceneName = "EnableCameraScene";
enum LoginTextures { enum LoginTextures {
@ -93,7 +94,7 @@ export class EnableCameraScene extends Phaser.Scene {
this.login(); this.login();
}); });
this.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').classList.add('active'); HtmlUtils.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').classList.add('active');
const mediaPromise = mediaManager.getCamera(); const mediaPromise = mediaManager.getCamera();
mediaPromise.then(this.getDevices.bind(this)); mediaPromise.then(this.getDevices.bind(this));
@ -150,10 +151,10 @@ export class EnableCameraScene extends Phaser.Scene {
* Function called each time a camera is changed * Function called each time a camera is changed
*/ */
private setupStream(stream: MediaStream): void { private setupStream(stream: MediaStream): void {
const img = this.getElementByIdOrFail<HTMLDivElement>('webRtcSetupNoVideo'); const img = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('webRtcSetupNoVideo');
img.style.display = 'none'; img.style.display = 'none';
const div = this.getElementByIdOrFail<HTMLVideoElement>('myCamVideoSetup'); const div = HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('myCamVideoSetup');
div.srcObject = stream; div.srcObject = stream;
this.soundMeter.connectToSource(stream, new window.AudioContext()); this.soundMeter.connectToSource(stream, new window.AudioContext());
@ -164,7 +165,7 @@ export class EnableCameraScene extends Phaser.Scene {
private updateWebCamName(): void { private updateWebCamName(): void {
if (this.camerasList.length > 1) { if (this.camerasList.length > 1) {
const div = this.getElementByIdOrFail<HTMLVideoElement>('myCamVideoSetup'); const div = HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('myCamVideoSetup');
let label = this.camerasList[this.cameraSelected].label; let label = this.camerasList[this.cameraSelected].label;
// remove text in parenthesis // remove text in parenthesis
@ -211,10 +212,10 @@ export class EnableCameraScene extends Phaser.Scene {
} }
private reposition(): void { private reposition(): void {
let div = this.getElementByIdOrFail<HTMLVideoElement>('myCamVideoSetup'); let div = HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('myCamVideoSetup');
let bounds = div.getBoundingClientRect(); let bounds = div.getBoundingClientRect();
if (!div.srcObject) { if (!div.srcObject) {
div = this.getElementByIdOrFail<HTMLVideoElement>('webRtcSetup'); div = HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('webRtcSetup');
bounds = div.getBoundingClientRect(); bounds = div.getBoundingClientRect();
} }
@ -255,7 +256,7 @@ export class EnableCameraScene extends Phaser.Scene {
} }
private login(): void { private login(): void {
this.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').style.display = 'none'; HtmlUtils.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').style.display = 'none';
this.soundMeter.stop(); this.soundMeter.stop();
window.removeEventListener('resize', this.repositionCallback); window.removeEventListener('resize', this.repositionCallback);
@ -276,13 +277,4 @@ export class EnableCameraScene extends Phaser.Scene {
} }
this.updateWebCamName(); this.updateWebCamName();
} }
private getElementByIdOrFail<T extends HTMLElement>(id: string): T {
const elem = document.getElementById(id);
if (elem === null) {
throw new Error("Cannot find HTML element with id '"+id+"'");
}
// FIXME: does not check the type of the returned type
return elem as T;
}
} }

View file

@ -40,7 +40,9 @@ export class UserInputManager {
initKeyBoardEvent(){ initKeyBoardEvent(){
this.KeysCode = [ this.KeysCode = [
{event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Z, false) }, {event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Z, false) },
{event: UserInputEvent.MoveUp, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.W, false) },
{event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q, false) }, {event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.Q, false) },
{event: UserInputEvent.MoveLeft, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A, false) },
{event: UserInputEvent.MoveDown, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S, false) }, {event: UserInputEvent.MoveDown, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.S, false) },
{event: UserInputEvent.MoveRight, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D, false) }, {event: UserInputEvent.MoveRight, keyInstance: this.Scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.D, false) },

View file

@ -16,31 +16,35 @@ class CoWebsiteManager {
private opened: iframeStates = iframeStates.closed; private opened: iframeStates = iframeStates.closed;
private observers = new Array<CoWebsiteStateChangedCallback>(); private observers = new Array<CoWebsiteStateChangedCallback>();
/**
* Quickly going in and out of an iframe trigger can create conflicts between the iframe states.
* So we use this promise to queue up every cowebsite state transition
*/
private currentOperationPromise: Promise<void> = Promise.resolve();
private cowebsiteDiv: HTMLDivElement;
private close(): HTMLDivElement { constructor() {
const cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteDivId); this.cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteDivId);
cowebsiteDiv.classList.remove('loaded'); //edit the css class to trigger the transition }
cowebsiteDiv.classList.add('hidden');
private close(): void {
this.cowebsiteDiv.classList.remove('loaded'); //edit the css class to trigger the transition
this.cowebsiteDiv.classList.add('hidden');
this.opened = iframeStates.closed; this.opened = iframeStates.closed;
return cowebsiteDiv;
} }
private load(): HTMLDivElement { private load(): void {
const cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteDivId); this.cowebsiteDiv.classList.remove('hidden'); //edit the css class to trigger the transition
cowebsiteDiv.classList.remove('hidden'); //edit the css class to trigger the transition this.cowebsiteDiv.classList.add('loading');
cowebsiteDiv.classList.add('loading');
this.opened = iframeStates.loading; this.opened = iframeStates.loading;
return cowebsiteDiv;
} }
private open(): HTMLDivElement { private open(): void {
const cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteDivId); this.cowebsiteDiv.classList.remove('loading', 'hidden'); //edit the css class to trigger the transition
cowebsiteDiv.classList.remove('loading', 'hidden'); //edit the css class to trigger the transition
this.opened = iframeStates.opened; this.opened = iframeStates.opened;
return cowebsiteDiv;
} }
public loadCoWebsite(url: string): void { public loadCoWebsite(url: string): void {
const cowebsiteDiv = this.load(); this.load();
cowebsiteDiv.innerHTML = ''; this.cowebsiteDiv.innerHTML = '';
const iframe = document.createElement('iframe'); const iframe = document.createElement('iframe');
iframe.id = 'cowebsite-iframe'; iframe.id = 'cowebsite-iframe';
@ -48,40 +52,42 @@ class CoWebsiteManager {
const onloadPromise = new Promise((resolve) => { const onloadPromise = new Promise((resolve) => {
iframe.onload = () => resolve(); iframe.onload = () => resolve();
}); });
cowebsiteDiv.appendChild(iframe); this.cowebsiteDiv.appendChild(iframe);
const onTimeoutPromise = new Promise((resolve) => { const onTimeoutPromise = new Promise((resolve) => {
setTimeout(() => resolve(), 2000); setTimeout(() => resolve(), 2000);
}); });
Promise.race([onloadPromise, onTimeoutPromise]).then(() => { this.currentOperationPromise = this.currentOperationPromise.then(() =>Promise.race([onloadPromise, onTimeoutPromise])).then(() => {
this.open(); this.open();
setTimeout(() => { setTimeout(() => {
this.fire(); this.fire();
}, animationTime) }, animationTime)
}); }).catch(() => this.closeCoWebsite());
} }
/** /**
* Just like loadCoWebsite but the div can be filled by the user. * Just like loadCoWebsite but the div can be filled by the user.
*/ */
public insertCoWebsite(callback: (cowebsite: HTMLDivElement) => Promise<void>): void { public insertCoWebsite(callback: (cowebsite: HTMLDivElement) => Promise<void>): void {
const cowebsiteDiv = this.load(); this.load();
callback(cowebsiteDiv).then(() => { this.currentOperationPromise = this.currentOperationPromise.then(() => callback(this.cowebsiteDiv)).then(() => {
this.open(); this.open();
setTimeout(() => { setTimeout(() => {
this.fire(); this.fire();
}, animationTime) }, animationTime);
}); }).catch(() => this.closeCoWebsite());
} }
public closeCoWebsite(): Promise<void> { public closeCoWebsite(): Promise<void> {
return new Promise((resolve, reject) => { this.currentOperationPromise = this.currentOperationPromise.then(() => new Promise((resolve, reject) => {
const cowebsiteDiv = this.close(); if(this.opened === iframeStates.closed) resolve(); //this method may be called twice, in case of iframe error for example
this.close();
this.fire(); this.fire();
setTimeout(() => { setTimeout(() => {
this.cowebsiteDiv.innerHTML = '';
resolve(); resolve();
setTimeout(() => cowebsiteDiv.innerHTML = '', 500)
}, animationTime) }, animationTime)
}); }));
return this.currentOperationPromise;
} }
public getGameSize(): {width: number, height: number} { public getGameSize(): {width: number, height: number} {
@ -104,6 +110,7 @@ class CoWebsiteManager {
} }
} }
//todo: is it still useful to allow any kind of observers?
public onStateChange(observer: CoWebsiteStateChangedCallback) { public onStateChange(observer: CoWebsiteStateChangedCallback) {
this.observers.push(observer); this.observers.push(observer);
} }

View file

@ -0,0 +1,231 @@
import {HtmlUtils} from "./HtmlUtils";
import {MediaManager, ReportCallback, UpdatedLocalStreamCallback} from "./MediaManager";
import {UserInputManager} from "../Phaser/UserInput/UserInputManager";
export type SendMessageCallback = (message:string) => void;
export class DiscussionManager {
private mainContainer: HTMLDivElement;
private divDiscuss?: HTMLDivElement;
private divParticipants?: HTMLDivElement;
private nbpParticipants?: HTMLParagraphElement;
private divMessages?: HTMLParagraphElement;
private buttonActiveDiscussion?: HTMLButtonElement;
private participants: Map<number|string, HTMLDivElement> = new Map<number|string, HTMLDivElement>();
private activeDiscussion: boolean = false;
private sendMessageCallBack : Map<number|string, SendMessageCallback> = new Map<number|string, SendMessageCallback>();
private userInputManager?: UserInputManager;
constructor(private mediaManager: MediaManager, name: string) {
this.mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
this.createDiscussPart(name);
}
private createDiscussPart(name: string) {
this.divDiscuss = document.createElement('div');
this.divDiscuss.classList.add('discussion');
const buttonCloseDiscussion: HTMLButtonElement = document.createElement('button');
this.buttonActiveDiscussion = document.createElement('button');
buttonCloseDiscussion.classList.add('close-btn');
buttonCloseDiscussion.innerHTML = `<img src="resources/logos/close.svg"/>`;
buttonCloseDiscussion.addEventListener('click', () => {
this.hideDiscussion();
this.showButtonDiscussionBtn();
});
this.buttonActiveDiscussion.classList.add('active-btn');
this.buttonActiveDiscussion.innerHTML = `<img src="resources/logos/discussion.svg"/>`;
this.buttonActiveDiscussion.addEventListener('click', () => {
this.showDiscussionPart();
});
this.divDiscuss.appendChild(buttonCloseDiscussion);
this.divDiscuss.appendChild(this.buttonActiveDiscussion);
const myName: HTMLParagraphElement = document.createElement('p');
myName.innerText = name.toUpperCase();
this.nbpParticipants = document.createElement('p');
this.nbpParticipants.innerText = 'PARTICIPANTS (1)';
this.divParticipants = document.createElement('div');
this.divParticipants.classList.add('participants');
this.divMessages = document.createElement('div');
this.divMessages.classList.add('messages');
this.divMessages.innerHTML = "<h2>Local messages</h2>"
this.divDiscuss.appendChild(myName);
this.divDiscuss.appendChild(this.nbpParticipants);
this.divDiscuss.appendChild(this.divParticipants);
this.divDiscuss.appendChild(this.divMessages);
const sendDivMessage: HTMLDivElement = document.createElement('div');
sendDivMessage.classList.add('send-message');
const inputMessage: HTMLInputElement = document.createElement('input');
inputMessage.type = "text";
inputMessage.addEventListener('keyup', (event: KeyboardEvent) => {
if (event.key === 'Enter') {
event.preventDefault();
if(inputMessage.value === null
|| inputMessage.value === ''
|| inputMessage.value === undefined) {
return;
}
this.addMessage(name, inputMessage.value, true);
for(const callback of this.sendMessageCallBack.values()) {
callback(inputMessage.value);
}
inputMessage.value = "";
}
});
sendDivMessage.appendChild(inputMessage);
this.divDiscuss.appendChild(sendDivMessage);
//append in main container
this.mainContainer.appendChild(this.divDiscuss);
this.addParticipant('me', 'Moi', undefined, true);
}
public addParticipant(
userId: number|string,
name: string|undefined,
img?: string|undefined,
isMe: boolean = false,
reportCallback?: ReportCallback
) {
const divParticipant: HTMLDivElement = document.createElement('div');
divParticipant.classList.add('participant');
divParticipant.id = `participant-${userId}`;
const divImgParticipant: HTMLImageElement = document.createElement('img');
divImgParticipant.src = 'resources/logos/boy.svg';
if (img !== undefined) {
divImgParticipant.src = img;
}
const divPParticipant: HTMLParagraphElement = document.createElement('p');
if(!name){
name = 'Anonymous';
}
divPParticipant.innerText = name;
divParticipant.appendChild(divImgParticipant);
divParticipant.appendChild(divPParticipant);
if(!isMe) {
const reportBanUserAction: HTMLButtonElement = document.createElement('button');
reportBanUserAction.classList.add('report-btn')
reportBanUserAction.innerText = 'Report';
reportBanUserAction.addEventListener('click', () => {
if(reportCallback) {
this.mediaManager.showReportModal(`${userId}`, name ?? '', reportCallback);
}else{
console.info('report feature is not activated!');
}
});
divParticipant.appendChild(reportBanUserAction);
}
this.divParticipants?.appendChild(divParticipant);
this.participants.set(userId, divParticipant);
this.showButtonDiscussionBtn();
this.updateParticipant(this.participants.size);
}
public updateParticipant(nb: number) {
if (!this.nbpParticipants) {
return;
}
this.nbpParticipants.innerText = `PARTICIPANTS (${nb})`;
}
public addMessage(name: string, message: string, isMe: boolean = false) {
const divMessage: HTMLDivElement = document.createElement('div');
divMessage.classList.add('message');
if(isMe){
divMessage.classList.add('me');
}
const pMessage: HTMLParagraphElement = document.createElement('p');
const date = new Date();
if(isMe){
name = 'Moi';
}
pMessage.innerHTML = `<span style="font-weight: bold">${name}</span>
<span style="color:#bac2cc;display:inline-block;font-size:12px;">
${date.getHours()}:${date.getMinutes()}
</span>`;
divMessage.appendChild(pMessage);
const userMessage: HTMLParagraphElement = document.createElement('p');
userMessage.innerText = message;
userMessage.classList.add('body');
divMessage.appendChild(userMessage);
this.divMessages?.appendChild(divMessage);
}
public removeParticipant(userId: number|string){
const element = this.participants.get(userId);
if(element){
element.remove();
this.participants.delete(userId);
}
//if all participant leave, hide discussion button
if(this.participants.size === 1){
this.hideButtonDiscussionBtn();
}
this.sendMessageCallBack.delete(userId);
}
public onSendMessageCallback(userId: string|number, callback: SendMessageCallback): void {
this.sendMessageCallBack.set(userId, callback);
}
get activatedDiscussion(){
return this.activeDiscussion;
}
private showButtonDiscussionBtn(){
//if it's first participant, show discussion button
if(this.activatedDiscussion || this.participants.size === 1) {
return;
}
this.buttonActiveDiscussion?.classList.add('active');
}
private showDiscussion(){
this.activeDiscussion = true;
if(this.userInputManager) {
this.userInputManager.clearAllInputKeyboard();
}
this.divDiscuss?.classList.add('active');
}
private hideDiscussion(){
this.activeDiscussion = false;
if(this.userInputManager) {
this.userInputManager.initKeyBoardEvent();
}
this.divDiscuss?.classList.remove('active');
}
private hideButtonDiscussionBtn(){
this.buttonActiveDiscussion?.classList.remove('active');
}
public setUserInputManager(userInputManager : UserInputManager){
this.userInputManager = userInputManager;
}
public showDiscussionPart(){
this.showDiscussion();
this.hideButtonDiscussionBtn();
}
}

View file

@ -50,8 +50,9 @@ class JitsiFactory {
delete options.jwt; delete options.jwt;
} }
return new Promise((resolve) => { return new Promise((resolve, reject) => {
options.onload = () => resolve(); //we want for the iframe to be loaded before triggering animations. options.onload = () => resolve(); //we want for the iframe to be loaded before triggering animations.
setTimeout(() => resolve(), 2000); //failsafe in case the iframe is deleted before loading or too long to load
this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options); this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options);
this.jitsiApi.executeCommand('displayName', playerName); this.jitsiApi.executeCommand('displayName', playerName);

View file

@ -1,5 +1,7 @@
import {DivImportance, layoutManager} from "./LayoutManager"; import {DivImportance, layoutManager} from "./LayoutManager";
import {HtmlUtils} from "./HtmlUtils"; import {HtmlUtils} from "./HtmlUtils";
import {DiscussionManager, SendMessageCallback} from "./DiscussionManager";
import {UserInputManager} from "../Phaser/UserInput/UserInputManager";
declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any declare const navigator:any; // eslint-disable-line @typescript-eslint/no-explicit-any
const videoConstraint: boolean|MediaTrackConstraints = { const videoConstraint: boolean|MediaTrackConstraints = {
@ -38,57 +40,65 @@ export class MediaManager {
private cinemaBtn: HTMLDivElement; private cinemaBtn: HTMLDivElement;
private monitorBtn: HTMLDivElement; private monitorBtn: HTMLDivElement;
private discussionManager: DiscussionManager;
private userInputManager?: UserInputManager;
private hasCamera = true;
constructor() { constructor() {
this.myCamVideo = this.getElementByIdOrFail<HTMLVideoElement>('myCamVideo'); this.myCamVideo = HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('myCamVideo');
this.webrtcInAudio = this.getElementByIdOrFail<HTMLAudioElement>('audio-webrtc-in'); this.webrtcInAudio = HtmlUtils.getElementByIdOrFail<HTMLAudioElement>('audio-webrtc-in');
this.webrtcInAudio.volume = 0.2; this.webrtcInAudio.volume = 0.2;
this.microphoneBtn = this.getElementByIdOrFail<HTMLDivElement>('btn-micro'); this.microphoneBtn = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('btn-micro');
this.microphoneClose = this.getElementByIdOrFail<HTMLImageElement>('microphone-close'); this.microphoneClose = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('microphone-close');
this.microphoneClose.style.display = "none"; this.microphoneClose.style.display = "none";
this.microphoneClose.addEventListener('click', (e: MouseEvent) => { this.microphoneClose.addEventListener('click', (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
this.enableMicrophone(); this.enableMicrophone();
//update tracking //update tracking
}); });
this.microphone = this.getElementByIdOrFail<HTMLImageElement>('microphone'); this.microphone = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('microphone');
this.microphone.addEventListener('click', (e: MouseEvent) => { this.microphone.addEventListener('click', (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
this.disableMicrophone(); this.disableMicrophone();
//update tracking //update tracking
}); });
this.cinemaBtn = this.getElementByIdOrFail<HTMLDivElement>('btn-video'); this.cinemaBtn = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('btn-video');
this.cinemaClose = this.getElementByIdOrFail<HTMLImageElement>('cinema-close'); this.cinemaClose = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('cinema-close');
this.cinemaClose.style.display = "none"; this.cinemaClose.style.display = "none";
this.cinemaClose.addEventListener('click', (e: MouseEvent) => { this.cinemaClose.addEventListener('click', (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
this.enableCamera(); this.enableCamera();
//update tracking //update tracking
}); });
this.cinema = this.getElementByIdOrFail<HTMLImageElement>('cinema'); this.cinema = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('cinema');
this.cinema.addEventListener('click', (e: MouseEvent) => { this.cinema.addEventListener('click', (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
this.disableCamera(); this.disableCamera();
//update tracking //update tracking
}); });
this.monitorBtn = this.getElementByIdOrFail<HTMLDivElement>('btn-monitor'); this.monitorBtn = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('btn-monitor');
this.monitorClose = this.getElementByIdOrFail<HTMLImageElement>('monitor-close'); this.monitorClose = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('monitor-close');
this.monitorClose.style.display = "block"; this.monitorClose.style.display = "block";
this.monitorClose.addEventListener('click', (e: MouseEvent) => { this.monitorClose.addEventListener('click', (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
this.enableScreenSharing(); this.enableScreenSharing();
//update tracking //update tracking
}); });
this.monitor = this.getElementByIdOrFail<HTMLImageElement>('monitor'); this.monitor = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('monitor');
this.monitor.style.display = "none"; this.monitor.style.display = "none";
this.monitor.addEventListener('click', (e: MouseEvent) => { this.monitor.addEventListener('click', (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
this.disableScreenSharing(); this.disableScreenSharing();
//update tracking //update tracking
}); });
this.discussionManager = new DiscussionManager(this,'');
} }
public onUpdateLocalStream(callback: UpdatedLocalStreamCallback): void { public onUpdateLocalStream(callback: UpdatedLocalStreamCallback): void {
@ -126,16 +136,19 @@ export class MediaManager {
} }
public showGameOverlay(){ public showGameOverlay(){
const gameOverlay = this.getElementByIdOrFail('game-overlay'); const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
gameOverlay.classList.add('active'); gameOverlay.classList.add('active');
} }
public hideGameOverlay(){ public hideGameOverlay(){
const gameOverlay = this.getElementByIdOrFail('game-overlay'); const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
gameOverlay.classList.remove('active'); gameOverlay.classList.remove('active');
} }
public enableCamera() { public enableCamera() {
if(!this.hasCamera){
return;
}
this.cinemaClose.style.display = "none"; this.cinemaClose.style.display = "none";
this.cinemaBtn.classList.remove("disabled"); this.cinemaBtn.classList.remove("disabled");
this.cinema.style.display = "block"; this.cinema.style.display = "block";
@ -146,13 +159,7 @@ export class MediaManager {
} }
public async disableCamera() { public async disableCamera() {
this.cinemaClose.style.display = "block"; this.disabledCameraView();
this.cinema.style.display = "none";
this.cinemaBtn.classList.add("disabled");
this.constraintsMedia.video = false;
this.myCamVideo.srcObject = null;
this.stopCamera();
if (this.constraintsMedia.audio !== false) { if (this.constraintsMedia.audio !== false) {
const stream = await this.getCamera(); const stream = await this.getCamera();
this.triggerUpdatedLocalStreamCallbacks(stream); this.triggerUpdatedLocalStreamCallbacks(stream);
@ -161,6 +168,15 @@ export class MediaManager {
} }
} }
private disabledCameraView(){
this.cinemaClose.style.display = "block";
this.cinema.style.display = "none";
this.cinemaBtn.classList.add("disabled");
this.constraintsMedia.video = false;
this.myCamVideo.srcObject = null;
this.stopCamera();
}
public enableMicrophone() { public enableMicrophone() {
this.microphoneClose.style.display = "none"; this.microphoneClose.style.display = "none";
this.microphone.style.display = "block"; this.microphone.style.display = "block";
@ -267,24 +283,33 @@ export class MediaManager {
} }
} }
try { return this.getLocalStream().catch(() => {
const stream = await navigator.mediaDevices.getUserMedia(this.constraintsMedia); console.info('Error get camera, trying with video option at null');
this.disabledCameraView();
return this.getLocalStream().then((stream : MediaStream) => {
this.hasCamera = false;
return stream;
}).catch((err) => {
console.info("error get media ", this.constraintsMedia.video, this.constraintsMedia.audio, err);
throw err;
});
});
//TODO resize remote cam
/*console.log(this.localStream.getTracks());
let videoMediaStreamTrack = this.localStream.getTracks().find((media : MediaStreamTrack) => media.kind === "video");
let {width, height} = videoMediaStreamTrack.getSettings();
console.info(`${width}x${height}`); // 6*/
}
private getLocalStream() : Promise<MediaStream> {
return navigator.mediaDevices.getUserMedia(this.constraintsMedia).then((stream : MediaStream) => {
this.localStream = stream; this.localStream = stream;
this.myCamVideo.srcObject = this.localStream; this.myCamVideo.srcObject = this.localStream;
return stream; return stream;
}).catch((err: Error) => {
//TODO resize remote cam
/*console.log(this.localStream.getTracks());
let videoMediaStreamTrack = this.localStream.getTracks().find((media : MediaStreamTrack) => media.kind === "video");
let {width, height} = videoMediaStreamTrack.getSettings();
console.info(`${width}x${height}`); // 6*/
} catch (err) {
console.info("error get media ", this.constraintsMedia.video, this.constraintsMedia.audio, err);
this.localStream = null;
throw err; throw err;
} });
} }
/** /**
@ -355,14 +380,17 @@ export class MediaManager {
layoutManager.add(DivImportance.Normal, userId, html); layoutManager.add(DivImportance.Normal, userId, html);
if (reportCallBack) { if (reportCallBack) {
const reportBtn = this.getElementByIdOrFail<HTMLDivElement>(`report-${userId}`); const reportBtn = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(`report-${userId}`);
reportBtn.addEventListener('click', (e: MouseEvent) => { reportBtn.addEventListener('click', (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
this.showReportModal(userId, userName, reportCallBack); this.showReportModal(userId, userName, reportCallBack);
}); });
} }
this.remoteVideo.set(userId, this.getElementByIdOrFail<HTMLVideoElement>(userId)); this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail<HTMLVideoElement>(userId));
//permit to create participant in discussion part
this.addNewParticipant(userId, userName, undefined, reportCallBack);
} }
addScreenSharingActiveVideo(userId: string, divImportance: DivImportance = DivImportance.Important){ addScreenSharingActiveVideo(userId: string, divImportance: DivImportance = DivImportance.Important){
@ -376,7 +404,7 @@ export class MediaManager {
layoutManager.add(divImportance, userId, html); layoutManager.add(divImportance, userId, html);
this.remoteVideo.set(userId, this.getElementByIdOrFail<HTMLVideoElement>(userId)); this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail<HTMLVideoElement>(userId));
} }
disabledMicrophoneByUserId(userId: number){ disabledMicrophoneByUserId(userId: number){
@ -437,6 +465,9 @@ export class MediaManager {
removeActiveVideo(userId: string){ removeActiveVideo(userId: string){
layoutManager.remove(userId); layoutManager.remove(userId);
this.remoteVideo.delete(userId); this.remoteVideo.delete(userId);
//permit to remove user in discussion part
this.removeParticipant(userId);
} }
removeActiveScreenSharingVideo(userId: string) { removeActiveScreenSharingVideo(userId: string) {
this.removeActiveVideo(`screen-sharing-${userId}`) this.removeActiveVideo(`screen-sharing-${userId}`)
@ -499,18 +530,9 @@ export class MediaManager {
return color; return color;
} }
private getElementByIdOrFail<T extends HTMLElement>(id: string): T { public showReportModal(userId: string, userName: string, reportCallBack: ReportCallback){
const elem = document.getElementById(id);
if (elem === null) {
throw new Error("Cannot find HTML element with id '"+id+"'");
}
// FIXME: does not check the type of the returned type
return elem as T;
}
private showReportModal(userId: string, userName: string, reportCallBack: ReportCallback){
//create report text area //create report text area
const mainContainer = this.getElementByIdOrFail<HTMLDivElement>('main-container'); const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
const divReport = document.createElement('div'); const divReport = document.createElement('div');
divReport.classList.add('modal-report-user'); divReport.classList.add('modal-report-user');
@ -565,7 +587,34 @@ export class MediaManager {
mainContainer.appendChild(divReport); mainContainer.appendChild(divReport);
} }
public addNewParticipant(userId: number|string, name: string|undefined, img?: string, reportCallBack?: ReportCallback){
this.discussionManager.addParticipant(userId, name, img, false, reportCallBack);
}
public removeParticipant(userId: number|string){
this.discussionManager.removeParticipant(userId);
}
public addNewMessage(name: string, message: string, isMe: boolean = false){
this.discussionManager.addMessage(name, message, isMe);
//when there are new message, show discussion
if(!this.discussionManager.activatedDiscussion) {
this.discussionManager.showDiscussionPart();
}
}
public addSendMessageCallback(userId: string|number, callback: SendMessageCallback){
this.discussionManager.onSendMessageCallback(userId, callback);
}
get activatedDiscussion(){
return this.discussionManager.activatedDiscussion;
}
public setUserInputManager(userInputManager : UserInputManager){
this.discussionManager.setUserInputManager(userInputManager);
}
} }
export const mediaManager = new MediaManager(); export const mediaManager = new MediaManager();

View file

@ -2,6 +2,7 @@ import * as SimplePeerNamespace from "simple-peer";
import {mediaManager} from "./MediaManager"; import {mediaManager} from "./MediaManager";
import {TURN_SERVER, TURN_USER, TURN_PASSWORD} from "../Enum/EnvironmentVariable"; import {TURN_SERVER, TURN_USER, TURN_PASSWORD} from "../Enum/EnvironmentVariable";
import {RoomConnection} from "../Connexion/RoomConnection"; import {RoomConnection} from "../Connexion/RoomConnection";
import {MESSAGE_TYPE_CONSTRAINT} from "./VideoPeer";
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
@ -148,6 +149,6 @@ export class ScreenSharingPeer extends Peer {
public stopPushingScreenSharingToRemoteUser(stream: MediaStream) { public stopPushingScreenSharingToRemoteUser(stream: MediaStream) {
this.removeStream(stream); this.removeStream(stream);
this.write(new Buffer(JSON.stringify({streamEnded: true}))); this.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, streamEnded: true})));
} }
} }

View file

@ -10,7 +10,7 @@ import {
UpdatedLocalStreamCallback UpdatedLocalStreamCallback
} from "./MediaManager"; } from "./MediaManager";
import {ScreenSharingPeer} from "./ScreenSharingPeer"; import {ScreenSharingPeer} from "./ScreenSharingPeer";
import {VideoPeer} from "./VideoPeer"; import {MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer} from "./VideoPeer";
import {RoomConnection} from "../Connexion/RoomConnection"; import {RoomConnection} from "../Connexion/RoomConnection";
export interface UserSimplePeerInterface{ export interface UserSimplePeerInterface{
@ -38,7 +38,7 @@ export class SimplePeer {
private readonly stopLocalScreenSharingStreamCallback: StopScreenSharingCallback; private readonly stopLocalScreenSharingStreamCallback: StopScreenSharingCallback;
private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>(); private readonly peerConnectionListeners: Array<PeerConnectionListener> = new Array<PeerConnectionListener>();
constructor(private Connection: RoomConnection, private enableReporting: boolean) { constructor(private Connection: RoomConnection, private enableReporting: boolean, private myName: string) {
// We need to go through this weird bound function pointer in order to be able to "free" this reference later. // We need to go through this weird bound function pointer in order to be able to "free" this reference later.
this.sendLocalVideoStreamCallback = this.sendLocalVideoStream.bind(this); this.sendLocalVideoStreamCallback = this.sendLocalVideoStream.bind(this);
this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this); this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this);
@ -145,6 +145,12 @@ export class SimplePeer {
mediaManager.addActiveVideo("" + user.userId, reportCallback, name); mediaManager.addActiveVideo("" + user.userId, reportCallback, name);
const peer = new VideoPeer(user.userId, user.initiator ? user.initiator : false, this.Connection); const peer = new VideoPeer(user.userId, user.initiator ? user.initiator : false, this.Connection);
//permit to send message
mediaManager.addSendMessageCallback(user.userId,(message: string) => {
peer.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_MESSAGE, name: this.myName.toUpperCase(), message: message})));
});
peer.toClose = false; peer.toClose = false;
// When a connection is established to a video stream, and if a screen sharing is taking place, // When a connection is established to a video stream, and if a screen sharing is taking place,
// the user sharing screen should also initiate a connection to the remote user! // the user sharing screen should also initiate a connection to the remote user!
@ -318,7 +324,7 @@ export class SimplePeer {
throw new Error('While adding media, cannot find user with ID ' + userId); throw new Error('While adding media, cannot find user with ID ' + userId);
} }
const localStream: MediaStream | null = mediaManager.localStream; const localStream: MediaStream | null = mediaManager.localStream;
PeerConnection.write(new Buffer(JSON.stringify(mediaManager.constraintsMedia))); PeerConnection.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...mediaManager.constraintsMedia})));
if(!localStream){ if(!localStream){
return; return;

View file

@ -5,6 +5,8 @@ import {RoomConnection} from "../Connexion/RoomConnection";
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer'); const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
export const MESSAGE_TYPE_CONSTRAINT = 'constraint';
export const MESSAGE_TYPE_MESSAGE = 'message';
/** /**
* A peer connection used to transmit video / audio signals between 2 peers. * A peer connection used to transmit video / audio signals between 2 peers.
*/ */
@ -78,19 +80,27 @@ export class VideoPeer extends Peer {
}); });
this.on('data', (chunk: Buffer) => { this.on('data', (chunk: Buffer) => {
const constraint = JSON.parse(chunk.toString('utf8')); const message = JSON.parse(chunk.toString('utf8'));
console.log("data", constraint); console.log("data", message);
if (constraint.audio) {
mediaManager.enabledMicrophoneByUserId(this.userId); if(message.type === MESSAGE_TYPE_CONSTRAINT) {
} else { const constraint = message;
mediaManager.disabledMicrophoneByUserId(this.userId); if (constraint.audio) {
mediaManager.enabledMicrophoneByUserId(this.userId);
} else {
mediaManager.disabledMicrophoneByUserId(this.userId);
}
if (constraint.video || constraint.screen) {
mediaManager.enabledVideoByUserId(this.userId);
} else {
this.stream(undefined);
mediaManager.disabledVideoByUserId(this.userId);
}
} }
if (constraint.video || constraint.screen) { if(message.type === 'message') {
mediaManager.enabledVideoByUserId(this.userId); mediaManager.addNewMessage(message.name, message.message);
} else {
this.stream(undefined);
mediaManager.disabledVideoByUserId(this.userId);
} }
}); });
@ -163,7 +173,7 @@ export class VideoPeer extends Peer {
private pushVideoToRemoteUser() { private pushVideoToRemoteUser() {
try { try {
const localStream: MediaStream | null = mediaManager.localStream; const localStream: MediaStream | null = mediaManager.localStream;
this.write(new Buffer(JSON.stringify(mediaManager.constraintsMedia))); this.write(new Buffer(JSON.stringify({type: MESSAGE_TYPE_CONSTRAINT, ...mediaManager.constraintsMedia})));
if(!localStream){ if(!localStream){
return; return;

View file

@ -45,7 +45,7 @@ module.exports = {
new webpack.ProvidePlugin({ new webpack.ProvidePlugin({
Phaser: 'phaser' Phaser: 'phaser'
}), }),
new webpack.EnvironmentPlugin(['API_URL', 'DEBUG_MODE', 'TURN_SERVER', 'TURN_USER', 'TURN_PASSWORD', 'JITSI_URL', 'JITSI_PRIVATE_MODE']) new webpack.EnvironmentPlugin(['API_URL', 'ADMIN_URL', 'DEBUG_MODE', 'TURN_SERVER', 'TURN_USER', 'TURN_PASSWORD', 'JITSI_URL', 'JITSI_PRIVATE_MODE'])
], ],
}; };

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

View file

@ -38,6 +38,10 @@ message CharacterLayerMessage {
/*********** CLIENT TO SERVER MESSAGES *************/ /*********** CLIENT TO SERVER MESSAGES *************/
message PingMessage {
}
message SetPlayerDetailsMessage { message SetPlayerDetailsMessage {
string name = 1; string name = 1;
repeated string characterLayers = 2; repeated string characterLayers = 2;

View file

@ -36,6 +36,15 @@
</head> </head>
<body class="choose-map"> <body class="choose-map">
<div class="container">
<div class="row">
<div class="col">
<a href="/" class="d-block mt-3 pixel-text">
&lt;&lt; BACK TO HOMEPAGE
</a>
</div>
</div>
</div>
<div class="container-fluid container-lg section pt-5"> <div class="container-fluid container-lg section pt-5">
<h1 class="text-center pixel-title">CHOOSE YOUR MAP&nbsp;!</h1> <h1 class="text-center pixel-title">CHOOSE YOUR MAP&nbsp;!</h1>
<div class="row"> <div class="row">

View file

@ -36,6 +36,15 @@
</head> </head>
<body class="create-map"> <body class="create-map">
<div class="container">
<div class="row">
<div class="col">
<a href="/" class="d-block mt-3 pixel-text">
&lt;&lt; BACK TO HOMEPAGE
</a>
</div>
</div>
</div>
<div class="container-fluid container-lg section pt-5"> <div class="container-fluid container-lg section pt-5">
<h1 class="text-center pixel-title">CREATE YOUR MAP&nbsp;!</h1> <h1 class="text-center pixel-title">CREATE YOUR MAP&nbsp;!</h1>
<div class="row"> <div class="row">
@ -44,7 +53,7 @@
</div> </div>
</div> </div>
<div class="row"> <div class="row">
< class="col"> <div class="col">
<h2 id="tools-you-will-need" class="pixel-title">Tools you will need</h2> <h2 id="tools-you-will-need" class="pixel-title">Tools you will need</h2>
<p>In order to build your own map for WorkAdventure, you need:</p> <p>In order to build your own map for WorkAdventure, you need:</p>
<ul> <ul>

View file

@ -10,6 +10,9 @@
gtag('js', new Date()); gtag('js', new Date());
gtag('config', 'UA-10196481-11'); gtag('config', 'UA-10196481-11');
if (window.location.host.endsWith("localhost")){
window['ga-disable-UA-10196481-11'] = true;
}
</script> </script>
<link rel="apple-touch-icon" sizes="57x57" href="static/images/favicons/apple-icon-57x57.png"> <link rel="apple-touch-icon" sizes="57x57" href="static/images/favicons/apple-icon-57x57.png">
<link rel="apple-touch-icon" sizes="60x60" href="static/images/favicons/apple-icon-60x60.png"> <link rel="apple-touch-icon" sizes="60x60" href="static/images/favicons/apple-icon-60x60.png">
@ -33,12 +36,12 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" />
<meta name="title" content="Workadventure" /> <meta name="title" content="Workadventure" />
<meta name="description" content="You are impatient to discover this new world? Click on 'Play online' and meet new people or share this adventure with your colleagues and friends by clicking on 'Private mode'" /> <meta name="description" content="You are impatient to discover this new world? Click on 'Work online' and meet new people or share this adventure with your colleagues and friends by clicking on 'Work in private'" />
<!-- Open Graph / Facebook --> <!-- Open Graph / Facebook -->
<meta property="og:url" content="https://workadventu.re/" /> <meta property="og:url" content="https://workadventu.re/" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:title" content="Workadventure" /> <meta property="og:title" content="Workadventure" />
<meta property="og:description" content="You are impatient to discover this new world? Click on 'Play online' and meet new people or share this adventure with your colleagues and friends by clicking on 'Private mode'" /> <meta property="og:description" content="You are impatient to discover this new world? Click on 'Work online' and meet new people or share this adventure with your colleagues and friends by clicking on 'Work in private'" />
<meta property="og:image" content="https://workadventu.re/static/images/meta-tags-image.jpg" /> <meta property="og:image" content="https://workadventu.re/static/images/meta-tags-image.jpg" />
<meta property="og:image:type" content="image/jpeg" /> <meta property="og:image:type" content="image/jpeg" />
<meta property="og:image:alt" content="workadventure" /> <meta property="og:image:alt" content="workadventure" />
@ -46,7 +49,7 @@
<meta name="twitter:site" content="@coding_machine"> <meta name="twitter:site" content="@coding_machine">
<meta name="twitter:url" content="https://workadventu.re/" /> <meta name="twitter:url" content="https://workadventu.re/" />
<meta name="twitter:title" content="Workadventure" /> <meta name="twitter:title" content="Workadventure" />
<meta name="twitter:description" content="You are impatient to discover this new world? Click on 'Play online' and meet new people or share this adventure with your colleagues and friends by clicking on 'Private mode'" /> <meta name="twitter:description" content="You are impatient to discover this new world? Click on 'Work online' and meet new people or share this adventure with your colleagues and friends by clicking on 'Work in private'" />
<meta name="twitter:image" content="https://workadventu.re/static/images/meta-tags-image.jpg" /> <meta name="twitter:image" content="https://workadventu.re/static/images/meta-tags-image.jpg" />
<link rel="stylesheet" href="main.css"> <link rel="stylesheet" href="main.css">
<script src="bundle.js"></script> <script src="bundle.js"></script>
@ -82,32 +85,42 @@
</div> </div>
<div class="col-2 col-md-6"> <div class="col-2 col-md-6">
<div class="social-links"> <div class="social-links">
<span class="share-title">Share your experience</span> <a href="https://www.facebook.com/workadventurebytcm">
<a onclick="shareFB()"> <img class="social-image" src="static/images/facebook_bw.png" />
<img class="social-image" src="static/images/facebook.png" />
</a> </a>
<a onclick="shareLI()"> <a href="https://www.linkedin.com/company/workadventure-by-tcm">
<img class="social-image" src="static/images/linkedin.png" /> <img class="social-image" src="static/images/linkedin_bw.png" />
</a> </a>
<a onclick="shareTW()"> <a href="https://twitter.com/Workadventure_">
<img class="social-image" src="static/images/twitter.png" /> <img class="social-image" src="static/images/twitter_bw.png" />
</a> </a>
</div> </div>
</div> </div>
</div> </div>
<div class="title title-main text-center"> <div class="title title-main text-center">
<h1>Your workplace<br/>but better</h1> <h1>Meet your teammates</h1>
<h3>You are impatient to discover this new world? Click on "Play online" and meet new people or share this adventure with your colleagues and friends by clicking on "Private mode"</h3> <h3>
WorkAdventure preserves your social interaction while COVID is still out there.
</h3>
<h3>
Stay connected with your teamworkers, by creating your own online workspace to work remotely.
</h3>
<h3>
Stay connected with your clients by providing a dedicated digital place to organize meetings, workshops.
</h3>
<h3>
Stay connected with your future collaborators by organizing online event.
</h3>
</div> </div>
<div class="row buttons-row justify-content-md-center pt-5"> <div class="row buttons-row justify-content-md-center pt-5">
<div class="col col-lg-3"> <div class="col col-lg-3">
<a class="custom-link start" href="/choose-map.html" title="PRIVATE MODE"> <a class="custom-link start" href="/choose-map.html" title="WORK IN PRIVATE">
PRIVATE MODE WORK IN PRIVATE
</a> </a>
</div> </div>
<div class="col col-lg-3"> <div class="col col-lg-3">
<a class="custom-link play" target="_BLANK" onclick="startGame()" title="PLAY ONLINE"> <a class="custom-link play" target="_BLANK" onclick="startGame()" title="WORK ONLINE">
PLAY ONLINE TRY IT NOW!
</a> </a>
</div> </div>
</div> </div>
@ -158,18 +171,19 @@
<p>Click the button below to come and say hi!</p> <p>Click the button below to come and say hi!</p>
<p class="bubble-action"><span onclick="startGame()"> <p class="bubble-action"><span onclick="startGame()">
<img src="static/images/playicon.png" /> <img src="static/images/playicon.png" />
START IN PUBLIC MODE TRY IT NOW !
</span></p> </span></p>
</div> </div>
</div> </div>
<div class="bubble bubble-4 b-revert" style="height: 254px;"> <div class="bubble bubble-4 b-revert" style="height: 254px;">
<div> <div>
<p>You can also create a private room with your friends or your team ! </p> <p>You can also create a private room with your friends or your team ! </p>
<p class="bubble-legend">To try, press button private mode</p> <p class="bubble-legend">To try, press button work in private</p>
<p class="bubble-action"><a href="/choose-map.html"> <p class="bubble-action">
<img src="static/images/playicon.png" /> <a href="/choose-map.html">
START IN PRIVATE MODE <img src="static/images/playicon.png" />
</a></p> CHOOSE YOU OWN MAP
</a>
<p> <p>
Dont forget to activate your mic and camera, lets play Dont forget to activate your mic and camera, lets play
</p> </p>
@ -179,7 +193,6 @@
<img src="static/images/story/character-walk-right.gif" style="display:none;" /> <img src="static/images/story/character-walk-right.gif" style="display:none;" />
</section> </section>
<script> <script>
gsap.to(".title-main", { gsap.to(".title-main", {
//y:-1000, //y:-1000,
scale: 0, scale: 0,
@ -287,39 +300,91 @@
<div class="container-fluid container-lg"> <div class="container-fluid container-lg">
<div class="row justify-content-md-center"> <div class="row justify-content-md-center">
<div class="col-12 col-md-12 text-center"> <div class="col-12 col-md-12 text-center">
<h3>How to play</h3> <h3>HOW IT WORKS</h3>
</div> </div>
<div class="col-12 col-md-4 text-center my-3 my-md-0"> <div class="col-12 col-md-4 text-center my-3 my-md-0">
<div class="image-item"> <div class="image-item">
<h2>Choose your map</h2> <h2>CHOOSE YOUR WORKSPACE</h2>
<div class="step-image"><img src="static/images/maps/office.png"></div> <div class="step-image"><img src="static/images/maps/office.png"></div>
</div> </div>
</div> </div>
<div class="col-12 col-md-4 text-center my-3 my-md-0"> <div class="col-12 col-md-4 text-center my-3 my-md-0">
<div class="image-item"> <div class="image-item">
<h2>Select your character</h2> <h2>SELECT YOUR WOKA</h2>
<div class="step-image"><img src="static/images/choose_character.png"></div> <div class="step-image"><img src="static/images/choose_character.png"></div>
</div> </div>
</div> </div>
<div class="col-12 col-md-4 text-center my-3 my-md-0"> <div class="col-12 col-md-4 text-center my-3 my-md-0">
<div class="image-item"> <div class="image-item">
<h2>Let's go explore and talk&nbsp;!</h2> <h2>LET'S GO TO YOUR OFFICE</h2>
<div class="step-image"><img src="static/images/interact.png"></div> <div class="step-image"><img src="static/images/interact.png"></div>
</div> </div>
</div> </div>
</div> </div>
<div class="row mt-5">
<div class="col-lg-9 text-right">
<p class="py-3 font-weight-bold" style="font-size: 1.25rem">
Want to try Woradventure with your team mates or friends?
</p>
</div>
<div class="col-lg-3">
<a class="custom-link relative" href="/choose-map.html" title="WORK IN PRIVATE">
GET STARTED!
</a>
</div>
</div>
<div class="social-links text-center pt-2 pb-4">
<span class="share-title">... and if you liked woradventure, share your experience!</span>
<a onclick="shareFB()">
<img class="social-image" src="static/images/facebook.png" />
</a>
<a onclick="shareLI()">
<img class="social-image" src="static/images/linkedin.png" />
</a>
<a onclick="shareTW()">
<img class="social-image" src="static/images/twitter.png" />
</a>
</div>
</div>
</div>
<div class="section bg-white text-center pt-5">
<div class="container-fluid container-lg">
<div class="row justify-content-md-center">
<div class="col-12 col-md-12 text-center">
<h3>WORKADVENTURE'S USE CASES</h3>
</div>
<p>
Workadventure is an intuitive and fun solution to professional issues:
</p>
<ul class="text-left">
<li>
Work in a remote way within a team,
</li>
<li>
Create an event (even a big one),
</li>
<li>
Stay connected and increase social interactions...
</li>
</ul>
<p>
Feel free to contact us if you need a specific map, need a dedicated admin console or any
support (for instance a large number of connexions) : <a href="mailto:workadventure@thecodingmachine.com" target="_blank">workadventure@thecodingmachine.com</a>
</p>
</div>
</div> </div>
</div> </div>
<div class="section bg-white"> <div class="section bg-white">
<div class="container-fluid container-lg"> <div class="container-fluid container-lg">
<div class="col-12 credits"> <div class="col-12 credits">
<h2>Future features</h2> <h2>CURRENT FEATURES</h2>
<h3>We have already thought of new features:</h3> <!-- <h3>We have already thought of new features:</h3>-->
<p>Share files with others</p> <p>Instant interaction with up to 4 people</p>
<p>Lock group conversations</p>
<p>Share screen with others</p> <p>Share screen with others</p>
<p>Interact with objects</p> <p>Organize a workshop and meeting with up to 60 people</p>
<h3>And you, what would you want?</h3> <p>Design your own digital space</p>
<p>... and infinite possibilities</p>
<h3>You need more? Do not hesitate to tell us!</h3>
<div class="row justify-content-md-center pt-5" style="margin-top: 65px;"> <div class="row justify-content-md-center pt-5" style="margin-top: 65px;">
<div class="col col-lg-3"> <div class="col col-lg-3">
<a class="custom-link contribute" target="_BLANK" href="https://docs.google.com/forms/d/e/1FAIpQLSdxvajEyqsn4X0ai0SoDAcdsa_JQPIfiP2Tp9PDzkfZA54v9Q/viewform" title="FEEDBACK"> <a class="custom-link contribute" target="_BLANK" href="https://docs.google.com/forms/d/e/1FAIpQLSdxvajEyqsn4X0ai0SoDAcdsa_JQPIfiP2Tp9PDzkfZA54v9Q/viewform" title="FEEDBACK">

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -80,11 +80,17 @@ header {
h1 { h1 {
font-family: 'Karmatic Arcade'; font-family: 'Karmatic Arcade';
font-size: 2.75rem; font-size: 2.75rem;
margin-bottom: 1.25rem; margin-bottom: 2rem;
} }
h3 { h3 {
min-height: 200px; text-align: left;
padding: 1rem; padding: 1rem 0 0 1rem;
&:before{
content: ">";
position: absolute;
left: -10px;
}
} }
@include media-breakpoint-down(xs) { @include media-breakpoint-down(xs) {
h3 { h3 {
@ -148,11 +154,10 @@ header {
left: 0; left: 0;
right: 0; right: 0;
bottom: 7px; bottom: 7px;
padding: 1.125rem; padding: 1.125rem 0.5rem;
text-align: center; text-align: center;
z-index: 2; z-index: 2;
transition: all .1s cubic-bezier(0.000, -0.600, 1.000, 1.650); /* custom */ transition: all .1s cubic-bezier(0.000, -0.600, 1.000, 1.650); /* custom */
//transition-timing-function: cubic-bezier(0.000, -0.600, 1.000, 1.650); /* custom */
@include media-breakpoint-down(sm) { @include media-breakpoint-down(sm) {
display: none; display: none;
} }
@ -171,33 +176,23 @@ header {
background-image: url('../images/btn-bg-3.png'); background-image: url('../images/btn-bg-3.png');
cursor: pointer; cursor: pointer;
} }
&.start {
/*padding-left: 55px;*/
&:before {
/*content: "";
position: absolute;
background: url('../images/playicon.png') no-repeat;
height: 20px;
width: 21px;
left: 36px;
top: 23px;*/
}
}
&.light{ &.light{
background-image: url('../images/btn-bg-light.png'); background-image: url('../images/btn-bg-light.png');
} }
&.relative{
/*&::after{ position: relative;
content: ""; left: auto;
position: absolute; right: auto;
background-size: 60%; bottom: auto;
width: 100%; }
height: 100%;
}*/
} }
.social-links a { .social-links a {
cursor: pointer; cursor: pointer;
&:hover{
text-decoration: none;
}
} }
img{ img{
@ -388,7 +383,7 @@ img{
} }
} }
&.how-to{ &.how-to{
padding: 6.25rem 0; padding: 6.25rem 0 0;
background: url('../images/bg-briques.jpg') repeat-x bottom; background: url('../images/bg-briques.jpg') repeat-x bottom;
.desktop-only { .desktop-only {
padding: 0 1.25rem 4rem; padding: 0 1.25rem 4rem;
@ -431,7 +426,7 @@ img{
margin: 0; margin: 0;
min-height: 6rem; min-height: 6rem;
font-family: 'Karmatic Arcade'; font-family: 'Karmatic Arcade';
font-size: 22px; font-size: 24px;
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
} }
} }
@ -503,6 +498,9 @@ img{
.pixel-title{ .pixel-title{
font-family: "Karmatic Arcade" !important; font-family: "Karmatic Arcade" !important;
} }
.pixel-text{
font-family: "VCR OSD Mono" !important;
}
h3 { h3 {
font-weight: 800; font-weight: 800;