Merge branch 'develop' of github.com:thecodingmachine/workadventure into windows-focus-blur-camera
This commit is contained in:
commit
00b83ae349
12
.github/workflows/build-and-deploy.yml
vendored
12
.github/workflows/build-and-deploy.yml
vendored
@ -20,7 +20,7 @@ jobs:
|
||||
|
||||
|
||||
# Create a slugified value of the branch
|
||||
- uses: rlespinasse/github-slug-action@master
|
||||
- uses: rlespinasse/github-slug-action@1.1.1
|
||||
|
||||
- name: "Build and push front image"
|
||||
uses: docker/build-push-action@v1
|
||||
@ -43,7 +43,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Create a slugified value of the branch
|
||||
- uses: rlespinasse/github-slug-action@master
|
||||
- uses: rlespinasse/github-slug-action@1.1.1
|
||||
|
||||
- name: "Build and push back image"
|
||||
uses: docker/build-push-action@v1
|
||||
@ -66,7 +66,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# Create a slugified value of the branch
|
||||
- uses: rlespinasse/github-slug-action@master
|
||||
- uses: rlespinasse/github-slug-action@1.1.1
|
||||
|
||||
- name: "Build and push back image"
|
||||
uses: docker/build-push-action@v1
|
||||
@ -90,7 +90,7 @@ jobs:
|
||||
|
||||
|
||||
# Create a slugified value of the branch
|
||||
- uses: rlespinasse/github-slug-action@master
|
||||
- uses: rlespinasse/github-slug-action@1.1.1
|
||||
|
||||
- name: "Build and push front image"
|
||||
uses: docker/build-push-action@v1
|
||||
@ -137,7 +137,7 @@ jobs:
|
||||
check_for_duplicate_msg: true
|
||||
|
||||
- name: Run Cypress tests
|
||||
uses: cypress-io/github-action@v1
|
||||
uses: cypress-io/github-action@v2
|
||||
if: ${{ env.GITHUB_REF_SLUG != 'master' }}
|
||||
env:
|
||||
CYPRESS_BASE_URL: https://play.${{ env.GITHUB_REF_SLUG }}.workadventure.test.thecodingmachine.com
|
||||
@ -148,7 +148,7 @@ jobs:
|
||||
working-directory: e2e
|
||||
|
||||
- name: Run Cypress tests in prod
|
||||
uses: cypress-io/github-action@v1
|
||||
uses: cypress-io/github-action@v2
|
||||
if: ${{ env.GITHUB_REF_SLUG == 'master' }}
|
||||
env:
|
||||
CYPRESS_BASE_URL: https://play.workadventu.re
|
||||
|
@ -49,7 +49,7 @@
|
||||
"multer": "^1.4.2",
|
||||
"prom-client": "^12.0.0",
|
||||
"query-string": "^6.13.3",
|
||||
"systeminformation": "^4.26.5",
|
||||
"systeminformation": "^4.27.11",
|
||||
"ts-node-dev": "^1.0.0-pre.44",
|
||||
"typescript": "^3.8.3",
|
||||
"uWebSockets.js": "uNetworking/uWebSockets.js#v18.5.0",
|
||||
|
@ -22,7 +22,7 @@ import {adminApi, CharacterTexture, FetchMemberDataByUuidResponse} from "../Serv
|
||||
import {SocketManager, socketManager} from "../Services/SocketManager";
|
||||
import {emitInBatch, resetPing} from "../Services/IoSocketHelpers";
|
||||
import {clientEventsEmitter} from "../Services/ClientEventsEmitter";
|
||||
import {ADMIN_API_TOKEN} from "../Enum/EnvironmentVariable";
|
||||
import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable";
|
||||
|
||||
export class IoSocketController {
|
||||
private nextUserId: number = 1;
|
||||
@ -166,18 +166,21 @@ export class IoSocketController {
|
||||
if(room.isFull){
|
||||
throw new Error('Room is full');
|
||||
}
|
||||
try {
|
||||
const userData = await adminApi.fetchMemberDataByUuid(userUuid);
|
||||
//console.log('USERDATA', userData)
|
||||
memberTags = userData.tags;
|
||||
memberTextures = userData.textures;
|
||||
if (!room.anonymous && room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && !room.canAccess(memberTags)) {
|
||||
throw new Error('No correct tags')
|
||||
if (ADMIN_API_URL) {
|
||||
try {
|
||||
const userData = await adminApi.fetchMemberDataByUuid(userUuid);
|
||||
//console.log('USERDATA', userData)
|
||||
memberTags = userData.tags;
|
||||
memberTextures = userData.textures;
|
||||
if (!room.anonymous && room.policyType === GameRoomPolicyTypes.USE_TAGS_POLICY && !room.canAccess(memberTags)) {
|
||||
throw new Error('No correct tags')
|
||||
}
|
||||
//console.log('access granted for user '+userUuid+' and room '+roomId);
|
||||
} catch (e) {
|
||||
console.log('access not granted for user '+userUuid+' and room '+roomId);
|
||||
console.error(e);
|
||||
throw new Error('Client cannot acces this ressource.')
|
||||
}
|
||||
//console.log('access granted for user '+userUuid+' and room '+roomId);
|
||||
} catch (e) {
|
||||
console.log('access not granted for user '+userUuid+' and room '+roomId);
|
||||
throw new Error('Client cannot acces this ressource.')
|
||||
}
|
||||
|
||||
// Generate characterLayers objects from characterLayers string[]
|
||||
@ -238,22 +241,24 @@ export class IoSocketController {
|
||||
socketManager.handleJoinRoom(client);
|
||||
resetPing(client);
|
||||
|
||||
//get data information and shwo messages
|
||||
adminApi.fetchMemberDataByUuid(client.userUuid).then((res: FetchMemberDataByUuidResponse) => {
|
||||
if (!res.messages) {
|
||||
return;
|
||||
}
|
||||
res.messages.forEach((c: unknown) => {
|
||||
const messageToSend = c as { type: string, message: string };
|
||||
socketManager.emitSendUserMessage({
|
||||
userUuid: client.userUuid,
|
||||
type: messageToSend.type,
|
||||
message: messageToSend.message
|
||||
})
|
||||
//get data information and show messages
|
||||
if (ADMIN_API_URL) {
|
||||
adminApi.fetchMemberDataByUuid(client.userUuid).then((res: FetchMemberDataByUuidResponse) => {
|
||||
if (!res.messages) {
|
||||
return;
|
||||
}
|
||||
res.messages.forEach((c: unknown) => {
|
||||
const messageToSend = c as { type: string, message: string };
|
||||
socketManager.emitSendUserMessage({
|
||||
userUuid: client.userUuid,
|
||||
type: messageToSend.type,
|
||||
message: messageToSend.message
|
||||
})
|
||||
});
|
||||
}).catch((err) => {
|
||||
console.error('fetchMemberDataByUuid => err', err);
|
||||
});
|
||||
}).catch((err) => {
|
||||
console.error('fetchMemberDataByUuid => err', err);
|
||||
});
|
||||
}
|
||||
},
|
||||
message: (ws, arrayBuffer, isBinary): void => {
|
||||
const client = ws as ExSocketInterface;
|
||||
|
@ -1,5 +1,4 @@
|
||||
import {App} from "../Server/sifrr.server";
|
||||
import {IoSocketController} from "_Controller/IoSocketController";
|
||||
import {HttpRequest, HttpResponse} from "uWebSockets.js";
|
||||
const register = require('prom-client').register;
|
||||
const collectDefaultMetrics = require('prom-client').collectDefaultMetrics;
|
||||
|
@ -3,7 +3,7 @@ const URL_ROOM_STARTED = "/Floor0/floor0.json";
|
||||
const MINIMUM_DISTANCE = process.env.MINIMUM_DISTANCE ? Number(process.env.MINIMUM_DISTANCE) : 64;
|
||||
const GROUP_RADIUS = process.env.GROUP_RADIUS ? Number(process.env.GROUP_RADIUS) : 48;
|
||||
const ALLOW_ARTILLERY = process.env.ALLOW_ARTILLERY ? process.env.ALLOW_ARTILLERY == 'true' : false;
|
||||
const ADMIN_API_URL = process.env.ADMIN_API_URL || 'http://admin';
|
||||
const ADMIN_API_URL = process.env.ADMIN_API_URL || '';
|
||||
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN || 'myapitoken';
|
||||
const MAX_USERS_PER_ROOM = parseInt(process.env.MAX_USERS_PER_ROOM || '') || 600;
|
||||
const CPU_OVERHEAT_THRESHOLD = Number(process.env.CPU_OVERHEAT_THRESHOLD) || 80;
|
||||
|
@ -152,7 +152,7 @@ export class GameRoom {
|
||||
closestItem.join(user);
|
||||
} else {
|
||||
const closestUser : User = closestItem;
|
||||
const group: Group = new Group([
|
||||
const group: Group = new Group(this.roomId,[
|
||||
user,
|
||||
closestUser
|
||||
], this.connectCallback, this.disconnectCallback, this.positionNotifier);
|
||||
@ -200,7 +200,6 @@ export class GameRoom {
|
||||
if (group === undefined) {
|
||||
throw new Error("The user is part of no group");
|
||||
}
|
||||
const oldPosition = group.getPosition();
|
||||
group.leave(user);
|
||||
if (group.isEmpty()) {
|
||||
this.positionNotifier.leave(group);
|
||||
@ -209,6 +208,7 @@ export class GameRoom {
|
||||
throw new Error("Could not find group "+group.getId()+" referenced by user "+user.id+" in World.");
|
||||
}
|
||||
this.groups.delete(group);
|
||||
//todo: is the group garbage collected?
|
||||
} else {
|
||||
group.updatePosition();
|
||||
//this.positionNotifier.updatePosition(group, group.getPosition(), oldPosition);
|
||||
|
@ -3,6 +3,7 @@ import { User } from "./User";
|
||||
import {PositionInterface} from "_Model/PositionInterface";
|
||||
import {Movable} from "_Model/Movable";
|
||||
import {PositionNotifier} from "_Model/PositionNotifier";
|
||||
import {gaugeManager} from "../Services/GaugeManager";
|
||||
|
||||
export class Group implements Movable {
|
||||
static readonly MAX_PER_GROUP = 4;
|
||||
@ -13,12 +14,23 @@ export class Group implements Movable {
|
||||
private users: Set<User>;
|
||||
private x!: number;
|
||||
private y!: number;
|
||||
private hasEditedGauge: boolean = false;
|
||||
private wasDestroyed: boolean = false;
|
||||
private roomId: string;
|
||||
|
||||
|
||||
constructor(users: User[], private connectCallback: ConnectCallback, private disconnectCallback: DisconnectCallback, private positionNotifier: PositionNotifier) {
|
||||
constructor(roomId: string, users: User[], private connectCallback: ConnectCallback, private disconnectCallback: DisconnectCallback, private positionNotifier: PositionNotifier) {
|
||||
this.roomId = roomId;
|
||||
this.users = new Set<User>();
|
||||
this.id = Group.nextId;
|
||||
Group.nextId++;
|
||||
//we only send a event for prometheus metrics if the group lives more than 5 seconds
|
||||
setTimeout(() => {
|
||||
if (!this.wasDestroyed) {
|
||||
this.hasEditedGauge = true;
|
||||
gaugeManager.incNbGroupsPerRoomGauge(roomId);
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
users.forEach((user: User) => {
|
||||
this.join(user);
|
||||
@ -113,9 +125,11 @@ export class Group implements Movable {
|
||||
*/
|
||||
destroy(): void
|
||||
{
|
||||
if (this.hasEditedGauge) gaugeManager.decNbGroupsPerRoomGauge(this.roomId);
|
||||
for (const user of this.users) {
|
||||
this.leave(user);
|
||||
}
|
||||
this.wasDestroyed = true;
|
||||
}
|
||||
|
||||
get getSize(){
|
||||
|
54
back/src/Services/GaugeManager.ts
Normal file
54
back/src/Services/GaugeManager.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import {Counter, Gauge} from "prom-client";
|
||||
|
||||
//this class should manage all the custom metrics used by prometheus
|
||||
class GaugeManager {
|
||||
private nbClientsGauge: Gauge<string>;
|
||||
private nbClientsPerRoomGauge: Gauge<string>;
|
||||
private nbGroupsPerRoomGauge: Gauge<string>;
|
||||
private nbGroupsPerRoomCounter: Counter<string>;
|
||||
|
||||
constructor() {
|
||||
this.nbClientsGauge = new Gauge({
|
||||
name: 'workadventure_nb_sockets',
|
||||
help: 'Number of connected sockets',
|
||||
labelNames: [ ]
|
||||
});
|
||||
this.nbClientsPerRoomGauge = new Gauge({
|
||||
name: 'workadventure_nb_clients_per_room',
|
||||
help: 'Number of clients per room',
|
||||
labelNames: [ 'room' ]
|
||||
});
|
||||
|
||||
this.nbGroupsPerRoomCounter = new Counter({
|
||||
name: 'workadventure_counter_groups_per_room',
|
||||
help: 'Counter of groups per room',
|
||||
labelNames: [ 'room' ]
|
||||
});
|
||||
this.nbGroupsPerRoomGauge = new Gauge({
|
||||
name: 'workadventure_nb_groups_per_room',
|
||||
help: 'Number of groups per room',
|
||||
labelNames: [ 'room' ]
|
||||
});
|
||||
}
|
||||
|
||||
incNbClientPerRoomGauge(roomId: string): void {
|
||||
this.nbClientsGauge.inc();
|
||||
this.nbClientsPerRoomGauge.inc({ room: roomId });
|
||||
}
|
||||
|
||||
decNbClientPerRoomGauge(roomId: string): void {
|
||||
this.nbClientsGauge.dec();
|
||||
this.nbClientsPerRoomGauge.dec({ room: roomId });
|
||||
}
|
||||
|
||||
incNbGroupsPerRoomGauge(roomId: string): void {
|
||||
this.nbGroupsPerRoomCounter.inc({ room: roomId })
|
||||
this.nbGroupsPerRoomGauge.inc({ room: roomId })
|
||||
}
|
||||
|
||||
decNbGroupsPerRoomGauge(roomId: string): void {
|
||||
this.nbGroupsPerRoomGauge.dec({ room: roomId })
|
||||
}
|
||||
}
|
||||
|
||||
export const gaugeManager = new GaugeManager();
|
@ -1,11 +1,11 @@
|
||||
import {ALLOW_ARTILLERY, SECRET_KEY} from "../Enum/EnvironmentVariable";
|
||||
import {ADMIN_API_URL, ALLOW_ARTILLERY, SECRET_KEY} from "../Enum/EnvironmentVariable";
|
||||
import {uuid} from "uuidv4";
|
||||
import Jwt from "jsonwebtoken";
|
||||
import {TokenInterface} from "../Controller/AuthenticateController";
|
||||
import {adminApi, AdminApiData} from "../Services/AdminApi";
|
||||
|
||||
class JWTTokenManager {
|
||||
|
||||
|
||||
public createJWTToken(userUuid: string) {
|
||||
return Jwt.sign({userUuid: userUuid}, SECRET_KEY, {expiresIn: '200d'}); //todo: add a mechanic to refresh or recreate token
|
||||
}
|
||||
@ -48,17 +48,21 @@ class JWTTokenManager {
|
||||
return;
|
||||
}
|
||||
|
||||
//verify user in admin
|
||||
adminApi.fetchCheckUserByToken(tokenInterface.userUuid).then(() => {
|
||||
resolve(tokenInterface.userUuid);
|
||||
}).catch((err) => {
|
||||
//anonymous user
|
||||
if(err.response && err.response.status && err.response.status === 404){
|
||||
if (ADMIN_API_URL) {
|
||||
//verify user in admin
|
||||
adminApi.fetchCheckUserByToken(tokenInterface.userUuid).then(() => {
|
||||
resolve(tokenInterface.userUuid);
|
||||
return;
|
||||
}
|
||||
reject(new Error('Authentication error, invalid token structure. ' + err));
|
||||
});
|
||||
}).catch((err) => {
|
||||
//anonymous user
|
||||
if(err.response && err.response.status && err.response.status === 404){
|
||||
resolve(tokenInterface.userUuid);
|
||||
return;
|
||||
}
|
||||
reject(err);
|
||||
});
|
||||
} else {
|
||||
resolve(tokenInterface.userUuid);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -66,7 +70,7 @@ class JWTTokenManager {
|
||||
private isValidToken(token: object): token is TokenInterface {
|
||||
return !(typeof((token as TokenInterface).userUuid) !== 'string');
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
export const jwtTokenManager = new JWTTokenManager();
|
||||
export const jwtTokenManager = new JWTTokenManager();
|
||||
|
@ -23,7 +23,6 @@ import {
|
||||
WebRtcStartMessage,
|
||||
QueryJitsiJwtMessage,
|
||||
SendJitsiJwtMessage,
|
||||
CharacterLayerMessage,
|
||||
SendUserMessage
|
||||
} from "../Messages/generated/messages_pb";
|
||||
import {PointInterface} from "../Model/Websocket/PointInterface";
|
||||
@ -37,11 +36,11 @@ import {Movable} from "../Model/Movable";
|
||||
import {PositionInterface} from "../Model/PositionInterface";
|
||||
import {adminApi, CharacterTexture} from "./AdminApi";
|
||||
import Direction = PositionMessage.Direction;
|
||||
import {Gauge} from "prom-client";
|
||||
import {emitError, emitInBatch} from "./IoSocketHelpers";
|
||||
import Jwt from "jsonwebtoken";
|
||||
import {JITSI_URL} from "../Enum/EnvironmentVariable";
|
||||
import {clientEventsEmitter} from "./ClientEventsEmitter";
|
||||
import {gaugeManager} from "./GaugeManager";
|
||||
|
||||
interface AdminSocketRoomsList {
|
||||
[index: string]: number;
|
||||
@ -58,30 +57,13 @@ export interface AdminSocketData {
|
||||
export class SocketManager {
|
||||
private Worlds: Map<string, GameRoom> = new Map<string, GameRoom>();
|
||||
private sockets: Map<number, ExSocketInterface> = new Map<number, ExSocketInterface>();
|
||||
private nbClientsGauge: Gauge<string>;
|
||||
private nbClientsPerRoomGauge: Gauge<string>;
|
||||
|
||||
|
||||
constructor() {
|
||||
this.nbClientsGauge = new Gauge({
|
||||
name: 'workadventure_nb_sockets',
|
||||
help: 'Number of connected sockets',
|
||||
labelNames: [ ]
|
||||
clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => {
|
||||
gaugeManager.incNbClientPerRoomGauge(roomId);
|
||||
});
|
||||
this.nbClientsPerRoomGauge = new Gauge({
|
||||
name: 'workadventure_nb_clients_per_room',
|
||||
help: 'Number of clients per room',
|
||||
labelNames: [ 'room' ]
|
||||
});
|
||||
|
||||
clientEventsEmitter.registerToClientJoin((clientUUid, roomId) => {
|
||||
this.nbClientsGauge.inc();
|
||||
// Let's log server load when a user joins
|
||||
console.log(new Date().toISOString() + ' A user joined (', this.sockets.size, ' connected users)');
|
||||
});
|
||||
clientEventsEmitter.registerToClientLeave((clientUUid, roomId) => {
|
||||
this.nbClientsGauge.dec();
|
||||
// Let's log server load when a user leaves
|
||||
console.log('A user left (', this.sockets.size, ' connected users)');
|
||||
clientEventsEmitter.registerToClientLeave((clientUUid: string, roomId: string) => {
|
||||
gaugeManager.decNbClientPerRoomGauge(roomId);
|
||||
});
|
||||
}
|
||||
|
||||
@ -107,7 +89,6 @@ export class SocketManager {
|
||||
const viewport = client.viewport;
|
||||
try {
|
||||
this.sockets.set(client.userId, client); //todo: should this be at the end of the function?
|
||||
clientEventsEmitter.emitClientJoin(client.userUuid, client.roomId);
|
||||
//join new previous room
|
||||
const gameRoom = this.joinRoom(client, position);
|
||||
|
||||
@ -377,8 +358,8 @@ export class SocketManager {
|
||||
} finally {
|
||||
//delete Client.roomId;
|
||||
this.sockets.delete(Client.userId);
|
||||
this.nbClientsPerRoomGauge.dec({ room: Client.roomId });
|
||||
clientEventsEmitter.emitClientLeave(Client.userUuid, Client.roomId);
|
||||
console.log('A user left (', this.sockets.size, ' connected users)');
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -410,8 +391,6 @@ export class SocketManager {
|
||||
private joinRoom(client : ExSocketInterface, position: PointInterface): GameRoom {
|
||||
|
||||
const roomId = client.roomId;
|
||||
//join user in room
|
||||
this.nbClientsPerRoomGauge.inc({ room: roomId });
|
||||
client.position = position;
|
||||
|
||||
const world = this.Worlds.get(roomId)
|
||||
@ -425,6 +404,8 @@ export class SocketManager {
|
||||
});
|
||||
//join world
|
||||
world.join(client, client.position);
|
||||
clientEventsEmitter.emitClientJoin(client.userUuid, client.roomId);
|
||||
console.log(new Date().toISOString() + ' A user joined (', this.sockets.size, ' connected users)');
|
||||
return world;
|
||||
}
|
||||
|
||||
|
@ -2345,10 +2345,10 @@ supports-color@^7.1.0:
|
||||
dependencies:
|
||||
has-flag "^4.0.0"
|
||||
|
||||
systeminformation@^4.26.5:
|
||||
version "4.27.5"
|
||||
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-4.27.5.tgz#af304fbfd0e7ba51c87512333691b58b4ad90e43"
|
||||
integrity sha512-EysogxKqREk54ZYDEFcsCODv8GymKZcyiSfegYit8dKhPjzuQr+KX4GFHjssWjYrWFEIM2bYNsFrZX5eufeAXg==
|
||||
systeminformation@^4.27.11:
|
||||
version "4.27.11"
|
||||
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-4.27.11.tgz#6dbe96e48091444f80dab6c05ee1901286826b60"
|
||||
integrity sha512-U7bigXbOnsB8k1vNHS0Y13RCsRz5/UohiUmND+3mMUL6vfzrpbe/h4ZqewowB+B+tJNnmGFDj08Z8xGfYo45dQ==
|
||||
|
||||
table@^5.2.3:
|
||||
version "5.4.6"
|
||||
|
@ -3,6 +3,7 @@
|
||||
local namespace = env.GITHUB_REF_SLUG,
|
||||
local tag = namespace,
|
||||
local url = if namespace == "master" then "workadventu.re" else namespace+".workadventure.test.thecodingmachine.com",
|
||||
local adminUrl = if namespace == "master" || namespace == "develop" || std.startsWith(namespace, "admin") then "https://admin."+url else null,
|
||||
"$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json",
|
||||
"version": "1.0",
|
||||
"containers": {
|
||||
@ -16,11 +17,12 @@
|
||||
"env": {
|
||||
"SECRET_KEY": "tempSecretKeyNeedsToChange",
|
||||
"ADMIN_API_TOKEN": env.ADMIN_API_TOKEN,
|
||||
"ADMIN_API_URL": "https://admin."+url,
|
||||
"JITSI_ISS": env.JITSI_ISS,
|
||||
"JITSI_URL": env.JITSI_URL,
|
||||
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
|
||||
}
|
||||
} + if adminUrl != null then {
|
||||
"ADMIN_API_URL": adminUrl,
|
||||
} else {}
|
||||
},
|
||||
"front": {
|
||||
"image": "thecodingmachine/workadventure-front:"+tag,
|
||||
|
22
front/dist/index.html
vendored
22
front/dist/index.html
vendored
@ -42,30 +42,14 @@
|
||||
<body id="body" style="margin: 0">
|
||||
<div class="main-container" id="main-container">
|
||||
<!-- Create the editor container -->
|
||||
<div id="game" class="game" style="/*background: red;*/">
|
||||
<div id="game-overlay" class="game-overlay" style="/*background: violet*/;">
|
||||
<div id="game" class="game">
|
||||
<div id="game-overlay" class="game-overlay">
|
||||
<div id="main-section" class="main-section">
|
||||
<!--<div style="background: lightpink;">a</div>
|
||||
<div style="background: lightpink;">a</div> -->
|
||||
</div>
|
||||
<aside id="sidebar" class="sidebar">
|
||||
<!--<div style="background: lightgreen;">a</div>
|
||||
<div style="background: green;">b</div>
|
||||
<div style="background: darkgreen;">c</div>
|
||||
<div style="background: darkgreen;">d</div>-->
|
||||
</aside>
|
||||
<div id="chat-mode" class="chat-mode three-col" style="display: none;">
|
||||
<!--<div style="background: lightgreen;">a</div>
|
||||
<div style="background: green;">b</div>
|
||||
<div style="background: darkgreen;">c</div>
|
||||
<div style="background: darkolivegreen;">d</div>
|
||||
<div style="background: darkolivegreen;">d</div>
|
||||
<div style="background: darkgreen;">c</div>
|
||||
<div style="background: green;">b</div>
|
||||
<div style="background: lightgreen;">last elem for game</div>-->
|
||||
</div>
|
||||
|
||||
|
||||
<div id="activeCam" class="activeCam">
|
||||
<div id="div-myCamVideo" class="video-container">
|
||||
<video id="myCamVideo" autoplay muted></video>
|
||||
@ -88,7 +72,7 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div id="cowebsite" class="cowebsite"></div>
|
||||
<div id="cowebsite" class="cowebsite hidden"></div>
|
||||
<div class="audio-playing">
|
||||
<img src="/resources/logos/megaphone.svg"/>
|
||||
</div>
|
||||
|
56
front/dist/resources/style/style.css
vendored
56
front/dist/resources/style/style.css
vendored
@ -242,15 +242,10 @@ body {
|
||||
.main-container {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@media (min-aspect-ratio: 1/1) {
|
||||
.main-container {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.game-overlay {
|
||||
flex-direction: row;
|
||||
}
|
||||
@ -266,12 +261,21 @@ body {
|
||||
.sidebar > div:hover {
|
||||
max-height: 25%;
|
||||
}
|
||||
|
||||
#cowebsite {
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 50%;
|
||||
height: 100vh;
|
||||
}
|
||||
#cowebsite.loading {
|
||||
transform: translateX(90%);
|
||||
}
|
||||
#cowebsite.hidden {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
@media (max-aspect-ratio: 1/1) {
|
||||
.main-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.game-overlay {
|
||||
flex-direction: column;
|
||||
}
|
||||
@ -288,24 +292,36 @@ body {
|
||||
.sidebar > div:hover {
|
||||
max-width: 25%;
|
||||
}
|
||||
|
||||
#cowebsite {
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
}
|
||||
#cowebsite.loading {
|
||||
transform: translateY(90%);
|
||||
}
|
||||
#cowebsite.hidden {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
}
|
||||
|
||||
.game {
|
||||
flex-basis: 100%;
|
||||
#game {
|
||||
width: 100%;
|
||||
position: relative; /* Position relative is needed for the game-overlay. */
|
||||
}
|
||||
|
||||
/* A potentially shared website could appear in an iframe in the cowebsite space. */
|
||||
.cowebsite {
|
||||
flex-basis: 100%;
|
||||
transition: flex-basis 0.5s;
|
||||
#cowebsite {
|
||||
position: fixed;
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
#cowebsite.loading {
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
/*.cowebsite:hover {
|
||||
flex-basis: 100%;
|
||||
}*/
|
||||
|
||||
.cowebsite > iframe {
|
||||
#cowebsite > iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ import CanvasTexture = Phaser.Textures.CanvasTexture;
|
||||
import GameObject = Phaser.GameObjects.GameObject;
|
||||
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
|
||||
import {GameMap} from "./GameMap";
|
||||
import {CoWebsiteManager} from "../../WebRtc/CoWebsiteManager";
|
||||
import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager";
|
||||
import {mediaManager} from "../../WebRtc/MediaManager";
|
||||
import {FourOFourSceneName} from "../Reconnecting/FourOFourScene";
|
||||
import {ItemFactoryInterface} from "../Items/ItemFactoryInterface";
|
||||
@ -292,13 +292,6 @@ export class GameScene extends ResizableScene implements CenterListener {
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
// TEST: let's load a module dynamically!
|
||||
/*let foo = "http://maps.workadventure.localhost/computer.js";
|
||||
import(/* webpackIgnore: true * / foo).then(result => {
|
||||
console.log(result);
|
||||
|
||||
});*/
|
||||
}
|
||||
|
||||
//hook initialisation
|
||||
@ -476,9 +469,9 @@ export class GameScene extends ResizableScene implements CenterListener {
|
||||
|
||||
this.gameMap.onPropertyChange('openWebsite', (newValue, oldValue) => {
|
||||
if (newValue === undefined) {
|
||||
CoWebsiteManager.closeCoWebsite();
|
||||
coWebsiteManager.closeCoWebsite();
|
||||
} else {
|
||||
CoWebsiteManager.loadCoWebsite(newValue as string);
|
||||
coWebsiteManager.loadCoWebsite(newValue as string);
|
||||
}
|
||||
});
|
||||
this.gameMap.onPropertyChange('jitsiRoom', (newValue, oldValue, allProps) => {
|
||||
|
@ -2,47 +2,90 @@ import {HtmlUtils} from "./HtmlUtils";
|
||||
|
||||
export type CoWebsiteStateChangedCallback = () => void;
|
||||
|
||||
export class CoWebsiteManager {
|
||||
enum iframeStates {
|
||||
closed = 1,
|
||||
loading, // loading an iframe can be slow, so we show some placeholder until it is ready
|
||||
opened,
|
||||
}
|
||||
|
||||
private static observers = new Array<CoWebsiteStateChangedCallback>();
|
||||
const cowebsiteDivId = "cowebsite"; // the id of the parent div of the iframe.
|
||||
const animationTime = 500; //time used by the css transitions, in ms.
|
||||
|
||||
public static loadCoWebsite(url: string): void {
|
||||
const cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("cowebsite");
|
||||
class CoWebsiteManager {
|
||||
|
||||
private opened: iframeStates = iframeStates.closed;
|
||||
|
||||
private observers = new Array<CoWebsiteStateChangedCallback>();
|
||||
|
||||
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');
|
||||
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');
|
||||
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
|
||||
this.opened = iframeStates.opened;
|
||||
return cowebsiteDiv;
|
||||
}
|
||||
|
||||
public loadCoWebsite(url: string): void {
|
||||
const cowebsiteDiv = this.load();
|
||||
cowebsiteDiv.innerHTML = '';
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.id = 'cowebsite-iframe';
|
||||
iframe.src = url;
|
||||
const onloadPromise = new Promise((resolve) => {
|
||||
iframe.onload = () => resolve();
|
||||
});
|
||||
cowebsiteDiv.appendChild(iframe);
|
||||
//iframe.onload = () => {
|
||||
// onload can be long to trigger. Maybe we should display the website, whatever happens, after 1 second?
|
||||
CoWebsiteManager.fire();
|
||||
//}
|
||||
const onTimeoutPromise = new Promise((resolve) => {
|
||||
setTimeout(() => resolve(), 2000);
|
||||
});
|
||||
Promise.race([onloadPromise, onTimeoutPromise]).then(() => {
|
||||
this.open();
|
||||
setTimeout(() => {
|
||||
this.fire();
|
||||
}, animationTime)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Just like loadCoWebsite but the div can be filled by the user.
|
||||
*/
|
||||
public static insertCoWebsite(callback: (cowebsite: HTMLDivElement) => void): void {
|
||||
const cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("cowebsite");
|
||||
cowebsiteDiv.innerHTML = '';
|
||||
|
||||
callback(cowebsiteDiv);
|
||||
//iframe.onload = () => {
|
||||
// onload can be long to trigger. Maybe we should display the website, whatever happens, after 1 second?
|
||||
CoWebsiteManager.fire();
|
||||
//}
|
||||
public insertCoWebsite(callback: (cowebsite: HTMLDivElement) => Promise<void>): void {
|
||||
const cowebsiteDiv = this.load();
|
||||
callback(cowebsiteDiv).then(() => {
|
||||
this.open();
|
||||
setTimeout(() => {
|
||||
this.fire();
|
||||
}, animationTime)
|
||||
});
|
||||
}
|
||||
|
||||
public static closeCoWebsite(): void {
|
||||
const cowebsiteDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("cowebsite");
|
||||
cowebsiteDiv.innerHTML = '';
|
||||
CoWebsiteManager.fire();
|
||||
public closeCoWebsite(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const cowebsiteDiv = this.close();
|
||||
this.fire();
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
setTimeout(() => cowebsiteDiv.innerHTML = '', 500)
|
||||
}, animationTime)
|
||||
});
|
||||
}
|
||||
|
||||
public static getGameSize(): {width: number, height: number} {
|
||||
const hasChildren = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("cowebsite").children.length > 0;
|
||||
if (hasChildren === false) {
|
||||
public getGameSize(): {width: number, height: number} {
|
||||
if (this.opened !== iframeStates.opened) {
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
@ -61,13 +104,15 @@ export class CoWebsiteManager {
|
||||
}
|
||||
}
|
||||
|
||||
public static onStateChange(observer: CoWebsiteStateChangedCallback) {
|
||||
CoWebsiteManager.observers.push(observer);
|
||||
public onStateChange(observer: CoWebsiteStateChangedCallback) {
|
||||
this.observers.push(observer);
|
||||
}
|
||||
|
||||
private static fire(): void {
|
||||
for (const callback of CoWebsiteManager.observers) {
|
||||
private fire(): void {
|
||||
for (const callback of this.observers) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const coWebsiteManager = new CoWebsiteManager();
|
@ -1,6 +1,6 @@
|
||||
import {CoWebsiteManager} from "./CoWebsiteManager";
|
||||
import {JITSI_URL} from "../Enum/EnvironmentVariable";
|
||||
import {mediaManager} from "./MediaManager";
|
||||
import {coWebsiteManager} from "./CoWebsiteManager";
|
||||
declare const window:any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
|
||||
const interfaceConfig = {
|
||||
@ -31,9 +31,9 @@ class JitsiFactory {
|
||||
private videoCallback = this.onVideoChange.bind(this);
|
||||
|
||||
public start(roomName: string, playerName:string, jwt?: string): void {
|
||||
CoWebsiteManager.insertCoWebsite((cowebsiteDiv => {
|
||||
coWebsiteManager.insertCoWebsite((cowebsiteDiv => {
|
||||
const domain = JITSI_URL;
|
||||
const options = {
|
||||
const options: any = { // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
roomName: roomName,
|
||||
jwt: jwt,
|
||||
width: "100%",
|
||||
@ -49,19 +49,23 @@ class JitsiFactory {
|
||||
if (!options.jwt) {
|
||||
delete options.jwt;
|
||||
}
|
||||
this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options);
|
||||
this.jitsiApi.executeCommand('displayName', playerName);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
options.onload = () => resolve(); //we want for the iframe to be loaded before triggering animations.
|
||||
this.jitsiApi = new window.JitsiMeetExternalAPI(domain, options);
|
||||
this.jitsiApi.executeCommand('displayName', playerName);
|
||||
|
||||
this.jitsiApi.addListener('audioMuteStatusChanged', this.audioCallback);
|
||||
this.jitsiApi.addListener('videoMuteStatusChanged', this.videoCallback);
|
||||
this.jitsiApi.addListener('audioMuteStatusChanged', this.audioCallback);
|
||||
this.jitsiApi.addListener('videoMuteStatusChanged', this.videoCallback);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
public async stop(): Promise<void> {
|
||||
await coWebsiteManager.closeCoWebsite();
|
||||
this.jitsiApi.removeListener('audioMuteStatusChanged', this.audioCallback);
|
||||
this.jitsiApi.removeListener('videoMuteStatusChanged', this.videoCallback);
|
||||
this.jitsiApi?.dispose();
|
||||
CoWebsiteManager.closeCoWebsite();
|
||||
}
|
||||
|
||||
private onAudioChange({muted}: {muted: boolean}): void {
|
||||
|
@ -10,12 +10,9 @@ import {FourOFourScene} from "./Phaser/Reconnecting/FourOFourScene";
|
||||
import WebGLRenderer = Phaser.Renderer.WebGL.WebGLRenderer;
|
||||
import {OutlinePipeline} from "./Phaser/Shaders/OutlinePipeline";
|
||||
import {CustomizeScene} from "./Phaser/Login/CustomizeScene";
|
||||
import {CoWebsiteManager} from "./WebRtc/CoWebsiteManager";
|
||||
import {gameManager} from "./Phaser/Game/GameManager";
|
||||
import {ResizableScene} from "./Phaser/Login/ResizableScene";
|
||||
import {EntryScene} from "./Phaser/Login/EntryScene";
|
||||
|
||||
//CoWebsiteManager.loadCoWebsite('https://thecodingmachine.com');
|
||||
import {coWebsiteManager} from "./WebRtc/CoWebsiteManager";
|
||||
|
||||
// Load Jitsi if the environment variable is set.
|
||||
if (JITSI_URL) {
|
||||
@ -24,7 +21,7 @@ if (JITSI_URL) {
|
||||
document.head.appendChild(jitsiScript);
|
||||
}
|
||||
|
||||
const {width, height} = CoWebsiteManager.getGameSize();
|
||||
const {width, height} = coWebsiteManager.getGameSize();
|
||||
|
||||
const config: GameConfig = {
|
||||
title: "WorkAdventure",
|
||||
@ -53,8 +50,7 @@ cypressAsserter.gameStarted();
|
||||
const game = new Phaser.Game(config);
|
||||
|
||||
window.addEventListener('resize', function (event) {
|
||||
const {width, height} = CoWebsiteManager.getGameSize();
|
||||
|
||||
const {width, height} = coWebsiteManager.getGameSize();
|
||||
game.scale.resize(width / RESOLUTION, height / RESOLUTION);
|
||||
|
||||
// Let's trigger the onResize method of any active scene that is a ResizableScene
|
||||
@ -64,8 +60,7 @@ window.addEventListener('resize', function (event) {
|
||||
}
|
||||
}
|
||||
});
|
||||
CoWebsiteManager.onStateChange(() => {
|
||||
const {width, height} = CoWebsiteManager.getGameSize();
|
||||
|
||||
coWebsiteManager.onStateChange(() => {
|
||||
const {width, height} = coWebsiteManager.getGameSize();
|
||||
game.scale.resize(width / RESOLUTION, height / RESOLUTION);
|
||||
});
|
||||
|
9
website/dist/choose-map.html
vendored
9
website/dist/choose-map.html
vendored
@ -36,6 +36,15 @@
|
||||
</head>
|
||||
|
||||
<body class="choose-map">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a href="/" class="d-block mt-3 pixel-text">
|
||||
<< BACK TO HOMEPAGE
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid container-lg section pt-5">
|
||||
<h1 class="text-center pixel-title">CHOOSE YOUR MAP !</h1>
|
||||
<div class="row">
|
||||
|
11
website/dist/create-map.html
vendored
11
website/dist/create-map.html
vendored
@ -36,6 +36,15 @@
|
||||
</head>
|
||||
|
||||
<body class="create-map">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<a href="/" class="d-block mt-3 pixel-text">
|
||||
<< BACK TO HOMEPAGE
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container-fluid container-lg section pt-5">
|
||||
<h1 class="text-center pixel-title">CREATE YOUR MAP !</h1>
|
||||
<div class="row">
|
||||
@ -44,7 +53,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
< class="col">
|
||||
<div class="col">
|
||||
<h2 id="tools-you-will-need" class="pixel-title">Tools you will need</h2>
|
||||
<p>In order to build your own map for WorkAdventure, you need:</p>
|
||||
<ul>
|
||||
|
41
website/dist/index.html
vendored
41
website/dist/index.html
vendored
@ -33,12 +33,12 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<meta name="title" content="Workadventure" />
|
||||
<meta name="description" content="You are impatient to discover this new world? Click on 'Play online' and meet new people or share this adventure with your colleagues and friends by clicking on 'Private mode'" />
|
||||
<meta name="description" content="You are impatient to discover this new world? Click on 'Work online' and meet new people or share this adventure with your colleagues and friends by clicking on 'Work in private'" />
|
||||
<!-- Open Graph / Facebook -->
|
||||
<meta property="og:url" content="https://workadventu.re/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="Workadventure" />
|
||||
<meta property="og:description" content="You are impatient to discover this new world? Click on 'Play online' and meet new people or share this adventure with your colleagues and friends by clicking on 'Private mode'" />
|
||||
<meta property="og:description" content="You are impatient to discover this new world? Click on 'Work online' and meet new people or share this adventure with your colleagues and friends by clicking on 'Work in private'" />
|
||||
<meta property="og:image" content="https://workadventu.re/static/images/meta-tags-image.jpg" />
|
||||
<meta property="og:image:type" content="image/jpeg" />
|
||||
<meta property="og:image:alt" content="workadventure" />
|
||||
@ -46,7 +46,7 @@
|
||||
<meta name="twitter:site" content="@coding_machine">
|
||||
<meta name="twitter:url" content="https://workadventu.re/" />
|
||||
<meta name="twitter:title" content="Workadventure" />
|
||||
<meta name="twitter:description" content="You are impatient to discover this new world? Click on 'Play online' and meet new people or share this adventure with your colleagues and friends by clicking on 'Private mode'" />
|
||||
<meta name="twitter:description" content="You are impatient to discover this new world? Click on 'Work online' and meet new people or share this adventure with your colleagues and friends by clicking on 'Work in private'" />
|
||||
<meta name="twitter:image" content="https://workadventu.re/static/images/meta-tags-image.jpg" />
|
||||
<link rel="stylesheet" href="main.css">
|
||||
<script src="bundle.js"></script>
|
||||
@ -97,17 +97,17 @@
|
||||
</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 "Play online" and meet new people or share this adventure with your colleagues and friends by clicking on "Private mode"</h3>
|
||||
<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>
|
||||
</div>
|
||||
<div class="row buttons-row justify-content-md-center pt-5">
|
||||
<div class="col col-lg-3">
|
||||
<a class="custom-link start" href="/choose-map.html" title="PRIVATE MODE">
|
||||
PRIVATE MODE
|
||||
<a class="custom-link start" href="/choose-map.html" title="WORK IN PRIVATE">
|
||||
WORK IN PRIVATE
|
||||
</a>
|
||||
</div>
|
||||
<div class="col col-lg-3">
|
||||
<a class="custom-link play" target="_BLANK" onclick="startGame()" title="PLAY ONLINE">
|
||||
PLAY ONLINE
|
||||
<a class="custom-link play" target="_BLANK" onclick="startGame()" title="WORK ONLINE">
|
||||
WORK ONLINE
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -165,10 +165,10 @@
|
||||
<div class="bubble bubble-4 b-revert" style="height: 254px;">
|
||||
<div>
|
||||
<p>You can also create a private room with your friends or your team ! </p>
|
||||
<p class="bubble-legend">To try, press button private mode</p>
|
||||
<p class="bubble-legend">To try, press button work in private</p>
|
||||
<p class="bubble-action"><a href="/choose-map.html">
|
||||
<img src="static/images/playicon.png" />
|
||||
START IN PRIVATE MODE
|
||||
START WORKING IN PRIVATE
|
||||
</a></p>
|
||||
<p>
|
||||
Don’t forget to activate your mic and camera, let’s play
|
||||
@ -287,23 +287,23 @@
|
||||
<div class="container-fluid container-lg">
|
||||
<div class="row justify-content-md-center">
|
||||
<div class="col-12 col-md-12 text-center">
|
||||
<h3>How to play</h3>
|
||||
<h3>HOW IT WORKS</h3>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 text-center my-3 my-md-0">
|
||||
<div class="image-item">
|
||||
<h2>Choose your map</h2>
|
||||
<h2>CHOOSE YOUR WORKSPACE</h2>
|
||||
<div class="step-image"><img src="static/images/maps/office.png"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 text-center my-3 my-md-0">
|
||||
<div class="image-item">
|
||||
<h2>Select your character</h2>
|
||||
<h2>SELECT YOUR WOKA</h2>
|
||||
<div class="step-image"><img src="static/images/choose_character.png"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 col-md-4 text-center my-3 my-md-0">
|
||||
<div class="image-item">
|
||||
<h2>Let's go explore and talk !</h2>
|
||||
<h2>LET'S GO TO YOUR OFFICE</h2>
|
||||
<div class="step-image"><img src="static/images/interact.png"></div>
|
||||
</div>
|
||||
</div>
|
||||
@ -313,13 +313,14 @@
|
||||
<div class="section bg-white">
|
||||
<div class="container-fluid container-lg">
|
||||
<div class="col-12 credits">
|
||||
<h2>Future features</h2>
|
||||
<h3>We have already thought of new features:</h3>
|
||||
<p>Share files with others</p>
|
||||
<p>Lock group conversations</p>
|
||||
<h2>CURRENT FEATURES</h2>
|
||||
<!-- <h3>We have already thought of new features:</h3>-->
|
||||
<p>Instant interaction with up to 4 people</p>
|
||||
<p>Share screen with others</p>
|
||||
<p>Interact with objects</p>
|
||||
<h3>And you, what would you want?</h3>
|
||||
<p>Organize a workshop and meeting with up to 60 people</p>
|
||||
<p>Design your own digital space</p>
|
||||
<p>... and infinite possibilities</p>
|
||||
<h3>You need more? Do not hesitate to tell us!</h3>
|
||||
<div class="row justify-content-md-center pt-5" style="margin-top: 65px;">
|
||||
<div class="col col-lg-3">
|
||||
<a class="custom-link contribute" target="_BLANK" href="https://docs.google.com/forms/d/e/1FAIpQLSdxvajEyqsn4X0ai0SoDAcdsa_JQPIfiP2Tp9PDzkfZA54v9Q/viewform" title="FEEDBACK">
|
||||
|
@ -148,7 +148,7 @@ header {
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 7px;
|
||||
padding: 1.125rem;
|
||||
padding: 1.125rem 0.5rem;
|
||||
text-align: center;
|
||||
z-index: 2;
|
||||
transition: all .1s cubic-bezier(0.000, -0.600, 1.000, 1.650); /* custom */
|
||||
@ -431,7 +431,7 @@ img{
|
||||
margin: 0;
|
||||
min-height: 6rem;
|
||||
font-family: 'Karmatic Arcade';
|
||||
font-size: 22px;
|
||||
font-size: 24px;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
}
|
||||
@ -503,6 +503,9 @@ img{
|
||||
.pixel-title{
|
||||
font-family: "Karmatic Arcade" !important;
|
||||
}
|
||||
.pixel-text{
|
||||
font-family: "VCR OSD Mono" !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: 800;
|
||||
|
Loading…
Reference in New Issue
Block a user