merge dev

This commit is contained in:
_Bastler 2021-06-04 08:53:45 +02:00
commit f24a547814
87 changed files with 1903 additions and 1726 deletions

View File

@ -2,7 +2,7 @@ name: Build, push and deploy Docker image
on: on:
push: push:
branches: [master] branches: [master, develop]
release: release:
types: [created] types: [created]
pull_request: pull_request:

View File

@ -17,11 +17,20 @@
- now use the 'Press Start 2P' font family and added an outline - now use the 'Press Start 2P' font family and added an outline
- As a result, we can now allow non-standard letters like french accents or chinese characters! - As a result, we can now allow non-standard letters like french accents or chinese characters!
- Added the contact card feature. (@Kharhamel)
- Click on another player to see its contact info.
- Premium-only feature unfortunately. I need to find a way to make it available for all.
- If no contact data is found (either because the user is anonymous or because no admin backend), display an error card.
- Mobile support has been improved - Mobile support has been improved
- WorkAdventure automatically sets the zoom level based on the viewport size to ensure a sensible size of the map is visible, whatever the viewport used - WorkAdventure automatically sets the zoom level based on the viewport size to ensure a sensible size of the map is visible, whatever the viewport used
- Mouse wheel support to zoom in / out - Mouse wheel support to zoom in / out
- Pinch support on mobile to zoom in / out - Pinch support on mobile to zoom in / out
- Improved virtual joystick size (adapts to the zoom level) - Improved virtual joystick size (adapts to the zoom level)
- Redesigned intermediate scenes
- Redesigned Select Companion scene
- Redesigned Enter Your Name scene
- Added a new `DISPLAY_TERMS_OF_USE` environment variable to trigger the display of terms of use
- New scripting API features: - New scripting API features:
- Use `WA.loadSound(): Sound` to load / play / stop a sound - Use `WA.loadSound(): Sound` to load / play / stop a sound

View File

@ -89,6 +89,9 @@ export class GameRoom {
public getUserByUuid(uuid: string): User|undefined { public getUserByUuid(uuid: string): User|undefined {
return this.usersByUuid.get(uuid); return this.usersByUuid.get(uuid);
} }
public getUserById(id: number): User|undefined {
return this.users.get(id);
}
public join(socket : UserSocket, joinRoomMessage: JoinRoomMessage): User { public join(socket : UserSocket, joinRoomMessage: JoinRoomMessage): User {
const positionMessage = joinRoomMessage.getPositionmessage(); const positionMessage = joinRoomMessage.getPositionmessage();

View File

@ -11,7 +11,7 @@ import {
JoinRoomMessage, JoinRoomMessage,
PlayGlobalMessage, PlayGlobalMessage,
PusherToBackMessage, PusherToBackMessage,
QueryJitsiJwtMessage, RefreshRoomPromptMessage, QueryJitsiJwtMessage, RefreshRoomPromptMessage, RequestVisitCardMessage,
ServerToAdminClientMessage, ServerToAdminClientMessage,
ServerToClientMessage, ServerToClientMessage,
SilentMessage, SilentMessage,
@ -74,6 +74,8 @@ const roomManager: IRoomManagerServer = {
socketManager.handleQueryJitsiJwtMessage(user, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage); socketManager.handleQueryJitsiJwtMessage(user, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage);
} else if (message.hasEmotepromptmessage()){ } else if (message.hasEmotepromptmessage()){
socketManager.handleEmoteEventMessage(room, user, message.getEmotepromptmessage() as EmotePromptMessage); socketManager.handleEmoteEventMessage(room, user, message.getEmotepromptmessage() as EmotePromptMessage);
} else if (message.hasRequestvisitcardmessage()) {
socketManager.handleRequestVisitCardMessage(room, user, message.getRequestvisitcardmessage() as RequestVisitCardMessage);
}else if (message.hasSendusermessage()) { }else if (message.hasSendusermessage()) {
const sendUserMessage = message.getSendusermessage(); const sendUserMessage = message.getSendusermessage();
if(sendUserMessage !== undefined) { if(sendUserMessage !== undefined) {

View File

@ -0,0 +1,22 @@
import {ADMIN_API_TOKEN, ADMIN_API_URL} from "../Enum/EnvironmentVariable";
import Axios from "axios";
class AdminApi {
fetchVisitCardUrl(membershipUuid: string): Promise<string> {
if (ADMIN_API_URL) {
return Axios.get(ADMIN_API_URL + '/api/membership/'+membershipUuid,
{headers: {"Authorization": `${ADMIN_API_TOKEN}`}}
).then((res) => {
return res.data;
}).catch(() => {
return 'INVALID';
});
} else {
return Promise.resolve('INVALID')
}
}
}
export const adminApi = new AdminApi();

View File

@ -27,7 +27,7 @@ import {
WorldFullWarningMessage, WorldFullWarningMessage,
UserLeftZoneMessage, UserLeftZoneMessage,
EmoteEventMessage, EmoteEventMessage,
BanUserMessage, RefreshRoomMessage, EmotePromptMessage, BanUserMessage, RefreshRoomMessage, EmotePromptMessage, RequestVisitCardMessage, VisitCardMessage,
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import {User, UserSocket} from "../Model/User"; import {User, UserSocket} from "../Model/User";
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
@ -51,6 +51,7 @@ import {Zone} from "_Model/Zone";
import Debug from "debug"; import Debug from "debug";
import {Admin} from "_Model/Admin"; import {Admin} from "_Model/Admin";
import crypto from "crypto"; import crypto from "crypto";
import {adminApi} from "./AdminApi";
const debug = Debug('sockermanager'); const debug = Debug('sockermanager');
@ -769,6 +770,21 @@ export class SocketManager {
emoteEventMessage.setActoruserid(user.id); emoteEventMessage.setActoruserid(user.id);
room.emitEmoteEvent(user, emoteEventMessage); room.emitEmoteEvent(user, emoteEventMessage);
} }
async handleRequestVisitCardMessage(room: GameRoom, user: User, requestvisitcardmessage: RequestVisitCardMessage): Promise<void> {
const targetUser = room.getUserById(requestvisitcardmessage.getTargetuserid());
if (!targetUser) {
throw 'Could not find user for id '+requestvisitcardmessage.getTargetuserid();
}
const url = await adminApi.fetchVisitCardUrl(targetUser.uuid);
const visitCardMessage = new VisitCardMessage();
visitCardMessage.setUrl(url);
const clientMessage = new ServerToClientMessage();
clientMessage.setVisitcardmessage(visitCardMessage);
user.socket.write(clientMessage);
}
} }
export const socketManager = new SocketManager(); export const socketManager = new SocketManager();

View File

@ -95,23 +95,10 @@
<div id="audioplayer" style="visibility: hidden"></div> <div id="audioplayer" style="visibility: hidden"></div>
</div> </div>
</div> </div>
<div class="audio-playing">
<img src="/resources/logos/megaphone.svg" />
</div>
</div> </div>
<div id="activeScreenSharing" class="active-screen-sharing active"> <div id="activeScreenSharing" class="active-screen-sharing active">
</div> </div>
<div id="webRtcSetup" class="webrtcsetup">
<img id="webRtcSetupNoVideo" class="background-img" src="resources/logos/cinema-close.svg">
<video id="myCamVideoSetup" autoplay muted></video>
</div>
<audio id="audio-webrtc-in">
<source src="/resources/objects/webrtc-in.mp3" type="audio/mp3">
</audio>
<audio id="audio-webrtc-out">
<source src="/resources/objects/webrtc-out.mp3" type="audio/mp3">
</audio>
<audio id="report-message"> <audio id="report-message">
<source src="/resources/objects/report-message.mp3" type="audio/mp3"> <source src="/resources/objects/report-message.mp3" type="audio/mp3">
</audio> </audio>

View File

@ -1,132 +0,0 @@
<style>
#customizeScene {
background: #0000;
/*border: 1px solid #ebeeee;*/
border-radius: 6px;
margin: 10px auto 0;
color: #ebeeee;
overflow: auto;
width: 28vw;
height: 48vh;
margin-left: -5vw;
/*max-width: 300px;
max-height: 48vh;*/
}
#customizeScene h1 {
background-image: linear-gradient(top, #f1f3f3, #d4dae0);
border-bottom: 1px solid #a6abaf;
border-radius: 6px 6px 0 0;
box-sizing: border-box;
color: #727678;
display: block;
height: 43px;
padding-top: 10px;
margin: 0;
text-align: center;
text-shadow: 0 -1px 0 rgba(0,0,0,0.2), 0 1px 0 #fff;
}
#customizeScene section {
margin: 10px;
}
#customizeScene section.action {
text-align: center;
position: sticky;
bottom: 0;
top: 100%;
}
#customizeScene section.action.action-move {
top: 55%;
}
#customizeScene button#customizeSceneFormCancel {
background-color: #aca6a600;
color: #292929;
}
#customizeScene section h6,
#customizeScene section h5{
margin: 1px;
}
#customizeScene section.text-center{
text-align: center;
}
#customizeScene section p{
text-align: left;
font-size: 8px;
margin: 10px 10px;
}
#customizeScene section p.err{
color: red;
text-align: center;
}
#customizeScene section p.info{
display: none;
text-align: center;
}
#customizeScene section input#customizeSceneLink{
background-color: #a1a3a3;
}
#customizeScene section button.customizeSceneButton{
position: absolute;
margin: 0;
top: -8vh;
font-size: 10px;
padding: 2px 4px;
}
#customizeScene section button.customizeSceneButton#customizeSceneButtonLeft{
left: 0vw;
}
#customizeScene section button.customizeSceneButton#customizeSceneButtonRight{
right: 0vw;
}
#customizeScene section button.customizeSceneButton#customizeSceneButtonUp{
left: calc(2vw + 40px);
transform: rotate(90deg);
margin-top: -20px;
}
#customizeScene section button.customizeSceneButton#customizeSceneButtonDown{
right: calc(2vw + 40px);
transform: rotate(90deg);
margin-top: 20px;
}
#customizeScene section.action img{
width: 8px;
height: 8px;
}
#customizeScene section.action a#customizeSceneFormBack img{
margin-top: -2px;
}
#customizeScene section.action button#customizeSceneFormSubmit img{
transform: rotate(180deg);
}
@media only screen and (max-width: 600px) {
#customizeScene {
overflow-y: scroll;
margin-left: 70vw;
width: 200px;
margin-top: 20vh;
}
}
@media only screen and (max-height: 400px) {
#customizeScene section.action {
top: 92%;
}
#customizeScene section.action.action-move {
top: 80%;
}
}
</style>
<form id="customizeScene" hidden>
<section class="text-center">
<h5>Custom your Avatar</h3>
</section>
<section class="action action-move">
<button class="customizeSceneButton nes-btn" id="customizeSceneButtonLeft"> < </button>
<button class="customizeSceneButton nes-btn" id="customizeSceneButtonRight"> > </button>
<!--<button class="customizeSceneButton" id="customizeSceneButtonUp"> < </button>
<button class="customizeSceneButton" id="customizeSceneButtonDown"> > </button>-->
</section>
<section class="action">
<a type="submit" class="nes-btn is-dark" id="customizeSceneFormBack">Back <img src="resources/objects/arrow_up.png"/></a>
<button type="submit" class="nes-btn" id="customizeSceneFormSubmit">Next <img src="resources/objects/arrow_up.png"/></button>
</section>
</form>

View File

@ -1,119 +0,0 @@
<style>
#enableCameraScene {
background: #000000;
/*border: 1px solid #ebeeee;*/
border-radius: 6px;
margin: 20px auto 0;
color: #ebeeee;
max-height: 48vh;
width: 42vw;
max-width: 300px;
overflow: hidden;
}
#enableCameraScene h1 {
background-image: linear-gradient(top, #f1f3f3, #d4dae0);
border-bottom: 1px solid #a6abaf;
border-radius: 6px 6px 0 0;
box-sizing: border-box;
color: #727678;
display: block;
height: 43px;
padding-top: 10px;
margin: 0;
text-align: center;
text-shadow: 0 -1px 0 rgba(0,0,0,0.2), 0 1px 0 #fff;
}
#enableCameraScene input {
font-size: 70%;
background: linear-gradient(top, #d6d7d7, #dee0e0);
border: 1px solid #a1a3a3;
border-radius: 4px;
box-shadow: 0 1px #fff;
box-sizing: border-box;
color: #696969;
height: 30px;
transition: box-shadow 0.3s;
width: 100%;
text-align: center;
}
#enableCameraScene section.title {
position: absolute;
top: 1vh;
width: 100%;
}
#enableCameraScene section.action{
text-align: center;
margin: 0;
position: absolute;
top: 40vh;
width: 100%;
}
#enableCameraScene button#enableCameraSceneFormCancel {
background-color: #c7c7c700;
color: #292929;
}
#enableCameraScene section h6,
#enableCameraScene section h5{
margin: 1px;
}
#enableCameraScene section.text-center{
text-align: center;
}
#enableCameraScene section a{
font-size: 8px;
text-decoration: underline;
color: #ebeeee;
}
#enableCameraScene section a:hover{
font-weight: 700;
}
#enableCameraScene section p{
text-align: left;
font-size: 8px;
margin: 10px 10px;
}
#enableCameraScene section p.err{
color: red;
text-align: center;
}
#enableCameraScene section p.info{
display: none;
text-align: center;
}
#enableCameraScene section input#enableCameraSceneLink{
background-color: #a1a3a3;
}
#enableCameraScene section img{
width: 160px;
margin: 20px 0;
}
/*@media only screen and (max-width: 800px),
only screen and (max-height: 600px) {
#enableCameraScene{
overflow-y: scroll;
}
}*/
</style>
<form id="enableCameraScene" hidden>
<!-- FIX me -->
<section class="title text-center">
<h5>Turn on your camera and microphone</h5>
</section>
<!--<section class="text-center">
<video id="myCamVideoSetup" autoplay muted></video>
</section>
<section class="text-center">
<h5>Select your camera</h3>
<select id="camera">
</select>
</section>
<section class="text-center">
<h5>Select your michrophone</h3>
<select id="michrophone">
</select>
</section>-->
<section class="action">
<button type="submit" id="enableCameraSceneFormSubmit" class="nes-btn">Let's go!</button>
</section>
</form>

View File

@ -1,105 +0,0 @@
<style>
#selectCompanionScene {
background: #0000;
/*border: 1px solid #ebeeee;*/
border-radius: 6px;
margin: 10px auto 0;
color: #ebeeee;
max-height: 40vh;
overflow: auto;
max-width: 300px;
width: 40vw;
}
#selectCompanionScene h1 {
background-image: linear-gradient(top, #f1f3f3, #d4dae0);
border-bottom: 1px solid #a6abaf;
border-radius: 6px 6px 0 0;
box-sizing: border-box;
color: #727678;
display: block;
height: 43px;
padding-top: 10px;
margin: 0;
text-align: center;
text-shadow: 0 -1px 0 rgba(0,0,0,0.2), 0 1px 0 #fff;
}
#selectCompanionScene section {
margin: 10px;
}
#selectCompanionScene section.action {
text-align: center;
margin: 0;
margin-top: 20vh;
}
#selectCompanionScene button#selectCompanionSceneFormCancel {
background-color: #aca6a600;
color: #292929;
}
#selectCompanionScene section h6,
#selectCompanionScene section h5{
margin: 1px;
}
#selectCompanionScene section.text-center{
text-align: center;
}
#selectCompanionScene section a:hover{
font-weight: 700;
}
#selectCompanionScene section p{
text-align: left;
font-size: 8px;
margin: 10px 10px;
}
#selectCompanionScene section p.err{
color: red;
text-align: center;
}
#selectCompanionScene section p.info{
display: none;
text-align: center;
}
#selectCompanionScene section input#selectCompanionSceneLink{
background-color: #a1a3a3;
}
#selectCompanionScene section img{
width: 160px;
margin: 20px 0;
}
#selectCompanionScene section button.selectCharacterButton{
position: absolute;
top: 20vh;
margin: 0;
width: 25px;
}
#selectCompanionScene section button.selectCharacterButton#selectCharacterButtonLeft{
left: 1vw;
}
#selectCompanionScene section button.selectCharacterButton#selectCharacterButtonRight{
right: 1vw;
}
#selectCompanionScene section button#selectCompanionSceneFormCustomYourOwnSubmit{
font-size: 14px;
width: auto;
margin-top: -2px;
background-color: #ffd700;
color: black;
}
@media only screen and (max-width: 800px),
only screen and (max-height: 600px) {
#selectCompanionScene{
overflow-y: scroll;
}
}
</style>
<form id="selectCompanionScene" hidden>
<section class="text-center">
<h5>Select your Companion</h5>
<button class="selectCharacterButton nes-btn" id="selectCharacterButtonLeft"> < </button>
<button class="selectCharacterButton nes-btn" id="selectCharacterButtonRight"> > </button>
</section>
<section class="action">
<a herf="#" class="nes-btn is-dark" id="selectCompanionSceneFormBack">No companion</a>
<button type="submit" class="nes-btn" id="selectCompanionSceneFormSubmit">Continue</button>
</section>
</form>

View File

@ -1,8 +1,10 @@
<style> <style>
#menuIcon button {
padding: 2px 8px;
}
#menuIcon button img{ #menuIcon button img{
width: 8px; width: 8px;
padding-top: 0; padding-top: 0;
cursor: url('/resources/logos/cursor_pointer.png'), pointer;
} }
#menuIcon section { #menuIcon section {
margin: 10px; margin: 10px;

View File

@ -1,23 +1,16 @@
<style> <style>
#gameReport { #gameReport {
background: #eceeee;
border: 1px solid #42464b;
border-radius: 6px;
margin: 2px auto 0; margin: 2px auto 0;
width: 298px; width: 298px;
} }
#gameReport h1 { #gameReport h1 {
background-image: linear-gradient(top, #f1f3f3, #d4dae0);
border-bottom: 1px solid #a6abaf; border-bottom: 1px solid #a6abaf;
border-radius: 6px 6px 0 0;
box-sizing: border-box; box-sizing: border-box;
color: #727678;
display: block; display: block;
height: 43px; height: 43px;
padding-top: 10px; padding-top: 10px;
margin: 0; margin: 0;
text-align: center; text-align: center;
text-shadow: 0 -1px 0 rgba(0,0,0,0.2), 0 1px 0 #fff;
} }
#gameReport h3 { #gameReport h3 {
margin: 0; margin: 0;
@ -44,14 +37,9 @@
#gameReport button { #gameReport button {
margin-top: 10px; margin-top: 10px;
font-size: 60%; font-size: 60%;
background-color: #dc3545;
color: white;
border-radius: 7px;
padding: 3px 10px 3px 10px; padding: 3px 10px 3px 10px;
} }
#gameReport button#gameReportFormCancel { #gameReport button#gameReportFormCancel {
background-color: #c7c7c700;
color: #292929;
display: block; display: block;
float: right; float: right;
} }
@ -84,9 +72,9 @@
} }
</style> </style>
<main id="gameReport" hidden> <main id="gameReport" class="nes-container is-dark" hidden>
<section> <section>
<button id="gameReportFormCancel">X</button> <button id="gameReportFormCancel" class="nes-btn is-dark">X</button>
<h1>Moderate <span id="nameReported"></span></h1> <h1>Moderate <span id="nameReported"></span></h1>
<p id="askActionP">What action do you want to take?</p> <p id="askActionP">What action do you want to take?</p>
</section> </section>
@ -94,7 +82,7 @@
<h3>Block: </h3> <h3>Block: </h3>
<p>Block any communication from and to this user. This can be reverted.</p> <p>Block any communication from and to this user. This can be reverted.</p>
<section class="action"> <section class="action">
<button id="toggleBlockButton">Block this user</button> <button id="toggleBlockButton" class="nes-btn is-error">Block this user</button>
</section> </section>
</section> </section>
<section id="reportSection" hidden> <section id="reportSection" hidden>
@ -107,7 +95,7 @@
<p class="err" id="gameReportErr"></p> <p class="err" id="gameReportErr"></p>
</section> </section>
<section class="action"> <section class="action">
<button type="submit" id="gameReportFormSubmit">Report this user</button> <button type="submit" id="gameReportFormSubmit" class="nes-btn is-error">Report this user</button>
</section> </section>
</form> </form>
</section> </section>

View File

@ -1,92 +0,0 @@
<style>
#helpCameraSettings {
background: #eceeee;
margin: 25px auto 0;
width: 400px;
max-height: calc(48vh - 150px);
max-width: 48vw;
overflow: auto;
padding-bottom: 10px;
}
#helpCameraSettings h1 {
background-image: linear-gradient(top, #f1f3f3, #d4dae0);
border-bottom: 1px solid #a6abaf;
border-radius: 6px 6px 0 0;
box-sizing: border-box;
color: #727678;
display: block;
height: 43px;
padding-top: 10px;
margin: 0;
text-align: center;
text-shadow: 0 -1px 0 rgba(0,0,0,0.2), 0 1px 0 #fff;
}
#helpCameraSettings section {
margin: 10px;
}
#helpCameraSettings section.action{
text-align: center;
margin: 0;
}
#helpCameraSettings button#helpCameraSettingsFormCancel {
background-color: #c7c7c700;
color: #292929;
}
#helpCameraSettings section h6,
#helpCameraSettings section h5{
margin: 1px;
}
#helpCameraSettings section.text-center{
text-align: center;
}
#helpCameraSettings section p{
font-size: 7px;
margin: 0px 20px;
}
#helpCameraSettings section p a{
font-size: 8px;
}
#helpCameraSettings section p.err{
color: #ff0000;
}
#helpCameraSettings section ul{
margin: 6px;
}
#helpCameraSettings section li{
text-align: left;
font-size: 7px;
}
#helpCameraSettings section img {
width: 200px;
margin-top: 10px;
}
@media only screen and (max-width: 800px),
only screen and (max-height: 600px) {
#helpCameraSettings{
overflow-y: scroll;
}
}
</style>
<form id="helpCameraSettings" class="nes-container" hidden>
<section class="text-center">
<h5>Camera/Microphone access needed</h5>
<p class="err" id="permissionError">Permission denied</p>
<p class="info">You must allow camera and microphone access in your browser.</p>
<ul>
<li>Please click on the lock or camera symbol on the side of the URL in the address bar. Here you can grant "always allow" access to your input devices.</li>
<li>Please ensure that you have a camera AND microphone plugged into your computer.</li>
</ul>
<p class="info">Once you've followed these steps, please refresh this page.</p>
<p>If you prefer to continue without allowing camera and microphone access, click on Continue</p>
<p id='browserHelpSetting'></p>
</section>
<!--<section class="text-center">
<p>If your problem persist, please contact us: <a id="mailto" href="mailto:workadventure@thecodingmachine.com?subject=Support camera and microphone settings" target="_blank"> workadventure@thecodingmachine.com</a>.</p>
</section>-->
</section>
<section class="action">
<a href="#" class="nes-btn" id="helpCameraSettingsFormRefresh">Refresh</a>
<button type="submit" class="nes-btn is-warning" id="helpCameraSettingsFormContinue">Continue</button>
</section>
</form>

View File

@ -1,97 +0,0 @@
<style>
#loginScene {
background: #000000;
/*border: 1px solid #ebeeee;*/
border-radius: 6px;
margin: 20px auto 0;
width: 90%;
max-width: 200px;
color: #ebeeee;
max-height: 40vh;
overflow: auto;
}
#loginScene h1 {
background-image: linear-gradient(top, #f1f3f3, #d4dae0);
border-bottom: 1px solid #a6abaf;
border-radius: 6px 6px 0 0;
box-sizing: border-box;
color: #727678;
display: block;
height: 43px;
padding-top: 10px;
margin: 0;
text-align: center;
text-shadow: 0 -1px 0 rgba(0,0,0,0.2), 0 1px 0 #fff;
}
#loginScene section {
margin: 10px;
}
#loginScene section.action{
text-align: center;
margin: 0;
}
#loginScene button#loginSceneFormCancel {
background-color: #c7c7c700;
color: #292929;
}
#loginScene section h6,
#loginScene section h5{
margin: 1px;
}
#loginScene section.text-center{
text-align: center;
}
#loginScene section a{
font-size: 8px;
text-decoration: underline;
color: #ebeeee;
}
#loginScene section a:hover{
font-weight: 700;
}
#loginScene section p{
text-align: left;
font-size: 8px;
margin: 10px 10px;
}
#loginScene section p.err{
color: red;
text-align: center;
}
#loginScene section p.info{
display: none;
text-align: center;
}
#loginScene section input#loginSceneLink{
background-color: #a1a3a3;
}
#loginScene section img{
width: 160px;
margin: 20px 0;
}
@media only screen and (max-width: 800px),
only screen and (max-height: 600px) {
#loginScene{
overflow-y: scroll;
margin-left: 80vw;
}
}
</style>
<form id="loginScene" hidden>
<section class="text-center">
<img src="resources/logos/logo.png">
</section>
<section class="text-center">
<h5>Enter your name</h5>
<p class="info">9 chars maximum</p>
<p class="err" id="errorLoginScene"></p>
</section>
<section>
<input type="text" name="email" class="nes-input" id="loginSceneName">
<p></p>
</section>
<section class="action">
<button class="nes-btn" type="submit" id="loginSceneFormSubmit">Continue</button>
</section>
</form>

View File

@ -1,118 +0,0 @@
<style>
#selectCharacterScene {
background: #0000;
/*border: 1px solid #ebeeee;*/
border-radius: 6px;
margin: 10px auto 0;
color: #ebeeee;
max-height: 100vh;
max-width: 300px;
width: 40vw;
overflow: hidden;
}
#selectCharacterScene h1 {
background-image: linear-gradient(top, #f1f3f3, #d4dae0);
border-bottom: 1px solid #a6abaf;
border-radius: 6px 6px 0 0;
box-sizing: border-box;
color: #727678;
display: block;
height: 43px;
padding-top: 10px;
margin: 0;
text-align: center;
text-shadow: 0 -1px 0 rgba(0,0,0,0.2), 0 1px 0 #fff;
}
#selectCharacterScene section {
margin: 10px;
}
#selectCharacterScene section.action {
text-align: center;
margin: 0;
margin-top: 38vh;
}
#selectCharacterScene button#selectCharacterSceneFormCancel {
background-color: #aca6a600;
color: #292929;
}
#selectCharacterScene section h6,
#selectCharacterScene section h5{
margin: 1px;
}
#selectCharacterScene section.text-center{
text-align: center;
}
#selectCharacterScene section a{
font-size: 8px;
text-decoration: underline;
color: #ebeeee;
}
#selectCharacterScene section a:hover{
font-weight: 700;
}
#selectCharacterScene section p{
text-align: left;
font-size: 8px;
margin: 10px 10px;
}
#selectCharacterScene section p.err{
color: red;
text-align: center;
}
#selectCharacterScene section p.info{
display: none;
text-align: center;
}
#selectCharacterScene section input#selectCharacterSceneLink{
background-color: #a1a3a3;
}
#selectCharacterScene section img{
width: 160px;
margin: 20px 0;
}
#selectCharacterScene section button.selectCharacterButton{
position: absolute;
top: 20vh;
margin: 0;
width: 25px;
}
#selectCharacterScene section button.selectCharacterButton#selectCharacterButtonLeft{
display: none;
left: 1vw;
}
#selectCharacterScene section button.selectCharacterButton#selectCharacterButtonRight{
display: none;
right: 1vw;
}
@media only screen and (max-width: 800px),
only screen and (max-height: 600px) {
#selectCharacterScene{
overflow-y: scroll;
width: 70vw;
margin-left: 70vw;
margin-top: 30vw;
}
#selectCharacterScene section button.selectCharacterButton#selectCharacterButtonRight{
display: block;
}
#selectCharacterScene section button.selectCharacterButton#selectCharacterButtonLeft{
display: block;
left: auto;
}
#selectCharacterScene section button.selectCharacterButton{
top: 40vh;
}
}
</style>
<form id="selectCharacterScene" hidden>
<section class="text-center">
<h5>Select your Avatar</h5>
<button class="selectCharacterButton nes-btn" id="selectCharacterButtonLeft"> < </button>
<button class="selectCharacterButton nes-btn" id="selectCharacterButtonRight"> > </button>
</section>
<section class="action">
<button type="submit" class="nes-btn" id="selectCharacterSceneFormSubmit">Continue</button>
<button type="submit" class="nes-btn is-warning" id="selectCharacterSceneFormCustomYourOwnSubmit">Custom your Avatar</button>
</section>
</form>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 B

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -50,7 +50,8 @@
"rxjs": "^6.6.3", "rxjs": "^6.6.3",
"sanitize-html": "^2.4.0", "sanitize-html": "^2.4.0",
"simple-peer": "^9.6.2", "simple-peer": "^9.6.2",
"socket.io-client": "^2.3.0" "socket.io-client": "^2.3.0",
"standardized-audio-context": "^25.2.4"
}, },
"scripts": { "scripts": {
"start": "TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open", "start": "TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open",

View File

@ -3,6 +3,8 @@ import {AUDIO_TYPE, MESSAGE_TYPE} from "./ConsoleGlobalMessageManager";
import {PUSHER_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable"; import {PUSHER_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable";
import type {RoomConnection} from "../Connexion/RoomConnection"; import type {RoomConnection} from "../Connexion/RoomConnection";
import type {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels"; import type {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels";
import {soundPlayingStore} from "../Stores/SoundPlayingStore";
import {soundManager} from "../Phaser/Game/SoundManager";
export class GlobalMessageManager { export class GlobalMessageManager {
@ -43,45 +45,8 @@ export class GlobalMessageManager {
} }
} }
private playAudioMessage(messageId : string, urlMessage: string){ private playAudioMessage(messageId : string, urlMessage: string) {
//delete previous elements soundPlayingStore.playSound(UPLOADER_URL + urlMessage);
const previousDivAudio = document.getElementsByClassName('audio-playing');
for(let i = 0; i < previousDivAudio.length; i++){
previousDivAudio[i].remove();
}
//create new element
const divAudio : HTMLDivElement = document.createElement('div');
divAudio.id = `audio-playing-${messageId}`;
divAudio.classList.add('audio-playing');
const imgAudio : HTMLImageElement = document.createElement('img');
imgAudio.src = '/resources/logos/megaphone.svg';
const pAudio : HTMLParagraphElement = document.createElement('p');
pAudio.textContent = 'Message audio'
divAudio.appendChild(imgAudio);
divAudio.appendChild(pAudio);
const mainSectionDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
mainSectionDiv.appendChild(divAudio);
const messageAudio : HTMLAudioElement = document.createElement('audio');
messageAudio.id = this.getHtmlMessageId(messageId);
messageAudio.autoplay = true;
messageAudio.style.display = 'none';
messageAudio.onended = () => {
divAudio.classList.remove('active');
messageAudio.remove();
setTimeout(() => {
divAudio.remove();
}, 1000);
}
messageAudio.onplay = () => {
divAudio.classList.add('active');
}
const messageAudioSource : HTMLSourceElement = document.createElement('source');
messageAudioSource.src = `${UPLOADER_URL}${urlMessage}`;
messageAudio.appendChild(messageAudioSource);
mainSectionDiv.appendChild(messageAudio);
} }
private playTextMessage(messageId : string, htmlMessage: string){ private playTextMessage(messageId : string, htmlMessage: string){

View File

@ -44,7 +44,13 @@ export class TypeMessageExt implements TypeMessageInterface{
mainSectionDiv.appendChild(div); mainSectionDiv.appendChild(div);
const reportMessageAudio = HtmlUtils.getElementByIdOrFail<HTMLAudioElement>('report-message'); const reportMessageAudio = HtmlUtils.getElementByIdOrFail<HTMLAudioElement>('report-message');
reportMessageAudio.play(); // FIXME: this will fail on iOS
// We should move the sound playing into the GameScene and listen to the event of a report using a store
try {
reportMessageAudio.play();
} catch (e) {
console.error(e);
}
this.nbSecond = this.maxNbSecond; this.nbSecond = this.maxNbSecond;
setTimeout((c) => { setTimeout((c) => {

View File

@ -1,18 +1,81 @@
<script lang="typescript"> <script lang="typescript">
import MenuIcon from "./Menu/MenuIcon.svelte"; import MenuIcon from "./Menu/MenuIcon.svelte";
import {menuIconVisible} from "../Stores/MenuStore"; import {menuIconVisible} from "../Stores/MenuStore";
import {gameOverlayVisibilityStore} from "../Stores/MediaStore"; import {enableCameraSceneVisibilityStore, gameOverlayVisibilityStore} from "../Stores/MediaStore";
import CameraControls from "./CameraControls.svelte"; import CameraControls from "./CameraControls.svelte";
import MyCamera from "./MyCamera.svelte"; import MyCamera from "./MyCamera.svelte";
import SelectCompanionScene from "./SelectCompanion/SelectCompanionScene.svelte";
import {selectCompanionSceneVisibleStore} from "../Stores/SelectCompanionStore";
import {selectCharacterSceneVisibleStore} from "../Stores/SelectCharacterStore";
import SelectCharacterScene from "./selectCharacter/SelectCharacterScene.svelte";
import {customCharacterSceneVisibleStore} from "../Stores/CustomCharacterStore";
import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte";
import LoginScene from "./Login/LoginScene.svelte";
import {loginSceneVisibleStore} from "../Stores/LoginSceneStore";
import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte";
import VisitCard from "./VisitCard/VisitCard.svelte";
import {requestVisitCardsStore} from "../Stores/GameStore";
import {Game} from "../Phaser/Game/Game";
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
import AudioPlaying from "./UI/AudioPlaying.svelte";
import {soundPlayingStore} from "../Stores/SoundPlayingStore";
export let game: Game;
</script> </script>
<div> <div>
{#if $loginSceneVisibleStore}
<div class="scrollable">
<LoginScene game={game}></LoginScene>
</div>
{/if}
{#if $selectCharacterSceneVisibleStore}
<div>
<SelectCharacterScene game={ game }></SelectCharacterScene>
</div>
{/if}
{#if $customCharacterSceneVisibleStore}
<div>
<CustomCharacterScene game={ game }></CustomCharacterScene>
</div>
{/if}
{#if $selectCompanionSceneVisibleStore}
<div>
<SelectCompanionScene game={ game }></SelectCompanionScene>
</div>
{/if}
{#if $enableCameraSceneVisibilityStore}
<div class="scrollable">
<EnableCameraScene game={game}></EnableCameraScene>
</div>
{/if}
{#if $soundPlayingStore}
<div>
<AudioPlaying url={$soundPlayingStore} />
</div>
{/if}
<!--
{#if $menuIconVisible}
<div>
<MenuIcon />
</div>
{/if}
-->
{#if $gameOverlayVisibilityStore} {#if $gameOverlayVisibilityStore}
<!-- {#if $menuIconVisible} <div>
<MenuIcon /> <MyCamera></MyCamera>
{/if} --> <CameraControls></CameraControls>
<MyCamera></MyCamera> </div>
<CameraControls></CameraControls> {/if}
{#if $helpCameraSettingsVisibleStore}
<div>
<HelpCameraSettingsPopup game={ game }></HelpCameraSettingsPopup>
</div>
{/if}
{#if $requestVisitCardsStore}
<VisitCard visitCardUrl={$requestVisitCardsStore}></VisitCard>
{/if} {/if}
</div> </div>

View File

@ -34,26 +34,28 @@
</script> </script>
<div class="btn-cam-action"> <div>
<div class="btn-micro nes-btn is-dark" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState}> <div class="btn-cam-action">
{#if $requestedMicrophoneState} <div class="btn-micro nes-btn is-dark" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState}>
<img src={microphoneImg} alt="Turn on microphone"> {#if $requestedMicrophoneState}
{:else} <img src={microphoneImg} alt="Turn on microphone">
<img src={microphoneCloseImg} alt="Turn off microphone"> {:else}
{/if} <img src={microphoneCloseImg} alt="Turn off microphone">
</div> {/if}
<div class="btn-video nes-btn is-dark" on:click={cameraClick} class:disabled={!$requestedCameraState}> </div>
{#if $requestedCameraState} <div class="btn-video nes-btn is-dark" on:click={cameraClick} class:disabled={!$requestedCameraState}>
<img src={cinemaImg} alt="Turn on webcam"> {#if $requestedCameraState}
{:else} <img src={cinemaImg} alt="Turn on webcam">
<img src={cinemaCloseImg} alt="Turn off webcam"> {:else}
{/if} <img src={cinemaCloseImg} alt="Turn off webcam">
</div> {/if}
<div class="btn-monitor nes-btn is-dark" on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore} class:enabled={$requestedScreenSharingState}> </div>
{#if $requestedScreenSharingState} <div class="btn-monitor nes-btn is-dark" on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore} class:enabled={$requestedScreenSharingState}>
<img src={monitorImg} alt="Start screen sharing"> {#if $requestedScreenSharingState}
{:else} <img src={monitorImg} alt="Start screen sharing">
<img src={monitorCloseImg} alt="Stop screen sharing"> {:else}
{/if} <img src={monitorCloseImg} alt="Stop screen sharing">
{/if}
</div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,119 @@
<script lang="typescript">
import { Game } from "../../Phaser/Game/Game";
import { CustomizeSceneName } from "../../Phaser/Login/CustomizeScene";
export let game: Game;
const customCharacterScene = game.scene.getScene(CustomizeSceneName);
let activeRow = customCharacterScene.activeRow;
function selectLeft() {
customCharacterScene.moveCursorHorizontally(-1);
}
function selectRight() {
customCharacterScene.moveCursorHorizontally(1);
}
function selectUp() {
customCharacterScene.moveCursorVertically(-1);
activeRow = customCharacterScene.activeRow;
}
function selectDown() {
customCharacterScene.moveCursorVertically(1);
activeRow = customCharacterScene.activeRow;
}
function previousScene() {
customCharacterScene.backToPreviousScene();
}
function finish() {
customCharacterScene.nextSceneToCamera();
}
</script>
<form class="customCharacterScene">
<section class="text-center">
<h2>Customize your WOKA</h2>
</section>
<section class="action action-move">
<button class="customCharacterSceneButton customCharacterSceneButtonLeft nes-btn" on:click|preventDefault={ selectLeft }> &lt; </button>
<button class="customCharacterSceneButton customCharacterSceneButtonRight nes-btn" on:click|preventDefault={ selectRight }> &gt; </button>
</section>
<section class="action">
{#if activeRow === 0}
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={ previousScene }>Return</button>
{/if}
{#if activeRow !== 0}
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={ selectUp }>Back <img src="resources/objects/arrow_up_black.png" alt=""/></button>
{/if}
{#if activeRow === 5}
<button type="submit" class="customCharacterSceneFormSubmit nes-btn is-primary" on:click|preventDefault={ finish }>Finish</button>
{/if}
{#if activeRow !== 5}
<button type="submit" class="customCharacterSceneFormSubmit nes-btn is-primary" on:click|preventDefault={ selectDown }>Next <img src="resources/objects/arrow_down.png" alt=""/></button>
{/if}
</section>
</form>
<style lang="scss">
form.customCharacterScene {
font-family: "Press Start 2P";
pointer-events: auto;
color: #ebeeee;
section {
margin: 10px;
&.action {
text-align: center;
margin-top: 55vh;
}
h2 {
font-family: "Press Start 2P";
margin: 1px;
}
&.text-center {
text-align: center;
}
button.customCharacterSceneButton {
position: absolute;
top: 33vh;
margin: 0;
}
button.customCharacterSceneFormBack {
color: #292929;
}
}
button {
font-family: "Press Start 2P";
&.customCharacterSceneButtonLeft {
left: 33vw;
}
&.customCharacterSceneButtonRight {
right: 33vw;
}
}
}
@media only screen and (max-width: 800px) {
form.customCharacterScene button.customCharacterSceneButtonLeft{
left: 5vw;
}
form.customCharacterScene button.customCharacterSceneButtonRight{
right: 5vw;
}
}
</style>

View File

@ -0,0 +1,217 @@
<script lang="typescript">
import {Game} from "../../Phaser/Game/Game";
import {EnableCameraScene, EnableCameraSceneName} from "../../Phaser/Login/EnableCameraScene";
import {
audioConstraintStore,
cameraListStore,
localStreamStore,
microphoneListStore,
videoConstraintStore
} from "../../Stores/MediaStore";
import {onDestroy} from "svelte";
import HorizontalSoundMeterWidget from "./HorizontalSoundMeterWidget.svelte";
import cinemaCloseImg from "../images/cinema-close.svg";
import cinemaImg from "../images/cinema.svg";
import microphoneImg from "../images/microphone.svg";
export let game: Game;
let selectedCamera : string|null = null;
let selectedMicrophone : string|null = null;
const enableCameraScene = game.scene.getScene(EnableCameraSceneName) as EnableCameraScene;
function submit() {
enableCameraScene.login();
}
function srcObject(node, stream) {
node.srcObject = stream;
return {
update(newStream) {
if (node.srcObject != newStream) {
node.srcObject = newStream
}
}
}
}
let stream: MediaStream | null;
const unsubscribe = localStreamStore.subscribe(value => {
if (value.type === 'success') {
stream = value.stream;
if (stream !== null) {
const videoTracks = stream.getVideoTracks();
if (videoTracks.length > 0) {
selectedCamera = videoTracks[0].getSettings().deviceId;
}
const audioTracks = stream.getAudioTracks();
if (audioTracks.length > 0) {
selectedMicrophone = audioTracks[0].getSettings().deviceId;
}
}
} else {
stream = null;
selectedCamera = null;
selectedMicrophone = null;
}
});
onDestroy(unsubscribe);
function normalizeDeviceName(label: string): string {
// remove text in parenthesis
return label.replace(/\([^()]*\)/g, '').trim();
}
function selectCamera() {
videoConstraintStore.setDeviceId(selectedCamera);
}
function selectMicrophone() {
audioConstraintStore.setDeviceId(selectedMicrophone);
}
</script>
<form class="enableCameraScene" on:submit|preventDefault={submit}>
<section class="text-center">
<h2>Turn on your camera and microphone</h2>
</section>
{#if $localStreamStore.stream}
<video class="myCamVideoSetup" use:srcObject={$localStreamStore.stream} autoplay muted playsinline></video>
{:else }
<div class="webrtcsetup">
<img class="background-img" src={cinemaCloseImg} alt="">
</div>
{/if}
<HorizontalSoundMeterWidget stream={stream}></HorizontalSoundMeterWidget>
<section class="selectWebcamForm">
{#if $cameraListStore.length > 1 }
<div class="control-group">
<img src={cinemaImg} alt="Camera" />
<div class="nes-select">
<select bind:value={selectedCamera} on:change={selectCamera}>
{#each $cameraListStore as camera}
<option value={camera.deviceId}>
{normalizeDeviceName(camera.label)}
</option>
{/each}
</select>
</div>
</div>
{/if}
{#if $microphoneListStore.length > 1 }
<div class="control-group">
<img src={microphoneImg} alt="Microphone" />
<div class="nes-select">
<select bind:value={selectedMicrophone} on:change={selectMicrophone}>
{#each $microphoneListStore as microphone}
<option value={microphone.deviceId}>
{normalizeDeviceName(microphone.label)}
</option>
{/each}
</select>
</div>
</div>
{/if}
</section>
<section class="action">
<button type="submit" class="nes-btn is-primary letsgo" >Let's go!</button>
</section>
</form>
<style lang="scss">
.enableCameraScene {
pointer-events: auto;
margin: 20px auto 0;
color: #ebeeee;
section.selectWebcamForm {
margin-top: 3vh;
margin-bottom: 3vh;
min-height: 10vh;
width: 50%;
margin-left: auto;
margin-right: auto;
select {
font-family: "Press Start 2P";
margin-top: 1vh;
margin-bottom: 1vh;
option {
font-family: "Press Start 2P";
}
}
}
section.action{
text-align: center;
margin: 0;
width: 100%;
}
h2{
font-family: "Press Start 2P";
margin: 1px;
}
section.text-center{
text-align: center;
}
button.letsgo {
font-size: 200%;
}
.control-group {
display: flex;
flex-direction: row;
img {
width: 30px;
margin-right: 10px;
}
}
.webrtcsetup{
margin-top: 2vh;
margin-left: auto;
margin-right: auto;
height: 28.125vw;
width: 50vw;
border: white 6px solid;
display: flex;
align-items: center;
justify-content: center;
img.background-img {
width: 40%;
}
}
.myCamVideoSetup {
margin-top: 2vh;
margin-left: auto;
margin-right: auto;
max-height: 50vh;
width: 50vw;
border: white 6px solid;
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
display: flex;
align-items: center;
justify-content: center;
}
}
</style>

View File

@ -0,0 +1,82 @@
<script lang="typescript">
import { AudioContext } from 'standardized-audio-context';
import {SoundMeter} from "../../Phaser/Components/SoundMeter";
import {onDestroy} from "svelte";
export let stream: MediaStream | null;
let volume = 0;
const NB_BARS = 20;
let timeout;
const soundMeter = new SoundMeter();
let display = false;
$: {
if (stream && stream.getAudioTracks().length > 0) {
display = true;
soundMeter.connectToSource(stream, new AudioContext());
if (timeout) {
clearInterval(timeout);
}
timeout = setInterval(() => {
try{
volume = parseInt((soundMeter.getVolume() / 100 * NB_BARS).toFixed(0));
//console.log(volume);
}catch(err){
}
}, 100);
} else {
display = false;
}
}
onDestroy(() => {
soundMeter.stop();
if (timeout) {
clearInterval(timeout);
}
})
function color(i: number, volume: number) {
const red = 255 * i / NB_BARS;
const green = 255 * (1 - i / NB_BARS);
let alpha = 1;
if (i >= volume) {
alpha = 0.5;
}
return 'background-color:rgba('+red+', '+green+', 0, '+alpha+')';
}
</script>
<div class="horizontal-sound-meter" class:active={display}>
{#each [...Array(NB_BARS).keys()] as i}
<div style={color(i, volume)}></div>
{/each}
</div>
<style lang="scss">
.horizontal-sound-meter {
display: flex;
flex-direction: row;
width: 50%;
height: 30px;
margin-left: auto;
margin-right: auto;
margin-top: 1vh;
}
.horizontal-sound-meter div {
margin-left: 5px;
flex-grow: 1;
}
</style>

View File

@ -0,0 +1,73 @@
<script lang="typescript">
import { fly } from 'svelte/transition';
import {helpCameraSettingsVisibleStore} from "../../Stores/HelpCameraSettingsStore";
import firefoxImg from "./images/help-setting-camera-permission-firefox.png";
import chromeImg from "./images/help-setting-camera-permission-chrome.png";
let isAndroid = window.navigator.userAgent.includes('Android');
let isFirefox = window.navigator.userAgent.includes('Firefox');
let isChrome = window.navigator.userAgent.includes('Chrome');
function refresh() {
window.location.reload();
}
function close() {
helpCameraSettingsVisibleStore.set(false);
}
</script>
<form class="helpCameraSettings nes-container" on:submit|preventDefault={close} transition:fly="{{ y: -900, duration: 500 }}">
<section>
<h2>Camera / Microphone access needed</h2>
<p class="err">Permission denied</p>
<p>You must allow camera and microphone access in your browser.</p>
<p>
{#if isFirefox }
<p class="err">Please click the "Remember this decision" checkbox, if you don't want Firefox to keep asking you the authorization.</p>
<img src={firefoxImg} alt="" />
{:else if isChrome && !isAndroid }
<img src={chromeImg} alt="" />
{/if}
</p>
</section>
<section>
<button class="helpCameraSettingsFormRefresh nes-btn" on:click|preventDefault={refresh}>Refresh</button>
<button type="submit" class="helpCameraSettingsFormContinue nes-btn is-primary" on:click|preventDefault={close}>Continue without webcam</button>
</section>
</form>
<style lang="scss">
.helpCameraSettings {
pointer-events: auto;
background: #eceeee;
margin-left: auto;
margin-right: auto;
margin-top: 10vh;
max-height: 80vh;
max-width: 80vw;
overflow: auto;
text-align: center;
h2 {
font-family: 'Press Start 2P';
}
section {
p {
margin: 15px;
font-family: 'Press Start 2P';
& .err {
color: #ff0000;
}
}
img {
max-width: 500px;
width: 100%;
}
}
}
</style>

View File

@ -0,0 +1,123 @@
<script lang="typescript">
import {Game} from "../../Phaser/Game/Game";
import {LoginSceneName} from "../../Phaser/Login/LoginScene";
import {DISPLAY_TERMS_OF_USE, MAX_USERNAME_LENGTH} from "../../Enum/EnvironmentVariable";
import logoImg from "../images/logo.png";
import {gameManager} from "../../Phaser/Game/GameManager";
import {maxUserNameLength} from "../../Connexion/LocalUser";
export let game: Game;
const loginScene = game.scene.getScene(LoginSceneName);
let name = gameManager.getPlayerName() || '';
let startValidating = false;
function submit() {
startValidating = true;
let finalName = name.trim();
if (finalName !== '') {
loginScene.login(finalName);
}
}
</script>
<form class="loginScene" on:submit|preventDefault={submit}>
<section class="text-center">
<img src={logoImg} alt="WorkAdventure logo" />
</section>
<section class="text-center">
<h2>Enter your name</h2>
</section>
<input type="text" name="loginSceneName" class="nes-input is-dark" autofocus maxlength={MAX_USERNAME_LENGTH} bind:value={name} on:keypress={() => {startValidating = true}} class:is-error={name.trim() === '' && startValidating} />
<section class="error-section">
{#if name.trim() === '' && startValidating }
<p class="err">The name is empty</p>
{/if}
</section>
{#if DISPLAY_TERMS_OF_USE}
<section class="terms-and-conditions">
<p>By continuing, you are agreeing our <a href="https://workadventu.re/terms-of-use" target="_blank">terms of use</a>, <a href="https://workadventu.re/privacy-policy" target="_blank">privacy policy</a> and <a href="https://workadventu.re/cookie-policy" target="_blank">cookie policy</a>.</p>
</section>
{/if}
<section class="action">
<button type="submit" class="nes-btn is-primary loginSceneFormSubmit">Continue</button>
</section>
</form>
<style lang="scss">
.loginScene {
pointer-events: auto;
margin: 20px auto 0;
width: 90%;
color: #ebeeee;
display: flex;
flex-flow: column wrap;
align-items: center;
input {
text-align: center;
font-family: "Press Start 2P";
max-width: 400px;
}
.terms-and-conditions {
max-width: 400px;
}
p.err {
color: #ce372b;
text-align: center;
}
section {
margin: 10px;
&.error-section {
min-height: 2rem;
margin: 0;
p {
margin: 0;
}
}
&.action {
text-align: center;
margin-top: 20px;
}
h2 {
font-family: "Press Start 2P";
margin: 1px;
}
&.text-center {
text-align: center;
}
a {
text-decoration: underline;
color: #ebeeee;
}
a:hover {
font-weight: 700;
}
p {
text-align: left;
margin: 10px 10px;
}
img {
width: 100%;
margin: 20px 0;
}
}
}
</style>

View File

@ -12,14 +12,10 @@
<style lang="scss"> <style lang="scss">
.menuIcon button { .menuIcon button {
background-color: black;
color: white;
border-radius: 7px;
padding: 2px 8px; padding: 2px 8px;
img { img {
width: 14px; width: 14px;
padding-top: 0; padding-top: 0;
/*cursor: url('/resources/logos/cursor_pointer.png'), pointer;*/
} }
} }
.menuIcon section { .menuIcon section {

View File

@ -38,7 +38,7 @@
<div> <div>
<div class="video-container nes-container is-rounded is-dark div-myCamVideo" class:hide={!$localStreamStore.constraints.video}> <div class="video-container nes-container is-rounded is-dark div-myCamVideo" class:hide={!$localStreamStore.constraints.video}>
<video class="myCamVideo" use:srcObject={$localStreamStore.stream} autoplay muted></video> <video class="myCamVideo" use:srcObject={$localStreamStore.stream} autoplay muted playsinline></video>
<!-- {#if stream} <!-- {#if stream}
<SoundMeterWidget stream={stream}></SoundMeterWidget> <SoundMeterWidget stream={stream}></SoundMeterWidget>
{/if} --> {/if} -->

View File

@ -0,0 +1,87 @@
<script lang="typescript">
import {Game} from "../../Phaser/Game/Game";
import {SelectCompanionSceneName} from "../../Phaser/Login/SelectCompanionScene";
export let game: Game;
const selectCompanionScene = game.scene.getScene(SelectCompanionSceneName);
function selectLeft() {
selectCompanionScene.moveToLeft();
}
function selectRight() {
selectCompanionScene.moveToRight();
}
function noCompanion() {
selectCompanionScene.closeScene();
}
function selectCompanion() {
selectCompanionScene.selectCompanion();
}
</script>
<form class="selectCompanionScene">
<section class="text-center">
<h2>Select your companion</h2>
<button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}> &lt; </button>
<button class="selectCharacterButton selectCharacterButtonRight nes-btn" on:click|preventDefault={selectRight}> &gt; </button>
</section>
<section class="action">
<button href="/" class="selectCompanionSceneFormBack nes-btn" on:click|preventDefault={noCompanion}>No companion</button>
<button type="submit" class="selectCompanionSceneFormSubmit nes-btn is-primary" on:click|preventDefault={selectCompanion}>Continue</button>
</section>
</form>
<style lang="scss">
form.selectCompanionScene {
font-family: "Press Start 2P";
pointer-events: auto;
color: #ebeeee;
section {
margin: 10px;
&.action {
text-align: center;
margin-top: 55vh;
}
h2 {
font-family: "Press Start 2P";
margin: 1px;
}
&.text-center {
text-align: center;
}
button.selectCharacterButton {
position: absolute;
top: 33vh;
margin: 0;
}
}
button.selectCharacterButtonLeft {
left: 33vw;
}
button.selectCharacterButtonRight {
right: 33vw;
}
}
@media only screen and (max-width: 800px) {
form.selectCompanionScene button.selectCharacterButtonLeft{
left: 5vw;
}
form.selectCompanionScene button.selectCharacterButtonRight{
right: 5vw;
}
}
</style>

View File

@ -1,33 +1,50 @@
<script lang="typescript"> <script lang="typescript">
import { AudioContext } from 'standardized-audio-context';
import {SoundMeter} from "../Phaser/Components/SoundMeter"; import {SoundMeter} from "../Phaser/Components/SoundMeter";
import {onDestroy} from "svelte"; import {onDestroy} from "svelte";
export let stream: MediaStream; export let stream: MediaStream|null;
let volume = 0; let volume = 0;
console.log('stream', stream); const NB_BARS = 5;
if (stream.getAudioTracks().length > 0) { let timeout;
const soundMeter = new SoundMeter(); const soundMeter = new SoundMeter();
soundMeter.connectToSource(stream, new AudioContext()); let display = false;
const timeout = setInterval(() => { $: {
try{ if (stream && stream.getAudioTracks().length > 0) {
volume = parseInt((soundMeter.getVolume() / 10).toFixed(0)); display = true;
console.log(volume); soundMeter.connectToSource(stream, new AudioContext());
}catch(err){
if (timeout) {
clearInterval(timeout);
} }
}, 100);
onDestroy(() => { timeout = setInterval(() => {
clearInterval(timeout); try{
}) volume = parseInt((soundMeter.getVolume() / 100 * NB_BARS).toFixed(0));
//console.log(volume);
}catch(err){
}
}, 100);
} else {
display = false;
}
} }
onDestroy(() => {
soundMeter.stop();
if (timeout) {
clearInterval(timeout);
}
})
</script> </script>
<div class="sound-progress" class:active={stream?.getAudioTracks().length > 0}> <div class="sound-progress" class:active={display}>
<span class:active={volume > 1}></span> <span class:active={volume > 1}></span>
<span class:active={volume > 2}></span> <span class:active={volume > 2}></span>
<span class:active={volume > 3}></span> <span class:active={volume > 3}></span>

View File

@ -0,0 +1,52 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import megaphoneImg from "./images/megaphone.svg";
import {soundPlayingStore} from "../../Stores/SoundPlayingStore";
import {afterUpdate, onMount} from "svelte";
export let url: string;
let audio: HTMLAudioElement;
function soundEnded() {
soundPlayingStore.soundEnded();
}
afterUpdate(() => {
audio.play();
});
</script>
<div class="audio-playing" transition:fly="{{ x: 210, duration: 500 }}">
<img src={megaphoneImg} alt="Audio playing" />
<p>Audio message</p>
<audio bind:this={audio} src={url} on:ended={soundEnded} >
<track kind="captions">
</audio>
</div>
<style lang="scss">
/*audio html when audio message playing*/
.audio-playing {
position: absolute;
width: 200px;
height: 54px;
right: 0;
top: 40px;
transition: all 0.1s ease-out;
background-color: black;
border-radius: 30px 0 0 30px;
display: inline-flex;
img {
border-radius: 50%;
background-color: #ffda01;
padding: 10px;
}
p {
color: white;
margin-left: 10px;
margin-top: 14px;
}
}
</style>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<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 451.7 512" style="enable-background:new 0 0 451.7 512;" xml:space="preserve">
<path d="M436.9,212.6L237.2,12.9c-11.7-11.7-30.7-11.7-42.4,0s-11.7,30.7,0,42.4L394.5,255c11.5,11.9,30.5,12.2,42.4,0.7
c11.9-11.5,12.2-30.5,0.7-42.4C437.4,213.1,437.2,212.8,436.9,212.6z"/>
<path d="M179.5,83.1l-1.5,7.5c-10.4,53-36,103.4-70.6,144.3l109,108.3c40.7-34.9,90.2-61.5,143.1-72.3l7.5-1.5L179.5,83.1z"/>
<path d="M87.4,257l-74.2,74.2c-17.6,17.6-17.6,46.1,0,63.6c0,0,0,0,0,0l42.4,42.4c17.6,17.6,46.1,17.6,63.6,0c0,0,0,0,0,0l74.2-74.2
L87.4,257z M98,373.7c-6.1,5.6-15.6,5.3-21.2-0.8c-5.4-5.8-5.4-14.7,0-20.5l21.2-21.2c6-5.8,15.5-5.6,21.2,0.4
c5.6,5.8,5.6,15,0,20.8L98,373.7z"/>
<path d="M256.1,445.3l20.4-20.4c17.6-17.6,17.6-46.1,0-63.6l-15.1-15.2c-8.4,5.7-16.4,11.7-24.2,18.3l18.1,18.1
c5.8,5.9,5.8,15.3,0,21.2l-20.7,20.8l-30.5-29.5l-42.4,42.4l68.1,65.9c11.7,11.7,30.7,11.7,42.4,0c11.7-11.7,11.7-30.7,0-42.4l0,0
L256.1,445.3z"/>
<path d="M316.7,0c-8.3,0-15,6.7-15,15v30c0,8.3,6.7,15,15,15c8.3,0,15-6.7,15-15V15C331.7,6.7,325,0,316.7,0z"/>
<path d="M436.7,120h-30c-8.3,0-15,6.7-15,15s6.7,15,15,15h30c8.3,0,15-6.7,15-15S445,120,436.7,120z"/>
<path d="M417.3,34.4c-5.9-5.9-15.4-5.9-21.2,0l-30,30c-6,5.8-6.1,15.3-0.4,21.2c5.8,6,15.3,6.1,21.2,0.4c0.1-0.1,0.2-0.2,0.4-0.4
l30-30C423.2,49.7,423.2,40.2,417.3,34.4z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,64 @@
<script lang="typescript">
import { fly } from 'svelte/transition';
import {requestVisitCardsStore} from "../../Stores/GameStore";
export let visitCardUrl: string;
function closeCard() {
requestVisitCardsStore.set(null);
}
</script>
<style lang="scss">
.visitCard {
pointer-events: all;
margin-left: auto;
margin-right: auto;
width: 515px;
margin-top: 200px;
.defaultCard {
border-radius: 5px;
border: 2px black solid;
background-color: whitesmoke;
width: 500px;
header {
padding: 5px;
}
}
iframe {
border: 0;
width: 515px;
height: 270px;
overflow: hidden;
}
button {
float: right;
}
}
</style>
<section class="visitCard" transition:fly="{{ y: -200, duration: 1000 }}">
{#if visitCardUrl === 'INVALID'}
<div class="defaultCard">
<header>
<h2>Sorry</h2>
<p style="font-style: italic;">This user doesn't have a contact card.</p>
</header>
<main style="padding: 5px; background-color: gray">
<p>Maybe he is offline, or this feature is deactivated.</p>
</main>
</div>
{:else}
<iframe title="visitCardTitle" src={visitCardUrl}></iframe>
{/if}
<div class="buttonContainer">
<button class="nes-btn is-popUpElement" on:click={closeCard}>Close</button>
</div>
</section>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,92 @@
<script lang="typescript">
import { Game } from "../../Phaser/Game/Game";
import { SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene";
export let game: Game;
const selectCharacterScene = game.scene.getScene(SelectCharacterSceneName);
function selectLeft() {
selectCharacterScene.moveToLeft();
}
function selectRight() {
selectCharacterScene.moveToRight();
}
function cameraScene() {
selectCharacterScene.nextSceneToCameraScene();
}
function customizeScene() {
selectCharacterScene.nextSceneToCustomizeScene();
}
</script>
<form class="selectCharacterScene">
<section class="text-center">
<h2>Select your WOKA</h2>
<button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={ selectLeft }> &lt; </button>
<button class="selectCharacterButton selectCharacterButtonRight nes-btn" on:click|preventDefault={ selectRight }> &gt; </button>
</section>
<section class="action">
<button type="submit" class="selectCharacterSceneFormSubmit nes-btn is-primary" on:click|preventDefault={ cameraScene }>Continue</button>
<button type="submit" class="selectCharacterSceneFormCustomYourOwnSubmit nes-btn" on:click|preventDefault={ customizeScene }>Customize your WOKA</button>
</section>
</form>
<style lang="scss">
form.selectCharacterScene {
font-family: "Press Start 2P";
pointer-events: auto;
color: #ebeeee;
section {
margin: 10px;
&.action {
text-align: center;
margin-top: 55vh;
}
h2 {
font-family: "Press Start 2P";
margin: 1px;
}
&.text-center {
text-align: center;
}
button.selectCharacterButton {
position: absolute;
top: 33vh;
margin: 0;
}
}
button {
font-family: "Press Start 2P";
&.selectCharacterButtonLeft {
left: 33vw;
}
&.selectCharacterButtonRight {
right: 33vw;
}
}
}
@media only screen and (max-width: 800px) {
form.selectCharacterScene button.selectCharacterButtonLeft{
left: 5vw;
}
form.selectCharacterScene button.selectCharacterButtonRight{
right: 5vw;
}
}
</style>

View File

@ -4,7 +4,7 @@ import {RoomConnection} from "./RoomConnection";
import type {OnConnectInterface, PositionInterface, ViewportInterface} from "./ConnexionModels"; import type {OnConnectInterface, PositionInterface, ViewportInterface} from "./ConnexionModels";
import {GameConnexionTypes, urlManager} from "../Url/UrlManager"; import {GameConnexionTypes, urlManager} from "../Url/UrlManager";
import {localUserStore} from "./LocalUserStore"; import {localUserStore} from "./LocalUserStore";
import {LocalUser} from "./LocalUser"; import {CharacterTexture, LocalUser} from "./LocalUser";
import {Room} from "./Room"; import {Room} from "./Room";
@ -46,8 +46,8 @@ class ConnectionManager {
urlManager.pushRoomIdToUrl(room); urlManager.pushRoomIdToUrl(room);
return Promise.resolve(room); return Promise.resolve(room);
} else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) { } else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) {
const localUser = localUserStore.getLocalUser();
let localUser = localUserStore.getLocalUser();
if (localUser && localUser.jwtToken && localUser.uuid && localUser.textures) { if (localUser && localUser.jwtToken && localUser.uuid && localUser.textures) {
this.localUser = localUser; this.localUser = localUser;
try { try {
@ -57,16 +57,42 @@ class ConnectionManager {
console.error('JWT token invalid. Did it expire? Login anonymously instead.'); console.error('JWT token invalid. Did it expire? Login anonymously instead.');
await this.anonymousLogin(); await this.anonymousLogin();
} }
} else { }else{
await this.anonymousLogin(); await this.anonymousLogin();
} }
let roomId: string
localUser = localUserStore.getLocalUser();
if(!localUser){
throw "Error to store local user data";
}
let roomId: string;
if (connexionType === GameConnexionTypes.empty) { if (connexionType === GameConnexionTypes.empty) {
roomId = START_ROOM_URL; roomId = START_ROOM_URL;
} else { } else {
roomId = window.location.pathname + window.location.search + window.location.hash; roomId = window.location.pathname + window.location.search + window.location.hash;
} }
return Promise.resolve(new Room(roomId));
//get detail map for anonymous login and set texture in local storage
const room = new Room(roomId);
const mapDetail = await room.getMapDetail();
if(mapDetail.textures != undefined && mapDetail.textures.length > 0) {
//check if texture was changed
if(localUser.textures.length === 0){
localUser.textures = mapDetail.textures;
}else{
mapDetail.textures.forEach((newTexture) => {
const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id);
if(localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1){
return;
}
localUser?.textures.push(newTexture)
});
}
this.localUser = localUser;
localUserStore.saveUser(localUser);
}
return Promise.resolve(room);
} }
return Promise.reject(new Error('Invalid URL')); return Promise.reject(new Error('Invalid URL'));

View File

@ -10,7 +10,7 @@ export interface CharacterTexture {
export const maxUserNameLength: number = MAX_USERNAME_LENGTH; export const maxUserNameLength: number = MAX_USERNAME_LENGTH;
export function isUserNameValid(value: unknown): boolean { export function isUserNameValid(value: unknown): boolean {
return typeof value === "string" && value.length > 0 && value.length < maxUserNameLength && value.indexOf(' ') === -1; return typeof value === "string" && value.length > 0 && value.length <= maxUserNameLength && value.indexOf(' ') === -1;
} }
export function areCharacterLayersValid(value: string[] | null): boolean { export function areCharacterLayersValid(value: string[] | null): boolean {
@ -24,6 +24,6 @@ export function areCharacterLayersValid(value: string[] | null): boolean {
} }
export class LocalUser { export class LocalUser {
constructor(public readonly uuid:string, public readonly jwtToken: string, public readonly textures: CharacterTexture[]) { constructor(public readonly uuid:string, public readonly jwtToken: string, public textures: CharacterTexture[]) {
} }
} }

View File

@ -1,10 +1,17 @@
import Axios from "axios"; import Axios from "axios";
import {PUSHER_URL} from "../Enum/EnvironmentVariable"; import {PUSHER_URL} from "../Enum/EnvironmentVariable";
import type {CharacterTexture} from "./LocalUser";
export class MapDetail{
constructor(public readonly mapUrl: string, public readonly textures : CharacterTexture[]|undefined) {
}
}
export class Room { export class Room {
public readonly id: string; public readonly id: string;
public readonly isPublic: boolean; public readonly isPublic: boolean;
private mapUrl: string|undefined; private mapUrl: string|undefined;
private textures: CharacterTexture[]|undefined;
private instance: string|undefined; private instance: string|undefined;
private _search: URLSearchParams; private _search: URLSearchParams;
@ -50,10 +57,10 @@ export class Room {
return {roomId, hash} return {roomId, hash}
} }
public async getMapUrl(): Promise<string> { public async getMapDetail(): Promise<MapDetail> {
return new Promise<string>((resolve, reject) => { return new Promise<MapDetail>((resolve, reject) => {
if (this.mapUrl !== undefined) { if (this.mapUrl !== undefined && this.textures != undefined) {
resolve(this.mapUrl); resolve(new MapDetail(this.mapUrl, this.textures));
return; return;
} }
@ -61,7 +68,7 @@ export class Room {
const match = /_\/[^/]+\/(.+)/.exec(this.id); const match = /_\/[^/]+\/(.+)/.exec(this.id);
if (!match) throw new Error('Could not extract url from "'+this.id+'"'); if (!match) throw new Error('Could not extract url from "'+this.id+'"');
this.mapUrl = window.location.protocol+'//'+match[1]; this.mapUrl = window.location.protocol+'//'+match[1];
resolve(this.mapUrl); resolve(new MapDetail(this.mapUrl, this.textures));
return; return;
} else { } else {
// We have a private ID, we need to query the map URL from the server. // We have a private ID, we need to query the map URL from the server.
@ -71,7 +78,7 @@ export class Room {
params: urlParts params: urlParts
}).then(({data}) => { }).then(({data}) => {
console.log('Map ', this.id, ' resolves to URL ', data.mapUrl); console.log('Map ', this.id, ' resolves to URL ', data.mapUrl);
resolve(data.mapUrl); resolve(data);
return; return;
}).catch((reason) => { }).catch((reason) => {
reject(reason); reject(reason);

View File

@ -30,7 +30,7 @@ import {
EmoteEventMessage, EmoteEventMessage,
EmotePromptMessage, EmotePromptMessage,
SendUserMessage, SendUserMessage,
BanUserMessage BanUserMessage, RequestVisitCardMessage
} from "../Messages/generated/messages_pb" } from "../Messages/generated/messages_pb"
import type {UserSimplePeerInterface} from "../WebRtc/SimplePeer"; import type {UserSimplePeerInterface} from "../WebRtc/SimplePeer";
@ -50,6 +50,7 @@ import {worldFullMessageStream} from "./WorldFullMessageStream";
import {worldFullWarningStream} from "./WorldFullWarningStream"; import {worldFullWarningStream} from "./WorldFullWarningStream";
import {connectionManager} from "./ConnectionManager"; import {connectionManager} from "./ConnectionManager";
import {emoteEventStream} from "./EmoteEventStream"; import {emoteEventStream} from "./EmoteEventStream";
import {requestVisitCardsStore} from "../Stores/GameStore";
const manualPingDelay = 20000; const manualPingDelay = 20000;
@ -203,6 +204,8 @@ export class RoomConnection implements RoomConnection {
adminMessagesService.onSendusermessage(message.getBanusermessage() as BanUserMessage); adminMessagesService.onSendusermessage(message.getBanusermessage() as BanUserMessage);
} else if (message.hasWorldfullwarningmessage()) { } else if (message.hasWorldfullwarningmessage()) {
worldFullWarningStream.onMessage(); worldFullWarningStream.onMessage();
} else if (message.hasVisitcardmessage()) {
requestVisitCardsStore.set(message?.getVisitcardmessage()?.getUrl() as unknown as string);
} else if (message.hasRefreshroommessage()) { } else if (message.hasRefreshroommessage()) {
//todo: implement a way to notify the user the room was refreshed. //todo: implement a way to notify the user the room was refreshed.
} else { } else {
@ -617,4 +620,14 @@ export class RoomConnection implements RoomConnection {
this.socket.send(clientToServerMessage.serializeBinary().buffer); this.socket.send(clientToServerMessage.serializeBinary().buffer);
} }
public requestVisitCardUrl(targetUserId: number): void {
const message = new RequestVisitCardMessage();
message.setTargetuserid(targetUserId);
const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setRequestvisitcardmessage(message);
this.socket.send(clientToServerMessage.serializeBinary().buffer);
}
} }

View File

@ -14,6 +14,7 @@ const POSITION_DELAY = 200; // Wait 200ms between sending position events
const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player
export const MAX_USERNAME_LENGTH = parseInt(process.env.MAX_USERNAME_LENGTH || '') || 8; export const MAX_USERNAME_LENGTH = parseInt(process.env.MAX_USERNAME_LENGTH || '') || 8;
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || '4'); export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || '4');
export const DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == 'true';
export const isMobile = ():boolean => ( ( window.innerWidth <= 800 ) || ( window.innerHeight <= 600 ) ); export const isMobile = ():boolean => ( ( window.innerWidth <= 800 ) || ( window.innerHeight <= 600 ) );

View File

@ -28,13 +28,13 @@ export class MobileJoystick extends VirtualJoystick {
this.visible = false; this.visible = false;
this.enable = false; this.enable = false;
this.scene.input.on('pointerdown', (pointer: { x: number; y: number; wasTouch: boolean; event: TouchEvent }) => { this.scene.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
if (!pointer.wasTouch) { if (!pointer.wasTouch) {
return; return;
} }
// Let's only display the joystick if there is one finger on the screen // Let's only display the joystick if there is one finger on the screen
if (pointer.event.touches.length === 1) { if ((pointer.event as TouchEvent).touches.length === 1) {
this.x = pointer.x; this.x = pointer.x;
this.y = pointer.y; this.y = pointer.y;
this.visible = true; this.visible = true;

View File

@ -1,3 +1,5 @@
import type {IAnalyserNode, IAudioContext, IMediaStreamAudioSourceNode} from 'standardized-audio-context';
/** /**
* Class to measure the sound volume of a media stream * Class to measure the sound volume of a media stream
*/ */
@ -5,10 +7,10 @@ export class SoundMeter {
private instant: number; private instant: number;
private clip: number; private clip: number;
//private script: ScriptProcessorNode; //private script: ScriptProcessorNode;
private analyser: AnalyserNode|undefined; private analyser: IAnalyserNode<IAudioContext>|undefined;
private dataArray: Uint8Array|undefined; private dataArray: Uint8Array|undefined;
private context: AudioContext|undefined; private context: IAudioContext|undefined;
private source: MediaStreamAudioSourceNode|undefined; private source: IMediaStreamAudioSourceNode<IAudioContext>|undefined;
constructor() { constructor() {
this.instant = 0.0; this.instant = 0.0;
@ -16,7 +18,7 @@ export class SoundMeter {
//this.script = context.createScriptProcessor(2048, 1, 1); //this.script = context.createScriptProcessor(2048, 1, 1);
} }
private init(context: AudioContext) { private init(context: IAudioContext) {
this.context = context; this.context = context;
this.analyser = this.context.createAnalyser(); this.analyser = this.context.createAnalyser();
@ -25,8 +27,12 @@ export class SoundMeter {
this.dataArray = new Uint8Array(bufferLength); this.dataArray = new Uint8Array(bufferLength);
} }
public connectToSource(stream: MediaStream, context: AudioContext): void public connectToSource(stream: MediaStream, context: IAudioContext): void
{ {
if (this.source !== undefined) {
this.stop();
}
this.init(context); this.init(context);
this.source = this.context?.createMediaStreamSource(stream); this.source = this.context?.createMediaStreamSource(stream);
@ -81,56 +87,3 @@ export class SoundMeter {
} }
// Meter class that generates a number correlated to audio volume.
// The meter class itself displays nothing, but it makes the
// instantaneous and time-decaying volumes available for inspection.
// It also reports on the fraction of samples that were at or near
// the top of the measurement range.
/*function SoundMeter(context) {
this.context = context;
this.instant = 0.0;
this.slow = 0.0;
this.clip = 0.0;
this.script = context.createScriptProcessor(2048, 1, 1);
const that = this;
this.script.onaudioprocess = function(event) {
const input = event.inputBuffer.getChannelData(0);
let i;
let sum = 0.0;
let clipcount = 0;
for (i = 0; i < input.length; ++i) {
sum += input[i] * input[i];
if (Math.abs(input[i]) > 0.99) {
clipcount += 1;
}
}
that.instant = Math.sqrt(sum / input.length);
that.slow = 0.95 * that.slow + 0.05 * that.instant;
that.clip = clipcount / input.length;
};
}
SoundMeter.prototype.connectToSource = function(stream, callback) {
console.log('SoundMeter connecting');
try {
this.mic = this.context.createMediaStreamSource(stream);
this.mic.connect(this.script);
// necessary to make sample run, but should not be.
this.script.connect(this.context.destination);
if (typeof callback !== 'undefined') {
callback(null);
}
} catch (e) {
console.error(e);
if (typeof callback !== 'undefined') {
callback(e);
}
}
};
SoundMeter.prototype.stop = function() {
this.mic.disconnect();
this.script.disconnect();
};
*/

View File

@ -1,44 +0,0 @@
import Container = Phaser.GameObjects.Container;
import type {Scene} from "phaser";
import GameObject = Phaser.GameObjects.GameObject;
import Rectangle = Phaser.GameObjects.Rectangle;
export class SoundMeterSprite extends Container {
private rectangles: Rectangle[] = new Array<Rectangle>();
private static readonly NB_BARS = 20;
constructor(scene: Scene, x?: number, y?: number, children?: GameObject[]) {
super(scene, x, y, children);
for (let i = 0; i < SoundMeterSprite.NB_BARS; i++) {
const rectangle = new Rectangle(scene, i * 13, 0, 10, 20, (Math.round(255 - i * 255 / SoundMeterSprite.NB_BARS) << 8) + (Math.round(i * 255 / SoundMeterSprite.NB_BARS) << 16));
this.add(rectangle);
this.rectangles.push(rectangle);
}
}
/**
* A number between 0 and 100
*
* @param volume
*/
public setVolume(volume: number): void {
const normalizedVolume = volume / 100 * SoundMeterSprite.NB_BARS;
for (let i = 0; i < SoundMeterSprite.NB_BARS; i++) {
if (normalizedVolume < i) {
this.rectangles[i].alpha = 0.5;
} else {
this.rectangles[i].alpha = 1;
}
}
}
public getWidth(): number {
return SoundMeterSprite.NB_BARS * 13;
}
}

View File

@ -3,6 +3,8 @@ import type {PointInterface} from "../../Connexion/ConnexionModels";
import {Character} from "../Entity/Character"; import {Character} from "../Entity/Character";
import type {PlayerAnimationDirections} from "../Player/Animation"; import type {PlayerAnimationDirections} from "../Player/Animation";
export const playerClickedEvent = 'playerClickedEvent';
/** /**
* Class representing the sprite of a remote player (a player that plays on another computer) * Class representing the sprite of a remote player (a player that plays on another computer)
*/ */
@ -25,6 +27,10 @@ export class RemotePlayer extends Character {
//set data //set data
this.userId = userId; this.userId = userId;
this.on('pointerdown', () => {
this.emit(playerClickedEvent, this.userId);
})
} }
updatePosition(position: PointInterface): void { updatePosition(position: PointInterface): void {
@ -40,6 +46,6 @@ export class RemotePlayer extends Character {
} }
isClickable(): boolean { isClickable(): boolean {
return false; //todo: make remote players clickable if they are logged in. return true; //todo: make remote players clickable if they are logged in.
} }
} }

View File

@ -69,7 +69,7 @@ export abstract class DirtyScene extends ResizableScene {
return this.dirty || this.objectListChanged; return this.dirty || this.objectListChanged;
} }
public onResize(ev: UIEvent): void { public onResize(): void {
this.objectListChanged = true; this.objectListChanged = true;
} }
} }

View File

@ -21,14 +21,22 @@ export class Game extends Phaser.Game {
constructor(GameConfig: Phaser.Types.Core.GameConfig) { constructor(GameConfig: Phaser.Types.Core.GameConfig) {
super(GameConfig); super(GameConfig);
window.addEventListener('resize', (event) => { this.scale.on(Phaser.Scale.Events.RESIZE, () => {
for (const scene of this.scene.getScenes(true)) {
if (scene instanceof ResizableScene) {
scene.onResize();
}
}
})
/*window.addEventListener('resize', (event) => {
// Let's trigger the onResize method of any active scene that is a ResizableScene // Let's trigger the onResize method of any active scene that is a ResizableScene
for (const scene of this.scene.getScenes(true)) { for (const scene of this.scene.getScenes(true)) {
if (scene instanceof ResizableScene) { if (scene instanceof ResizableScene) {
scene.onResize(event); scene.onResize(event);
} }
} }
}); });*/
} }
public step(time: number, delta: number) public step(time: number, delta: number)

View File

@ -2,12 +2,14 @@ import {GameScene} from "./GameScene";
import {connectionManager} from "../../Connexion/ConnectionManager"; import {connectionManager} from "../../Connexion/ConnectionManager";
import type {Room} from "../../Connexion/Room"; import type {Room} from "../../Connexion/Room";
import {MenuScene, MenuSceneName} from "../Menu/MenuScene"; import {MenuScene, MenuSceneName} from "../Menu/MenuScene";
import {HelpCameraSettingsScene, HelpCameraSettingsSceneName} from "../Menu/HelpCameraSettingsScene";
import {LoginSceneName} from "../Login/LoginScene"; import {LoginSceneName} from "../Login/LoginScene";
import {SelectCharacterSceneName} from "../Login/SelectCharacterScene"; import {SelectCharacterSceneName} from "../Login/SelectCharacterScene";
import {EnableCameraSceneName} from "../Login/EnableCameraScene"; import {EnableCameraSceneName} from "../Login/EnableCameraScene";
import {localUserStore} from "../../Connexion/LocalUserStore"; import {localUserStore} from "../../Connexion/LocalUserStore";
import Axios from "axios"; import Axios from "axios";
import {get} from "svelte/store";
import {requestedCameraState, requestedMicrophoneState} from "../../Stores/MediaStore";
import {helpCameraSettingsVisibleStore} from "../../Stores/HelpCameraSettingsStore";
export interface HasMovedEvent { export interface HasMovedEvent {
direction: string; direction: string;
@ -82,11 +84,11 @@ export class GameManager {
public async loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin): Promise<void> { public async loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin): Promise<void> {
const roomID = room.id; const roomID = room.id;
const mapUrl = await room.getMapUrl(); const mapDetail = await room.getMapDetail();
const gameIndex = scenePlugin.getIndex(roomID); const gameIndex = scenePlugin.getIndex(roomID);
if(gameIndex === -1){ if(gameIndex === -1){
const game : Phaser.Scene = new GameScene(room, mapUrl); const game : Phaser.Scene = new GameScene(room, mapDetail.mapUrl);
scenePlugin.add(roomID, game, false); scenePlugin.add(roomID, game, false);
} }
} }
@ -95,7 +97,11 @@ export class GameManager {
console.log('starting '+ (this.currentGameSceneName || this.startRoom.id)) console.log('starting '+ (this.currentGameSceneName || this.startRoom.id))
scenePlugin.start(this.currentGameSceneName || this.startRoom.id); scenePlugin.start(this.currentGameSceneName || this.startRoom.id);
scenePlugin.launch(MenuSceneName); scenePlugin.launch(MenuSceneName);
scenePlugin.launch(HelpCameraSettingsSceneName);//700
if(!localUserStore.getHelpCameraSettingsShown() && (!get(requestedMicrophoneState) || !get(requestedCameraState))){
helpCameraSettingsVisibleStore.set(true);
localUserStore.setHelpCameraSettingsShown();
}
} }
public gameSceneIsCreated(scene: GameScene) { public gameSceneIsCreated(scene: GameScene) {

View File

@ -29,7 +29,7 @@ import type {AddPlayerInterface} from "./AddPlayerInterface";
import {PlayerAnimationDirections} from "../Player/Animation"; import {PlayerAnimationDirections} from "../Player/Animation";
import {PlayerMovement} from "./PlayerMovement"; import {PlayerMovement} from "./PlayerMovement";
import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator"; import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator";
import {RemotePlayer} from "../Entity/RemotePlayer"; import {playerClickedEvent, RemotePlayer} from "../Entity/RemotePlayer";
import {Queue} from 'queue-typescript'; import {Queue} from 'queue-typescript';
import {SimplePeer, UserSimplePeerInterface} from "../../WebRtc/SimplePeer"; import {SimplePeer, UserSimplePeerInterface} from "../../WebRtc/SimplePeer";
import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene"; import {ReconnectingSceneName} from "../Reconnecting/ReconnectingScene";
@ -163,6 +163,7 @@ export class GameScene extends DirtyScene implements CenterListener {
private createPromise: Promise<void>; private createPromise: Promise<void>;
private createPromiseResolve!: (value?: void | PromiseLike<void>) => void; private createPromiseResolve!: (value?: void | PromiseLike<void>) => void;
private iframeSubscriptionList! : Array<Subscription>; private iframeSubscriptionList! : Array<Subscription>;
private peerStoreUnsubscribe!: () => void;
MapUrlFile: string; MapUrlFile: string;
RoomId: string; RoomId: string;
instance: string; instance: string;
@ -231,11 +232,11 @@ export class GameScene extends DirtyScene implements CenterListener {
this.load.image(joystickBaseKey, joystickBaseImg); this.load.image(joystickBaseKey, joystickBaseImg);
this.load.image(joystickThumbKey, joystickThumbImg); this.load.image(joystickThumbKey, joystickThumbImg);
} }
//todo: in an emote manager. this.load.audio('audio-webrtc-in', '/resources/objects/webrtc-in.mp3');
this.load.spritesheet('emote-music', 'resources/emotes/pipo-popupemotes005.png', { this.load.audio('audio-webrtc-out', '/resources/objects/webrtc-out.mp3');
frameHeight: 32, //this.load.audio('audio-report-message', '/resources/objects/report-message.mp3');
frameWidth: 32, this.sound.pauseOnBlur = false;
});
this.load.on(FILE_LOAD_ERROR, (file: {src: string}) => { this.load.on(FILE_LOAD_ERROR, (file: {src: string}) => {
// If we happen to be in HTTP and we are trying to load a URL in HTTPS only... (this happens only in dev environments) // If we happen to be in HTTP and we are trying to load a URL in HTTPS only... (this happens only in dev environments)
if (window.location.protocol === 'http:' && file.src === this.MapUrlFile && file.src.startsWith('http:') && this.originalMapUrl === undefined) { if (window.location.protocol === 'http:' && file.src === this.MapUrlFile && file.src.startsWith('http:') && this.originalMapUrl === undefined) {
@ -528,6 +529,21 @@ export class GameScene extends DirtyScene implements CenterListener {
} }
this.emoteManager = new EmoteManager(this); this.emoteManager = new EmoteManager(this);
let oldPeerNumber = 0;
this.peerStoreUnsubscribe = peerStore.subscribe((peers) => {
const newPeerNumber = peers.size;
if (newPeerNumber > oldPeerNumber) {
this.sound.play('audio-webrtc-in', {
volume: 0.2
});
} else if (newPeerNumber < oldPeerNumber) {
this.sound.play('audio-webrtc-out', {
volume: 0.2
});
}
oldPeerNumber = newPeerNumber;
});
} }
/** /**
@ -1004,6 +1020,9 @@ export class GameScene extends DirtyScene implements CenterListener {
this.userInputManager.destroy(); this.userInputManager.destroy();
this.pinchManager?.destroy(); this.pinchManager?.destroy();
this.emoteManager.destroy(); this.emoteManager.destroy();
this.peerStoreUnsubscribe();
mediaManager.hideGameOverlay();
for(const iframeEvents of this.iframeSubscriptionList){ for(const iframeEvents of this.iframeSubscriptionList){
iframeEvents.unsubscribe(); iframeEvents.unsubscribe();
@ -1193,7 +1212,10 @@ export class GameScene extends DirtyScene implements CenterListener {
this.companion, this.companion,
this.companion !== null ? lazyLoadCompanionResource(this.load, this.companion) : undefined this.companion !== null ? lazyLoadCompanionResource(this.load, this.companion) : undefined
); );
this.CurrentPlayer.on('pointerdown', () => { this.CurrentPlayer.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
if (pointer.wasTouch && (pointer.event as TouchEvent).touches.length > 1) {
return; //we don't want the menu to open when pinching on a touch screen.
}
this.emoteManager.getMenuImages().then((emoteMenuElements) => this.CurrentPlayer.openOrCloseEmoteMenu(emoteMenuElements)) this.emoteManager.getMenuImages().then((emoteMenuElements) => this.CurrentPlayer.openOrCloseEmoteMenu(emoteMenuElements))
}) })
this.CurrentPlayer.on(requestEmoteEventName, (emoteKey: string) => { this.CurrentPlayer.on(requestEmoteEventName, (emoteKey: string) => {
@ -1396,6 +1418,9 @@ export class GameScene extends DirtyScene implements CenterListener {
addPlayerData.companion, addPlayerData.companion,
addPlayerData.companion !== null ? lazyLoadCompanionResource(this.load, addPlayerData.companion) : undefined addPlayerData.companion !== null ? lazyLoadCompanionResource(this.load, addPlayerData.companion) : undefined
); );
player.on(playerClickedEvent, (userID:number) => {
this.connection?.requestVisitCardUrl(userID);
})
this.MapPlayers.add(player); this.MapPlayers.add(player);
this.MapPlayersByKey.set(player.userId, player); this.MapPlayersByKey.set(player.userId, player);
player.updatePosition(addPlayerData.position); player.updatePosition(addPlayerData.position);
@ -1499,8 +1524,8 @@ export class GameScene extends DirtyScene implements CenterListener {
this.connection?.emitActionableEvent(itemId, eventName, state, parameters); this.connection?.emitActionableEvent(itemId, eventName, state, parameters);
} }
public onResize(ev: UIEvent): void { public onResize(): void {
super.onResize(ev); super.onResize();
this.reposition(); this.reposition();
// Send new viewport to server // Send new viewport to server

View File

@ -17,7 +17,9 @@ class SoundManager {
return res(sound); return res(sound);
} }
loadPlugin.audio(soundUrl, soundUrl); loadPlugin.audio(soundUrl, soundUrl);
loadPlugin.once('filecomplete-audio-' + soundUrl, () => res(soundManager.add(soundUrl))); loadPlugin.once('filecomplete-audio-' + soundUrl, () => {
res(soundManager.add(soundUrl));
});
loadPlugin.start(); loadPlugin.start();
}); });
this.soundPromises.set(soundUrl,soundPromise); this.soundPromises.set(soundUrl,soundPromise);

View File

@ -11,6 +11,10 @@ import {AbstractCharacterScene} from "./AbstractCharacterScene";
import {areCharacterLayersValid} from "../../Connexion/LocalUser"; import {areCharacterLayersValid} from "../../Connexion/LocalUser";
import { MenuScene } from "../Menu/MenuScene"; import { MenuScene } from "../Menu/MenuScene";
import { SelectCharacterSceneName } from "./SelectCharacterScene"; import { SelectCharacterSceneName } from "./SelectCharacterScene";
import {customCharacterSceneVisibleStore} from "../../Stores/CustomCharacterStore";
import {selectCharacterSceneVisibleStore} from "../../Stores/SelectCharacterStore";
import {waScaleManager} from "../Services/WaScaleManager";
import {isMobile} from "../../Enum/EnvironmentVariable";
export const CustomizeSceneName = "CustomizeScene"; export const CustomizeSceneName = "CustomizeScene";
@ -22,10 +26,10 @@ export class CustomizeScene extends AbstractCharacterScene {
private selectedLayers: number[] = [0]; private selectedLayers: number[] = [0];
private containersRow: Container[][] = []; private containersRow: Container[][] = [];
private activeRow:number = 0; public activeRow:number = 0;
private layers: BodyResourceDescriptionInterface[][] = []; private layers: BodyResourceDescriptionInterface[][] = [];
private customizeSceneElement!: Phaser.GameObjects.DOMElement; protected lazyloadingAttempt = true; //permit to update texture loaded after renderer
constructor() { constructor() {
super({ super({
@ -36,7 +40,6 @@ export class CustomizeScene extends AbstractCharacterScene {
preload() { preload() {
this.load.html(customizeSceneKey, 'resources/html/CustomCharacterScene.html'); this.load.html(customizeSceneKey, 'resources/html/CustomCharacterScene.html');
this.layers = loadAllLayers(this.load);
this.loadCustomSceneSelectCharacters().then((bodyResourceDescriptions) => { this.loadCustomSceneSelectCharacters().then((bodyResourceDescriptions) => {
bodyResourceDescriptions.forEach((bodyResourceDescription) => { bodyResourceDescriptions.forEach((bodyResourceDescription) => {
if(bodyResourceDescription.level == undefined || bodyResourceDescription.level < 0 || bodyResourceDescription.level > 5 ){ if(bodyResourceDescription.level == undefined || bodyResourceDescription.level < 0 || bodyResourceDescription.level > 5 ){
@ -44,43 +47,28 @@ export class CustomizeScene extends AbstractCharacterScene {
} }
this.layers[bodyResourceDescription.level].unshift(bodyResourceDescription); this.layers[bodyResourceDescription.level].unshift(bodyResourceDescription);
}); });
this.lazyloadingAttempt = true;
}); });
this.layers = loadAllLayers(this.load);
this.lazyloadingAttempt = false;
//this function must stay at the end of preload function //this function must stay at the end of preload function
addLoader(this); addLoader(this);
} }
create() { create() {
this.customizeSceneElement = this.add.dom(-1000, 0).createFromCache(customizeSceneKey); customCharacterSceneVisibleStore.set(true);
this.centerXDomElement(this.customizeSceneElement, 150); this.events.addListener('wake', () => {
MenuScene.revealMenusAfterInit(this.customizeSceneElement, customizeSceneKey); waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 3 : 1;
this.customizeSceneElement.addListener('click'); customCharacterSceneVisibleStore.set(true);
this.customizeSceneElement.on('click', (event:MouseEvent) => {
event.preventDefault();
if((event?.target as HTMLInputElement).id === 'customizeSceneButtonLeft') {
this.moveCursorHorizontally(-1);
}else if((event?.target as HTMLInputElement).id === 'customizeSceneButtonRight') {
this.moveCursorHorizontally(1);
}else if((event?.target as HTMLInputElement).id === 'customizeSceneButtonDown') {
this.moveCursorVertically(1);
}else if((event?.target as HTMLInputElement).id === 'customizeSceneButtonUp') {
this.moveCursorVertically(-1);
}else if((event?.target as HTMLInputElement).id === 'customizeSceneFormBack') {
if(this.activeRow > 0){
this.moveCursorVertically(-1);
}else{
this.backToPreviousScene();
}
}else if((event?.target as HTMLButtonElement).id === 'customizeSceneFormSubmit') {
if(this.activeRow < 5){
this.moveCursorVertically(1);
}else{
this.nextSceneToCamera();
}
}
}); });
waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 3 : 1;
this.Rectangle = this.add.rectangle(this.cameras.main.worldView.x + this.cameras.main.width / 2, this.cameras.main.worldView.y + this.cameras.main.height / 3, 32, 33) this.Rectangle = this.add.rectangle(this.cameras.main.worldView.x + this.cameras.main.width / 2, this.cameras.main.worldView.y + this.cameras.main.height / 3, 32, 33)
this.Rectangle.setStrokeStyle(2, 0xFFFFFF); this.Rectangle.setStrokeStyle(2, 0xFFFFFF);
this.add.existing(this.Rectangle); this.add.existing(this.Rectangle);
@ -116,7 +104,7 @@ export class CustomizeScene extends AbstractCharacterScene {
this.onResize(); this.onResize();
} }
private moveCursorHorizontally(index: number): void { public moveCursorHorizontally(index: number): void {
this.selectedLayers[this.activeRow] += index; this.selectedLayers[this.activeRow] += index;
if (this.selectedLayers[this.activeRow] < 0) { if (this.selectedLayers[this.activeRow] < 0) {
this.selectedLayers[this.activeRow] = 0 this.selectedLayers[this.activeRow] = 0
@ -128,27 +116,7 @@ export class CustomizeScene extends AbstractCharacterScene {
this.saveInLocalStorage(); this.saveInLocalStorage();
} }
private moveCursorVertically(index:number): void { public moveCursorVertically(index:number): void {
if(index === -1 && this.activeRow === 5){
const button = this.customizeSceneElement.getChildByID('customizeSceneFormSubmit') as HTMLButtonElement;
button.innerHTML = `Next <img src="resources/objects/arrow_up.png"/>`;
}
if(index === 1 && this.activeRow === 4){
const button = this.customizeSceneElement.getChildByID('customizeSceneFormSubmit') as HTMLButtonElement;
button.innerText = 'Finish';
}
if(index === -1 && this.activeRow === 1){
const button = this.customizeSceneElement.getChildByID('customizeSceneFormBack') as HTMLButtonElement;
button.innerText = `Return`;
}
if(index === 1 && this.activeRow === 0){
const button = this.customizeSceneElement.getChildByID('customizeSceneFormBack') as HTMLButtonElement;
button.innerHTML = `Back <img src="resources/objects/arrow_up.png"/>`;
}
this.activeRow += index; this.activeRow += index;
if (this.activeRow < 0) { if (this.activeRow < 0) {
@ -262,6 +230,10 @@ export class CustomizeScene extends AbstractCharacterScene {
update(time: number, delta: number): void { update(time: number, delta: number): void {
if(this.lazyloadingAttempt){
this.moveLayers();
this.lazyloadingAttempt = false;
}
} }
public onResize(): void { public onResize(): void {
@ -269,8 +241,6 @@ export class CustomizeScene extends AbstractCharacterScene {
this.Rectangle.x = this.cameras.main.worldView.x + this.cameras.main.width / 2; this.Rectangle.x = this.cameras.main.worldView.x + this.cameras.main.width / 2;
this.Rectangle.y = this.cameras.main.worldView.y + this.cameras.main.height / 3; this.Rectangle.y = this.cameras.main.worldView.y + this.cameras.main.height / 3;
this.centerXDomElement(this.customizeSceneElement, 150);
} }
private nextSceneToCamera(){ private nextSceneToCamera(){
@ -288,12 +258,16 @@ export class CustomizeScene extends AbstractCharacterScene {
gameManager.setCharacterLayers(layers); gameManager.setCharacterLayers(layers);
this.scene.sleep(CustomizeSceneName); this.scene.sleep(CustomizeSceneName);
this.scene.remove(SelectCharacterSceneName); waScaleManager.restoreZoom();
this.events.removeListener('wake');
gameManager.tryResumingGame(this, EnableCameraSceneName); gameManager.tryResumingGame(this, EnableCameraSceneName);
customCharacterSceneVisibleStore.set(false);
} }
private backToPreviousScene(){ private backToPreviousScene(){
this.scene.sleep(CustomizeSceneName); this.scene.sleep(CustomizeSceneName);
waScaleManager.restoreZoom();
this.scene.run(SelectCharacterSceneName); this.scene.run(SelectCharacterSceneName);
customCharacterSceneVisibleStore.set(false);
} }
} }

View File

@ -3,7 +3,6 @@ import {TextField} from "../Components/TextField";
import Image = Phaser.GameObjects.Image; import Image = Phaser.GameObjects.Image;
import {mediaManager} from "../../WebRtc/MediaManager"; import {mediaManager} from "../../WebRtc/MediaManager";
import {SoundMeter} from "../Components/SoundMeter"; import {SoundMeter} from "../Components/SoundMeter";
import {SoundMeterSprite} from "../Components/SoundMeterSprite";
import {HtmlUtils} from "../../WebRtc/HtmlUtils"; import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager"; import {PinchManager} from "../UserInput/PinchManager";
@ -11,304 +10,41 @@ import Zone = Phaser.GameObjects.Zone;
import { MenuScene } from "../Menu/MenuScene"; import { MenuScene } from "../Menu/MenuScene";
import {ResizableScene} from "./ResizableScene"; import {ResizableScene} from "./ResizableScene";
import { import {
audioConstraintStore,
enableCameraSceneVisibilityStore, enableCameraSceneVisibilityStore,
localStreamStore,
mediaStreamConstraintsStore,
videoConstraintStore
} from "../../Stores/MediaStore"; } from "../../Stores/MediaStore";
import type {Unsubscriber} from "svelte/store";
export const EnableCameraSceneName = "EnableCameraScene"; export const EnableCameraSceneName = "EnableCameraScene";
enum LoginTextures {
playButton = "play_button",
icon = "icon",
mainFont = "main_font",
arrowRight = "arrow_right",
arrowUp = "arrow_up"
}
const enableCameraSceneKey = 'enableCameraScene';
export class EnableCameraScene extends ResizableScene { export class EnableCameraScene extends ResizableScene {
private textField!: TextField;
private cameraNameField!: TextField;
private arrowLeft!: Image;
private arrowRight!: Image;
private arrowDown!: Image;
private arrowUp!: Image;
private microphonesList: MediaDeviceInfo[] = new Array<MediaDeviceInfo>();
private camerasList: MediaDeviceInfo[] = new Array<MediaDeviceInfo>();
private cameraSelected: number = 0;
private microphoneSelected: number = 0;
private soundMeter: SoundMeter;
private soundMeterSprite!: SoundMeterSprite;
private microphoneNameField!: TextField;
private enableCameraSceneElement!: Phaser.GameObjects.DOMElement;
private mobileTapZone!: Zone;
private localStreamStoreUnsubscriber!: Unsubscriber;
constructor() { constructor() {
super({ super({
key: EnableCameraSceneName key: EnableCameraSceneName
}); });
this.soundMeter = new SoundMeter();
} }
preload() { preload() {
this.load.html(enableCameraSceneKey, 'resources/html/EnableCameraScene.html');
this.load.image(LoginTextures.playButton, "resources/objects/play_button.png");
this.load.image(LoginTextures.arrowRight, "resources/objects/arrow_right.png");
this.load.image(LoginTextures.arrowUp, "resources/objects/arrow_up.png");
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
this.load.bitmapFont(LoginTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
} }
create() { create() {
this.enableCameraSceneElement = this.add.dom(-1000, 0).createFromCache(enableCameraSceneKey);
this.centerXDomElement(this.enableCameraSceneElement, 300);
MenuScene.revealMenusAfterInit(this.enableCameraSceneElement, enableCameraSceneKey);
const continuingButton = this.enableCameraSceneElement.getChildByID('enableCameraSceneFormSubmit') as HTMLButtonElement;
continuingButton.addEventListener('click', (e) => {
e.preventDefault();
this.login();
});
if (touchScreenManager.supportTouchScreen) {
new PinchManager(this);
}
//this.scale.setZoom(ZOOM_LEVEL);
//Phaser.Display.Align.In.BottomCenter(this.pressReturnField, zone);
/* FIX ME */
this.textField = new TextField(this, this.scale.width / 2, 20, '');
// For mobile purposes - we need a big enough touchable area.
this.mobileTapZone = this.add.zone(this.scale.width / 2,this.scale.height - 30,200,50)
.setInteractive().on("pointerdown", () => {
this.login();
});
this.cameraNameField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height - 60, '');
this.microphoneNameField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height - 40, '');
this.arrowRight = new Image(this, 0, 0, LoginTextures.arrowRight);
this.arrowRight.setVisible(false);
this.arrowRight.setInteractive().on('pointerdown', this.nextCam.bind(this));
this.add.existing(this.arrowRight);
this.arrowLeft = new Image(this, 0, 0, LoginTextures.arrowRight);
this.arrowLeft.setVisible(false);
this.arrowLeft.flipX = true;
this.arrowLeft.setInteractive().on('pointerdown', this.previousCam.bind(this));
this.add.existing(this.arrowLeft);
this.arrowUp = new Image(this, 0, 0, LoginTextures.arrowUp);
this.arrowUp.setVisible(false);
this.arrowUp.setInteractive().on('pointerdown', this.previousMic.bind(this));
this.add.existing(this.arrowUp);
this.arrowDown = new Image(this, 0, 0, LoginTextures.arrowUp);
this.arrowDown.setVisible(false);
this.arrowDown.flipY = true;
this.arrowDown.setInteractive().on('pointerdown', this.nextMic.bind(this));
this.add.existing(this.arrowDown);
this.input.keyboard.on('keyup-ENTER', () => { this.input.keyboard.on('keyup-ENTER', () => {
this.login(); this.login();
}); });
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').classList.add('active');
this.localStreamStoreUnsubscriber = localStreamStore.subscribe((result) => {
if (result.type === 'error') {
// TODO: proper handling of the error
throw result.error;
}
this.getDevices();
if (result.stream !== null) {
this.setupStream(result.stream);
}
});
/*const mediaPromise = mediaManager.getCamera();
mediaPromise.then(this.getDevices.bind(this));
mediaPromise.then(this.setupStream.bind(this));*/
this.input.keyboard.on('keydown-RIGHT', this.nextCam.bind(this));
this.input.keyboard.on('keydown-LEFT', this.previousCam.bind(this));
this.input.keyboard.on('keydown-DOWN', this.nextMic.bind(this));
this.input.keyboard.on('keydown-UP', this.previousMic.bind(this));
this.soundMeterSprite = new SoundMeterSprite(this, 50, 50);
this.soundMeterSprite.setVisible(false);
this.add.existing(this.soundMeterSprite);
this.onResize();
enableCameraSceneVisibilityStore.showEnableCameraScene(); enableCameraSceneVisibilityStore.showEnableCameraScene();
} }
private previousCam(): void {
if (this.cameraSelected === 0 || this.camerasList.length === 0) {
return;
}
this.cameraSelected--;
videoConstraintStore.setDeviceId(this.camerasList[this.cameraSelected].deviceId);
//mediaManager.setCamera(this.camerasList[this.cameraSelected].deviceId).then(this.setupStream.bind(this));
}
private nextCam(): void {
if (this.cameraSelected === this.camerasList.length - 1 || this.camerasList.length === 0) {
return;
}
this.cameraSelected++;
videoConstraintStore.setDeviceId(this.camerasList[this.cameraSelected].deviceId);
// TODO: the change of camera should be OBSERVED (reactive)
//mediaManager.setCamera(this.camerasList[this.cameraSelected].deviceId).then(this.setupStream.bind(this));
}
private previousMic(): void {
if (this.microphoneSelected === 0 || this.microphonesList.length === 0) {
return;
}
this.microphoneSelected--;
audioConstraintStore.setDeviceId(this.microphonesList[this.microphoneSelected].deviceId);
//mediaManager.setMicrophone(this.microphonesList[this.microphoneSelected].deviceId).then(this.setupStream.bind(this));
}
private nextMic(): void {
if (this.microphoneSelected === this.microphonesList.length - 1 || this.microphonesList.length === 0) {
return;
}
this.microphoneSelected++;
audioConstraintStore.setDeviceId(this.microphonesList[this.microphoneSelected].deviceId);
// TODO: the change of camera should be OBSERVED (reactive)
//mediaManager.setMicrophone(this.microphonesList[this.microphoneSelected].deviceId).then(this.setupStream.bind(this));
}
/**
* Function called each time a camera is changed
*/
private setupStream(stream: MediaStream): void {
const img = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('webRtcSetupNoVideo');
img.style.display = 'none';
const div = HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('myCamVideoSetup');
div.srcObject = stream;
this.soundMeter.connectToSource(stream, new window.AudioContext());
this.soundMeterSprite.setVisible(true);
this.updateWebCamName();
}
private updateWebCamName(): void {
if (this.camerasList.length > 1) {
let label = this.camerasList[this.cameraSelected].label;
// remove text in parenthesis
label = label.replace(/\([^()]*\)/g, '').trim();
// remove accents
label = label.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
this.cameraNameField.text = label;
this.arrowRight.setVisible(this.cameraSelected < this.camerasList.length - 1);
this.arrowLeft.setVisible(this.cameraSelected > 0);
}
if (this.microphonesList.length > 1) {
let label = this.microphonesList[this.microphoneSelected].label;
// remove text in parenthesis
label = label.replace(/\([^()]*\)/g, '').trim();
// remove accents
label = label.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
this.microphoneNameField.text = label;
this.arrowDown.setVisible(this.microphoneSelected < this.microphonesList.length - 1);
this.arrowUp.setVisible(this.microphoneSelected > 0);
}
}
public onResize(): void { public onResize(): void {
let div = HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('myCamVideoSetup');
let bounds = div.getBoundingClientRect();
if (!div.srcObject) {
div = HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('webRtcSetup');
bounds = div.getBoundingClientRect();
}
this.textField.x = this.game.renderer.width / 2;
this.mobileTapZone.x = this.game.renderer.width / 2;
this.cameraNameField.x = this.game.renderer.width / 2;
this.microphoneNameField.x = this.game.renderer.width / 2;
this.cameraNameField.y = bounds.top / this.scale.zoom - 8;
this.soundMeterSprite.x = this.game.renderer.width / 2 - this.soundMeterSprite.getWidth() / 2;
this.soundMeterSprite.y = bounds.bottom / this.scale.zoom + 16;
this.microphoneNameField.y = this.soundMeterSprite.y + 22;
this.arrowRight.x = bounds.right / this.scale.zoom + 16;
this.arrowRight.y = (bounds.top + bounds.height / 2) / this.scale.zoom;
this.arrowLeft.x = bounds.left / this.scale.zoom - 16;
this.arrowLeft.y = (bounds.top + bounds.height / 2) / this.scale.zoom;
this.arrowDown.x = this.microphoneNameField.x + this.microphoneNameField.width / 2 + 16;
this.arrowDown.y = this.microphoneNameField.y;
this.arrowUp.x = this.microphoneNameField.x - this.microphoneNameField.width / 2 - 16;
this.arrowUp.y = this.microphoneNameField.y;
const actionBtn = document.querySelector<HTMLDivElement>('#enableCameraScene .action');
if (actionBtn !== null) {
actionBtn.style.top = (this.scale.height - 65) + 'px';
}
} }
update(time: number, delta: number): void { update(time: number, delta: number): void {
this.soundMeterSprite.setVolume(this.soundMeter.getVolume());
this.centerXDomElement(this.enableCameraSceneElement, 300);
} }
private login(): void { public login(): void {
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').style.display = 'none';
this.soundMeter.stop();
enableCameraSceneVisibilityStore.hideEnableCameraScene(); enableCameraSceneVisibilityStore.hideEnableCameraScene();
this.localStreamStoreUnsubscriber();
//mediaManager.stopCamera();
//mediaManager.stopMicrophone();
this.scene.sleep(EnableCameraSceneName); this.scene.sleep(EnableCameraSceneName);
gameManager.goToStartingMap(this.scene); gameManager.goToStartingMap(this.scene);
} }
private async getDevices() {
// TODO: switch this in a store.
const mediaDeviceInfos = await navigator.mediaDevices.enumerateDevices();
this.microphonesList = [];
this.camerasList = [];
for (const mediaDeviceInfo of mediaDeviceInfos) {
if (mediaDeviceInfo.kind === 'audioinput') {
this.microphonesList.push(mediaDeviceInfo);
} else if (mediaDeviceInfo.kind === 'videoinput') {
this.camerasList.push(mediaDeviceInfo);
}
}
this.updateWebCamName();
}
} }

View File

@ -1,17 +1,12 @@
import {gameManager} from "../Game/GameManager"; import {gameManager} from "../Game/GameManager";
import {SelectCharacterSceneName} from "./SelectCharacterScene"; import {SelectCharacterSceneName} from "./SelectCharacterScene";
import {ResizableScene} from "./ResizableScene"; import {ResizableScene} from "./ResizableScene";
import { localUserStore } from "../../Connexion/LocalUserStore"; import {loginSceneVisibleStore} from "../../Stores/LoginSceneStore";
import {MenuScene} from "../Menu/MenuScene";
import { isUserNameValid } from "../../Connexion/LocalUser";
export const LoginSceneName = "LoginScene"; export const LoginSceneName = "LoginScene";
const loginSceneKey = 'loginScene';
export class LoginScene extends ResizableScene { export class LoginScene extends ResizableScene {
private loginSceneElement!: Phaser.GameObjects.DOMElement;
private name: string = ''; private name: string = '';
constructor() { constructor() {
@ -22,65 +17,25 @@ export class LoginScene extends ResizableScene {
} }
preload() { preload() {
this.load.html(loginSceneKey, 'resources/html/loginScene.html');
} }
create() { create() {
this.loginSceneElement = this.add.dom(-1000, 0).createFromCache(loginSceneKey); loginSceneVisibleStore.set(true);
this.centerXDomElement(this.loginSceneElement, 200);
MenuScene.revealMenusAfterInit(this.loginSceneElement, loginSceneKey);
const pErrorElement = this.loginSceneElement.getChildByID('errorLoginScene') as HTMLInputElement;
const inputElement = this.loginSceneElement.getChildByID('loginSceneName') as HTMLInputElement;
inputElement.value = localUserStore.getName() ?? '';
inputElement.focus();
inputElement.addEventListener('keypress', (event: KeyboardEvent) => {
if(inputElement.value.length > 7){
event.preventDefault();
return;
}
pErrorElement.innerHTML = '';
if(inputElement.value && !isUserNameValid(inputElement.value)){
pErrorElement.innerHTML = 'Invalid user name: No spaces are allowed.';
}
if (event.key === 'Enter') {
event.preventDefault();
this.login(inputElement);
return;
}
});
const continuingButton = this.loginSceneElement.getChildByID('loginSceneFormSubmit') as HTMLButtonElement;
continuingButton.addEventListener('click', (e) => {
e.preventDefault();
this.login(inputElement);
});
} }
private login(inputElement: HTMLInputElement): void { public login(name: string): void {
const pErrorElement = this.loginSceneElement.getChildByID('errorLoginScene') as HTMLInputElement; name = name.trim();
this.name = inputElement.value; gameManager.setPlayerName(name);
if (this.name === '') {
pErrorElement.innerHTML = 'The name is empty';
return
}
if(!isUserNameValid(this.name)){
pErrorElement.innerHTML = 'Invalid user name: only letters and numbers are allowed. No spaces.';
return
}
if (this.name === '') return
gameManager.setPlayerName(this.name);
this.scene.stop(LoginSceneName) this.scene.stop(LoginSceneName)
gameManager.tryResumingGame(this, SelectCharacterSceneName); gameManager.tryResumingGame(this, SelectCharacterSceneName);
this.scene.remove(LoginSceneName) this.scene.remove(LoginSceneName);
loginSceneVisibleStore.set(false);
} }
update(time: number, delta: number): void { update(time: number, delta: number): void {
} }
public onResize(ev: UIEvent): void { public onResize(): void {
this.centerXDomElement(this.loginSceneElement, 200);
} }
} }

View File

@ -2,7 +2,7 @@ import {Scene} from "phaser";
import DOMElement = Phaser.GameObjects.DOMElement; import DOMElement = Phaser.GameObjects.DOMElement;
export abstract class ResizableScene extends Scene { export abstract class ResizableScene extends Scene {
public abstract onResize(ev: UIEvent): void; public abstract onResize(): void;
/** /**
* Centers the DOM element on the X axis. * Centers the DOM element on the X axis.
@ -17,7 +17,7 @@ export abstract class ResizableScene extends Scene {
&& object.node && object.node
&& object.node.getBoundingClientRect().width > 0 && object.node.getBoundingClientRect().width > 0
? (object.node.getBoundingClientRect().width / 2 / this.scale.zoom) ? (object.node.getBoundingClientRect().width / 2 / this.scale.zoom)
: (300 / this.scale.zoom) : (defaultWidth / this.scale.zoom)
); );
} }
} }

View File

@ -4,49 +4,50 @@ export class SelectCharacterMobileScene extends SelectCharacterScene {
create(){ create(){
super.create(); super.create();
this.onResize();
this.selectedRectangle.destroy(); this.selectedRectangle.destroy();
} }
protected defineSetupPlayer(numero: number){ protected defineSetupPlayer(num: number){
const deltaX = 30; const deltaX = 30;
const deltaY = 2; const deltaY = 2;
let [playerX, playerY] = this.getCharacterPosition(); let [playerX, playerY] = this.getCharacterPosition();
let playerVisible = true; let playerVisible = true;
let playerScale = 1.5; let playerScale = 1.5;
let playserOpactity = 1; let playerOpacity = 1;
if( this.currentSelectUser !== numero ){ if( this.currentSelectUser !== num ){
playerVisible = false; playerVisible = false;
} }
if( numero === (this.currentSelectUser + 1) ){ if( num === (this.currentSelectUser + 1) ){
playerY -= deltaY; playerY -= deltaY;
playerX += deltaX; playerX += deltaX;
playerScale = 0.8; playerScale = 0.8;
playserOpactity = 0.6; playerOpacity = 0.6;
playerVisible = true; playerVisible = true;
} }
if( numero === (this.currentSelectUser + 2) ){ if( num === (this.currentSelectUser + 2) ){
playerY -= deltaY; playerY -= deltaY;
playerX += (deltaX * 2); playerX += (deltaX * 2);
playerScale = 0.8; playerScale = 0.8;
playserOpactity = 0.6; playerOpacity = 0.6;
playerVisible = true; playerVisible = true;
} }
if( numero === (this.currentSelectUser - 1) ){ if( num === (this.currentSelectUser - 1) ){
playerY -= deltaY; playerY -= deltaY;
playerX -= deltaX; playerX -= deltaX;
playerScale = 0.8; playerScale = 0.8;
playserOpactity = 0.6; playerOpacity = 0.6;
playerVisible = true; playerVisible = true;
} }
if( numero === (this.currentSelectUser - 2) ){ if( num === (this.currentSelectUser - 2) ){
playerY -= deltaY; playerY -= deltaY;
playerX -= (deltaX * 2); playerX -= (deltaX * 2);
playerScale = 0.8; playerScale = 0.8;
playserOpactity = 0.6; playerOpacity = 0.6;
playerVisible = true; playerVisible = true;
} }
return {playerX, playerY, playerScale, playserOpactity, playerVisible} return {playerX, playerY, playerScale, playerOpacity, playerVisible}
} }
/** /**

View File

@ -1,4 +1,3 @@
import {isMobile} from "../../Enum/EnvironmentVariable";
import {gameManager} from "../Game/GameManager"; import {gameManager} from "../Game/GameManager";
import Rectangle = Phaser.GameObjects.Rectangle; import Rectangle = Phaser.GameObjects.Rectangle;
import {EnableCameraSceneName} from "./EnableCameraScene"; import {EnableCameraSceneName} from "./EnableCameraScene";
@ -12,6 +11,10 @@ import {areCharacterLayersValid} from "../../Connexion/LocalUser";
import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager"; import {PinchManager} from "../UserInput/PinchManager";
import {MenuScene} from "../Menu/MenuScene"; import {MenuScene} from "../Menu/MenuScene";
import {selectCharacterSceneVisibleStore} from "../../Stores/SelectCharacterStore";
import {customCharacterSceneVisibleStore} from "../../Stores/CustomCharacterStore";
import {waScaleManager} from "../Services/WaScaleManager";
import {isMobile} from "../../Enum/EnvironmentVariable";
//todo: put this constants in a dedicated file //todo: put this constants in a dedicated file
export const SelectCharacterSceneName = "SelectCharacterScene"; export const SelectCharacterSceneName = "SelectCharacterScene";
@ -28,6 +31,9 @@ export class SelectCharacterScene extends AbstractCharacterScene {
protected selectCharacterSceneElement!: Phaser.GameObjects.DOMElement; protected selectCharacterSceneElement!: Phaser.GameObjects.DOMElement;
protected currentSelectUser = 0; protected currentSelectUser = 0;
protected pointerClicked: boolean = false;
protected lazyloadingAttempt = true; //permit to update texture loaded after renderer
constructor() { constructor() {
super({ super({
@ -42,44 +48,36 @@ export class SelectCharacterScene extends AbstractCharacterScene {
bodyResourceDescriptions.forEach((bodyResourceDescription) => { bodyResourceDescriptions.forEach((bodyResourceDescription) => {
this.playerModels.push(bodyResourceDescription); this.playerModels.push(bodyResourceDescription);
}); });
}) this.lazyloadingAttempt = true;
});
this.playerModels = loadAllDefaultModels(this.load); this.playerModels = loadAllDefaultModels(this.load);
this.lazyloadingAttempt = false;
//this function must stay at the end of preload function //this function must stay at the end of preload function
addLoader(this); addLoader(this);
} }
create() { create() {
selectCharacterSceneVisibleStore.set(true);
this.selectCharacterSceneElement = this.add.dom(-1000, 0).createFromCache(selectCharacterKey); this.events.addListener('wake', () => {
this.centerXDomElement(this.selectCharacterSceneElement, 150); waScaleManager.saveZoom();
MenuScene.revealMenusAfterInit(this.selectCharacterSceneElement, selectCharacterKey); waScaleManager.zoomModifier = isMobile() ? 2 : 1;
selectCharacterSceneVisibleStore.set(true);
this.selectCharacterSceneElement.addListener('click');
this.selectCharacterSceneElement.on('click', (event:MouseEvent) => {
event.preventDefault();
if((event?.target as HTMLInputElement).id === 'selectCharacterButtonLeft') {
this.moveToLeft();
}else if((event?.target as HTMLInputElement).id === 'selectCharacterButtonRight') {
this.moveToRight();
}else if((event?.target as HTMLInputElement).id === 'selectCharacterSceneFormSubmit') {
this.nextSceneToCameraScene();
}else if((event?.target as HTMLInputElement).id === 'selectCharacterSceneFormCustomYourOwnSubmit') {
this.nextSceneToCustomizeScene();
}
}); });
if (touchScreenManager.supportTouchScreen) { if (touchScreenManager.supportTouchScreen) {
new PinchManager(this); new PinchManager(this);
} }
waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 2 : 1;
const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16; const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16;
this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xFFFFFF); this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xFFFFFF);
this.selectedRectangle.setDepth(2); this.selectedRectangle.setDepth(2);
/*create user*/ /*create user*/
this.createCurrentPlayer(); this.createCurrentPlayer();
const playerNumber = localUserStore.getPlayerCharacterIndex();
this.input.keyboard.on('keyup-ENTER', () => { this.input.keyboard.on('keyup-ENTER', () => {
return this.nextSceneToCameraScene(); return this.nextSceneToCameraScene();
@ -97,10 +95,6 @@ export class SelectCharacterScene extends AbstractCharacterScene {
this.input.keyboard.on('keydown-DOWN', () => { this.input.keyboard.on('keydown-DOWN', () => {
this.moveToDown(); this.moveToDown();
}); });
if (isMobile()) {
this.selectedRectangle.destroy();
}
} }
protected nextSceneToCameraScene(): void { protected nextSceneToCameraScene(): void {
@ -111,9 +105,12 @@ export class SelectCharacterScene extends AbstractCharacterScene {
return; return;
} }
this.scene.stop(SelectCharacterSceneName); this.scene.stop(SelectCharacterSceneName);
waScaleManager.restoreZoom();
gameManager.setCharacterLayers([this.selectedPlayer.texture.key]); gameManager.setCharacterLayers([this.selectedPlayer.texture.key]);
gameManager.tryResumingGame(this, EnableCameraSceneName); gameManager.tryResumingGame(this, EnableCameraSceneName);
this.scene.remove(SelectCharacterSceneName); this.players = [];
selectCharacterSceneVisibleStore.set(false);
this.events.removeListener('wake');
} }
protected nextSceneToCustomizeScene(): void { protected nextSceneToCustomizeScene(): void {
@ -121,7 +118,9 @@ export class SelectCharacterScene extends AbstractCharacterScene {
return; return;
} }
this.scene.sleep(SelectCharacterSceneName); this.scene.sleep(SelectCharacterSceneName);
waScaleManager.restoreZoom();
this.scene.run(CustomizeSceneName); this.scene.run(CustomizeSceneName);
selectCharacterSceneVisibleStore.set(false);
} }
createCurrentPlayer(): void { createCurrentPlayer(): void {
@ -138,15 +137,16 @@ export class SelectCharacterScene extends AbstractCharacterScene {
repeat: -1 repeat: -1
}); });
player.setInteractive().on("pointerdown", () => { player.setInteractive().on("pointerdown", () => {
if(this.currentSelectUser === i){ if (this.pointerClicked || this.currentSelectUser === i) {
return; return;
} }
this.pointerClicked = true;
this.currentSelectUser = i; this.currentSelectUser = i;
this.moveUser(); this.moveUser();
setTimeout(() => {this.pointerClicked = false;}, 100);
}); });
this.players.push(player); this.players.push(player);
} }
this.selectedPlayer = this.players[this.currentSelectUser]; this.selectedPlayer = this.players[this.currentSelectUser];
this.selectedPlayer.play(this.playerModels[this.currentSelectUser].name); this.selectedPlayer.play(this.playerModels[this.currentSelectUser].name);
} }
@ -191,70 +191,35 @@ export class SelectCharacterScene extends AbstractCharacterScene {
this.moveUser(); this.moveUser();
} }
protected defineSetupPlayer(numero: number){ protected defineSetupPlayer(num: number){
const deltaX = isMobile() ? 30 : 32; const deltaX = 32;
const deltaY = isMobile() ? 2 : 32; const deltaY = 32;
let [playerX, playerY] = this.getCharacterPosition(); // player X and player y are middle of the let [playerX, playerY] = this.getCharacterPosition(); // player X and player y are middle of the
let playerVisible = true; playerX = ( (playerX - (deltaX * 2.5)) + ((deltaX) * (num % this.nbCharactersPerRow)) ); // calcul position on line users
let playerScale = 1; playerY = ( (playerY - (deltaY * 2)) + ((deltaY) * ( Math.floor(num / this.nbCharactersPerRow) )) ); // calcul position on column users
let playserOpactity = 1;
if (!isMobile()) { const playerVisible = true;
playerX = ( (playerX - (deltaX * 2.5)) + ((deltaX) * (numero % this.nbCharactersPerRow)) ); // calcul position on line users const playerScale = 1;
playerY = ( (playerY - (deltaY * 2)) + ((deltaY) * ( Math.floor(numero / this.nbCharactersPerRow) )) ); // calcul position on column users const playerOpacity = 1;
// if selected // if selected
if( numero === this.currentSelectUser ){ if( num === this.currentSelectUser ){
this.selectedRectangle.setX(playerX); this.selectedRectangle.setX(playerX);
this.selectedRectangle.setY(playerY); this.selectedRectangle.setY(playerY);
}
} else {
playerScale = 1.5;
if( this.currentSelectUser !== numero ){
playerVisible = false;
}
if( numero === (this.currentSelectUser + 1) ){
playerY -= deltaY;
playerX += deltaX;
playerScale = 0.8;
playserOpactity = 0.6;
playerVisible = true;
}
if( numero === (this.currentSelectUser + 2) ){
playerY -= deltaY;
playerX += (deltaX * 2);
playerScale = 0.8;
playserOpactity = 0.6;
playerVisible = true;
}
if( numero === (this.currentSelectUser - 1) ){
playerY -= deltaY;
playerX -= deltaX;
playerScale = 0.8;
playserOpactity = 0.6;
playerVisible = true;
}
if( numero === (this.currentSelectUser - 2) ){
playerY -= deltaY;
playerX -= (deltaX * 2);
playerScale = 0.8;
playserOpactity = 0.6;
playerVisible = true;
}
} }
return {playerX, playerY, playerScale, playserOpactity, playerVisible} return {playerX, playerY, playerScale, playerOpacity, playerVisible}
} }
protected setUpPlayer(player: Phaser.Physics.Arcade.Sprite, numero: number){ protected setUpPlayer(player: Phaser.Physics.Arcade.Sprite, num: number){
const {playerX, playerY, playerScale, playserOpactity, playerVisible} = this.defineSetupPlayer(numero); const {playerX, playerY, playerScale, playerOpacity, playerVisible} = this.defineSetupPlayer(num);
player.setBounce(0.2); player.setBounce(0.2);
player.setCollideWorldBounds(true); player.setCollideWorldBounds(false);
player.setVisible( playerVisible ); player.setVisible( playerVisible );
player.setScale(playerScale, playerScale); player.setScale(playerScale, playerScale);
player.setAlpha(playserOpactity); player.setAlpha(playerOpacity);
player.setX(playerX); player.setX(playerX);
player.setY(playerY); player.setY(playerY);
} }
@ -265,7 +230,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
protected getCharacterPosition(): [number, number] { protected getCharacterPosition(): [number, number] {
return [ return [
this.game.renderer.width / 2, this.game.renderer.width / 2,
this.game.renderer.height / (isMobile() ? 3 : 2.5) this.game.renderer.height / 2.5
]; ];
} }
@ -278,12 +243,14 @@ export class SelectCharacterScene extends AbstractCharacterScene {
} }
update(time: number, delta: number): void { update(time: number, delta: number): void {
if(this.lazyloadingAttempt){
this.moveUser();
this.lazyloadingAttempt = false;
}
} }
public onResize(ev: UIEvent): void { public onResize(): void {
//move position of user //move position of user
this.moveUser(); this.moveUser();
this.centerXDomElement(this.selectCharacterSceneElement, 150);
} }
} }

View File

@ -10,17 +10,18 @@ import { getAllCompanionResources } from "../Companion/CompanionTexturesLoadingM
import {touchScreenManager} from "../../Touch/TouchScreenManager"; import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager"; import {PinchManager} from "../UserInput/PinchManager";
import { MenuScene } from "../Menu/MenuScene"; import { MenuScene } from "../Menu/MenuScene";
import {selectCompanionSceneVisibleStore} from "../../Stores/SelectCompanionStore";
import {waScaleManager} from "../Services/WaScaleManager";
import {isMobile} from "../../Enum/EnvironmentVariable";
export const SelectCompanionSceneName = "SelectCompanionScene"; export const SelectCompanionSceneName = "SelectCompanionScene";
const selectCompanionSceneKey = 'selectCompanionScene';
export class SelectCompanionScene extends ResizableScene { export class SelectCompanionScene extends ResizableScene {
private selectedCompanion!: Phaser.Physics.Arcade.Sprite; private selectedCompanion!: Phaser.Physics.Arcade.Sprite;
private companions: Array<Phaser.Physics.Arcade.Sprite> = new Array<Phaser.Physics.Arcade.Sprite>(); private companions: Array<Phaser.Physics.Arcade.Sprite> = new Array<Phaser.Physics.Arcade.Sprite>();
private companionModels: Array<CompanionResourceDescriptionInterface> = []; private companionModels: Array<CompanionResourceDescriptionInterface> = [];
private saveZoom: number = 0;
private selectCompanionSceneElement!: Phaser.GameObjects.DOMElement;
private currentCompanion = 0; private currentCompanion = 0;
constructor() { constructor() {
@ -30,8 +31,6 @@ export class SelectCompanionScene extends ResizableScene {
} }
preload() { preload() {
this.load.html(selectCompanionSceneKey, 'resources/html/SelectCompanionScene.html');
getAllCompanionResources(this.load).forEach(model => { getAllCompanionResources(this.load).forEach(model => {
this.companionModels.push(model); this.companionModels.push(model);
}); });
@ -42,30 +41,17 @@ export class SelectCompanionScene extends ResizableScene {
create() { create() {
this.selectCompanionSceneElement = this.add.dom(-1000, 0).createFromCache(selectCompanionSceneKey); selectCompanionSceneVisibleStore.set(true);
this.centerXDomElement(this.selectCompanionSceneElement, 150);
MenuScene.revealMenusAfterInit(this.selectCompanionSceneElement, selectCompanionSceneKey);
this.selectCompanionSceneElement.addListener('click'); waScaleManager.saveZoom();
this.selectCompanionSceneElement.on('click', (event:MouseEvent) => { waScaleManager.zoomModifier = isMobile() ? 2 : 1;
event.preventDefault();
if((event?.target as HTMLInputElement).id === 'selectCharacterButtonLeft') {
this.moveToLeft();
}else if((event?.target as HTMLInputElement).id === 'selectCharacterButtonRight') {
this.moveToRight();
}else if((event?.target as HTMLInputElement).id === 'selectCompanionSceneFormSubmit') {
this.nextScene();
}else if((event?.target as HTMLInputElement).id === 'selectCompanionSceneFormBack') {
this._nextScene();
}
});
if (touchScreenManager.supportTouchScreen) { if (touchScreenManager.supportTouchScreen) {
new PinchManager(this); new PinchManager(this);
} }
// input events // input events
this.input.keyboard.on('keyup-ENTER', this.nextScene.bind(this)); this.input.keyboard.on('keyup-ENTER', this.selectCompanion.bind(this));
this.input.keyboard.on('keydown-RIGHT', this.moveToRight.bind(this)); this.input.keyboard.on('keydown-RIGHT', this.moveToRight.bind(this));
this.input.keyboard.on('keydown-LEFT', this.moveToLeft.bind(this)); this.input.keyboard.on('keydown-LEFT', this.moveToLeft.bind(this));
@ -89,18 +75,20 @@ export class SelectCompanionScene extends ResizableScene {
} }
private nextScene(): void { public selectCompanion(): void {
localUserStore.setCompanion(this.companionModels[this.currentCompanion].name); localUserStore.setCompanion(this.companionModels[this.currentCompanion].name);
gameManager.setCompanion(this.companionModels[this.currentCompanion].name); gameManager.setCompanion(this.companionModels[this.currentCompanion].name);
this._nextScene(); this.closeScene();
} }
private _nextScene(){ public closeScene(){
// next scene // next scene
this.scene.stop(SelectCompanionSceneName); this.scene.stop(SelectCompanionSceneName);
waScaleManager.restoreZoom();
gameManager.tryResumingGame(this, EnableCameraSceneName); gameManager.tryResumingGame(this, EnableCameraSceneName);
this.scene.remove(SelectCompanionSceneName); this.scene.remove(SelectCompanionSceneName);
selectCompanionSceneVisibleStore.set(false);
} }
private createCurrentCompanion(): void { private createCurrentCompanion(): void {
@ -126,10 +114,8 @@ export class SelectCompanionScene extends ResizableScene {
this.selectedCompanion = this.companions[this.currentCompanion]; this.selectedCompanion = this.companions[this.currentCompanion];
} }
public onResize(ev: UIEvent): void { public onResize(): void {
this.moveCompanion(); this.moveCompanion();
this.centerXDomElement(this.selectCompanionSceneElement, 150);
} }
private updateSelectedCompanion(): void { private updateSelectedCompanion(): void {
@ -147,15 +133,7 @@ export class SelectCompanionScene extends ResizableScene {
this.updateSelectedCompanion(); this.updateSelectedCompanion();
} }
private moveToLeft(){ public moveToRight(){
if(this.currentCompanion === 0){
return;
}
this.currentCompanion -= 1;
this.moveCompanion();
}
private moveToRight(){
if(this.currentCompanion === (this.companions.length - 1)){ if(this.currentCompanion === (this.companions.length - 1)){
return; return;
} }
@ -163,38 +141,46 @@ export class SelectCompanionScene extends ResizableScene {
this.moveCompanion(); this.moveCompanion();
} }
private defineSetupCompanion(numero: number){ public moveToLeft(){
if(this.currentCompanion === 0){
return;
}
this.currentCompanion -= 1;
this.moveCompanion();
}
private defineSetupCompanion(num: number){
const deltaX = 30; const deltaX = 30;
const deltaY = 2; const deltaY = 2;
let [companionX, companionY] = this.getCompanionPosition(); let [companionX, companionY] = this.getCompanionPosition();
let companionVisible = true; let companionVisible = true;
let companionScale = 1.5; let companionScale = 1.5;
let companionOpactity = 1; let companionOpactity = 1;
if( this.currentCompanion !== numero ){ if( this.currentCompanion !== num ){
companionVisible = false; companionVisible = false;
} }
if( numero === (this.currentCompanion + 1) ){ if( num === (this.currentCompanion + 1) ){
companionY -= deltaY; companionY -= deltaY;
companionX += deltaX; companionX += deltaX;
companionScale = 0.8; companionScale = 0.8;
companionOpactity = 0.6; companionOpactity = 0.6;
companionVisible = true; companionVisible = true;
} }
if( numero === (this.currentCompanion + 2) ){ if( num === (this.currentCompanion + 2) ){
companionY -= deltaY; companionY -= deltaY;
companionX += (deltaX * 2); companionX += (deltaX * 2);
companionScale = 0.8; companionScale = 0.8;
companionOpactity = 0.6; companionOpactity = 0.6;
companionVisible = true; companionVisible = true;
} }
if( numero === (this.currentCompanion - 1) ){ if( num === (this.currentCompanion - 1) ){
companionY -= deltaY; companionY -= deltaY;
companionX -= deltaX; companionX -= deltaX;
companionScale = 0.8; companionScale = 0.8;
companionOpactity = 0.6; companionOpactity = 0.6;
companionVisible = true; companionVisible = true;
} }
if( numero === (this.currentCompanion - 2) ){ if( num === (this.currentCompanion - 2) ){
companionY -= deltaY; companionY -= deltaY;
companionX -= (deltaX * 2); companionX -= (deltaX * 2);
companionScale = 0.8; companionScale = 0.8;

View File

@ -1,152 +0,0 @@
import {mediaManager} from "../../WebRtc/MediaManager";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import {localUserStore} from "../../Connexion/LocalUserStore";
import {DirtyScene} from "../Game/DirtyScene";
import {get} from "svelte/store";
import {requestedCameraState, requestedMicrophoneState} from "../../Stores/MediaStore";
export const HelpCameraSettingsSceneName = 'HelpCameraSettingsScene';
const helpCameraSettings = 'helpCameraSettings';
/**
* The scene that show how to permit Camera and Microphone access if there are not already allowed
*/
export class HelpCameraSettingsScene extends DirtyScene {
private helpCameraSettingsElement!: Phaser.GameObjects.DOMElement;
private helpCameraSettingsOpened: boolean = false;
constructor() {
super({key: HelpCameraSettingsSceneName});
}
preload() {
this.load.html(helpCameraSettings, 'resources/html/helpCameraSettings.html');
}
create(){
this.createHelpCameraSettings();
}
private createHelpCameraSettings() : void {
const middleX = this.getMiddleX();
this.helpCameraSettingsElement = this.add.dom(middleX, -800, undefined, {overflow: 'scroll'}).createFromCache(helpCameraSettings);
this.revealMenusAfterInit(this.helpCameraSettingsElement, helpCameraSettings);
this.helpCameraSettingsElement.addListener('click');
this.helpCameraSettingsElement.on('click', (event:MouseEvent) => {
if((event?.target as HTMLInputElement).id === 'mailto') {
return;
}
event.preventDefault();
if((event?.target as HTMLInputElement).id === 'helpCameraSettingsFormRefresh') {
window.location.reload();
}else if((event?.target as HTMLInputElement).id === 'helpCameraSettingsFormContinue') {
this.closeHelpCameraSettingsOpened();
}
});
if(!localUserStore.getHelpCameraSettingsShown() && (!get(requestedMicrophoneState) || !get(requestedCameraState))){
this.openHelpCameraSettingsOpened();
localUserStore.setHelpCameraSettingsShown();
}
mediaManager.setHelpCameraSettingsCallBack(() => {
this.openHelpCameraSettingsOpened();
});
}
private openHelpCameraSettingsOpened(): void{
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').style.display = 'none';
this.helpCameraSettingsOpened = true;
try{
if(window.navigator.userAgent.includes('Firefox')){
HtmlUtils.getElementByIdOrFail<HTMLParagraphElement>('browserHelpSetting').innerHTML ='<img src="/resources/objects/help-setting-camera-permission-firefox.png"/>';
}else if(window.navigator.userAgent.includes('Chrome')){
HtmlUtils.getElementByIdOrFail<HTMLParagraphElement>('browserHelpSetting').innerHTML ='<img src="/resources/objects/help-setting-camera-permission-chrome.png"/>';
}
}catch(err) {
console.error('openHelpCameraSettingsOpened => getElementByIdOrFail => error', err);
}
const middleY = this.getMiddleY();
const middleX = this.getMiddleX();
this.tweens.add({
targets: this.helpCameraSettingsElement,
y: middleY,
x: middleX,
duration: 1000,
ease: 'Power3',
overflow: 'scroll'
});
this.dirty = true;
}
private closeHelpCameraSettingsOpened(): void{
const middleX = this.getMiddleX();
/*const helpCameraSettingsInfo = this.helpCameraSettingsElement.getChildByID('helpCameraSettings') as HTMLParagraphElement;
helpCameraSettingsInfo.innerText = '';
helpCameraSettingsInfo.style.display = 'none';*/
this.helpCameraSettingsOpened = false;
this.tweens.add({
targets: this.helpCameraSettingsElement,
y: -1000,
x: middleX,
duration: 1000,
ease: 'Power3',
overflow: 'scroll'
});
this.dirty = true;
}
private revealMenusAfterInit(menuElement: Phaser.GameObjects.DOMElement, rootDomId: string) {
//Dom elements will appear inside the viewer screen when creating before being moved out of it, which create a flicker effect.
//To prevent this, we put a 'hidden' attribute on the root element, we remove it only after the init is done.
setTimeout(() => {
(menuElement.getChildByID(rootDomId) as HTMLElement).hidden = false;
}, 250);
}
update(time: number, delta: number): void {
this.dirty = false;
}
public onResize(ev: UIEvent): void {
super.onResize(ev);
if (this.helpCameraSettingsOpened) {
const middleX = this.getMiddleX();
const middleY = this.getMiddleY();
this.tweens.add({
targets: this.helpCameraSettingsElement,
x: middleX,
y: middleY,
duration: 1000,
ease: 'Power3'
});
this.dirty = true;
}
}
private getMiddleX() : number{
return (this.scale.width / 2) -
(
this.helpCameraSettingsElement
&& this.helpCameraSettingsElement.node
&& this.helpCameraSettingsElement.node.getBoundingClientRect().width > 0
? (this.helpCameraSettingsElement.node.getBoundingClientRect().width / (2 * this.scale.zoom))
: (400 / 2)
);
}
private getMiddleY() : number{
const middleY = ((this.scale.height) - (
(this.helpCameraSettingsElement
&& this.helpCameraSettingsElement.node
&& this.helpCameraSettingsElement.node.getBoundingClientRect().height > 0
? this.helpCameraSettingsElement.node.getBoundingClientRect().height : 400 /*FIXME to use a const will be injected in HTMLElement*/)/this.scale.zoom)) / 2;
return (middleY > 0 ? middleY : 0);
}
public isDirty(): boolean {
return this.dirty;
}
}

View File

@ -2,6 +2,7 @@ import {HdpiManager} from "./HdpiManager";
import ScaleManager = Phaser.Scale.ScaleManager; import ScaleManager = Phaser.Scale.ScaleManager;
import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager"; import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager";
import type {Game} from "../Game/Game"; import type {Game} from "../Game/Game";
import {ResizableScene} from "../Login/ResizableScene";
class WaScaleManager { class WaScaleManager {
@ -9,6 +10,7 @@ class WaScaleManager {
private scaleManager!: ScaleManager; private scaleManager!: ScaleManager;
private game!: Game; private game!: Game;
private actualZoom: number = 1; private actualZoom: number = 1;
private _saveZoom: number = 1;
public constructor(private minGamePixelsNumber: number, private absoluteMinPixelNumber: number) { public constructor(private minGamePixelsNumber: number, private absoluteMinPixelNumber: number) {
this.hdpiManager = new HdpiManager(minGamePixelsNumber, absoluteMinPixelNumber); this.hdpiManager = new HdpiManager(minGamePixelsNumber, absoluteMinPixelNumber);
@ -30,13 +32,19 @@ class WaScaleManager {
const { game: gameSize, real: realSize } = this.hdpiManager.getOptimalGameSize({width: width * devicePixelRatio, height: height * devicePixelRatio}); const { game: gameSize, real: realSize } = this.hdpiManager.getOptimalGameSize({width: width * devicePixelRatio, height: height * devicePixelRatio});
this.actualZoom = realSize.width / gameSize.width / devicePixelRatio; this.actualZoom = realSize.width / gameSize.width / devicePixelRatio;
this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio); this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio)
this.scaleManager.resize(gameSize.width, gameSize.height); this.scaleManager.resize(gameSize.width, gameSize.height);
// Override bug in canvas resizing in Phaser. Let's resize the canvas ourselves // Override bug in canvas resizing in Phaser. Let's resize the canvas ourselves
const style = this.scaleManager.canvas.style; const style = this.scaleManager.canvas.style;
style.width = Math.ceil(realSize.width / devicePixelRatio) + 'px'; style.width = Math.ceil(realSize.width / devicePixelRatio) + 'px';
style.height = Math.ceil(realSize.height / devicePixelRatio) + 'px'; style.height = Math.ceil(realSize.height / devicePixelRatio) + 'px';
// Note: onResize will be called twice (once here and once is Game.ts), but we have no better way.
for (const scene of this.game.scene.getScenes(true)) {
if (scene instanceof ResizableScene) {
scene.onResize();
}
}
this.game.markDirty(); this.game.markDirty();
} }
@ -50,6 +58,16 @@ class WaScaleManager {
this.applyNewSize(); this.applyNewSize();
} }
public saveZoom(): void {
this._saveZoom = this.hdpiManager.zoomModifier;
console.log(this._saveZoom);
}
public restoreZoom(): void{
this.hdpiManager.zoomModifier = this._saveZoom;
this.applyNewSize();
}
/** /**
* This is used to scale back the ui components to counter-act the zoom. * This is used to scale back the ui components to counter-act the zoom.
*/ */

View File

@ -0,0 +1,3 @@
import { derived, writable, Writable } from "svelte/store";
export const customCharacterSceneVisibleStore = writable(false);

View File

@ -1,3 +1,5 @@
import { derived, writable, Writable } from "svelte/store"; import { writable } from "svelte/store";
export const userMovingStore = writable(false); export const userMovingStore = writable(false);
export const requestVisitCardsStore = writable<string|null>(null);

View File

@ -0,0 +1,3 @@
import { writable } from "svelte/store";
export const helpCameraSettingsVisibleStore = writable(false);

View File

@ -0,0 +1,3 @@
import { writable } from "svelte/store";
export const loginSceneVisibleStore = writable(false);

View File

@ -209,10 +209,14 @@ function createVideoConstraintStore() {
return { return {
subscribe, subscribe,
setDeviceId: (deviceId: string) => update((constraints) => { setDeviceId: (deviceId: string|undefined) => update((constraints) => {
constraints.deviceId = { if (deviceId !== undefined) {
exact: deviceId constraints.deviceId = {
}; exact: deviceId
};
} else {
delete constraints.deviceId;
}
return constraints; return constraints;
}), }),
@ -241,15 +245,19 @@ function createAudioConstraintStore() {
return { return {
subscribe, subscribe,
setDeviceId: (deviceId: string) => update((constraints) => { setDeviceId: (deviceId: string|undefined) => update((constraints) => {
selectedDeviceId = deviceId; selectedDeviceId = deviceId;
if (typeof(constraints) === 'boolean') { if (typeof(constraints) === 'boolean') {
constraints = {} constraints = {}
} }
constraints.deviceId = { if (deviceId !== undefined) {
exact: selectedDeviceId constraints.deviceId = {
}; exact: selectedDeviceId
};
} else {
delete constraints.deviceId;
}
return constraints; return constraints;
}) })
@ -410,13 +418,15 @@ export const localStreamStore = derived<Readable<MediaStreamConstraints>, LocalS
error: new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.'), error: new Error('Unable to access your camera or microphone. You need to use a HTTPS connection.'),
constraints constraints
}); });
return;
} else { } else {
//throw new Error('Unable to access your camera or microphone. Your browser is too old.'); //throw new Error('Unable to access your camera or microphone. Your browser is too old.');
set({ set({
type: 'error', type: 'error',
error: new Error('Unable to access your camera or microphone. Your browser is too old.'), error: new Error('Unable to access your camera or microphone. Your browser is too old. Please consider upgrading your browser or try using a recent version of Chrome.'),
constraints constraints
}); });
return;
} }
} }
@ -508,3 +518,79 @@ export const obtainedMediaConstraintStore = derived(localStreamStore, ($localStr
return $localStreamStore.constraints; return $localStreamStore.constraints;
}); });
/**
* Device list
*/
export const deviceListStore = readable<MediaDeviceInfo[]>([], function start(set) {
let deviceListCanBeQueried = false;
const queryDeviceList = () => {
// Note: so far, we are ignoring any failures.
navigator.mediaDevices.enumerateDevices().then((mediaDeviceInfos) => {
set(mediaDeviceInfos);
}).catch((e) => {
console.error(e);
throw e;
});
};
const unsubscribe = localStreamStore.subscribe((streamResult) => {
if (streamResult.type === "success" && streamResult.stream !== null) {
if (deviceListCanBeQueried === false) {
queryDeviceList();
deviceListCanBeQueried = true;
}
}
});
if (navigator.mediaDevices) {
navigator.mediaDevices.addEventListener('devicechange', queryDeviceList);
}
return function stop() {
unsubscribe();
if (navigator.mediaDevices) {
navigator.mediaDevices.removeEventListener('devicechange', queryDeviceList);
}
};
});
export const cameraListStore = derived(deviceListStore, ($deviceListStore) => {
return $deviceListStore.filter(device => device.kind === 'videoinput');
});
export const microphoneListStore = derived(deviceListStore, ($deviceListStore) => {
return $deviceListStore.filter(device => device.kind === 'audioinput');
});
// TODO: detect the new webcam and automatically switch on it.
cameraListStore.subscribe((devices) => {
// If the selected camera is unplugged, let's remove the constraint on deviceId
const constraints = get(videoConstraintStore);
if (!constraints.deviceId) {
return;
}
// If we cannot find the device ID, let's remove it.
// @ts-ignore
if (!devices.find(device => device.deviceId === constraints.deviceId.exact)) {
videoConstraintStore.setDeviceId(undefined);
}
});
microphoneListStore.subscribe((devices) => {
// If the selected camera is unplugged, let's remove the constraint on deviceId
const constraints = get(audioConstraintStore);
if (typeof constraints === 'boolean') {
return;
}
if (!constraints.deviceId) {
return;
}
// If we cannot find the device ID, let's remove it.
// @ts-ignore
if (!devices.find(device => device.deviceId === constraints.deviceId.exact)) {
audioConstraintStore.setDeviceId(undefined);
}
});

View File

@ -126,7 +126,7 @@ export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstra
let currentStreamPromise: Promise<MediaStream>; let currentStreamPromise: Promise<MediaStream>;
if (navigator.getDisplayMedia) { if (navigator.getDisplayMedia) {
currentStreamPromise = navigator.getDisplayMedia({constraints}); currentStreamPromise = navigator.getDisplayMedia({constraints});
} else if (navigator.mediaDevices.getDisplayMedia) { } else if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
currentStreamPromise = navigator.mediaDevices.getDisplayMedia({constraints}); currentStreamPromise = navigator.mediaDevices.getDisplayMedia({constraints});
} else { } else {
stopScreenSharing(); stopScreenSharing();
@ -183,7 +183,7 @@ export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstra
* A store containing whether the screen sharing button should be displayed or hidden. * A store containing whether the screen sharing button should be displayed or hidden.
*/ */
export const screenSharingAvailableStore = derived(peerStore, ($peerStore, set) => { export const screenSharingAvailableStore = derived(peerStore, ($peerStore, set) => {
if (!navigator.getDisplayMedia && !navigator.mediaDevices.getDisplayMedia) { if (!navigator.getDisplayMedia && (!navigator.mediaDevices || !navigator.mediaDevices.getDisplayMedia)) {
set(false); set(false);
return; return;
} }

View File

@ -0,0 +1,3 @@
import { derived, writable, Writable } from "svelte/store";
export const selectCharacterSceneVisibleStore = writable(false);

View File

@ -0,0 +1,3 @@
import { derived, writable, Writable } from "svelte/store";
export const selectCompanionSceneVisibleStore = writable(false);

View File

@ -0,0 +1,22 @@
import { writable } from "svelte/store";
/**
* A store that contains the URL of the sound currently playing
*/
function createSoundPlayingStore() {
const { subscribe, set, update } = writable<string|null>(null);
return {
subscribe,
playSound: (url: string) => {
set(url);
},
soundEnded: () => {
set(null);
}
};
}
export const soundPlayingStore = createSoundPlayingStore();

View File

@ -11,7 +11,7 @@ enum iframeStates {
const cowebsiteDivId = 'cowebsite'; // the id of the whole container. const cowebsiteDivId = 'cowebsite'; // the id of the whole container.
const cowebsiteMainDomId = 'cowebsite-main'; // the id of the parent div of the iframe. const cowebsiteMainDomId = 'cowebsite-main'; // the id of the parent div of the iframe.
const cowebsiteAsideDomId = 'cowebsite-aside'; // the id of the parent div of the iframe. const cowebsiteAsideDomId = 'cowebsite-aside'; // the id of the parent div of the iframe.
const cowebsiteCloseButtonId = 'cowebsite-close'; export const cowebsiteCloseButtonId = 'cowebsite-close';
const cowebsiteFullScreenButtonId = 'cowebsite-fullscreen'; const cowebsiteFullScreenButtonId = 'cowebsite-fullscreen';
const cowebsiteOpenFullScreenImageId = 'cowebsite-fullscreen-open'; const cowebsiteOpenFullScreenImageId = 'cowebsite-fullscreen-open';
const cowebsiteCloseFullScreenImageId = 'cowebsite-fullscreen-close'; const cowebsiteCloseFullScreenImageId = 'cowebsite-fullscreen-close';
@ -67,10 +67,15 @@ class CoWebsiteManager {
this.initResizeListeners(); this.initResizeListeners();
HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId).addEventListener('click', () => { const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
buttonCloseFrame.addEventListener('click', () => {
buttonCloseFrame.blur();
this.closeCoWebsite(); this.closeCoWebsite();
}); });
HtmlUtils.getElementByIdOrFail(cowebsiteFullScreenButtonId).addEventListener('click', () => {
const buttonFullScreenFrame = HtmlUtils.getElementByIdOrFail(cowebsiteFullScreenButtonId);
buttonFullScreenFrame.addEventListener('click', () => {
buttonFullScreenFrame.blur();
this.fullscreen(); this.fullscreen();
}); });
HtmlUtils.getElementByIdOrFail(cowebsiteFocusButtonId).addEventListener('click', () => { HtmlUtils.getElementByIdOrFail(cowebsiteFocusButtonId).addEventListener('click', () => {
@ -178,7 +183,10 @@ class CoWebsiteManager {
setTimeout(() => { setTimeout(() => {
this.fire(); this.fire();
}, animationTime) }, animationTime)
}).catch(() => this.closeCoWebsite()); }).catch((err) => {
console.error('Error loadCoWebsite => ', err);
this.closeCoWebsite()
});
} }
/** /**
@ -192,7 +200,10 @@ class CoWebsiteManager {
setTimeout(() => { setTimeout(() => {
this.fire(); this.fire();
}, animationTime); }, animationTime);
}).catch(() => this.closeCoWebsite()); }).catch((err) => {
console.error('Error insertCoWebsite => ', err);
this.closeCoWebsite();
});
} }
public closeCoWebsite(): Promise<void> { public closeCoWebsite(): Promise<void> {

View File

@ -12,6 +12,7 @@ import {
import { import {
screenSharingLocalStreamStore screenSharingLocalStreamStore
} from "../Stores/ScreenSharingStore"; } from "../Stores/ScreenSharingStore";
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
export type UpdatedLocalStreamCallback = (media: MediaStream|null) => void; export type UpdatedLocalStreamCallback = (media: MediaStream|null) => void;
export type StartScreenSharingCallback = (media: MediaStream) => void; export type StartScreenSharingCallback = (media: MediaStream) => void;
@ -20,16 +21,15 @@ export type ReportCallback = (message: string) => void;
export type ShowReportCallBack = (userId: string, userName: string|undefined) => void; export type ShowReportCallBack = (userId: string, userName: string|undefined) => void;
export type HelpCameraSettingsCallBack = () => void; export type HelpCameraSettingsCallBack = () => void;
import {cowebsiteCloseButtonId} from "./CoWebsiteManager";
export class MediaManager { export class MediaManager {
private remoteVideo: Map<string, HTMLVideoElement> = new Map<string, HTMLVideoElement>(); private remoteVideo: Map<string, HTMLVideoElement> = new Map<string, HTMLVideoElement>();
webrtcInAudio: HTMLAudioElement;
//FIX ME SOUNDMETER: check stalability of sound meter calculation //FIX ME SOUNDMETER: check stalability of sound meter calculation
//mySoundMeterElement: HTMLDivElement; //mySoundMeterElement: HTMLDivElement;
private webrtcOutAudio: HTMLAudioElement;
startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>(); startScreenSharingCallBacks : Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>(); stopScreenSharingCallBacks : Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
showReportModalCallBacks : Set<ShowReportCallBack> = new Set<ShowReportCallBack>(); showReportModalCallBacks : Set<ShowReportCallBack> = new Set<ShowReportCallBack>();
helpCameraSettingsCallBacks : Set<HelpCameraSettingsCallBack> = new Set<HelpCameraSettingsCallBack>();
private focused : boolean = true; private focused : boolean = true;
@ -44,11 +44,6 @@ export class MediaManager {
constructor() { constructor() {
this.webrtcInAudio = HtmlUtils.getElementByIdOrFail<HTMLAudioElement>('audio-webrtc-in');
this.webrtcOutAudio = HtmlUtils.getElementByIdOrFail<HTMLAudioElement>('audio-webrtc-out');
this.webrtcInAudio.volume = 0.2;
this.webrtcOutAudio.volume = 0.2;
this.pingCameraStatus(); this.pingCameraStatus();
//FIX ME SOUNDMETER: check stability of sound meter calculation //FIX ME SOUNDMETER: check stability of sound meter calculation
@ -63,8 +58,8 @@ export class MediaManager {
localStreamStore.subscribe((result) => { localStreamStore.subscribe((result) => {
if (result.type === 'error') { if (result.type === 'error') {
console.error(result.error); console.error(result.error);
layoutManager.addInformation('warning', 'Camera access denied. Click here and check navigators permissions.', () => { layoutManager.addInformation('warning', 'Camera access denied. Click here and check your browser permissions.', () => {
this.showHelpCameraSettingsCallBack(); helpCameraSettingsVisibleStore.set(true);
}, this.userInputManager); }, this.userInputManager);
return; return;
} }
@ -73,8 +68,8 @@ export class MediaManager {
screenSharingLocalStreamStore.subscribe((result) => { screenSharingLocalStreamStore.subscribe((result) => {
if (result.type === 'error') { if (result.type === 'error') {
console.error(result.error); console.error(result.error);
layoutManager.addInformation('warning', 'Screen sharing denied. Click here and check navigators permissions.', () => { layoutManager.addInformation('warning', 'Screen sharing denied. Click here and check your browser permissions.', () => {
this.showHelpCameraSettingsCallBack(); helpCameraSettingsVisibleStore.set(true);
}, this.userInputManager); }, this.userInputManager);
return; return;
} }
@ -106,11 +101,14 @@ export class MediaManager {
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay'); const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
gameOverlay.classList.add('active'); gameOverlay.classList.add('active');
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail('cowebsite-close'); const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
const functionTrigger = () => { const functionTrigger = () => {
this.triggerCloseJitsiFrameButton(); this.triggerCloseJitsiFrameButton();
} }
buttonCloseFrame.removeEventListener('click', functionTrigger); buttonCloseFrame.removeEventListener('click', () => {
buttonCloseFrame.blur();
functionTrigger();
});
gameOverlayVisibilityStore.showGameOverlay(); gameOverlayVisibilityStore.showGameOverlay();
} }
@ -119,17 +117,19 @@ export class MediaManager {
const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay'); const gameOverlay = HtmlUtils.getElementByIdOrFail('game-overlay');
gameOverlay.classList.remove('active'); gameOverlay.classList.remove('active');
const buttonCloseFrame = HtmlUtils.getElementByIdOrFail('cowebsite-close'); const buttonCloseFrame = HtmlUtils.getElementByIdOrFail(cowebsiteCloseButtonId);
const functionTrigger = () => { const functionTrigger = () => {
this.triggerCloseJitsiFrameButton(); this.triggerCloseJitsiFrameButton();
} }
buttonCloseFrame.addEventListener('click', functionTrigger); buttonCloseFrame.addEventListener('click', () => {
buttonCloseFrame.blur();
functionTrigger();
});
gameOverlayVisibilityStore.hideGameOverlay(); gameOverlayVisibilityStore.hideGameOverlay();
} }
addActiveVideo(user: UserSimplePeerInterface, userName: string = ""){ addActiveVideo(user: UserSimplePeerInterface, userName: string = ""){
this.webrtcInAudio.play();
const userId = ''+user.userId const userId = ''+user.userId
userName = userName.toUpperCase(); userName = userName.toUpperCase();
@ -277,10 +277,6 @@ export class MediaManager {
this.removeActiveVideo(this.getScreenSharingId(userId)) this.removeActiveVideo(this.getScreenSharingId(userId))
} }
playWebrtcOutSound(): void {
this.webrtcOutAudio.play();
}
isConnecting(userId: string): void { isConnecting(userId: string): void {
const connectingSpinnerDiv = this.getSpinner(userId); const connectingSpinnerDiv = this.getSpinner(userId);
if (connectingSpinnerDiv === null) { if (connectingSpinnerDiv === null) {
@ -395,16 +391,6 @@ export class MediaManager {
this.showReportModalCallBacks.add(callback); this.showReportModalCallBacks.add(callback);
} }
public setHelpCameraSettingsCallBack(callback: HelpCameraSettingsCallBack){
this.helpCameraSettingsCallBacks.add(callback);
}
private showHelpCameraSettingsCallBack(){
for(const callBack of this.helpCameraSettingsCallBacks){
callBack();
}
}
//FIX ME SOUNDMETER: check stalability of sound meter calculation //FIX ME SOUNDMETER: check stalability of sound meter calculation
/*updateSoudMeter(){ /*updateSoudMeter(){
try{ try{
@ -450,12 +436,32 @@ export class MediaManager {
public getNotification(){ public getNotification(){
//Get notification //Get notification
if (!DISABLE_NOTIFICATIONS && window.Notification && Notification.permission !== "granted") { if (!DISABLE_NOTIFICATIONS && window.Notification && Notification.permission !== "granted") {
Notification.requestPermission().catch((err) => { if (this.checkNotificationPromise()) {
console.error(`Notification permission error`, err); Notification.requestPermission().catch((err) => {
}); console.error(`Notification permission error`, err);
});
} else {
Notification.requestPermission();
}
} }
} }
/**
* Return true if the browser supports the modern version of the Notification API (which is Promise based) or false
* if we are on Safari...
*
* See https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API
*/
private checkNotificationPromise(): boolean {
try {
Notification.requestPermission().then();
} catch(e) {
return false;
}
return true;
}
public createNotification(userName: string){ public createNotification(userName: string){
if(this.focused){ if(this.focused){
return; return;

View File

@ -60,6 +60,7 @@ export class ScreenSharingPeer extends Peer {
const message = JSON.parse(chunk.toString('utf8')); const message = JSON.parse(chunk.toString('utf8'));
if (message.streamEnded !== true) { if (message.streamEnded !== true) {
console.error('Unexpected message on screen sharing peer connection'); console.error('Unexpected message on screen sharing peer connection');
return;
} }
mediaManager.removeActiveScreenSharingVideo("" + this.userId); mediaManager.removeActiveScreenSharingVideo("" + this.userId);
}); });

View File

@ -246,7 +246,6 @@ export class SimplePeer {
* This is triggered twice. Once by the server, and once by a remote client disconnecting * This is triggered twice. Once by the server, and once by a remote client disconnecting
*/ */
private closeConnection(userId : number) { private closeConnection(userId : number) {
mediaManager.playWebrtcOutSound();
try { try {
const peer = this.PeerConnectionArray.get(userId); const peer = this.PeerConnectionArray.get(userId);
if (peer === undefined) { if (peer === undefined) {
@ -263,7 +262,7 @@ export class SimplePeer {
const userIndex = this.Users.findIndex(user => user.userId === userId); const userIndex = this.Users.findIndex(user => user.userId === userId);
if(userIndex < 0){ if(userIndex < 0){
throw 'Couln\'t delete user'; throw 'Couldn\'t delete user';
} else { } else {
this.Users.splice(userIndex, 1); this.Users.splice(userIndex, 1);
} }

View File

@ -13,7 +13,6 @@ import WebFontLoaderPlugin from 'phaser3-rex-plugins/plugins/webfontloader-plugi
import {EntryScene} from "./Phaser/Login/EntryScene"; import {EntryScene} from "./Phaser/Login/EntryScene";
import {coWebsiteManager} from "./WebRtc/CoWebsiteManager"; import {coWebsiteManager} from "./WebRtc/CoWebsiteManager";
import {MenuScene} from "./Phaser/Menu/MenuScene"; import {MenuScene} from "./Phaser/Menu/MenuScene";
import {HelpCameraSettingsScene} from "./Phaser/Menu/HelpCameraSettingsScene";
import {localUserStore} from "./Connexion/LocalUserStore"; import {localUserStore} from "./Connexion/LocalUserStore";
import {ErrorScene} from "./Phaser/Reconnecting/ErrorScene"; import {ErrorScene} from "./Phaser/Reconnecting/ErrorScene";
import {iframeListener} from "./Api/IframeListener"; import {iframeListener} from "./Api/IframeListener";
@ -95,7 +94,7 @@ const config: GameConfig = {
ErrorScene, ErrorScene,
CustomizeScene, CustomizeScene,
MenuScene, MenuScene,
HelpCameraSettingsScene], ],
//resolution: window.devicePixelRatio / 2, //resolution: window.devicePixelRatio / 2,
fps: fps, fps: fps,
dom: { dom: {
@ -151,7 +150,9 @@ iframeListener.init();
const app = new App({ const app = new App({
target: HtmlUtils.getElementByIdOrFail('svelte-overlay'), target: HtmlUtils.getElementByIdOrFail('svelte-overlay'),
props: { }, props: {
game: game
},
}) })
export default app export default app

View File

@ -1 +1,9 @@
@import "~@fontsource/press-start-2p/index.css"; @import "~@fontsource/press-start-2p/index.css";
*{
font-family: PixelFont-7,monospace;
}
.nes-btn {
font-family: "Press Start 2P";
}

View File

@ -2,6 +2,6 @@
@import "nes.custom.css"; @import "nes.custom.css";
@import "cowebsite.scss"; @import "cowebsite.scss";
@import "cowebsite-mobile.scss"; @import "cowebsite-mobile.scss";
@import "style.css"; @import "style";
@import "mobile-style.scss"; @import "mobile-style.scss";
@import "fonts.scss"; @import "fonts.scss";

View File

@ -154,6 +154,8 @@ body .message-info.warning {
} }
video.myCamVideo{ video.myCamVideo{
background-color: #00000099;
max-height: 20vh;
width: 15vw; width: 15vw;
-webkit-transform: scaleX(-1); -webkit-transform: scaleX(-1);
transform: scaleX(-1); transform: scaleX(-1);
@ -347,42 +349,6 @@ video.myCamVideo{
} }
} }
.webrtcsetup {
display: none;
position: absolute;
top: 140px;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
height: 50%;
width: 50%;
border: white 6px solid;
}
.webrtcsetup .background-img {
position: relative;
display: block;
width: 40%;
height: 60%;
margin-left: auto;
margin-right: auto;
top: 50%;
transform: translateY(-50%);
}
#myCamVideoSetup {
width: 100%;
height: 100%;
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
}
.webrtcsetup.active {
display: block;
}
/* New layout */ /* New layout */
body { body {
@ -884,38 +850,6 @@ input[type=range]:focus::-ms-fill-upper {
} }
/*audio html when audio message playing*/
.main-container .audio-playing {
position: absolute;
width: 200px;
height: 54px;
right: -210px;
top: 40px;
transition: all 0.1s ease-out;
background-color: black;
border-radius: 30px 0 0 30px;
display: inline-flex;
}
.main-container .audio-playing.active {
right: 0;
}
.main-container .audio-playing img {
/*width: 30px;*/
border-radius: 50%;
background-color: #ffda01;
padding: 10px;
}
.main-container .audio-playing p {
color: white;
margin-left: 10px;
margin-top: 14px;
}
/* VIDEO QUALITY */ /* VIDEO QUALITY */
.main-console div.setting h1 { .main-console div.setting h1 {
@ -1307,4 +1241,22 @@ div.action.danger p.action-body {
height: 100%; height: 100%;
pointer-events: none; pointer-events: none;
z-index: 999; z-index: 999;
& > div {
position: relative;
width: 100%;
height: 100%;
& > div {
position: absolute;
width: 100%;
height: 100%;
}
& > div.scrollable {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
pointer-events: auto;
}
}
} }

View File

@ -7,6 +7,7 @@ import MiniCssExtractPlugin from 'mini-css-extract-plugin';
import sveltePreprocess from 'svelte-preprocess'; import sveltePreprocess from 'svelte-preprocess';
import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin"; import ForkTsCheckerWebpackPlugin from "fork-ts-checker-webpack-plugin";
import NodePolyfillPlugin from 'node-polyfill-webpack-plugin'; import NodePolyfillPlugin from 'node-polyfill-webpack-plugin';
import {DISPLAY_TERMS_OF_USE} from "./src/Enum/EnvironmentVariable";
const mode = process.env.NODE_ENV ?? 'development'; const mode = process.env.NODE_ENV ?? 'development';
const isProduction = mode === 'production'; const isProduction = mode === 'production';
@ -88,7 +89,16 @@ module.exports = {
preprocess: sveltePreprocess({ preprocess: sveltePreprocess({
scss: true, scss: true,
sass: true, sass: true,
}) }),
onwarn: function (warning: { code: string }, handleWarning: (warning: { code: string }) => void) {
// See https://github.com/sveltejs/svelte/issues/4946#issuecomment-662168782
if (warning.code === 'a11y-no-onchange') { return }
if (warning.code === 'a11y-autofocus') { return }
// process as usual
handleWarning(warning);
}
} }
} }
}, },
@ -174,7 +184,8 @@ module.exports = {
'JITSI_PRIVATE_MODE': null, 'JITSI_PRIVATE_MODE': null,
'START_ROOM_URL': null, 'START_ROOM_URL': null,
'MAX_USERNAME_LENGTH': 8, 'MAX_USERNAME_LENGTH': 8,
'MAX_PER_GROUP': 4 'MAX_PER_GROUP': 4,
'DISPLAY_TERMS_OF_USE': false,
}) })
], ],

View File

@ -30,6 +30,13 @@
chalk "^2.0.0" chalk "^2.0.0"
js-tokens "^4.0.0" js-tokens "^4.0.0"
"@babel/runtime@^7.14.0":
version "7.14.0"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6"
integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==
dependencies:
regenerator-runtime "^0.13.4"
"@discoveryjs/json-ext@^0.5.0": "@discoveryjs/json-ext@^0.5.0":
version "0.5.3" version "0.5.3"
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz#90420f9f9c6d3987f176a19a7d8e764271a2f55d" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.3.tgz#90420f9f9c6d3987f176a19a7d8e764271a2f55d"
@ -778,6 +785,14 @@ atob@^2.1.2:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
automation-events@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/automation-events/-/automation-events-4.0.1.tgz#93acef8a457cbea65f16fdcef8f349fd2fafe298"
integrity sha512-8bQx+PVtNDMD5F2H40cQs7oexZve3Z0xC9fWRQT4fltF65f/WsSpjM4jpulL4K4yLLB71oi4/xVJJCJ5I/Kjbw==
dependencies:
"@babel/runtime" "^7.14.0"
tslib "^2.2.0"
available-typed-arrays@^1.0.2: available-typed-arrays@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz#6b098ca9d8039079ee3f77f7b783c4480ba513f5"
@ -4394,6 +4409,11 @@ rechoir@^0.7.0:
dependencies: dependencies:
resolve "^1.9.0" resolve "^1.9.0"
regenerator-runtime@^0.13.4:
version "0.13.7"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55"
integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==
regex-not@^1.0.0, regex-not@^1.0.2: regex-not@^1.0.0, regex-not@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
@ -4976,6 +4996,15 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
standardized-audio-context@^25.2.4:
version "25.2.4"
resolved "https://registry.yarnpkg.com/standardized-audio-context/-/standardized-audio-context-25.2.4.tgz#d64dbdd70615171ec90d1b7151a0d945af94cf3d"
integrity sha512-uQKZXRnXrPVO1V6SwZ7PiV3RkQqRY3n7i6Q8nbTXYvoz8NftRNzfOIlwefpuC8LVLUUs9dhbKTpP+WOA82dkBw==
dependencies:
"@babel/runtime" "^7.14.0"
automation-events "^4.0.1"
tslib "^2.2.0"
static-extend@^0.1.1: static-extend@^0.1.1:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"
@ -5306,7 +5335,7 @@ tslib@^1.8.1, tslib@^1.9.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.3: tslib@^2.0.3, tslib@^2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w== integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
@ -5715,9 +5744,9 @@ wrappy@1:
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
ws@^6.2.1: ws@^6.2.1:
version "6.2.1" version "6.2.2"
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.2.tgz#dd5cdbd57a9979916097652d78f1cc5faea0c32e"
integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== integrity sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==
dependencies: dependencies:
async-limiter "~1.0.0" async-limiter "~1.0.0"

View File

@ -0,0 +1,164 @@
{ "compressionlevel":-1,
"height":10,
"infinite":false,
"layers":[
{
"data":[33, 34, 34, 34, 34, 34, 34, 34, 34, 35, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 49, 50, 50, 50, 50, 50, 50, 50, 50, 51],
"height":10,
"id":3,
"name":"bottom",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 52, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":1,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":2,
"name":"floorLayer",
"objects":[
{
"height":254.57168784029,
"id":1,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":12,
"text":"Test 1 : \nBlock permission to camera and\/or microphone access.\n\nResult 1 :\nOrange popup show at the bottom of the screen.\nIf you click on it, the HelpCameraSetting popup open.\n\nTest 2 : \nReload the page and block permission to camera and\/or microphone access on the camera setting page.\n\nResult 2 : \nOrange popup show at the bottom of the screen.\nIf you click on it, the HelpCameraSetting popup open.\n",
"wrap":true
},
"type":"",
"visible":true,
"width":295.278811252269,
"x":12.2517014519056,
"y":49.3021778584392
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":6,
"nextobjectid":2,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"1.4.3",
"tileheight":32,
"tilesets":[
{
"columns":8,
"firstgid":1,
"image":"Validation\/tileset_dungeon.png",
"imageheight":256,
"imagewidth":256,
"margin":0,
"name":"dungeon",
"spacing":0,
"tilecount":64,
"tileheight":32,
"tiles":[
{
"id":0,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":1,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":2,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":5,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":8,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":10,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":16,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":17,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
},
{
"id":18,
"properties":[
{
"name":"collides",
"type":"bool",
"value":true
}]
}],
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.4,
"width":10
}

View File

@ -82,6 +82,14 @@
<a href="#" class="testLink" data-testmap="energy.json" target="_blank">Test energy consumption</a> <a href="#" class="testLink" data-testmap="energy.json" target="_blank">Test energy consumption</a>
</td> </td>
</tr> </tr>
<tr>
<td>
<input type="radio" name="test-help-camera-setting"> Success <input type="radio" name="test-help-camera-setting"> Failure <input type="radio" name="test-help-camera-setting" checked> Pending
</td>
<td>
<a href="#" class="testLink" data-testmap="help_camera_setting.json" target="_blank">Test the HelpCameraSettingScene</a>
</td>
</tr>
</table> </table>
<script> <script>

View File

@ -75,6 +75,14 @@ message EmoteEventMessage {
string emote = 2; string emote = 2;
} }
message RequestVisitCardMessage {
int32 targetUserId = 1;
}
message VisitCardMessage {
string url = 1;
}
message QueryJitsiJwtMessage { message QueryJitsiJwtMessage {
string jitsiRoom = 1; string jitsiRoom = 1;
string tag = 2; // FIXME: rather than reading the tag from the query, we should read it from the current map! string tag = 2; // FIXME: rather than reading the tag from the query, we should read it from the current map!
@ -94,6 +102,7 @@ message ClientToServerMessage {
ReportPlayerMessage reportPlayerMessage = 11; ReportPlayerMessage reportPlayerMessage = 11;
QueryJitsiJwtMessage queryJitsiJwtMessage = 12; QueryJitsiJwtMessage queryJitsiJwtMessage = 12;
EmotePromptMessage emotePromptMessage = 13; EmotePromptMessage emotePromptMessage = 13;
RequestVisitCardMessage requestVisitCardMessage = 14;
} }
} }
@ -259,6 +268,7 @@ message ServerToClientMessage {
RefreshRoomMessage refreshRoomMessage = 17; RefreshRoomMessage refreshRoomMessage = 17;
WorldConnexionMessage worldConnexionMessage = 18; WorldConnexionMessage worldConnexionMessage = 18;
EmoteEventMessage emoteEventMessage = 19; EmoteEventMessage emoteEventMessage = 19;
VisitCardMessage visitCardMessage = 20;
} }
} }
@ -330,6 +340,7 @@ message PusherToBackMessage {
SendUserMessage sendUserMessage = 12; SendUserMessage sendUserMessage = 12;
BanUserMessage banUserMessage = 13; BanUserMessage banUserMessage = 13;
EmotePromptMessage emotePromptMessage = 14; EmotePromptMessage emotePromptMessage = 14;
RequestVisitCardMessage requestVisitCardMessage = 15;
} }
} }

View File

@ -110,12 +110,10 @@ export class AuthenticateController extends BaseController {
private anonymLogin(){ private anonymLogin(){
this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { this.App.options("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
this.addCorsHeaders(res); this.addCorsHeaders(res);
res.end(); res.end();
}); });
this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => { this.App.post("/anonymLogin", (res: HttpResponse, req: HttpRequest) => {
res.onAborted(() => { res.onAborted(() => {
console.warn('Login request was aborted'); console.warn('Login request was aborted');
}) })

View File

@ -13,7 +13,12 @@ import {
PlayGlobalMessage, PlayGlobalMessage,
ReportPlayerMessage, ReportPlayerMessage,
EmoteEventMessage, EmoteEventMessage,
QueryJitsiJwtMessage, SendUserMessage, ServerToClientMessage, CompanionMessage, EmotePromptMessage QueryJitsiJwtMessage,
SendUserMessage,
ServerToClientMessage,
CompanionMessage,
EmotePromptMessage,
RequestVisitCardMessage
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import {UserMovesMessage} from "../Messages/generated/messages_pb"; import {UserMovesMessage} from "../Messages/generated/messages_pb";
import {TemplatedApp} from "uWebSockets.js" import {TemplatedApp} from "uWebSockets.js"
@ -333,6 +338,9 @@ export class IoSocketController {
socketManager.handleQueryJitsiJwtMessage(client, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage); socketManager.handleQueryJitsiJwtMessage(client, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage);
} else if (message.hasEmotepromptmessage()){ } else if (message.hasEmotepromptmessage()){
socketManager.handleEmotePromptMessage(client, message.getEmotepromptmessage() as EmotePromptMessage); socketManager.handleEmotePromptMessage(client, message.getEmotepromptmessage() as EmotePromptMessage);
} else if (message.hasRequestvisitcardmessage()) {
socketManager.handleRequestVisitCardMessage(client, message.getRequestvisitcardmessage() as RequestVisitCardMessage);
} }
/* Ok is false if backpressure was built up, wait for drain */ /* Ok is false if backpressure was built up, wait for drain */

View File

@ -24,7 +24,13 @@ import {
AdminPusherToBackMessage, AdminPusherToBackMessage,
ServerToAdminClientMessage, ServerToAdminClientMessage,
EmoteEventMessage, EmoteEventMessage,
UserJoinedRoomMessage, UserLeftRoomMessage, AdminMessage, BanMessage, RefreshRoomMessage, EmotePromptMessage UserJoinedRoomMessage,
UserLeftRoomMessage,
AdminMessage,
BanMessage,
RefreshRoomMessage,
EmotePromptMessage,
RequestVisitCardMessage
} from "../Messages/generated/messages_pb"; } from "../Messages/generated/messages_pb";
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils"; import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
import {JITSI_ISS, SECRET_JITSI_KEY} from "../Enum/EnvironmentVariable"; import {JITSI_ISS, SECRET_JITSI_KEY} from "../Enum/EnvironmentVariable";
@ -294,6 +300,7 @@ export class SocketManager implements ZoneEventListener {
throw 'reported socket user not found'; throw 'reported socket user not found';
} }
//TODO report user on admin application //TODO report user on admin application
//todo: move to back because this fail if the reported player is in another pusher.
await adminApi.reportPlayer(reportedSocket.userUuid, reportPlayerMessage.getReportcomment(), client.userUuid, client.roomId.split('/')[2]) await adminApi.reportPlayer(reportedSocket.userUuid, reportPlayerMessage.getReportcomment(), client.userUuid, client.roomId.split('/')[2])
} catch (e) { } catch (e) {
console.error('An error occurred on "handleReportMessage"'); console.error('An error occurred on "handleReportMessage"');
@ -597,6 +604,13 @@ export class SocketManager implements ZoneEventListener {
client.backConnection.write(pusherToBackMessage); client.backConnection.write(pusherToBackMessage);
} }
handleRequestVisitCardMessage(client: ExSocketInterface, requestVisitCardMessage: RequestVisitCardMessage) {
const pusherToBackMessage = new PusherToBackMessage();
pusherToBackMessage.setRequestvisitcardmessage(requestVisitCardMessage);
client.backConnection.write(pusherToBackMessage);
}
} }
export const socketManager = new SocketManager(); export const socketManager = new SocketManager();