Merge branch 'master' of github.com:thecodingmachine/workadventure into scaling

# Conflicts:
#	back/src/Services/SocketManager.ts
#	deeployer.libsonnet
#	docker-compose.yaml
#	front/src/Connexion/RoomConnection.ts
#	front/src/Enum/EnvironmentVariable.ts
#	front/src/Phaser/Game/GameScene.ts
#	front/webpack.config.js
#	pusher/src/Controller/IoSocketController.ts
This commit is contained in:
David Négrier 2020-12-11 13:00:11 +01:00
commit 6f2c319785
59 changed files with 7667 additions and 569 deletions

View File

@ -20,7 +20,7 @@ jobs:
# 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"
uses: docker/build-push-action@v1

View File

@ -6,8 +6,13 @@ class GaugeManager {
private nbClientsPerRoomGauge: Gauge<string>;
private nbGroupsPerRoomGauge: Gauge<string>;
private nbGroupsPerRoomCounter: Counter<string>;
private nbRoomsGauge: Gauge<string>;
constructor() {
this.nbRoomsGauge = new Gauge({
name: 'workadventure_nb_rooms',
help: 'Number of active rooms'
});
this.nbClientsGauge = new Gauge({
name: 'workadventure_nb_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 {
this.nbClientsGauge.inc();
this.nbClientsPerRoomGauge.inc({ room: roomId });

View File

@ -320,6 +320,7 @@ export class SocketManager {
room.leave(user);
if (room.isEmpty()) {
this.rooms.delete(room.roomId);
gaugeManager.decNbRoomGauge();
debug('Room is empty. Deleting room "%s"', room.roomId);
}
} finally {
@ -349,6 +350,7 @@ export class SocketManager {
world.tags = data.tags
world.policyType = Number(data.policy_type)
}
gaugeManager.incNbRoomGauge();
this.rooms.set(roomId, world);
}
return Promise.resolve(world)
@ -718,6 +720,7 @@ export class SocketManager {
room.adminLeave(admin);
if (room.isEmpty()) {
this.rooms.delete(room.roomId);
gaugeManager.decNbRoomGauge();
debug('Room is empty. Deleting room "%s"', room.roomId);
}
}

View File

@ -54,6 +54,7 @@
"env": {
"API_URL": "pusher."+url,
"UPLOADER_URL": "uploader."+url,
"ADMIN_URL": "admin."+url,
"JITSI_URL": env.JITSI_URL,
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
"TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443",

View File

@ -28,6 +28,7 @@ services:
NODE_ENV: development
API_URL: pusher.workadventure.localhost
UPLOADER_URL: uploader.workadventure.localhost
ADMIN_URL: admin.workadventure.localhost
STARTUP_COMMAND_1: yarn install
TURN_SERVER: "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443"
TURN_USER: workadventure

1
front/.gitignore vendored
View File

@ -1,5 +1,6 @@
/node_modules/
/dist/bundle.js
/dist/tests/
/yarn-error.log
/dist/webpack.config.js
/dist/webpack.config.js.map

View File

@ -72,7 +72,11 @@
</div>
</div>
<div id="cowebsite" class="cowebsite hidden"></div>
<div id="cowebsite" class="cowebsite hidden">
<button class="close-btn" id="cowebsite-close">
<img src="resources/logos/close.svg"/>
</button>
</div>
<div class="audio-playing">
<img src="/resources/logos/megaphone.svg"/>
</div>

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{
overflow: hidden;
}
body button:focus,
body img:focus,
body input:focus {
outline: -webkit-focus-ring-color auto 0;
}
body .message-info{
width: 20%;
height: auto;
@ -72,14 +80,16 @@ body .message-info.warning{
#div-myCamVideo {
position: absolute;
right: 0;
bottom: 0;
right: 15px;
bottom: 15px;
border-radius: 15px 15px 15px 15px;
}
video#myCamVideo{
width: 15vw;
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
border-radius: 15px 15px 15px 15px;
/*width: 200px;*/
/*height: 113px;*/
}
@ -274,6 +284,23 @@ body {
#cowebsite.hidden {
transform: translateX(100%);
}
#cowebsite .close-btn{
position: absolute;
top: 10px;
right: -100px;
background: none;
border: none;
cursor: pointer;
animation: right .2s ease;
}
#cowebsite .close-btn img{
height: 15px;
right: 15px;
}
#cowebsite:hover .close-btn{
right: 10px;
}
}
@media (max-aspect-ratio: 1/1) {
.game-overlay {
@ -372,6 +399,7 @@ body {
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;
cursor: pointer;
border-radius: 15px 15px 15px 15px;
}
.sidebar > div:hover {
@ -384,6 +412,7 @@ body {
justify-content: center;
flex-direction: column;
overflow: hidden;
border-radius: 15px;
}
.chat-mode {
@ -435,7 +464,7 @@ body {
max-height: 80%;
top: -80%;
left: 10%;
background: #000000a6;
background: #333333;
z-index: 200;
transition: all 0.1s ease-out;
}
@ -532,7 +561,7 @@ body {
border: 1px solid black;
background-color: #00000000;
color: #ffda01;
border-radius: 10px;
border-radius: 15px;
padding: 10px 30px;
transition: all .2s ease;
}
@ -627,6 +656,7 @@ div.modal-report-user{
left: calc(50% - 400px);
top: 100px;
background-color: #000000ad;
border-radius: 15px;
}
.modal-report-user textarea{
@ -638,6 +668,7 @@ div.modal-report-user{
color: white;
width: calc(100% - 60px);
margin: 30px;
border-radius: 15px;
}
.modal-report-user img{
@ -669,7 +700,7 @@ div.modal-report-user{
border: 1px solid black;
background-color: #00000000;
color: #ffda01;
border-radius: 10px;
border-radius: 15px;
padding: 10px 30px;
transition: all .2s ease;
}
@ -701,3 +732,217 @@ div.modal-report-user{
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;
}
/** Action button **/
div.action{
position: absolute;
width: 100%;
height: auto;
text-align: center;
bottom: 40px;
transition: all .5s ease;
animation: mymove .5s;
animation-iteration-count: infinite;
animation-timing-function: ease-in-out;
}
div.action p.action-body{
padding: 10px;
background-color: #2d2d2dba;
color: #fff;
font-size: 12px;
text-align: center;
max-width: 150px;
margin-left: calc(50% - 75px);
border-radius: 15px;
}
@keyframes mymove {
0% {bottom: 40px;}
50% {bottom: 30px;}
100% {bottom: 40px;}
}

View File

@ -12,12 +12,14 @@ const URL_ROOM_STARTED = '/Floor0/floor0.json';
class ConnectionManager {
private localUser!:LocalUser;
private connexionType?: GameConnexionTypes
/**
* Tries to login to the node server and return the starting map url to be loaded
*/
public async initGameConnexion(): Promise<Room> {
const connexionType = urlManager.getGameConnexionType();
this.connexionType = connexionType;
if(connexionType === GameConnexionTypes.register) {
const organizationMemberToken = urlManager.getOrganizationToken();
const data = await Axios.post(`${API_URL}/register`, {organizationMemberToken}).then(res => res.data);
@ -31,7 +33,7 @@ class ConnectionManager {
const room = new Room(window.location.pathname + window.location.hash);
return Promise.resolve(room);
} else if (connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) {
} else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) {
const localUser = localUserStore.getLocalUser();
if (localUser && localUser.jwtToken && localUser.uuid && localUser.textures) {
@ -55,18 +57,6 @@ class ConnectionManager {
}
const room = new Room(roomId);
return Promise.resolve(room);
} else if (connexionType == GameConnexionTypes.organization) {
const localUser = localUserStore.getLocalUser();
if (localUser) {
this.localUser = localUser;
await this.verifyToken(localUser.jwtToken);
const room = new Room(window.location.pathname + window.location.hash);
return Promise.resolve(room);
} else {
//todo: find some kind of fallback?
return Promise.reject('Could not find a user in localstorage');
}
}
return Promise.reject('Invalid URL');
@ -116,6 +106,10 @@ class ConnectionManager {
});
});
}
get getConnexionType(){
return this.connexionType;
}
}
export const connectionManager = new ConnectionManager();

View File

@ -6,10 +6,8 @@ export class Room {
public readonly isPublic: boolean;
private mapUrl: string|undefined;
private instance: string|undefined;
public readonly hash: string;
constructor(id: string) {
this.hash = '';
if (id.startsWith('/')) {
id = id.substr(1);
}
@ -24,12 +22,29 @@ export class Room {
const indexOfHash = this.id.indexOf('#');
if (indexOfHash !== -1) {
const idWithHash = this.id;
this.id = this.id.substr(0, indexOfHash);
this.hash = idWithHash.substr(indexOfHash + 1);
}
}
public static getIdFromIdentifier(identifier: string, baseUrl: string, currentInstance: string): {roomId: string, hash: string} {
let roomId = '';
let hash = '';
if (!identifier.startsWith('/_/') && !identifier.startsWith('/@/')) { //relative file link
const absoluteExitSceneUrl = new URL(identifier, baseUrl);
roomId = '_/'+currentInstance+'/'+absoluteExitSceneUrl.hostname + absoluteExitSceneUrl.pathname; //in case of a relative url, we need to create a public roomId
hash = absoluteExitSceneUrl.hash;
hash = hash.substring(1); //remove the leading diese
} else { //absolute room Id
const parts = identifier.split('#');
roomId = parts[0];
roomId = roomId.substring(1); //remove the leading slash
if (parts.length > 1) {
hash = parts[1]
}
}
return {roomId, hash}
}
public async getMapUrl(): Promise<string> {
return new Promise<string>((resolve, reject) => {
if (this.mapUrl !== undefined) {

View File

@ -1,13 +1,13 @@
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 || "pusher.workadventure.localhost");
const UPLOADER_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.UPLOADER_URL || 'uploader.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_USER: string = process.env.TURN_USER || 'g.parant@thecodingmachine.com';
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_PRIVATE_MODE : boolean = process.env.JITSI_PRIVATE_MODE == "true";
const RESOLUTION = 3;
const RESOLUTION = 2;
const ZOOM_LEVEL = 1/*3/4*/;
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

View File

@ -39,20 +39,16 @@ export class GameManager {
public async loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin): Promise<void> {
const roomID = room.id;
const mapUrl = await room.getMapUrl();
console.log('Loading map '+roomID+' at url '+mapUrl);
const gameIndex = scenePlugin.getIndex(mapUrl);
const gameIndex = scenePlugin.getIndex(roomID);
if(gameIndex === -1){
const game : Phaser.Scene = GameScene.createFromUrl(room, mapUrl);
console.log('Adding scene '+mapUrl);
scenePlugin.add(mapUrl, game, false);
const game : Phaser.Scene = new GameScene(room, mapUrl);
scenePlugin.add(roomID, game, false);
}
}
public async goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin) {
const url = await this.startRoom.getMapUrl();
console.log('Starting scene '+url);
scenePlugin.start(url);
public goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin): void {
scenePlugin.start(this.startRoom.id);
}
}

View File

@ -31,7 +31,12 @@ import {Queue} from 'queue-typescript';
import {SimplePeer, UserSimplePeerInterface} from "../../WebRtc/SimplePeer";
import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene";
import {loadAllLayers, loadObject, loadPlayerCharacters} from "../Entity/body_character";
import {CenterListener, layoutManager, LayoutMode} from "../../WebRtc/LayoutManager";
import {
CenterListener,
layoutManager,
LayoutMode,
ON_ACTION_TRIGGER_BUTTON, TRIGGER_JITSI_PROPERTIES, TRIGGER_WEBSITE_PROPERTIES
} from "../../WebRtc/LayoutManager";
import Texture = Phaser.Textures.Texture;
import Sprite = Phaser.GameObjects.Sprite;
import CanvasTexture = Phaser.Textures.CanvasTexture;
@ -54,6 +59,7 @@ import {ConsoleGlobalMessageManager} from "../../Administration/ConsoleGlobalMes
import {ResizableScene} from "../Login/ResizableScene";
import {Room} from "../../Connexion/Room";
import {jitsiFactory} from "../../WebRtc/JitsiFactory";
import {urlManager} from "../../Url/UrlManager";
export interface GameSceneInitInterface {
initPosition: PointInterface|null,
@ -90,6 +96,8 @@ interface DeleteGroupEventInterface {
groupId: number
}
const defaultStartLayerName = 'start';
export class GameScene extends ResizableScene implements CenterListener {
GameManager : GameManager;
Terrains : Array<Phaser.Tilemaps.Tileset>;
@ -119,7 +127,6 @@ export class GameScene extends ResizableScene implements CenterListener {
private createPromise: Promise<void>;
private createPromiseResolve!: (value?: void | PromiseLike<void>) => void;
MapKey: string;
MapUrlFile: string;
RoomId: string;
instance: string;
@ -133,7 +140,6 @@ export class GameScene extends ResizableScene implements CenterListener {
y: -1000
}
private PositionNextScene: Array<Array<{ key: string, hash: string }>> = new Array<Array<{ key: string, hash: string }>>();
private presentationModeSprite!: Sprite;
private chatModeSprite!: Sprite;
private gameMap!: GameMap;
@ -142,18 +148,11 @@ export class GameScene extends ResizableScene implements CenterListener {
private outlinedItem: ActionableItem|null = null;
private userInputManager!: UserInputManager;
private isReconnecting: boolean = false;
private startLayerName!: string | null;
static createFromUrl(room: Room, mapUrlFile: string, gameSceneKey: string|null = null): GameScene {
// We use the map URL as a key
if (gameSceneKey === null) {
gameSceneKey = mapUrlFile;
}
return new GameScene(room, mapUrlFile, gameSceneKey);
}
constructor(private room: Room, MapUrlFile: string, gameSceneKey: string) {
constructor(private room: Room, MapUrlFile: string) {
super({
key: gameSceneKey
key: room.id
});
this.GameManager = gameManager;
@ -161,7 +160,6 @@ export class GameScene extends ResizableScene implements CenterListener {
this.groups = new Map<number, Sprite>();
this.instance = room.getInstance();
this.MapKey = MapUrlFile;
this.MapUrlFile = MapUrlFile;
this.RoomId = room.id;
@ -180,15 +178,15 @@ export class GameScene extends ResizableScene implements CenterListener {
file: file.src
});
});
this.load.on('filecomplete-tilemapJSON-'+this.MapKey, (key: string, type: string, data: unknown) => {
this.load.on('filecomplete-tilemapJSON-'+this.MapUrlFile, (key: string, type: string, data: unknown) => {
this.onMapLoad(data);
});
//TODO strategy to add access token
this.load.tilemapTiledJSON(this.MapKey, this.MapUrlFile);
this.load.tilemapTiledJSON(this.MapUrlFile, this.MapUrlFile);
// If the map has already been loaded as part of another GameScene, the "on load" event will not be triggered.
// In this case, we check in the cache to see if the map is here and trigger the event manually.
if (this.cache.tilemap.exists(this.MapKey)) {
const data = this.cache.tilemap.get(this.MapKey);
if (this.cache.tilemap.exists(this.MapUrlFile)) {
const data = this.cache.tilemap.get(this.MapUrlFile);
this.onMapLoad(data);
}
@ -299,7 +297,7 @@ export class GameScene extends ResizableScene implements CenterListener {
//hook initialisation
init(initData : GameSceneInitInterface) {
if (initData.initPosition !== undefined) {
this.initPosition = initData.initPosition;
this.initPosition = initData.initPosition; //todo: still used?
}
if (initData.initPosition !== undefined) {
this.isReconnecting = initData.reconnecting;
@ -308,8 +306,11 @@ export class GameScene extends ResizableScene implements CenterListener {
//hook create scene
create(): void {
urlManager.pushRoomIdToUrl(this.room);
this.startLayerName = urlManager.getStartLayerNameFromUrl();
//initalise map
this.Map = this.add.tilemap(this.MapKey);
this.Map = this.add.tilemap(this.MapUrlFile);
this.gameMap = new GameMap(this.mapFile);
const mapDirUrl = this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf('/'));
this.mapFile.tilesets.forEach((tileset: ITiledTileSet) => {
@ -319,25 +320,21 @@ export class GameScene extends ResizableScene implements CenterListener {
//permit to set bound collision
this.physics.world.setBounds(0, 0, this.Map.widthInPixels, this.Map.heightInPixels);
// Let's alter browser history
let path = this.room.id;
if (this.room.hash) {
path += '#'+this.room.hash;
}
window.history.pushState({}, 'WorkAdventure', path);
//add layer on map
this.Layers = new Array<Phaser.Tilemaps.StaticTilemapLayer>();
let depth = -2;
for (const layer of this.mapFile.layers) {
if (layer.type === 'tilelayer') {
this.addLayer(this.Map.createStaticLayer(layer.name, this.Terrains, 0, 0).setDepth(depth));
const exitSceneUrl = this.getExitSceneUrl(layer);
if (exitSceneUrl !== undefined) {
this.loadNextGame(exitSceneUrl);
}
const exitUrl = this.getExitUrl(layer);
if (exitUrl !== undefined) {
this.loadNextGame(exitUrl);
}
if (layer.type === 'tilelayer' && this.getExitSceneUrl(layer) !== undefined) {
this.loadNextGameFromExitSceneUrl(layer, this.mapFile.width);
} else if (layer.type === 'tilelayer' && this.getExitUrl(layer) !== undefined) {
console.log('Loading exitUrl ', this.getExitUrl(layer))
this.loadNextGameFromExitUrl(layer, this.mapFile.width);
}
if (layer.type === 'objectgroup' && layer.name === 'floorLayer') {
depth = 10000;
@ -347,51 +344,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.');
}
// 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) {
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;
}
this.initStartXAndStartY();
//add entities
this.Objects = new Array<Phaser.Physics.Arcade.Sprite>();
//init event click
this.EventToClickOnTile();
//initialise list of other player
this.MapPlayers = this.physics.add.group({immovable: true});
//create input to move
this.userInputManager = new UserInputManager(this);
mediaManager.setUserInputManager(this.userInputManager);
//notify game manager can to create currentUser in map
this.createCurrentPlayer();
@ -476,35 +439,7 @@ export class GameScene extends ResizableScene implements CenterListener {
// From now, this game scene will be notified of reposition events
layoutManager.setListener(this);
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);
}
});
this.triggerOnMapLayerPropertyChange();
const camera = this.cameras.main;
@ -543,7 +478,6 @@ export class GameScene extends ResizableScene implements CenterListener {
if (position === undefined) {
throw new Error('Position missing from UserMovedMessage');
}
//console.log('Received position ', position.getX(), position.getY(), "from user", message.getUserid());
const messageUserMoved: MessageUserMovedInterface = {
userId: message.getUserid(),
@ -576,7 +510,7 @@ export class GameScene extends ResizableScene implements CenterListener {
this.simplePeer.unregister();
const gameSceneKey = 'somekey' + Math.round(Math.random() * 10000);
const game: Phaser.Scene = GameScene.createFromUrl(this.room, this.MapUrlFile, gameSceneKey);
const game: Phaser.Scene = new GameScene(this.room, this.MapUrlFile);
this.scene.add(gameSceneKey, game, true,
{
initPosition: {
@ -607,7 +541,7 @@ export class GameScene extends ResizableScene implements CenterListener {
});
// 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.UserMessageManager = new UserMessageManager(this.connection);
@ -638,15 +572,103 @@ export class GameScene extends ResizableScene implements CenterListener {
if (this.connection.hasTag('admin')) {
this.ConsoleGlobalMessageManager = new ConsoleGlobalMessageManager(this.connection, this.userInputManager);
}
console.log('wakingup');
this.scene.wake();
this.scene.sleep(ReconnectingSceneName);
//init user position and play trigger to check layers properties
this.gameMap.setPosition(this.CurrentPlayer.x, this.CurrentPlayer.y);
return this.connection;
});
}
private triggerOnMapLayerPropertyChange(){
this.gameMap.onPropertyChange('exitSceneUrl', (newValue, oldValue) => {
if (newValue) this.onMapExit(newValue as string);
});
this.gameMap.onPropertyChange('exitUrl', (newValue, oldValue) => {
if (newValue) this.onMapExit(newValue as string);
});
this.gameMap.onPropertyChange('openWebsite', (newValue, oldValue, allProps) => {
if (newValue === undefined) {
layoutManager.removeActionButton('openWebsite', this.userInputManager);
coWebsiteManager.closeCoWebsite();
}else{
const openWebsiteFunction = () => {
coWebsiteManager.loadCoWebsite(newValue as string);
layoutManager.removeActionButton('openWebsite', this.userInputManager);
};
const openWebsiteTriggerValue = allProps.get(TRIGGER_WEBSITE_PROPERTIES);
if(openWebsiteTriggerValue && openWebsiteTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
layoutManager.addActionButton('openWebsite', 'Click on SPACE to open the web site', () => {
openWebsiteFunction();
}, this.userInputManager);
}else{
openWebsiteFunction();
}
}
});
this.gameMap.onPropertyChange('jitsiRoom', (newValue, oldValue, allProps) => {
if (newValue === undefined) {
layoutManager.removeActionButton('jitsiRoom', this.userInputManager);
this.stopJitsi();
}else{
const openJitsiRoomFunction = () => {
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);
}
layoutManager.removeActionButton('jitsiRoom', this.userInputManager);
}
const jitsiTriggerValue = allProps.get(TRIGGER_JITSI_PROPERTIES);
if(jitsiTriggerValue && jitsiTriggerValue === ON_ACTION_TRIGGER_BUTTON) {
layoutManager.addActionButton('jitsiRoom', 'Click on SPACE to enter in jitsi meet room', () => {
openJitsiRoomFunction();
}, this.userInputManager);
}else{
openJitsiRoomFunction();
}
}
})
this.gameMap.onPropertyChange('silent', (newValue, oldValue) => {
if (newValue === undefined || newValue === false || newValue === '') {
this.connection.setSilent(false);
} else {
this.connection.setSilent(true);
}
});
}
private onMapExit(exitKey: string) {
const {roomId, hash} = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance);
if (!roomId) throw new Error('Could not find the room from its exit key: '+exitKey);
urlManager.pushStartLayerNameToUrl(hash);
if (roomId !== 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(roomId);
} else {
//if the exit points to the current map, we simply teleport the user back to the startLayer
this.initPositionFromLayerName(hash || defaultStartLayerName);
this.CurrentPlayer.x = this.startX;
this.CurrentPlayer.y = this.startY;
}
}
private switchLayoutMode(): void {
//if discussion is activated, this layout cannot be activated
if(mediaManager.activatedDiscussion){
return;
}
const mode = layoutManager.getLayoutMode();
if (mode === LayoutMode.Presentation) {
layoutManager.switchLayoutMode(LayoutMode.VideoChat);
@ -659,18 +681,51 @@ 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.startLayerName) {
this.initPositionFromLayerName(this.startLayerName);
}
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(defaultStartLayerName);
}
}
// 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 === defaultStartLayerName || this.isStartLayer(layer))) {
const startPosition = this.startUser(layer);
this.startX = startPosition.x;
this.startY = startPosition.y;
}
}
}
private getExitUrl(layer: ITiledMapLayer): string|undefined {
return this.getProperty(layer, "exitUrl") as string|undefined;
}
/**
* @deprecated the map property exitSceneUrl is deprecated
*/
private getExitSceneUrl(layer: ITiledMapLayer): string|undefined {
return this.getProperty(layer, "exitSceneUrl") as string|undefined;
}
private getExitSceneInstance(layer: ITiledMapLayer): string|undefined {
return this.getProperty(layer, "exitInstance") as string|undefined;
}
private isStartLayer(layer: ITiledMapLayer): boolean {
return this.getProperty(layer, "startLayer") == true;
}
@ -680,85 +735,20 @@ export class GameScene extends ResizableScene implements CenterListener {
if (!properties) {
return undefined;
}
const obj = properties.find((property: ITiledMapLayerProperty) => property.name === name);
const obj = properties.find((property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase());
if (obj === undefined) {
return undefined;
}
return obj.value;
}
private loadNextGameFromExitSceneUrl(layer: ITiledMapLayer, mapWidth: number) {
const exitSceneUrl = this.getExitSceneUrl(layer);
if (exitSceneUrl === undefined) {
throw new Error('Layer is not an exit scene layer.');
}
let instance = this.getExitSceneInstance(layer);
if (instance === undefined) {
instance = this.instance;
}
//console.log('existSceneUrl', exitSceneUrl);
//console.log('existSceneInstance', instance);
const absoluteExitSceneUrl = new URL(exitSceneUrl, this.MapUrlFile).href;
const absoluteExitSceneUrlWithoutProtocol = absoluteExitSceneUrl.toString().substr(absoluteExitSceneUrl.toString().indexOf('://')+3);
const roomId = '_/'+instance+'/'+absoluteExitSceneUrlWithoutProtocol;
this.loadNextGame(layer, mapWidth, roomId);
}
private loadNextGameFromExitUrl(layer: ITiledMapLayer, mapWidth: number) {
const exitUrl = this.getExitUrl(layer);
if (exitUrl === undefined) {
throw new Error('Layer is not an exit layer.');
}
const fullPath = new URL(exitUrl, window.location.toString()).pathname;
this.loadNextGame(layer, mapWidth, fullPath);
}
/**
*
* @param layer
* @param mapWidth
*/
//todo: push that into the gameManager
private loadNextGame(layer: ITiledMapLayer, mapWidth: number, roomId: string){
private async loadNextGame(exitSceneIdentifier: string){
const {roomId, hash} = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance);
const room = new Room(roomId);
gameManager.loadMap(room, this.scene);
const exitSceneKey = roomId;
const tiles : number[] = layer.data as number[];
for (let key=0; key < tiles.length; key++) {
const objectKey = tiles[key];
if(objectKey === 0){
continue;
}
//key + 1 because the start x = 0;
const y : number = parseInt(((key + 1) / mapWidth).toString());
const x : number = key - (y * mapWidth);
let hash = new URL(roomId, this.MapUrlFile).hash;
if (hash) {
hash = hash.substr(1);
await gameManager.loadMap(room, this.scene);
}
//push and save switching case
if (this.PositionNextScene[y] === undefined) {
this.PositionNextScene[y] = new Array<{key: string, hash: string}>();
}
room.getMapUrl().then((url: string) => {
this.PositionNextScene[y][x] = {
key: url,
hash
}
})
}
}
/**
* @param layer
*/
private startUser(layer: ITiledMapLayer): PositionInterface {
const tiles = layer.data;
if (typeof(tiles) === 'string') {
@ -914,22 +904,12 @@ 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 delta The delta time in ms since the last frame. This is a smoothed and capped value based on the FPS rate.
*/
update(time: number, delta: number) : void {
mediaManager.setLastUpdateScene();
this.currentTick = time;
this.CurrentPlayer.moveUser(delta);
@ -967,27 +947,6 @@ export class GameScene extends ResizableScene implements CenterListener {
}
player.updatePosition(moveEvent);
});
const nextSceneKey = this.checkToExit();
if (nextSceneKey) {
// 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);
}
}
private checkToExit(): {key: string, hash: string} | null {
const x = Math.floor(this.CurrentPlayer.x / 32);
const y = Math.floor(this.CurrentPlayer.y / 32);
if (this.PositionNextScene[y] !== undefined && this.PositionNextScene[y][x] !== undefined) {
return this.PositionNextScene[y][x];
} else {
return null;
}
}
/**
@ -1077,11 +1036,6 @@ export class GameScene extends ResizableScene implements CenterListener {
await Promise.all(loadPromises);
player.addTextures(characterLayerList, 1);
//init collision
/*this.physics.add.collider(this.CurrentPlayer, player, (CurrentPlayer: CurrentGamerInterface, MapPlayer: GamerInterface) => {
CurrentPlayer.say("Hello, how are you ? ");
});*/
}
/**
@ -1124,7 +1078,6 @@ export class GameScene extends ResizableScene implements CenterListener {
// 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.
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);
}
@ -1173,11 +1126,6 @@ export class GameScene extends ResizableScene implements CenterListener {
/**
* 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) {
this.connection.emitActionableEvent(itemId, eventName, state, parameters);
@ -1230,12 +1178,19 @@ export class GameScene extends ResizableScene implements CenterListener {
jitsiFactory.start(roomName, gameManager.getPlayerName(), jwt);
this.connection.setSilent(true);
mediaManager.hideGameOverlay();
//permit to stop jitsi when user close iframe
mediaManager.addTriggerCloseJitsiFrameButton('close-jisi',() => {
this.stopJitsi();
});
}
public stopJitsi(): void {
this.connection.setSilent(false);
jitsiFactory.stop();
mediaManager.showGameOverlay();
mediaManager.removeTriggerCloseJitsiFrameButton('close-jisi');
}
private loadSpritesheet(name: string, url: string): Promise<void> {

View File

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

@ -31,7 +31,8 @@ export class EntryScene extends Scene {
create() {
gameManager.init(this.scene).then(() => {
this.scene.start(LoginSceneName);
}).catch(() => {
}).catch((err) => {
console.error(err)
this.scene.start(FourOFourSceneName, {
url: window.location.pathname.toString()
});

View File

@ -79,4 +79,11 @@ export class UserInputManager {
return event;
});
}
addSpaceEventListner(callback : Function){
this.Scene.input.keyboard.addListener('keyup-SPACE', callback);
}
removeSpaceEventListner(callback : Function){
this.Scene.input.keyboard.removeListener('keyup-SPACE', callback);
}
}

View File

@ -1,3 +1,4 @@
import {Room} from "../Connexion/Room";
export enum GameConnexionTypes {
anonymous=1,
@ -45,6 +46,20 @@ class UrlManager {
return newUrl;
}
public pushRoomIdToUrl(room:Room): void {
if (window.location.pathname === room.id) return;
const hash = window.location.hash;
history.pushState({}, 'WorkAdventure', room.id+hash);
}
public getStartLayerNameFromUrl(): string|null {
const hash = window.location.hash;
return hash.length > 1 ? hash.substring(1) : null;
}
pushStartLayerNameToUrl(startLayerName: string): void {
window.location.hash = startLayerName;
}
}
export const urlManager = new UrlManager();

View File

@ -21,31 +21,32 @@ class CoWebsiteManager {
* So we use this promise to queue up every cowebsite state transition
*/
private currentOperationPromise: Promise<void> = Promise.resolve();
private cowebsiteDiv: HTMLDivElement;
private close(): HTMLDivElement {
const cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteDivId);
cowebsiteDiv.classList.remove('loaded'); //edit the css class to trigger the transition
cowebsiteDiv.classList.add('hidden');
constructor() {
this.cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteDivId);
}
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;
return cowebsiteDiv;
}
private load(): HTMLDivElement {
const cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteDivId);
cowebsiteDiv.classList.remove('hidden'); //edit the css class to trigger the transition
cowebsiteDiv.classList.add('loading');
private load(): void {
this.cowebsiteDiv.classList.remove('hidden'); //edit the css class to trigger the transition
this.cowebsiteDiv.classList.add('loading');
this.opened = iframeStates.loading;
return cowebsiteDiv;
}
private open(): HTMLDivElement {
const cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(cowebsiteDivId);
cowebsiteDiv.classList.remove('loading', 'hidden'); //edit the css class to trigger the transition
private open(): void {
this.cowebsiteDiv.classList.remove('loading', 'hidden'); //edit the css class to trigger the transition
this.opened = iframeStates.opened;
return cowebsiteDiv;
}
public loadCoWebsite(url: string): void {
const cowebsiteDiv = this.load();
cowebsiteDiv.innerHTML = '';
this.load();
this.cowebsiteDiv.innerHTML = `<button class="close-btn" id="cowebsite-close">
<img src="resources/logos/close.svg">
</button>`;
const iframe = document.createElement('iframe');
iframe.id = 'cowebsite-iframe';
@ -53,7 +54,7 @@ class CoWebsiteManager {
const onloadPromise = new Promise((resolve) => {
iframe.onload = () => resolve();
});
cowebsiteDiv.appendChild(iframe);
this.cowebsiteDiv.appendChild(iframe);
const onTimeoutPromise = new Promise((resolve) => {
setTimeout(() => resolve(), 2000);
});
@ -69,23 +70,25 @@ class CoWebsiteManager {
* Just like loadCoWebsite but the div can be filled by the user.
*/
public insertCoWebsite(callback: (cowebsite: HTMLDivElement) => Promise<void>): void {
const cowebsiteDiv = this.load();
this.currentOperationPromise = this.currentOperationPromise.then(() => callback(cowebsiteDiv)).then(() => {
this.load();
this.currentOperationPromise = this.currentOperationPromise.then(() => callback(this.cowebsiteDiv)).then(() => {
this.open();
setTimeout(() => {
this.fire();
}, animationTime)
}, animationTime);
}).catch(() => this.closeCoWebsite());
}
public closeCoWebsite(): Promise<void> {
this.currentOperationPromise = this.currentOperationPromise.then(() => new Promise((resolve, reject) => {
if(this.opened === iframeStates.closed) resolve(); //this method may be called twice, in case of iframe error for example
const cowebsiteDiv = this.close();
this.close();
this.fire();
setTimeout(() => {
this.cowebsiteDiv.innerHTML = `<button class="close-btn" id="cowebsite-close">
<img src="resources/logos/close.svg">
</button>`;
resolve();
setTimeout(() => cowebsiteDiv.innerHTML = '', 500)
}, animationTime)
}));
return this.currentOperationPromise;
@ -111,6 +114,7 @@ class CoWebsiteManager {
}
}
//todo: is it still useful to allow any kind of observers?
public onStateChange(observer: CoWebsiteStateChangedCallback) {
this.observers.push(observer);
}

View File

@ -0,0 +1,238 @@
import {HtmlUtils} from "./HtmlUtils";
import {MediaManager, ReportCallback, UpdatedLocalStreamCallback} from "./MediaManager";
import {UserInputManager} from "../Phaser/UserInput/UserInputManager";
import {connectionManager} from "../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../Url/UrlManager";
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
&& connectionManager.getConnexionType
&& connectionManager.getConnexionType !== GameConnexionTypes.anonymous
) {
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;
}
return new Promise((resolve) => {
return new Promise((resolve, reject) => {
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.executeCommand('displayName', playerName);
@ -62,6 +63,9 @@ class JitsiFactory {
}
public async stop(): Promise<void> {
if(!this.jitsiApi){
return;
}
await coWebsiteManager.closeCoWebsite();
this.jitsiApi.removeListener('audioMuteStatusChanged', this.audioCallback);
this.jitsiApi.removeListener('videoMuteStatusChanged', this.videoCallback);

View File

@ -1,3 +1,4 @@
import { UserInputManager } from "../Phaser/UserInput/UserInputManager";
import {HtmlUtils} from "./HtmlUtils";
export enum LayoutMode {
@ -22,6 +23,10 @@ export interface CenterListener {
onCenterChange(): void;
}
export const ON_ACTION_TRIGGER_BUTTON = 'onaction';
export const TRIGGER_WEBSITE_PROPERTIES = 'openWebsiteTrigger';
export const TRIGGER_JITSI_PROPERTIES = 'jitsiTrigger';
/**
* This class is in charge of the video-conference layout.
* It receives positioning requests for videos and does its best to place them on the screen depending on the active layout mode.
@ -33,6 +38,9 @@ class LayoutManager {
private normalDivs: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
private listener: CenterListener|null = null;
private actionButtonTrigger: Map<string, Function> = new Map<string, Function>();
private actionButtonInformation: Map<string, HTMLDivElement> = new Map<string, HTMLDivElement>();
public setListener(centerListener: CenterListener|null) {
this.listener = centerListener;
}
@ -305,6 +313,48 @@ class LayoutManager {
}
}
}
public addActionButton(id: string, text: string, callBack: Function, userInputManager: UserInputManager){
//delete previous element
this.removeActionButton(id, userInputManager);
//create div and text html component
const p = document.createElement('p');
p.classList.add('action-body');
p.innerText = text;
const div = document.createElement('div');
div.classList.add('action');
div.id = id;
div.appendChild(p);
this.actionButtonInformation.set(id, div);
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
mainContainer.appendChild(div);
const callBackFunctionTrigger = (() => {
console.log('user click on space => ', id);
callBack();
});
//add trigger action
this.actionButtonTrigger.set(id, callBackFunctionTrigger);
userInputManager.addSpaceEventListner(callBackFunctionTrigger);
}
public removeActionButton(id: string, userInputManager: UserInputManager){
//delete previous element
const previousDiv = this.actionButtonInformation.get(id);
if(previousDiv){
previousDiv.remove();
this.actionButtonInformation.delete(id);
}
const previousEventCallback = this.actionButtonTrigger.get(id);
if(previousEventCallback){
userInputManager.removeSpaceEventListner(previousEventCallback);
}
}
}
const layoutManager = new LayoutManager();

View File

@ -1,5 +1,7 @@
import {DivImportance, layoutManager} from "./LayoutManager";
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
const videoConstraint: boolean|MediaTrackConstraints = {
@ -38,59 +40,99 @@ export class MediaManager {
private cinemaBtn: HTMLDivElement;
private monitorBtn: HTMLDivElement;
private previousConstraint : MediaStreamConstraints;
private focused : boolean = true;
private lastUpdateScene : Date = new Date();
private setTimeOutlastUpdateScene? : NodeJS.Timeout;
private discussionManager: DiscussionManager;
private userInputManager?: UserInputManager;
private hasCamera = true;
private triggerCloseJistiFrame : Map<String, Function> = new Map<String, Function>();
constructor() {
this.myCamVideo = this.getElementByIdOrFail<HTMLVideoElement>('myCamVideo');
this.webrtcInAudio = this.getElementByIdOrFail<HTMLAudioElement>('audio-webrtc-in');
this.myCamVideo = HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('myCamVideo');
this.webrtcInAudio = HtmlUtils.getElementByIdOrFail<HTMLAudioElement>('audio-webrtc-in');
this.webrtcInAudio.volume = 0.2;
this.microphoneBtn = this.getElementByIdOrFail<HTMLDivElement>('btn-micro');
this.microphoneClose = this.getElementByIdOrFail<HTMLImageElement>('microphone-close');
this.microphoneBtn = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('btn-micro');
this.microphoneClose = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('microphone-close');
this.microphoneClose.style.display = "none";
this.microphoneClose.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.enableMicrophone();
//update tracking
});
this.microphone = this.getElementByIdOrFail<HTMLImageElement>('microphone');
this.microphone = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('microphone');
this.microphone.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.disableMicrophone();
//update tracking
});
this.cinemaBtn = this.getElementByIdOrFail<HTMLDivElement>('btn-video');
this.cinemaClose = this.getElementByIdOrFail<HTMLImageElement>('cinema-close');
this.cinemaBtn = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('btn-video');
this.cinemaClose = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('cinema-close');
this.cinemaClose.style.display = "none";
this.cinemaClose.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.enableCamera();
//update tracking
});
this.cinema = this.getElementByIdOrFail<HTMLImageElement>('cinema');
this.cinema = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('cinema');
this.cinema.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.disableCamera();
//update tracking
});
this.monitorBtn = this.getElementByIdOrFail<HTMLDivElement>('btn-monitor');
this.monitorClose = this.getElementByIdOrFail<HTMLImageElement>('monitor-close');
this.monitorBtn = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('btn-monitor');
this.monitorClose = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('monitor-close');
this.monitorClose.style.display = "block";
this.monitorClose.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.enableScreenSharing();
//update tracking
});
this.monitor = this.getElementByIdOrFail<HTMLImageElement>('monitor');
this.monitor = HtmlUtils.getElementByIdOrFail<HTMLImageElement>('monitor');
this.monitor.style.display = "none";
this.monitor.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
this.disableScreenSharing();
//update tracking
});
this.previousConstraint = JSON.parse(JSON.stringify(this.constraintsMedia));
this.pingCameraStatus();
this.checkActiveUser(); //todo: desactivated in case of bug
this.discussionManager = new DiscussionManager(this,'');
}
public setLastUpdateScene(){
this.lastUpdateScene = new Date();
}
public blurCamera() {
if(!this.focused){
return;
}
this.focused = false;
this.previousConstraint = JSON.parse(JSON.stringify(this.constraintsMedia));
this.disableCamera();
}
public focusCamera() {
if(this.focused){
return;
}
this.focused = true;
this.applyPreviousConfig();
}
public onUpdateLocalStream(callback: UpdatedLocalStreamCallback): void {
@ -128,22 +170,29 @@ export class MediaManager {
}
public showGameOverlay(){
const gameOverlay = this.getElementByIdOrFail('game-overlay');
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
gameOverlay.classList.add('active');
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail('cowebsite-close');
const functionTrigger = () => {
this.triggerCloseJitsiFrameButton();
}
buttonCloseFrame.removeEventListener('click', functionTrigger);
}
public hideGameOverlay(){
const gameOverlay = this.getElementByIdOrFail('game-overlay');
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
gameOverlay.classList.remove('active');
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail('cowebsite-close');
const functionTrigger = () => {
this.triggerCloseJitsiFrameButton();
}
buttonCloseFrame.addEventListener('click', functionTrigger);
}
public enableCamera() {
if(!this.hasCamera){
return;
}
this.cinemaClose.style.display = "none";
this.cinemaBtn.classList.remove("disabled");
this.cinema.style.display = "block";
this.enableCameraStyle();
this.constraintsMedia.video = videoConstraint;
this.getCamera().then((stream: MediaStream) => {
this.triggerUpdatedLocalStreamCallbacks(stream);
@ -151,7 +200,8 @@ export class MediaManager {
}
public async disableCamera() {
this.disabledCameraView();
this.disableCameraStyle();
if (this.constraintsMedia.audio !== false) {
const stream = await this.getCamera();
this.triggerUpdatedLocalStreamCallbacks(stream);
@ -160,19 +210,8 @@ 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() {
this.microphoneClose.style.display = "none";
this.microphone.style.display = "block";
this.microphoneBtn.classList.remove("disabled");
this.enableMicrophoneStyle();
this.constraintsMedia.audio = true;
this.getCamera().then((stream) => {
@ -181,10 +220,7 @@ export class MediaManager {
}
public async disableMicrophone() {
this.microphoneClose.style.display = "block";
this.microphone.style.display = "none";
this.microphoneBtn.classList.add("disabled");
this.constraintsMedia.audio = false;
this.disableMicrophoneStyle();
this.stopMicrophone();
if (this.constraintsMedia.video !== false) {
@ -195,6 +231,52 @@ export class MediaManager {
}
}
private applyPreviousConfig() {
this.constraintsMedia = this.previousConstraint;
if(!this.constraintsMedia.video){
this.disableCameraStyle();
}else{
this.enableCameraStyle();
}
if(!this.constraintsMedia.audio){
this.disableMicrophoneStyle()
}else{
this.enableMicrophoneStyle()
}
this.getCamera().then((stream: MediaStream) => {
this.triggerUpdatedLocalStreamCallbacks(stream);
});
}
private enableCameraStyle(){
this.cinemaClose.style.display = "none";
this.cinemaBtn.classList.remove("disabled");
this.cinema.style.display = "block";
}
private disableCameraStyle(){
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();
}
private enableMicrophoneStyle(){
this.microphoneClose.style.display = "none";
this.microphone.style.display = "block";
this.microphoneBtn.classList.remove("disabled");
}
private disableMicrophoneStyle(){
this.microphoneClose.style.display = "block";
this.microphone.style.display = "none";
this.microphoneBtn.classList.add("disabled");
this.constraintsMedia.audio = false;
}
private enableScreenSharing() {
this.monitorClose.style.display = "none";
this.monitor.style.display = "block";
@ -277,7 +359,7 @@ export class MediaManager {
return this.getLocalStream().catch(() => {
console.info('Error get camera, trying with video option at null');
this.disabledCameraView();
this.disableCameraStyle();
return this.getLocalStream().then((stream : MediaStream) => {
this.hasCamera = false;
return stream;
@ -372,14 +454,17 @@ export class MediaManager {
layoutManager.add(DivImportance.Normal, userId, html);
if (reportCallBack) {
const reportBtn = this.getElementByIdOrFail<HTMLDivElement>(`report-${userId}`);
const reportBtn = HtmlUtils.getElementByIdOrFail<HTMLDivElement>(`report-${userId}`);
reportBtn.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
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){
@ -393,7 +478,7 @@ export class MediaManager {
layoutManager.add(divImportance, userId, html);
this.remoteVideo.set(userId, this.getElementByIdOrFail<HTMLVideoElement>(userId));
this.remoteVideo.set(userId, HtmlUtils.getElementByIdOrFail<HTMLVideoElement>(userId));
}
disabledMicrophoneByUserId(userId: number){
@ -401,7 +486,7 @@ export class MediaManager {
if(!element){
return;
}
element.classList.add('active')
element.classList.add('active') //todo: why does a method 'disable' add a class 'active'?
}
enabledMicrophoneByUserId(userId: number){
@ -409,7 +494,7 @@ export class MediaManager {
if(!element){
return;
}
element.classList.remove('active')
element.classList.remove('active') //todo: why does a method 'enable' remove a class 'active'?
}
disabledVideoByUserId(userId: number) {
@ -434,7 +519,7 @@ export class MediaManager {
}
}
addStreamRemoteVideo(userId: string, stream : MediaStream){
addStreamRemoteVideo(userId: string, stream : MediaStream): void {
const remoteVideo = this.remoteVideo.get(userId);
if (remoteVideo === undefined) {
throw `Unable to find video for ${userId}`;
@ -454,6 +539,9 @@ export class MediaManager {
removeActiveVideo(userId: string){
layoutManager.remove(userId);
this.remoteVideo.delete(userId);
//permit to remove user in discussion part
this.removeParticipant(userId);
}
removeActiveScreenSharingVideo(userId: string) {
this.removeActiveVideo(`screen-sharing-${userId}`)
@ -516,18 +604,9 @@ export class MediaManager {
return color;
}
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;
}
private showReportModal(userId: string, userName: string, reportCallBack: ReportCallback){
public showReportModal(userId: string, userName: string, reportCallBack: ReportCallback){
//create report text area
const mainContainer = this.getElementByIdOrFail<HTMLDivElement>('main-container');
const mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
const divReport = document.createElement('div');
divReport.classList.add('modal-report-user');
@ -582,7 +661,73 @@ export class MediaManager {
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 addTriggerCloseJitsiFrameButton(id: String, Function: Function){
this.triggerCloseJistiFrame.set(id, Function);
}
public removeTriggerCloseJitsiFrameButton(id: String){
this.triggerCloseJistiFrame.delete(id);
}
private triggerCloseJitsiFrameButton(): void {
for (const callback of this.triggerCloseJistiFrame.values()) {
callback();
}
}
/**
* For some reasons, the microphone muted icon or the stream is not always up to date.
* Here, every 30 seconds, we are "reseting" the streams and sending again the constraints to the other peers via the data channel again (see SimplePeer::pushVideoToRemoteUser)
**/
private pingCameraStatus(){
/*setInterval(() => {
console.log('ping camera status');
this.triggerUpdatedLocalStreamCallbacks(this.localStream);
}, 30000);*/
}
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);
}
//check if user is active
private checkActiveUser(){
if(this.setTimeOutlastUpdateScene){
clearTimeout(this.setTimeOutlastUpdateScene);
}
this.setTimeOutlastUpdateScene = setTimeout(() => {
const now = new Date();
//if last update is more of 10 sec
if( (now.getTime() - this.lastUpdateScene.getTime()) > 10000) {
this.blurCamera();
}else{
this.focusCamera();
}
this.checkActiveUser();
}, this.focused ? 10000 : 1000);
}
}
export const mediaManager = new MediaManager();

View File

@ -2,6 +2,7 @@ import * as SimplePeerNamespace from "simple-peer";
import {mediaManager} from "./MediaManager";
import {TURN_SERVER, TURN_USER, TURN_PASSWORD} from "../Enum/EnvironmentVariable";
import {RoomConnection} from "../Connexion/RoomConnection";
import {MESSAGE_TYPE_CONSTRAINT} from "./VideoPeer";
const Peer: SimplePeerNamespace.SimplePeer = require('simple-peer');
@ -148,6 +149,6 @@ export class ScreenSharingPeer extends Peer {
public stopPushingScreenSharingToRemoteUser(stream: MediaStream) {
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

@ -1,7 +1,6 @@
import {
WebRtcDisconnectMessageInterface,
WebRtcSignalReceivedMessageInterface,
WebRtcStartMessageInterface
} from "../Connexion/ConnexionModels";
import {
mediaManager,
@ -10,7 +9,7 @@ import {
UpdatedLocalStreamCallback
} from "./MediaManager";
import {ScreenSharingPeer} from "./ScreenSharingPeer";
import {VideoPeer} from "./VideoPeer";
import {MESSAGE_TYPE_CONSTRAINT, MESSAGE_TYPE_MESSAGE, VideoPeer} from "./VideoPeer";
import {RoomConnection} from "../Connexion/RoomConnection";
export interface UserSimplePeerInterface{
@ -29,7 +28,7 @@ export interface PeerConnectionListener {
* This class manages connections to all the peers in the same group as me.
*/
export class SimplePeer {
private Users: Array<UserSimplePeerInterface> = new Array<UserSimplePeerInterface>();
private Users: Array<UserSimplePeerInterface> = new Array<UserSimplePeerInterface>(); //todo: this array should be fusionned with PeerConnectionArray
private PeerScreenSharingConnectionArray: Map<number, ScreenSharingPeer> = new Map<number, ScreenSharingPeer>();
private PeerConnectionArray: Map<number, VideoPeer> = new Map<number, VideoPeer>();
@ -38,7 +37,7 @@ export class SimplePeer {
private readonly stopLocalScreenSharingStreamCallback: StopScreenSharingCallback;
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.
this.sendLocalVideoStreamCallback = this.sendLocalVideoStream.bind(this);
this.sendLocalScreenSharingStreamCallback = this.sendLocalScreenSharingStream.bind(this);
@ -95,12 +94,9 @@ export class SimplePeer {
this.Users.push(user);
// Note: the clients array contain the list of all clients (even the ones we are already connected to in case a user joints a group)
// So we can receive a request we already had before. (which will abort at the first line of createPeerConnection)
// TODO: refactor this to only send a message to connect to one user (rather than several users). => DONE
// This would be symmetrical to the way we handle disconnection.
//console.log('Start message', data);
//start connection
//this.startWebRtc();
console.log('receiveWebrtcStart. Initiator: ', user.initiator)
if(!user.initiator){
return;
@ -145,6 +141,12 @@ export class SimplePeer {
mediaManager.addActiveVideo("" + user.userId, reportCallback, name);
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;
// 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!
@ -198,8 +200,6 @@ export class SimplePeer {
/**
* This is triggered twice. Once by the server, and once by a remote client disconnecting
*
* @param userId
*/
private closeConnection(userId : number) {
try {
@ -220,6 +220,12 @@ export class SimplePeer {
for (const peerConnectionListener of this.peerConnectionListeners) {
peerConnectionListener.onDisconnect(userId);
}
const userIndex = this.Users.findIndex(user => user.userId === userId);
if(userIndex < 0){
throw 'Couln\'t delete user';
} else {
this.Users.splice(userIndex, 1);
}
} catch (err) {
console.error("closeConnection", err)
}
@ -227,8 +233,6 @@ export class SimplePeer {
/**
* This is triggered twice. Once by the server, and once by a remote client disconnecting
*
* @param userId
*/
private closeScreenSharingConnection(userId : number) {
try {
@ -240,7 +244,6 @@ export class SimplePeer {
}
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.
//console.log('Closing connection with '+userId);
peer.destroy();
if(!this.PeerScreenSharingConnectionArray.delete(userId)){
throw 'Couln\'t delete peer screen sharing connexion';
@ -307,10 +310,6 @@ export class SimplePeer {
}
}
/**
*
* @param userId
*/
private pushVideoToRemoteUser(userId : number) {
try {
const PeerConnection = this.PeerConnectionArray.get(userId);
@ -318,13 +317,16 @@ export class SimplePeer {
throw new Error('While adding media, cannot find user with ID ' + userId);
}
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){
return;
}
for (const track of localStream.getTracks()) {
//todo: this is a ugly hack to reduce the amount of error in console. Find a better way.
if ((track as any).added !== undefined) continue; // eslint-disable-line @typescript-eslint/no-explicit-any
(track as any).added = true; // eslint-disable-line @typescript-eslint/no-explicit-any
PeerConnection.addTrack(track, localStream);
}
}catch (e) {

View File

@ -5,6 +5,8 @@ import {RoomConnection} from "../Connexion/RoomConnection";
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.
*/
@ -78,20 +80,24 @@ export class VideoPeer extends Peer {
});
this.on('data', (chunk: Buffer) => {
const constraint = JSON.parse(chunk.toString('utf8'));
console.log("data", constraint);
if (constraint.audio) {
const message = JSON.parse(chunk.toString('utf8'));
console.log("data", message);
if(message.type === MESSAGE_TYPE_CONSTRAINT) {
if (message.audio) {
mediaManager.enabledMicrophoneByUserId(this.userId);
} else {
mediaManager.disabledMicrophoneByUserId(this.userId);
}
if (constraint.video || constraint.screen) {
if (message.video || message.screen) {
mediaManager.enabledVideoByUserId(this.userId);
} else {
this.stream(undefined);
mediaManager.disabledVideoByUserId(this.userId);
}
} else if(message.type === 'message') {
mediaManager.addNewMessage(message.name, message.message);
}
});
this.once('finish', () => {
@ -112,21 +118,15 @@ export class VideoPeer extends Peer {
/**
* Sends received stream to screen.
*/
private stream(stream?: MediaStream) {
//console.log(`VideoPeer::stream => ${this.userId}`, stream);
if(!stream){
mediaManager.disabledVideoByUserId(this.userId);
mediaManager.disabledMicrophoneByUserId(this.userId);
} else {
private stream(stream: MediaStream) {
try {
mediaManager.addStreamRemoteVideo("" + this.userId, stream);
}catch (err){
console.error(err);
//Force add streem video
setTimeout(() => {
/*setTimeout(() => {
this.stream(stream);
}, 500);
}
}, 500);*/ //todo: find a way to prevent infinite regression.
}
}
@ -163,7 +163,7 @@ export class VideoPeer extends Peer {
private pushVideoToRemoteUser() {
try {
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){
return;

View File

@ -0,0 +1,42 @@
import "jasmine";
import {Room} from "../../../src/Connexion/Room";
describe("Room getIdFromIdentifier()", () => {
it("should work with an absolute room id and no hash as parameter", () => {
const {roomId, hash} = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json', '', '');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual('');
});
it("should work with an absolute room id and a hash as parameters", () => {
const {roomId, hash} = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json#start', '', '');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual("start");
});
it("should work with an absolute room id, regardless of baseUrl or instance", () => {
const {roomId, hash} = Room.getIdFromIdentifier('/_/global/maps.workadventu.re/test2.json', 'https://another.domain/_/global/test.json', 'lol');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual('');
});
it("should work with a relative file link and no hash as parameters", () => {
const {roomId, hash} = Room.getIdFromIdentifier('./test2.json', 'https://maps.workadventu.re/test.json', 'global');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual('');
});
it("should work with a relative file link with no dot", () => {
const {roomId, hash} = Room.getIdFromIdentifier('test2.json', 'https://maps.workadventu.re/test.json', 'global');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual('');
});
it("should work with a relative file link two levels deep", () => {
const {roomId, hash} = Room.getIdFromIdentifier('../floor1/Floor1.json', 'https://maps.workadventu.re/floor0/Floor0.json', 'global');
expect(roomId).toEqual('_/global/maps.workadventu.re/floor1/Floor1.json');
expect(hash).toEqual('');
});
it("should work with a relative file link and a hash as parameters", () => {
const {roomId, hash} = Room.getIdFromIdentifier('./test2.json#start', 'https://maps.workadventu.re/test.json', 'global');
expect(roomId).toEqual('_/global/maps.workadventu.re/test2.json');
expect(hash).toEqual("start");
});
});

View File

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

1
maps/.gitignore vendored
View File

@ -1 +1,2 @@
/node_modules/
dist/

BIN
maps/Floor0/desks.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 96 KiB

BIN
maps/Floor0/logotcm.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
maps/Floor0/signages.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 64 KiB

BIN
maps/Floor0/walls.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

BIN
maps/Floor0/walls2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
maps/Floor0/xmas.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 671 KiB

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

BIN
maps/Floor1/parquet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

2005
maps/Floor2/floor2.json Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

BIN
maps/Floor2/general.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

BIN
maps/Floor2/parquet.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

1577
maps/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,9 @@
import {OK} from "http-status-codes";
import {URL_ROOM_STARTED} from "../Enum/EnvironmentVariable";
import {HttpRequest, HttpResponse, TemplatedApp} from "uWebSockets.js";
import {BaseController} from "./BaseController";
import {parse} from "query-string";
import {adminApi} from "../Services/AdminApi";
//todo: delete this
export class MapController extends BaseController{
constructor(private App : TemplatedApp) {

View File

@ -10,6 +10,9 @@
gtag('js', new Date());
gtag('config', 'UA-10196481-11');
if (window.location.host.endsWith("localhost")){
window['ga-disable-UA-10196481-11'] = true;
}
</script>
<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">
@ -82,22 +85,32 @@
</div>
<div class="col-2 col-md-6">
<div class="social-links">
<span class="share-title">Share your experience</span>
<a onclick="shareFB()">
<img class="social-image" src="static/images/facebook.png" />
<a href="https://www.facebook.com/workadventurebytcm">
<img class="social-image" src="static/images/facebook_bw.png" />
</a>
<a onclick="shareLI()">
<img class="social-image" src="static/images/linkedin.png" />
<a href="https://www.linkedin.com/company/workadventure-by-tcm">
<img class="social-image" src="static/images/linkedin_bw.png" />
</a>
<a onclick="shareTW()">
<img class="social-image" src="static/images/twitter.png" />
<a href="https://twitter.com/Workadventure_">
<img class="social-image" src="static/images/twitter_bw.png" />
</a>
</div>
</div>
</div>
<div class="title title-main text-center">
<h1>Your workplace<br/>but better</h1>
<h3>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"</h3>
<h1>Meet your teammates</h1>
<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 class="row buttons-row justify-content-md-center pt-5">
<div class="col col-lg-3">
@ -107,7 +120,7 @@
</div>
<div class="col col-lg-3">
<a class="custom-link play" target="_BLANK" onclick="startGame()" title="WORK ONLINE">
WORK ONLINE
TRY IT NOW!
</a>
</div>
</div>
@ -158,7 +171,7 @@
<p>Click the button below to come and say hi!</p>
<p class="bubble-action"><span onclick="startGame()">
<img src="static/images/playicon.png" />
START IN PUBLIC MODE
TRY IT NOW !
</span></p>
</div>
</div>
@ -166,10 +179,11 @@
<div>
<p>You can also create a private room with your friends or your team ! </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">
<a href="/choose-map.html">
<img src="static/images/playicon.png" />
START WORKING IN PRIVATE
</a></p>
CHOOSE YOU OWN MAP
</a>
<p>
Dont forget to activate your mic and camera, lets play
</p>
@ -179,7 +193,6 @@
<img src="static/images/story/character-walk-right.gif" style="display:none;" />
</section>
<script>
gsap.to(".title-main", {
//y:-1000,
scale: 0,
@ -308,6 +321,57 @@
</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 class="section bg-white">

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

@ -2251,9 +2251,9 @@
}
},
"dot-prop": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz",
"integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==",
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.1.tgz",
"integrity": "sha512-l0p4+mIuJIua0mhxGoh4a+iNL9bmeK5DvnSVQa6T0OhrVmaEa1XScX5Etc673FePCJOArq/4Pa2cLGODUWTPOQ==",
"dev": true,
"requires": {
"is-obj": "^1.0.0"

View File

@ -80,11 +80,17 @@ header {
h1 {
font-family: 'Karmatic Arcade';
font-size: 2.75rem;
margin-bottom: 1.25rem;
margin-bottom: 2rem;
}
h3 {
min-height: 200px;
padding: 1rem;
text-align: left;
padding: 1rem 0 0 1rem;
&:before{
content: ">";
position: absolute;
left: -10px;
}
}
@include media-breakpoint-down(xs) {
h3 {
@ -152,7 +158,6 @@ header {
text-align: center;
z-index: 2;
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) {
display: none;
}
@ -171,33 +176,23 @@ header {
background-image: url('../images/btn-bg-3.png');
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{
background-image: url('../images/btn-bg-light.png');
}
/*&::after{
content: "";
position: absolute;
background-size: 60%;
width: 100%;
height: 100%;
}*/
&.relative{
position: relative;
left: auto;
right: auto;
bottom: auto;
}
}
.social-links a {
cursor: pointer;
&:hover{
text-decoration: none;
}
}
img{
@ -388,7 +383,7 @@ img{
}
}
&.how-to{
padding: 6.25rem 0;
padding: 6.25rem 0 0;
background: url('../images/bg-briques.jpg') repeat-x bottom;
.desktop-only {
padding: 0 1.25rem 4rem;