Merge remote-tracking branch 'origin/develop' into swaggerPusher

This commit is contained in:
CEC 2022-04-28 11:37:21 +02:00
commit 231facbae2
125 changed files with 2241 additions and 1581 deletions

View File

@ -60,6 +60,10 @@ jobs:
run: yarn build run: yarn build
working-directory: "desktop/local-app" working-directory: "desktop/local-app"
- name: "Set desktop app version"
run: node helpers/set-version.js
working-directory: "desktop/electron"
- name: "Install dependencies" - name: "Install dependencies"
run: yarn install --froze-lockfile run: yarn install --froze-lockfile
working-directory: "desktop/electron" working-directory: "desktop/electron"
@ -68,15 +72,19 @@ jobs:
run: yarn build run: yarn build
working-directory: "desktop/electron" working-directory: "desktop/electron"
- name: "Build app" - name: "Install electron tools"
run: yarn bundle --publish never run: yarn electron-builder install-app-deps
working-directory: "desktop/electron"
- name: "Build app for testing"
run: yarn electron-builder --publish never
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
working-directory: "desktop/electron" working-directory: "desktop/electron"
if: ${{ github.event_name != 'release' }} if: ${{ github.event_name != 'release' }}
- name: "Build & publish App" - name: "Build & release app"
run: yarn release run: yarn electron-builder --publish always
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
working-directory: "desktop/electron" working-directory: "desktop/electron"

View File

@ -27,8 +27,8 @@
"no-unused-vars": "off", "no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": [ "@typescript-eslint/no-unused-vars": [
"error" "error", { "args": "none", "caughtErrors": "all", "varsIgnorePattern": "_exhaustiveCheck" }
], ],
"no-throw-literal": "error" "no-throw-literal": "error"
} }
} }

View File

@ -144,9 +144,8 @@ export class GameRoom {
joinRoomMessage.getUseruuid(), joinRoomMessage.getUseruuid(),
joinRoomMessage.getIpaddress(), joinRoomMessage.getIpaddress(),
position, position,
false,
this.positionNotifier, this.positionNotifier,
joinRoomMessage.getAway(), joinRoomMessage.getStatus(),
socket, socket,
joinRoomMessage.getTagList(), joinRoomMessage.getTagList(),
joinRoomMessage.getVisitcardurl(), joinRoomMessage.getVisitcardurl(),
@ -208,6 +207,9 @@ export class GameRoom {
updatePlayerDetails(user: User, playerDetailsMessage: SetPlayerDetailsMessage) { updatePlayerDetails(user: User, playerDetailsMessage: SetPlayerDetailsMessage) {
user.updateDetails(playerDetailsMessage); user.updateDetails(playerDetailsMessage);
if (user.group !== undefined && user.silent) {
this.leaveGroup(user);
}
} }
private updateUserGroup(user: User): void { private updateUserGroup(user: User): void {
@ -345,21 +347,6 @@ export class GameRoom {
}); });
} }
setSilent(user: User, silent: boolean) {
if (user.silent === silent) {
return;
}
user.silent = silent;
if (silent && user.group !== undefined) {
this.leaveGroup(user);
}
if (!silent) {
// If we are back to life, let's trigger a position update to see if we can join some group.
this.updatePosition(user, user.getPosition());
}
}
/** /**
* Makes a user leave a group and closes and destroy the group if the group contains only one remaining person. * Makes a user leave a group and closes and destroy the group if the group contains only one remaining person.
* *
@ -572,11 +559,7 @@ export class GameRoom {
return { return {
mapUrl, mapUrl,
policy_type: 1,
tags: [],
authenticationMandatory: null, authenticationMandatory: null,
roomSlug: null,
contactPage: null,
group: null, group: null,
}; };
} }

View File

@ -5,6 +5,7 @@ import { Movable } from "../Model/Movable";
import { PositionNotifier } from "../Model/PositionNotifier"; import { PositionNotifier } from "../Model/PositionNotifier";
import { ServerDuplexStream } from "grpc"; import { ServerDuplexStream } from "grpc";
import { import {
AvailabilityStatus,
BatchMessage, BatchMessage,
CompanionMessage, CompanionMessage,
FollowAbortMessage, FollowAbortMessage,
@ -30,9 +31,8 @@ export class User implements Movable {
public readonly uuid: string, public readonly uuid: string,
public readonly IPAddress: string, public readonly IPAddress: string,
private position: PointInterface, private position: PointInterface,
public silent: boolean,
private positionNotifier: PositionNotifier, private positionNotifier: PositionNotifier,
private away: boolean, private status: AvailabilityStatus,
public readonly socket: UserSocket, public readonly socket: UserSocket,
public readonly tags: string[], public readonly tags: string[],
public readonly visitCardUrl: string | null, public readonly visitCardUrl: string | null,
@ -90,8 +90,12 @@ export class User implements Movable {
return this.outlineColor; return this.outlineColor;
} }
public isAway(): boolean { public getStatus(): AvailabilityStatus {
return this.away; return this.status;
}
public get silent(): boolean {
return this.status === AvailabilityStatus.SILENT || this.status === AvailabilityStatus.JITSI;
} }
get following(): User | undefined { get following(): User | undefined {
@ -134,9 +138,11 @@ export class User implements Movable {
} }
this.voiceIndicatorShown = details.getShowvoiceindicator()?.getValue(); this.voiceIndicatorShown = details.getShowvoiceindicator()?.getValue();
const away = details.getAway(); const status = details.getStatus();
if (away) { let sendStatusUpdate = false;
this.away = away.getValue(); if (status && status !== this.status) {
this.status = status;
sendStatusUpdate = true;
} }
const playerDetails = new SetPlayerDetailsMessage(); const playerDetails = new SetPlayerDetailsMessage();
@ -144,11 +150,14 @@ export class User implements Movable {
if (this.outlineColor !== undefined) { if (this.outlineColor !== undefined) {
playerDetails.setOutlinecolor(new UInt32Value().setValue(this.outlineColor)); playerDetails.setOutlinecolor(new UInt32Value().setValue(this.outlineColor));
} }
if (details.getRemoveoutlinecolor()) {
playerDetails.setRemoveoutlinecolor(new BoolValue().setValue(true));
}
if (this.voiceIndicatorShown !== undefined) { if (this.voiceIndicatorShown !== undefined) {
playerDetails.setShowvoiceindicator(new BoolValue().setValue(this.voiceIndicatorShown)); playerDetails.setShowvoiceindicator(new BoolValue().setValue(this.voiceIndicatorShown));
} }
if (details.getAway() !== undefined) { if (sendStatusUpdate) {
playerDetails.setAway(new BoolValue().setValue(this.away)); playerDetails.setStatus(details.getStatus());
} }
this.positionNotifier.updatePlayerDetails(this, playerDetails); this.positionNotifier.updatePlayerDetails(this, playerDetails);

View File

@ -71,7 +71,6 @@ export class Zone {
/** /**
* Notify listeners of this zone that this user entered * Notify listeners of this zone that this user entered
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
private notifyEnter(thing: Movable, oldZone: Zone | null, position: PositionInterface) { private notifyEnter(thing: Movable, oldZone: Zone | null, position: PositionInterface) {
for (const listener of this.listeners) { for (const listener of this.listeners) {
this.onEnters(thing, oldZone, listener); this.onEnters(thing, oldZone, listener);

View File

@ -22,7 +22,6 @@ import {
SendUserMessage, SendUserMessage,
ServerToAdminClientMessage, ServerToAdminClientMessage,
SetPlayerDetailsMessage, SetPlayerDetailsMessage,
SilentMessage,
UserMovesMessage, UserMovesMessage,
VariableMessage, VariableMessage,
WebRtcSignalToServerMessage, WebRtcSignalToServerMessage,
@ -80,8 +79,6 @@ const roomManager: IRoomManagerServer = {
user, user,
message.getUsermovesmessage() as UserMovesMessage message.getUsermovesmessage() as UserMovesMessage
); );
} else if (message.hasSilentmessage()) {
socketManager.handleSilentMessage(room, user, message.getSilentmessage() as SilentMessage);
} else if (message.hasItemeventmessage()) { } else if (message.hasItemeventmessage()) {
socketManager.handleItemEvent( socketManager.handleItemEvent(
room, room,

View File

@ -4,12 +4,10 @@ import { VariablesRepositoryInterface } from "./VariablesRepositoryInterface";
* Mock class in charge of NOT saving/loading variables from the data store * Mock class in charge of NOT saving/loading variables from the data store
*/ */
export class VoidVariablesRepository implements VariablesRepositoryInterface { export class VoidVariablesRepository implements VariablesRepositoryInterface {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
loadVariables(roomUrl: string): Promise<{ [key: string]: string }> { loadVariables(roomUrl: string): Promise<{ [key: string]: string }> {
return Promise.resolve({}); return Promise.resolve({});
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
saveVariable(roomUrl: string, key: string, value: string): Promise<number> { saveVariable(roomUrl: string, key: string, value: string): Promise<number> {
return Promise.resolve(0); return Promise.resolve(0);
} }

View File

@ -5,7 +5,6 @@ import {
PointMessage, PointMessage,
RoomJoinedMessage, RoomJoinedMessage,
ServerToClientMessage, ServerToClientMessage,
SilentMessage,
SubMessage, SubMessage,
UserMovedMessage, UserMovedMessage,
UserMovesMessage, UserMovesMessage,
@ -160,10 +159,6 @@ export class SocketManager {
room.updatePlayerDetails(user, playerDetailsMessage); room.updatePlayerDetails(user, playerDetailsMessage);
} }
handleSilentMessage(room: GameRoom, user: User, silentMessage: SilentMessage) {
room.setSilent(user, silentMessage.getSilent());
}
handleItemEvent(room: GameRoom, user: User, itemEventMessage: ItemEventMessage) { handleItemEvent(room: GameRoom, user: User, itemEventMessage: ItemEventMessage) {
const itemEvent = ProtobufUtils.toItemEvent(itemEventMessage); const itemEvent = ProtobufUtils.toItemEvent(itemEventMessage);
@ -328,7 +323,7 @@ export class SocketManager {
userJoinedZoneMessage.setUserid(thing.id); userJoinedZoneMessage.setUserid(thing.id);
userJoinedZoneMessage.setUseruuid(thing.uuid); userJoinedZoneMessage.setUseruuid(thing.uuid);
userJoinedZoneMessage.setName(thing.name); userJoinedZoneMessage.setName(thing.name);
userJoinedZoneMessage.setAway(thing.isAway()); userJoinedZoneMessage.setStatus(thing.getStatus());
userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers)); userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
userJoinedZoneMessage.setFromzone(this.toProtoZone(fromZone)); userJoinedZoneMessage.setFromzone(this.toProtoZone(fromZone));
@ -656,7 +651,7 @@ export class SocketManager {
userJoinedMessage.setUserid(thing.id); userJoinedMessage.setUserid(thing.id);
userJoinedMessage.setUseruuid(thing.uuid); userJoinedMessage.setUseruuid(thing.uuid);
userJoinedMessage.setName(thing.name); userJoinedMessage.setName(thing.name);
userJoinedMessage.setAway(thing.isAway()); userJoinedMessage.setStatus(thing.getStatus());
userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers)); userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition())); userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
if (thing.visitCardUrl) { if (thing.visitCardUrl) {

View File

@ -6,6 +6,7 @@ import { Zone } from "../src/Model/Zone";
import { Movable } from "../src/Model/Movable"; import { Movable } from "../src/Model/Movable";
import { PositionInterface } from "../src/Model/PositionInterface"; import { PositionInterface } from "../src/Model/PositionInterface";
import { ZoneSocket } from "../src/RoomManager"; import { ZoneSocket } from "../src/RoomManager";
import { AvailabilityStatus } from "../src/Messages/generated/messages_pb";
describe("PositionNotifier", () => { describe("PositionNotifier", () => {
it("should receive notifications when player moves", () => { it("should receive notifications when player moves", () => {
@ -40,9 +41,8 @@ describe("PositionNotifier", () => {
moving: false, moving: false,
direction: "down", direction: "down",
}, },
false,
positionNotifier, positionNotifier,
false, AvailabilityStatus.ONLINE,
{} as UserSocket, {} as UserSocket,
[], [],
null, null,
@ -60,9 +60,8 @@ describe("PositionNotifier", () => {
moving: false, moving: false,
direction: "down", direction: "down",
}, },
false,
positionNotifier, positionNotifier,
false, AvailabilityStatus.ONLINE,
{} as UserSocket, {} as UserSocket,
[], [],
null, null,
@ -150,9 +149,8 @@ describe("PositionNotifier", () => {
moving: false, moving: false,
direction: "down", direction: "down",
}, },
false,
positionNotifier, positionNotifier,
false, AvailabilityStatus.ONLINE,
{} as UserSocket, {} as UserSocket,
[], [],
null, null,
@ -170,9 +168,8 @@ describe("PositionNotifier", () => {
moving: false, moving: false,
direction: "down", direction: "down",
}, },
false,
positionNotifier, positionNotifier,
false, AvailabilityStatus.ONLINE,
{} as UserSocket, {} as UserSocket,
[], [],
null, null,

View File

@ -30,7 +30,7 @@ ADMIN_API_URL=
DATA_DIR=./wa DATA_DIR=./wa
# The URL used by default, in the form: "/_/global/map/url.json" # The URL used by default, in the form: "/_/global/map/url.json"
START_ROOM_URL=/_/global/maps.workadventu.re/Floor0/floor0.json START_ROOM_URL=/_/global/thecodingmachine.github.io/workadventure-map-starter-kit/map.json
# If you want to have a contact page in your menu, # If you want to have a contact page in your menu,
# you MUST set CONTACT_URL to the URL of the page that you want # you MUST set CONTACT_URL to the URL of the page that you want

34
contrib/docker/README.md Normal file
View File

@ -0,0 +1,34 @@
# Deploying WorkAdventure in production
This directory contains a sample production deployment of WorkAdventure using docker-compose.
Every production environment is different and this docker-compose file will not
fit all use cases. But it is intended to be a good starting point for you
to build your own deployment.
In this docker-compose file, you will find:
- A reverse-proxy (Traefik) that dispatches requests to the WorkAdventure containers and handles HTTPS certificates using LetsEncrypt
- A front container (nginx) that servers static files (HTML/JS/CSS)
- A pusher container (NodeJS) that is the point of entry for users (you can start many if you want to increase performance)
- A back container (NodeJS) that shares your rooms information
- An icon container to fetch the favicon of sites imported in iframes
- A Redis server to store values from variables originating from the Scripting API
```mermaid
graph LR
A[Browser] --> B(Traefik)
subgraph docker-compose
B --> C(Front)
B --> D(Pusher)
B --> E(Icon)
D --> F(Back)
F --> G(Redis)
end
A .-> H(Map)
F .-> H
```
**Important**: the default docker-compose file does **not** contain a container dedicated to hosting maps. The documentation and
tutorials are relying on GitHub Pages to host the maps. If you want to self-host your maps, you will need to add a simple
HTTP server (nginx / Apache, ...) and properly configure the [CORS settings as explained in the documentation](../../docs/maps/hosting.md).

View File

@ -95,6 +95,7 @@ services:
- JITSI_ISS - JITSI_ISS
- MAX_PER_GROUP - MAX_PER_GROUP
- STORE_VARIABLES_FOR_LOCAL_MAPS - STORE_VARIABLES_FOR_LOCAL_MAPS
- REDIS_HOST=redis
labels: labels:
- "traefik.http.routers.back.rule=Host(`${BACK_HOST}`)" - "traefik.http.routers.back.rule=Host(`${BACK_HOST}`)"
- "traefik.http.routers.back.entryPoints=web" - "traefik.http.routers.back.entryPoints=web"
@ -117,3 +118,11 @@ services:
- "traefik.http.routers.icon-ssl.service=icon" - "traefik.http.routers.icon-ssl.service=icon"
- "traefik.http.routers.icon-ssl.tls=true" - "traefik.http.routers.icon-ssl.tls=true"
- "traefik.http.routers.icon-ssl.tls.certresolver=myresolver" - "traefik.http.routers.icon-ssl.tls.certresolver=myresolver"
redis:
image: redis:6
volumes:
- redisdata:/data
volumes:
redisdata:

View File

@ -28,5 +28,4 @@ publish:
provider: github provider: github
owner: thecodingmachine owner: thecodingmachine
repo: workadventure repo: workadventure
vPrefixedTagName: false releaseType: release
releaseType: draft

View File

@ -0,0 +1,18 @@
const path = require('path');
const fs = require('fs');
let version = '0.0.0';
if (process.env.GITHUB_REF.startsWith('refs/tags/v')) {
version = process.env.GITHUB_REF.replace('refs/tags/v', '');
}
console.log('Version:', version);
const packageJsonPath = path.resolve(__dirname, '..', 'package.json');
let data = fs.readFileSync(packageJsonPath, 'utf8');
data = data.replace('managedbyci', version);
fs.writeFileSync(packageJsonPath, data);

View File

@ -1,6 +1,6 @@
{ {
"name": "workadventure-desktop", "name": "workadventure-desktop",
"version": "1.0.0", "version": "managedbyci",
"description": "Desktop application for WorkAdventure", "description": "Desktop application for WorkAdventure",
"author": "thecodingmachine", "author": "thecodingmachine",
"main": "dist/main.js", "main": "dist/main.js",
@ -11,7 +11,6 @@
"dev": "yarn build --watch --onSuccess 'yarn electron dist/main.js'", "dev": "yarn build --watch --onSuccess 'yarn electron dist/main.js'",
"dev:local-app": "cd ../local-app && yarn && yarn dev", "dev:local-app": "cd ../local-app && yarn && yarn dev",
"bundle": "yarn build:local-app && yarn build && electron-builder install-app-deps && electron-builder", "bundle": "yarn build:local-app && yarn build && electron-builder install-app-deps && electron-builder",
"release": "yarn bundle",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"test": "exit 0", "test": "exit 0",
"lint": "yarn eslint src/ . --ext .ts", "lint": "yarn eslint src/ . --ext .ts",
@ -32,13 +31,13 @@
}, },
"devDependencies": { "devDependencies": {
"@types/auto-launch": "^5.0.2", "@types/auto-launch": "^5.0.2",
"@typescript-eslint/eslint-plugin": "^2.26.0", "@typescript-eslint/eslint-plugin": "^5.18.0",
"@typescript-eslint/parser": "^2.26.0", "@typescript-eslint/parser": "^5.18.0",
"electron": "^17.0.1", "electron": "^18.0.3",
"electron-builder": "^22.14.13", "electron-builder": "^22.14.13",
"eslint": "^6.8.0", "eslint": "^8.12.0",
"prettier": "^2.5.1", "prettier": "^2.6.2",
"tsup": "^5.11.13", "tsup": "^5.12.4",
"typescript": "^3.8.3" "typescript": "^4.6.3"
} }
} }

View File

@ -10,7 +10,7 @@ import { setLogLevel } from "./log";
import "./serve"; // prepare custom url scheme import "./serve"; // prepare custom url scheme
import { loadShortcuts } from "./shortcuts"; import { loadShortcuts } from "./shortcuts";
function init() { async function init() {
const appLock = app.requestSingleInstanceLock(); const appLock = app.requestSingleInstanceLock();
if (!appLock) { if (!appLock) {
@ -21,7 +21,7 @@ function init() {
app.on("second-instance", () => { app.on("second-instance", () => {
// re-create window if closed // re-create window if closed
createWindow(); void createWindow();
const mainWindow = getWindow(); const mainWindow = getWindow();
@ -36,15 +36,15 @@ function init() {
}); });
// This method will be called when Electron has finished loading // This method will be called when Electron has finished loading
app.whenReady().then(async () => { await app.whenReady().then(async () => {
await settings.init(); await settings.init();
setLogLevel(settings.get("log_level") || "info"); setLogLevel(settings.get("log_level") || "info");
autoUpdater.init(); await autoUpdater.init();
// enable auto launch // enable auto launch
updateAutoLaunch(); await updateAutoLaunch();
// load ipc handler // load ipc handler
ipc(); ipc();
@ -72,7 +72,7 @@ function init() {
// On macOS it's common to re-create a window in the app when the // On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open. // dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) { if (BrowserWindow.getAllWindows().length === 0) {
createWindow(); void createWindow();
} }
}); });

View File

@ -38,32 +38,35 @@ export async function manualRequestUpdateCheck() {
isManualRequestedUpdate = false; isManualRequestedUpdate = false;
} }
function init() { async function init() {
autoUpdater.logger = log; autoUpdater.logger = log;
autoUpdater.on("update-downloaded", ({ releaseNotes, releaseName }) => { autoUpdater.on(
(async () => { "update-downloaded",
const dialogOpts = { ({ releaseNotes, releaseName }: { releaseNotes: string; releaseName: string }) => {
type: "question", void (async () => {
buttons: ["Install and Restart", "Install Later"], const dialogOpts = {
defaultId: 0, type: "question",
title: "WorkAdventure - Update", buttons: ["Install and Restart", "Install Later"],
message: process.platform === "win32" ? releaseNotes : releaseName, defaultId: 0,
detail: "A new version has been downloaded. Restart the application to apply the updates.", title: "WorkAdventure - Update",
}; message: process.platform === "win32" ? releaseNotes : releaseName,
detail: "A new version has been downloaded. Restart the application to apply the updates.",
};
const { response } = await dialog.showMessageBox(dialogOpts); const { response } = await dialog.showMessageBox(dialogOpts);
if (response === 0) { if (response === 0) {
await sleep(1000); await sleep(1000);
autoUpdater.quitAndInstall(); autoUpdater.quitAndInstall();
// Force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app. // Force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
// app.confirmedExitPrompt = true; // app.confirmedExitPrompt = true;
app.quit(); app.quit();
} }
})(); })();
}); }
);
if (process.platform === "linux" && !process.env.APPIMAGE) { if (process.platform === "linux" && !process.env.APPIMAGE) {
autoUpdater.autoDownload = false; autoUpdater.autoDownload = false;
@ -85,7 +88,7 @@ function init() {
} }
}); });
checkForUpdates(); await checkForUpdates();
// run update check every hour again // run update check every hour again
setInterval(() => checkForUpdates, 1000 * 60 * 1); setInterval(() => checkForUpdates, 1000 * 60 * 1);

View File

@ -1,4 +1,4 @@
import { ipcMain, app } from "electron"; import { ipcMain, app, desktopCapturer } from "electron";
import electronIsDev from "electron-is-dev"; import electronIsDev from "electron-is-dev";
import { createAndShowNotification } from "./notification"; import { createAndShowNotification } from "./notification";
import { Server } from "./preload-local-app/types"; import { Server } from "./preload-local-app/types";
@ -30,10 +30,18 @@ export default () => {
ipcMain.handle("get-version", () => (electronIsDev ? "dev" : app.getVersion())); ipcMain.handle("get-version", () => (electronIsDev ? "dev" : app.getVersion()));
// app ipc // app ipc
ipcMain.on("app:notify", (event, txt) => { ipcMain.on("app:notify", (event, txt: string) => {
createAndShowNotification({ body: txt }); createAndShowNotification({ body: txt });
}); });
ipcMain.handle("app:getDesktopCapturerSources", async (event, options: Electron.SourcesOptions) => {
return (await desktopCapturer.getSources(options)).map((source) => ({
id: source.id,
name: source.name,
thumbnailURL: source.thumbnail.toDataURL(),
}));
});
// local-app ipc // local-app ipc
ipcMain.handle("local-app:showLocalApp", () => { ipcMain.handle("local-app:showLocalApp", () => {
hideAppView(); hideAppView();
@ -43,7 +51,7 @@ export default () => {
return settings.get("servers"); return settings.get("servers");
}); });
ipcMain.handle("local-app:selectServer", (event, serverId: string) => { ipcMain.handle("local-app:selectServer", async (event, serverId: string) => {
const servers = settings.get("servers") || []; const servers = settings.get("servers") || [];
const selectedServer = servers.find((s) => s._id === serverId); const selectedServer = servers.find((s) => s._id === serverId);
@ -51,7 +59,7 @@ export default () => {
return new Error("Server not found"); return new Error("Server not found");
} }
showAppView(selectedServer.url); await showAppView(selectedServer.url);
return true; return true;
}); });

View File

@ -15,6 +15,7 @@ function onError(e: Error) {
function onRejection(reason: Error) { function onRejection(reason: Error) {
if (reason instanceof Error) { if (reason instanceof Error) {
let _reason = reason; let _reason = reason;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const errPrototype = Object.getPrototypeOf(reason); const errPrototype = Object.getPrototypeOf(reason);
const nameProperty = Object.getOwnPropertyDescriptor(errPrototype, "name"); const nameProperty = Object.getOwnPropertyDescriptor(errPrototype, "name");

View File

@ -2,4 +2,4 @@ import app from "./app";
import log from "./log"; import log from "./log";
log.init(); log.init();
app.init(); void app.init();

View File

@ -8,6 +8,7 @@ const api: WorkAdventureDesktopApi = {
notify: (txt) => ipcRenderer.send("app:notify", txt), notify: (txt) => ipcRenderer.send("app:notify", txt),
onMuteToggle: (callback) => ipcRenderer.on("app:on-mute-toggle", callback), onMuteToggle: (callback) => ipcRenderer.on("app:on-mute-toggle", callback),
onCameraToggle: (callback) => ipcRenderer.on("app:on-camera-toggle", callback), onCameraToggle: (callback) => ipcRenderer.on("app:on-camera-toggle", callback),
getDesktopCapturerSources: (options) => ipcRenderer.invoke("app:getDesktopCapturerSources", options),
}; };
contextBridge.exposeInMainWorld("WAD", api); contextBridge.exposeInMainWorld("WAD", api);

View File

@ -1,3 +1,15 @@
// copy of Electron.SourcesOptions to avoid Electron dependency in front
export interface SourcesOptions {
types: string[];
thumbnailSize?: { height: number; width: number };
}
export interface DesktopCapturerSource {
id: string;
name: string;
thumbnailURL: string;
}
export type WorkAdventureDesktopApi = { export type WorkAdventureDesktopApi = {
desktop: boolean; desktop: boolean;
isDevelopment: () => Promise<boolean>; isDevelopment: () => Promise<boolean>;
@ -5,4 +17,5 @@ export type WorkAdventureDesktopApi = {
notify: (txt: string) => void; notify: (txt: string) => void;
onMuteToggle: (callback: () => void) => void; onMuteToggle: (callback: () => void) => void;
onCameraToggle: (callback: () => void) => void; onCameraToggle: (callback: () => void) => void;
getDesktopCapturerSources: (options: SourcesOptions) => Promise<DesktopCapturerSource[]>;
}; };

View File

@ -36,14 +36,14 @@ export function createTray() {
}, },
{ {
label: "Check for updates", label: "Check for updates",
async click() { click() {
await autoUpdater.manualRequestUpdateCheck(); void autoUpdater.manualRequestUpdateCheck();
}, },
}, },
{ {
label: "Open Logs", label: "Open Logs",
click() { click() {
log.openLog(); void log.openLog();
}, },
}, },
{ {

View File

@ -115,7 +115,7 @@ export async function createWindow() {
} }
} }
export function showAppView(url?: string) { export async function showAppView(url?: string) {
if (!appView) { if (!appView) {
throw new Error("App view not found"); throw new Error("App view not found");
} }
@ -130,7 +130,7 @@ export function showAppView(url?: string) {
mainWindow.addBrowserView(appView); mainWindow.addBrowserView(appView);
if (url && url !== appViewUrl) { if (url && url !== appViewUrl) {
appView.webContents.loadURL(url); await appView.webContents.loadURL(url);
appViewUrl = url; appViewUrl = url;
} }

File diff suppressed because it is too large Load Diff

View File

@ -166,7 +166,8 @@ return [
], ],
[ [
'title' => 'Troubleshooting', 'title' => 'Troubleshooting',
'url' => '/map-building/troubleshooting', 'url' => '/map-building/troubleshooting.md',
'view' => 'content.map.troubleshooting' 'markdown' => 'maps.troubleshooting',
'editUrl' => 'https://github.com/thecodingmachine/workadventure/edit/develop/docs/maps/troubleshooting.md',
], ],
]; ];

View File

@ -18,11 +18,18 @@ In order to create a zone that opens websites:
{.alert.alert-warning} {.alert.alert-warning}
A website can explicitly forbid another website from loading it in an iFrame using A website can explicitly forbid another website from loading it in an iFrame using
the [X-Frame-Options HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options). the [X-Frame-Options HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options). You can
read more about this common issue and possible workaround the [troubleshooting guide](troubleshooting.md#embedding-an-iframe-is-forbidden).
{.alert.alert-info} {.alert.alert-info}
As an alternative, you may also put the `openWebsite` properties on a layer (rather than putting them on an "area" object) As an alternative, you may also put the `openWebsite` properties on a layer (rather than putting them on an "area" object)
but we advise to stick with "area" objects for better performance! but we advise sticking with "area" objects for better performance!
{.alert.alert-warning}
If the website you are embedding is using cookies, those cookies must be configured with the `SameSite=none` attribute. Otherwise,
they will be ignored by the browser. If you manage to see the website you embed but cannot log into it, the `SameSite` attribute is most
likely the culprit. You can read more about this common issue and possible workaround the [troubleshooting guide](troubleshooting.md#i-cannot-log-into-my-embedded-website).
## Integrating a Youtube video ## Integrating a Youtube video

View File

@ -0,0 +1,94 @@
{.section-title.accent.text-primary}
# Troubleshooting
## Look at the browser console
If your map is not displayed correctly (most notably if you are getting a black screen), open your browser console.
This is usually done by pressing the F12 key and selecting the "console" tab.
Scan the output. Towards the end, you might see a message explaining why your map cannot be loaded.
## Check webserver CORS settings
If you are hosting the map you built on your own webserver and if the map does not load, please check that
[your webserver CORS settings are correctly configured](hosting.md).
## Issues embedding a website
When you are embedding a website in WorkAdventure (whether it is using the [`openWebsite` property](opening-a-website.md) or
the [integrated website in a map](website-in-map.md) feature or the [Scripting API](scripting.md)), WorkAdventure
will open your website using an iFrame.
Browsers have various security measures in place, and website owners can use those measures to prevent websites from
being used inside iFrames (either partially or completely).
In the chapters below, we will list what can possibly prevent you from embedding a website, and see what are your options.
### Embedding an iFrame is forbidden
The worst that can happen is that the website you are trying to embed completely denies you the authorisation.
A website owner can do that using the [`X-Frame-Options` HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options),
or the newer [`Content-Security-Policy` HTTP header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy).
Take a look at the headers of the page you are trying to load.
{.alert.alert-info}
You can view the headers of the web page you try to load in the developer tools of your browser (usually accessible using the F12 key
of your keyboard), in the network tab. Click on the top-most request and check the "Response Headers".
Below is what you can see when opening a Youtube video page:
![](images/x-frame-options.png)
`X-Frame-Options: DENY` or `X-Frame-Options: SAMEORIGIN` will prevent WorkAdventure from loading the page.
`Content-Security-Policy` header have also the potential to prevent WorkAdventure from loading the page.
If the website you are trying to embed has one of these headers set, here are your options:
- if you have control over the website or know the owner, you can contact the owner/administrator of the website and ask for an exception
- otherwise, you can look for an "embed" option. Some websites have special pages that can be embedded. For instance,
YouTube has special "embed" links that can be used to embed a video in your website. A lot of websites have the same feature (you
can usually find those links in the "share" section)
If none of these options are available to you, as a last resort, you can use the [`openTab` property](opening-a-website.md) instead of the `openWebsite` property.
It will open your webpage in another tab instead of opening it in an iFrame.
### I cannot log into my embedded website
When you log into a website, the website is issuing a "cookie". The cookie is a unique identifier that allows the website
to recognize you and to identify you. To improve the privacy of their users, browsers can sometimes treat cookies
inside iFrames as "third-party cookies" and discard them.
Cookies can come with a `SameSite` attribute.
The `SameSite` attribute can take these values: "Lax", "Strict" or "None". The only value that allows using the
cookie inside an iFrame is "None".
{.alert.alert-info}
The `SameSite` attribute of your cookie MUST be set to "None" if you want to be able to use this cookie from an iFrame inside WorkAdventure.
**Default values**:
If the "SameSite" attribute is not explicitly set, [the behaviour depends on the browser](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#browser_compatibility).
Chrome, Edge and Opera will default to "Lax".
Firefox and Safari will default to "None" (as of 2022/04/25).
As a result, a website that does not set the `SameSite` attribute on cookies will work correctly in Firefox and Safari but
login will fail on Chrome, Edge and Opera.
If the website you are trying to embed has the `SameSite` attribute set to a value other than "None", here are your options:
- if you have control over the website or know the owner, you can contact the owner/administrator of the website and ask
the owner/administrator to change the `SameSite` settings.
- otherwise, you will have to use the [`openTab` property](opening-a-website.md) instead of the `openWebsite` property.
It will open your webpage in another tab instead of in an iFrame.
## Need some help?
<div class="card bg-red text-white"><div class="card-body">
<p>WorkAdventure is a constantly evolving project and there is plenty of room for improvement regarding map editing.</p>
<p>If you are facing any troubles, do not hesitate to seek help in
<a href="https://discord.gg/G6Xh9ZM9aR">our Discord server</a> or open an "issue" in the
<a href="https://github.com/thecodingmachine/workadventure/issues" target="_blank">GitHub WorkAdventure account</a>.
</p>
</div></div>

View File

@ -36,7 +36,7 @@ module.exports = {
"eol-last": ["error", "always"], "eol-last": ["error", "always"],
"@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-explicit-any": "error",
"no-throw-literal": "error", "no-throw-literal": "error",
"@typescript-eslint/no-unused-vars": ["error"], "@typescript-eslint/no-unused-vars": ["error", { "args": "none", "caughtErrors": "all", "varsIgnorePattern": "_exhaustiveCheck" }],
// TODO: remove those ignored rules and write a stronger code! // TODO: remove those ignored rules and write a stronger code!
"@typescript-eslint/no-unsafe-call": "off", "@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/restrict-plus-operands": "off", "@typescript-eslint/restrict-plus-operands": "off",

View File

@ -1,5 +1,3 @@
src/Messages/generated src/Messages/generated
src/Messages/JsonMessages src/Messages/JsonMessages
src/i18n/i18n-svelte.ts src/i18n/i18n-*.ts
src/i18n/i18n-types.ts
src/i18n/i18n-util.ts

View File

@ -1,5 +1,5 @@
{ {
"$schema": "https://unpkg.com/typesafe-i18n@2.59.0/schema/typesafe-i18n.json", "$schema": "https://unpkg.com/typesafe-i18n@5.4.0/schema/typesafe-i18n.json",
"baseLocale": "en-US", "baseLocale": "en-US",
"adapter": "svelte" "adapter": "svelte"
} }

View File

@ -5,7 +5,7 @@
"license": "SEE LICENSE IN LICENSE.txt", "license": "SEE LICENSE IN LICENSE.txt",
"devDependencies": { "devDependencies": {
"@geprog/vite-plugin-env-config": "^4.0.3", "@geprog/vite-plugin-env-config": "^4.0.3",
"@home-based-studio/phaser3-utils": "^0.4.2", "@home-based-studio/phaser3-utils": "^0.4.7",
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.36", "@sveltejs/vite-plugin-svelte": "^1.0.0-next.36",
"@tsconfig/svelte": "^1.0.10", "@tsconfig/svelte": "^1.0.10",
"@types/google-protobuf": "^3.7.3", "@types/google-protobuf": "^3.7.3",
@ -42,7 +42,7 @@
"buffer": "^6.0.3", "buffer": "^6.0.3",
"cancelable-promise": "^4.2.1", "cancelable-promise": "^4.2.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"deep-copy-ts": "^0.5.0", "deep-copy-ts": "^0.5.4",
"easystarjs": "^0.4.4", "easystarjs": "^0.4.4",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"google-protobuf": "^3.13.0", "google-protobuf": "^3.13.0",
@ -60,7 +60,7 @@
"standardized-audio-context": "^25.2.4", "standardized-audio-context": "^25.2.4",
"ts-deferred": "^1.0.4", "ts-deferred": "^1.0.4",
"ts-proto": "^1.96.0", "ts-proto": "^1.96.0",
"typesafe-i18n": "^2.59.0", "typesafe-i18n": "^5.4.0",
"uuidv4": "^6.2.10", "uuidv4": "^6.2.10",
"zod": "^3.14.3" "zod": "^3.14.3"
}, },

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -239,7 +239,7 @@ class IframeListener {
} else if (iframeEvent.type === "cameraFollowPlayer") { } else if (iframeEvent.type === "cameraFollowPlayer") {
this._cameraFollowPlayerStream.next(iframeEvent.data); this._cameraFollowPlayerStream.next(iframeEvent.data);
} else if (iframeEvent.type === "chat") { } else if (iframeEvent.type === "chat") {
scriptUtils.sendAnonymousChat(iframeEvent.data); scriptUtils.sendAnonymousChat(iframeEvent.data, iframe.contentWindow ?? undefined);
} else if (iframeEvent.type === "openPopup") { } else if (iframeEvent.type === "openPopup") {
this._openPopupStream.next(iframeEvent.data); this._openPopupStream.next(iframeEvent.data);
} else if (iframeEvent.type === "closePopup") { } else if (iframeEvent.type === "closePopup") {
@ -294,7 +294,6 @@ class IframeListener {
handleMenuUnregisterEvent(iframeEvent.data.name); handleMenuUnregisterEvent(iframeEvent.data.name);
} else { } else {
// Keep the line below. It will throw an error if we forget to handle one of the possible values. // Keep the line below. It will throw an error if we forget to handle one of the possible values.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _exhaustiveCheck: never = iframeEvent; const _exhaustiveCheck: never = iframeEvent;
} }
} }
@ -400,13 +399,20 @@ class IframeListener {
this.scripts.delete(scriptUrl); this.scripts.delete(scriptUrl);
} }
sendUserInputChat(message: string) { /**
this.postMessage({ * @param message The message to dispatch
type: "userInputChat", * @param exceptOrigin Don't dispatch the message to exceptOrigin (to avoid infinite loops)
data: { */
message: message, sendUserInputChat(message: string, exceptOrigin?: Window) {
} as UserInputChatEvent, this.postMessage(
}); {
type: "userInputChat",
data: {
message: message,
} as UserInputChatEvent,
},
exceptOrigin
);
} }
sendEnterEvent(name: string) { sendEnterEvent(name: string) {
@ -522,8 +528,11 @@ class IframeListener {
/** /**
* Sends the message... to all allowed iframes. * Sends the message... to all allowed iframes.
*/ */
public postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) { public postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>, exceptOrigin?: Window) {
for (const iframe of this.iframes) { for (const iframe of this.iframes) {
if (exceptOrigin === iframe.contentWindow) {
continue;
}
iframe.contentWindow?.postMessage(message, "*"); iframe.contentWindow?.postMessage(message, "*");
} }
} }

View File

@ -11,9 +11,9 @@ class ScriptUtils {
window.location.href = url; window.location.href = url;
} }
public sendAnonymousChat(chatEvent: ChatEvent) { public sendAnonymousChat(chatEvent: ChatEvent, origin?: Window) {
const userId = playersStore.addFacticePlayer(chatEvent.author); const userId = playersStore.addFacticePlayer(chatEvent.author);
chatMessagesStore.addExternalMessage(userId, chatEvent.message); chatMessagesStore.addExternalMessage(userId, chatEvent.message, origin);
} }
} }

View File

@ -1,10 +1,10 @@
import { isSilentStore, requestedCameraState, requestedMicrophoneState } from "../../Stores/MediaStore"; import { requestedCameraState, requestedMicrophoneState, silentStore } from "../../Stores/MediaStore";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { WorkAdventureDesktopApi } from "@wa-preload-app"; import { WorkAdventureDesktopApi } from "@wa-preload-app";
declare global { declare global {
interface Window { interface Window {
WAD: WorkAdventureDesktopApi; WAD?: WorkAdventureDesktopApi;
} }
} }
@ -36,8 +36,8 @@ class DesktopApi {
} }
}); });
isSilentStore.subscribe((value) => { silentStore.subscribe((silent) => {
this.isSilent = value; this.isSilent = silent;
}); });
} }
} }

View File

@ -88,7 +88,6 @@ export function createState(target: "global" | "player"): WorkadventureStateComm
} }
return target.loadVariable(p.toString()); return target.loadVariable(p.toString());
}, },
// eslint-disable-next-line @typescript-eslint/no-unused-vars
set(target: WorkadventureStateCommands, p: PropertyKey, value: unknown, receiver: unknown): boolean { set(target: WorkadventureStateCommands, p: PropertyKey, value: unknown, receiver: unknown): boolean {
// Note: when using "set", there is no way to wait, so we ignore the return of the promise. // Note: when using "set", there is no way to wait, so we ignore the return of the promise.
// User must use WA.state.saveVariable to have error message. // User must use WA.state.saveVariable to have error message.

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { requestedScreenSharingState, screenSharingAvailableStore } from "../Stores/ScreenSharingStore"; import { requestedScreenSharingState, screenSharingAvailableStore } from "../Stores/ScreenSharingStore";
import { isSilentStore, requestedCameraState, requestedMicrophoneState } from "../Stores/MediaStore"; import { requestedCameraState, requestedMicrophoneState, silentStore } from "../Stores/MediaStore";
import monitorImg from "./images/monitor.svg"; import monitorImg from "./images/monitor.svg";
import monitorCloseImg from "./images/monitor-close.svg"; import monitorCloseImg from "./images/monitor-close.svg";
import cinemaImg from "./images/cinema.svg"; import cinemaImg from "./images/cinema.svg";
@ -13,7 +13,6 @@
import lockImg from "./images/lock.svg"; import lockImg from "./images/lock.svg";
import { LayoutMode } from "../WebRtc/LayoutManager"; import { LayoutMode } from "../WebRtc/LayoutManager";
import { peerStore } from "../Stores/PeerStore"; import { peerStore } from "../Stores/PeerStore";
import { onDestroy } from "svelte";
import { embedScreenLayout } from "../Stores/EmbedScreensStore"; import { embedScreenLayout } from "../Stores/EmbedScreensStore";
import { followRoleStore, followStateStore, followUsersStore } from "../Stores/FollowStore"; import { followRoleStore, followStateStore, followUsersStore } from "../Stores/FollowStore";
import { gameManager } from "../Phaser/Game/GameManager"; import { gameManager } from "../Phaser/Game/GameManager";
@ -22,7 +21,7 @@
const gameScene = gameManager.getCurrentGameScene(); const gameScene = gameManager.getCurrentGameScene();
function screenSharingClick(): void { function screenSharingClick(): void {
if (isSilent) return; if ($silentStore) return;
if ($requestedScreenSharingState === true) { if ($requestedScreenSharingState === true) {
requestedScreenSharingState.disableScreenSharing(); requestedScreenSharingState.disableScreenSharing();
} else { } else {
@ -31,7 +30,7 @@
} }
function cameraClick(): void { function cameraClick(): void {
if (isSilent) return; if ($silentStore) return;
if ($requestedCameraState === true) { if ($requestedCameraState === true) {
requestedCameraState.disableWebcam(); requestedCameraState.disableWebcam();
} else { } else {
@ -40,7 +39,7 @@
} }
function microphoneClick(): void { function microphoneClick(): void {
if (isSilent) return; if ($silentStore) return;
if ($requestedMicrophoneState === true) { if ($requestedMicrophoneState === true) {
requestedMicrophoneState.disableMicrophone(); requestedMicrophoneState.disableMicrophone();
} else { } else {
@ -75,12 +74,6 @@
function lockClick() { function lockClick() {
gameScene.connection?.emitLockGroup(!$currentPlayerGroupLockStateStore); gameScene.connection?.emitLockGroup(!$currentPlayerGroupLockStateStore);
} }
let isSilent: boolean;
const unsubscribeIsSilent = isSilentStore.subscribe((value) => {
isSilent = value;
});
onDestroy(unsubscribeIsSilent);
</script> </script>
<div class="btn-cam-action"> <div class="btn-cam-action">
@ -94,7 +87,7 @@
<div <div
class="btn-follow" class="btn-follow"
class:hide={($peerStore.size === 0 && $followStateStore === "off") || isSilent} class:hide={($peerStore.size === 0 && $followStateStore === "off") || $silentStore}
class:disabled={$followStateStore !== "off"} class:disabled={$followStateStore !== "off"}
on:click={followClick} on:click={followClick}
> >
@ -103,7 +96,7 @@
<div <div
class="btn-lock" class="btn-lock"
class:hide={$peerStore.size === 0 || isSilent} class:hide={$peerStore.size === 0 || $silentStore}
class:disabled={$currentPlayerGroupLockStateStore} class:disabled={$currentPlayerGroupLockStateStore}
on:click={lockClick} on:click={lockClick}
> >
@ -113,26 +106,26 @@
<div <div
class="btn-monitor" class="btn-monitor"
on:click={screenSharingClick} on:click={screenSharingClick}
class:hide={!$screenSharingAvailableStore || isSilent} class:hide={!$screenSharingAvailableStore || $silentStore}
class:enabled={$requestedScreenSharingState} class:enabled={$requestedScreenSharingState}
> >
{#if $requestedScreenSharingState && !isSilent} {#if $requestedScreenSharingState && !$silentStore}
<img class="noselect" src={monitorImg} alt="Start screen sharing" /> <img class="noselect" src={monitorImg} alt="Start screen sharing" />
{:else} {:else}
<img class="noselect" src={monitorCloseImg} alt="Stop screen sharing" /> <img class="noselect" src={monitorCloseImg} alt="Stop screen sharing" />
{/if} {/if}
</div> </div>
<div class="btn-video" on:click={cameraClick} class:disabled={!$requestedCameraState || isSilent}> <div class="btn-video" on:click={cameraClick} class:disabled={!$requestedCameraState || $silentStore}>
{#if $requestedCameraState && !isSilent} {#if $requestedCameraState && !$silentStore}
<img class="noselect" src={cinemaImg} alt="Turn on webcam" /> <img class="noselect" src={cinemaImg} alt="Turn on webcam" />
{:else} {:else}
<img class="noselect" src={cinemaCloseImg} alt="Turn off webcam" /> <img class="noselect" src={cinemaCloseImg} alt="Turn off webcam" />
{/if} {/if}
</div> </div>
<div class="btn-micro" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState || isSilent}> <div class="btn-micro" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState || $silentStore}>
{#if $requestedMicrophoneState && !isSilent} {#if $requestedMicrophoneState && !$silentStore}
<img class="noselect" src={microphoneImg} alt="Turn on microphone" /> <img class="noselect" src={microphoneImg} alt="Turn on microphone" />
{:else} {:else}
<img class="noselect" src={microphoneCloseImg} alt="Turn off microphone" /> <img class="noselect" src={microphoneCloseImg} alt="Turn off microphone" />

View File

@ -19,6 +19,5 @@
padding-top: 2%; padding-top: 2%;
height: 100%; height: 100%;
position: relative; position: relative;
z-index: 200;
} }
</style> </style>

View File

@ -67,10 +67,11 @@
{/key} {/key}
{:else if $highlightedEmbedScreen.type === "cowebsite"} {:else if $highlightedEmbedScreen.type === "cowebsite"}
{#key $highlightedEmbedScreen.embed.getId()} {#key $highlightedEmbedScreen.embed.getId()}
<div <div class="highlighted-cowebsite-container nes-container is-rounded">
id={"cowebsite-slot-" + $highlightedEmbedScreen.embed.getId()} <div
class="highlighted-cowebsite nes-container is-rounded" id={"cowebsite-slot-" + $highlightedEmbedScreen.embed.getId()}
> class="highlighted-cowebsite"
/>
<div class="actions"> <div class="actions">
<button type="button" class="nes-btn is-error close" on:click={closeCoWebsite} <button type="button" class="nes-btn is-error close" on:click={closeCoWebsite}
>&times;</button >&times;</button
@ -120,20 +121,29 @@
.highlighted-cowebsite { .highlighted-cowebsite {
height: 100% !important; height: 100% !important;
width: 96%; width: 100% !important;
background-color: rgba(#000000, 0.6); position: relative;
margin: 0 !important; z-index: 200;
.actions { &-container {
z-index: 200; height: 100% !important;
position: relative; width: 96%;
display: flex; background-color: rgba(#000000, 0.6);
flex-direction: row; margin: 0 !important;
justify-content: end; padding: 0 !important;
gap: 2%; .actions {
z-index: 202;
position: absolute;
width: 100%;
top: 0;
display: flex;
flex-direction: row;
justify-content: end;
gap: 2%;
button { button {
pointer-events: all; pointer-events: all;
}
} }
} }
} }

View File

@ -53,11 +53,14 @@
background: #eceeee; background: #eceeee;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
position: absolute;
left: 0; left: 0;
right: 0; right: 0;
margin-top: 4%; margin-top: 4%;
max-height: 80vh; max-height: 80vh;
max-width: 80vw; max-width: 80vw;
margin-left: 10%;
margin-right: 10%;
z-index: 600; z-index: 600;
overflow: auto; overflow: auto;
text-align: center; text-align: center;

View File

@ -38,6 +38,7 @@
import { actionsMenuStore } from "../Stores/ActionsMenuStore"; import { actionsMenuStore } from "../Stores/ActionsMenuStore";
import ActionsMenu from "./ActionsMenu/ActionsMenu.svelte"; import ActionsMenu from "./ActionsMenu/ActionsMenu.svelte";
import Lazy from "./Lazy.svelte"; import Lazy from "./Lazy.svelte";
import { showDesktopCapturerSourcePicker } from "../Stores/ScreenSharingStore";
let mainLayout: HTMLDivElement; let mainLayout: HTMLDivElement;
@ -67,6 +68,11 @@
</aside> </aside>
<section id="main-layout-main"> <section id="main-layout-main">
<Lazy
when={$showDesktopCapturerSourcePicker}
component={() => import("./Video/DesktopCapturerSourcePicker.svelte")}
/>
{#if $menuVisiblilityStore} {#if $menuVisiblilityStore}
<Menu /> <Menu />
{/if} {/if}

View File

@ -88,16 +88,11 @@
} }
} }
function translateMenuName(menu: MenuItem) { $: subMenuTranslations = $subMenusStore.map((subMenu) =>
if (menu.type === "scripting") { subMenu.type === "scripting" ? subMenu.label : $LL.menu.sub[subMenu.key]()
return menu.label; );
} $: activeSubMenuTranslation =
activeSubMenu.type === "scripting" ? activeSubMenu.label : $LL.menu.sub[activeSubMenu.key]();
// Bypass the proxy of typesafe for getting the menu name : https://github.com/ivanhofer/typesafe-i18n/issues/156
const getMenuName = $LL.menu.sub[menu.key];
return getMenuName();
}
</script> </script>
<svelte:window on:keydown={onKeyDown} /> <svelte:window on:keydown={onKeyDown} />
@ -106,20 +101,20 @@
<div class="menu-nav-sidebar nes-container is-rounded" transition:fly={{ x: -1000, duration: 500 }}> <div class="menu-nav-sidebar nes-container is-rounded" transition:fly={{ x: -1000, duration: 500 }}>
<h2>{$LL.menu.title()}</h2> <h2>{$LL.menu.title()}</h2>
<nav> <nav>
{#each $subMenusStore as submenu} {#each $subMenusStore as submenu, i}
<button <button
type="button" type="button"
class="nes-btn {activeSubMenu === submenu ? 'is-disabled' : ''}" class="nes-btn {activeSubMenu === submenu ? 'is-disabled' : ''}"
on:click|preventDefault={() => void switchMenu(submenu)} on:click|preventDefault={() => void switchMenu(submenu)}
> >
{translateMenuName(submenu)} {subMenuTranslations[i]}
</button> </button>
{/each} {/each}
</nav> </nav>
</div> </div>
<div class="menu-submenu-container nes-container is-rounded" transition:fly={{ y: -1000, duration: 500 }}> <div class="menu-submenu-container nes-container is-rounded" transition:fly={{ y: -1000, duration: 500 }}>
<button type="button" class="nes-btn is-error close" on:click={closeMenu}>&times</button> <button type="button" class="nes-btn is-error close" on:click={closeMenu}>&times</button>
<h2>{translateMenuName(activeSubMenu)}</h2> <h2>{activeSubMenuTranslation}</h2>
<svelte:component this={activeComponent} {...props} /> <svelte:component this={activeComponent} {...props} />
</div> </div>
</div> </div>

View File

@ -28,12 +28,12 @@
let previewCameraPrivacySettings = valueCameraPrivacySettings; let previewCameraPrivacySettings = valueCameraPrivacySettings;
let previewMicrophonePrivacySettings = valueMicrophonePrivacySettings; let previewMicrophonePrivacySettings = valueMicrophonePrivacySettings;
function saveSetting() { async function saveSetting() {
let change = false; let change = false;
if (valueLocale !== previewValueLocale) { if (valueLocale !== previewValueLocale) {
previewValueLocale = valueLocale; previewValueLocale = valueLocale;
setCurrentLocale(valueLocale as Locales); await setCurrentLocale(valueLocale as Locales);
} }
if (valueVideo !== previewValueVideo) { if (valueVideo !== previewValueVideo) {
@ -174,7 +174,7 @@
<div class="nes-select is-dark"> <div class="nes-select is-dark">
<select class="languages-switcher" bind:value={valueLocale}> <select class="languages-switcher" bind:value={valueLocale}>
{#each displayableLocales as locale (locale.id)} {#each displayableLocales as locale (locale.id)}
<option value={locale.id}>{`${locale.language} (${locale.country})`}</option> <option value={locale.id}>{`${locale.language} (${locale.region})`}</option>
{/each} {/each}
</select> </select>
</div> </div>

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { localVolumeStore, obtainedMediaConstraintStore } from "../Stores/MediaStore"; import { localVolumeStore, obtainedMediaConstraintStore, silentStore } from "../Stores/MediaStore";
import { localStreamStore, isSilentStore } from "../Stores/MediaStore"; import { localStreamStore } from "../Stores/MediaStore";
import SoundMeterWidget from "./SoundMeterWidget.svelte"; import SoundMeterWidget from "./SoundMeterWidget.svelte";
import { onDestroy, onMount } from "svelte"; import { onDestroy, onMount } from "svelte";
import { srcObject } from "./Video/utils"; import { srcObject } from "./Video/utils";
@ -20,11 +20,6 @@
unsubscribeLocalStreamStore(); unsubscribeLocalStreamStore();
}); });
let isSilent: boolean;
const unsubscribeIsSilent = isSilentStore.subscribe((value) => {
isSilent = value;
});
let cameraContainer: HTMLDivElement; let cameraContainer: HTMLDivElement;
onMount(() => { onMount(() => {
@ -40,16 +35,14 @@
} }
}); });
}); });
onDestroy(unsubscribeIsSilent);
</script> </script>
<div <div
class="nes-container is-rounded my-cam-video-container" class="nes-container is-rounded my-cam-video-container"
class:hide={($localStreamStore.type !== "success" || !$obtainedMediaConstraintStore.video) && !isSilent} class:hide={($localStreamStore.type !== "success" || !$obtainedMediaConstraintStore.video) && !$silentStore}
bind:this={cameraContainer} bind:this={cameraContainer}
> >
{#if isSilent} {#if $silentStore}
<div class="is-silent">{$LL.camera.my.silentZone()}</div> <div class="is-silent">{$LL.camera.my.silentZone()}</div>
{:else if $localStreamStore.type === "success" && $localStreamStore.stream} {:else if $localStreamStore.type === "success" && $localStreamStore.stream}
<video class="my-cam-video" use:srcObject={stream} autoplay muted playsinline /> <video class="my-cam-video" use:srcObject={stream} autoplay muted playsinline />

View File

@ -2,47 +2,46 @@
import { fly } from "svelte/transition"; import { fly } from "svelte/transition";
import { errorScreenStore } from "../../Stores/ErrorScreenStore"; import { errorScreenStore } from "../../Stores/ErrorScreenStore";
import { gameManager } from "../../Phaser/Game/GameManager"; import { gameManager } from "../../Phaser/Game/GameManager";
import { connectionManager } from "../../Connexion/ConnectionManager";
import { get } from "svelte/store";
import { onDestroy } from "svelte";
import logoImg from "../images/logo-min-white.png"; import logoImg from "../images/logo-min-white.png";
let logo = gameManager?.currentStartedRoom?.loginSceneLogo ?? logoImg; let logo = gameManager?.currentStartedRoom?.loginSceneLogo ?? logoImg;
import error from "../images/error.png";
import cup from "../images/cup.png";
import reload from "../images/reload.png"; import reload from "../images/reload.png";
import external from "../images/external-link.png";
import { get } from "svelte/store";
let errorScreen = get(errorScreenStore); let errorScreen = get(errorScreenStore);
function click() { function click() {
if (errorScreen.urlToRedirect) window.location.replace(errorScreen.urlToRedirect); if (errorScreen.type === "unauthorized") void connectionManager.logout();
else if (errorScreen.type === "redirect" && window.history.length > 2) history.back();
else window.location.reload(); else window.location.reload();
} }
let details = errorScreen.details; let details = errorScreen.details;
let timeVar = errorScreen.timeToRetry ?? 0; let timeVar = errorScreen.timeToRetry ?? 0;
if (errorScreen.type === "retry") { if (errorScreen.type === "retry") {
setInterval(() => { let interval = setInterval(() => {
if (timeVar <= 1000) click(); if (timeVar <= 1000) click();
timeVar -= 1000; timeVar -= 1000;
}, 1000); }, 1000);
onDestroy(() => clearInterval(interval));
} }
$: detailsStylized = details.replace("{time}", `${timeVar / 1000}`); $: detailsStylized = (details ?? "").replace("{time}", `${timeVar / 1000}`);
</script> </script>
<main class="errorScreen" transition:fly={{ y: -200, duration: 500 }}> <main class="errorScreen" transition:fly={{ y: -200, duration: 500 }}>
<div style="width: 90%;"> <div style="width: 90%;">
<img src={logo} alt="WorkAdventure" class="logo" /> <img src={logo} alt="WorkAdventure" class="logo" />
<div><img src={$errorScreenStore.type === "retry" ? cup : error} alt="" class="icon" /></div> <div><img src={$errorScreenStore.image} alt="" class="icon" /></div>
{#if $errorScreenStore.type !== "retry"}<h2>{$errorScreenStore.title}</h2>{/if} {#if $errorScreenStore.type !== "retry"}<h2>{$errorScreenStore.title}</h2>{/if}
<p>{$errorScreenStore.subtitle}</p> <p>{$errorScreenStore.subtitle}</p>
{#if $errorScreenStore.type !== "retry"}<p class="code">Code : {$errorScreenStore.code}</p>{/if} {#if $errorScreenStore.type !== "retry"}<p class="code">Code : {$errorScreenStore.code}</p>{/if}
<p class="details"> <p class="details">
{detailsStylized}{#if $errorScreenStore.type === "retry"}<div class="loading" />{/if} {detailsStylized}{#if $errorScreenStore.type === "retry"}<div class="loading" />{/if}
</p> </p>
{#if ($errorScreenStore.type === "retry" && $errorScreenStore.canRetryManual) || ($errorScreenStore.type === "redirect" && (window.history.length > 2 || $errorScreenStore.urlToRedirect))} {#if ($errorScreenStore.type === "retry" && $errorScreenStore.canRetryManual) || $errorScreenStore.type === "unauthorized"}
<button type="button" class="nes-btn is-primary button" on:click={click}> <button type="button" class="nes-btn is-primary button" on:click={click}>
<img src={$errorScreenStore.type === "retry" ? reload : external} alt="" class="reload" /> {#if $errorScreenStore.type === "retry"}<img src={reload} alt="" class="reload" />{/if}
{$errorScreenStore.buttonTitle} {$errorScreenStore.buttonTitle}
</button> </button>
{/if} {/if}
@ -74,10 +73,14 @@
.logo { .logo {
width: 50%; width: 50%;
margin-bottom: 50px; margin-bottom: 50px;
max-height: 25vh;
max-width: 50vw;
} }
.icon { .icon {
height: 125px; height: 125px;
margin-bottom: 25px; margin-bottom: 25px;
max-height: 25vh;
max-width: 50vw;
} }
h2 { h2 {
font-family: "Press Start 2P"; font-family: "Press Start 2P";

View File

@ -0,0 +1,171 @@
<script lang="ts">
import { fly } from "svelte/transition";
import {
desktopCapturerSourcePromiseResolve,
showDesktopCapturerSourcePicker,
} from "../../Stores/ScreenSharingStore";
import { onDestroy, onMount } from "svelte";
import type { DesktopCapturerSource } from "@wa-preload-app";
let desktopCapturerSources: DesktopCapturerSource[] = [];
let interval: ReturnType<typeof setInterval>;
async function getDesktopCapturerSources() {
if (!window.WAD) {
throw new Error("This component can only be used in the desktop app");
}
desktopCapturerSources = await window.WAD.getDesktopCapturerSources({
thumbnailSize: {
height: 144,
width: 256,
},
types: ["screen", "window"],
});
}
onMount(async () => {
await getDesktopCapturerSources();
interval = setInterval(() => {
void getDesktopCapturerSources();
}, 1000);
});
onDestroy(() => {
clearInterval(interval);
});
function selectDesktopCapturerSource(source: DesktopCapturerSource) {
if (!desktopCapturerSourcePromiseResolve) {
throw new Error("desktopCapturerSourcePromiseResolve is not defined");
}
desktopCapturerSourcePromiseResolve(source);
close();
}
function cancel() {
if (!desktopCapturerSourcePromiseResolve) {
throw new Error("desktopCapturerSourcePromiseResolve is not defined");
}
desktopCapturerSourcePromiseResolve(null);
close();
}
function close() {
$showDesktopCapturerSourcePicker = false;
}
</script>
<div class="source-picker nes-container is-rounded" transition:fly={{ y: -50, duration: 500 }}>
<button type="button" class="nes-btn is-error close" on:click={cancel}>&times</button>
<h2>Select a Screen or Window to share!</h2>
<section class="streams">
{#each desktopCapturerSources as source}
<div
class="media-box nes-container is-rounded clickable"
on:click|preventDefault={() => selectDesktopCapturerSource(source)}
>
<img src={source.thumbnailURL} alt={source.name} />
<div class="container">
{source.name}
</div>
</div>
{/each}
</section>
</div>
<style lang="scss">
.source-picker {
position: absolute;
pointer-events: auto;
background: #eceeee;
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
margin-top: 4%;
height: 80vh;
width: 80vw;
max-width: 1024px;
z-index: 900;
text-align: center;
display: flex;
flex-direction: column;
background-color: #333333;
color: whitesmoke;
.nes-btn.is-error.close {
position: absolute;
top: -20px;
right: -20px;
}
h2 {
font-family: "Press Start 2P";
}
section.streams {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
overflow-y: auto;
justify-content: center;
align-content: flex-start;
height: 100%;
}
.media-box {
position: relative;
padding: 0;
width: calc(100% / 3 - 20px);
max-width: 256px;
padding-bottom: calc(min((100% / 3 - 20px), 256px) * (144px / 256px));
max-height: 144px;
justify-content: center;
background-color: #000;
background-clip: padding-box;
&.clickable * {
cursor: url("../../../style/images/cursor_pointer.png"), pointer;
}
&:hover {
transform: scale(1.05);
}
img {
position: absolute;
top: 50%;
left: 50%;
max-width: 100%;
max-height: 100%;
transform: translate(-50%, -50%);
}
&.nes-container.is-rounded {
border-image-outset: 1;
}
div.container {
position: absolute;
width: 90%;
height: auto;
left: 5%;
top: calc(100% - 28px);
text-align: center;
padding: 2px 36px;
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
font-size: 14px;
margin: 2px;
background-color: white;
color: #333333;
border: solid 3px black;
border-radius: 8px;
font-style: normal;
}
}
}
</style>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -2,18 +2,19 @@
import type { Game } from "../../Phaser/Game/Game"; import type { Game } from "../../Phaser/Game/Game";
import { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene"; import { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene";
import LL from "../../i18n/i18n-svelte"; import LL from "../../i18n/i18n-svelte";
import { customizeAvailableStore } from "../../Stores/SelectCharacterSceneStore"; import { customizeAvailableStore, selectedCollection } from "../../Stores/SelectCharacterSceneStore";
export let game: Game; export let game: Game;
const selectCharacterScene = game.scene.getScene(SelectCharacterSceneName) as SelectCharacterScene; const selectCharacterScene = game.scene.getScene(SelectCharacterSceneName) as SelectCharacterScene;
const showArrows = selectCharacterScene.getCollectionKeysSize() > 1;
function selectLeft() { function selectLeft() {
selectCharacterScene.moveToLeft(); selectCharacterScene.selectPreviousCollection();
} }
function selectRight() { function selectRight() {
selectCharacterScene.moveToRight(); selectCharacterScene.selectNextCollection();
} }
function cameraScene() { function cameraScene() {
@ -25,83 +26,72 @@
} }
</script> </script>
<form class="selectCharacterScene"> <section class="text-center">
<section class="text-center"> <h2>{$LL.woka.selectWoka.title()}</h2>
<h2>{$LL.woka.selectWoka.title()}</h2> </section>
<button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}> <section class="category">
&lt; {#if showArrows}
</button> <button class="selectCharacterButton nes-btn" on:click|preventDefault={selectLeft}> &lt; </button>
<button class="selectCharacterButton selectCharacterButtonRight nes-btn" on:click|preventDefault={selectRight}> <strong class="category-text">{$selectedCollection}</strong>
&gt; <button class="selectCharacterButton nes-btn" on:click|preventDefault={selectRight}> &gt; </button>
</button> {/if}
</section> </section>
<section class="action"> <section class="action">
<button
type="submit"
class="selectCharacterSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={cameraScene}>{$LL.woka.selectWoka.continue()}</button
>
{#if $customizeAvailableStore}
<button <button
type="submit" type="submit"
class="selectCharacterSceneFormSubmit nes-btn is-primary" class="selectCharacterSceneFormCustomYourOwnSubmit nes-btn"
on:click|preventDefault={cameraScene}>{$LL.woka.selectWoka.continue()}</button on:click|preventDefault={customizeScene}>{$LL.woka.selectWoka.customize()}</button
> >
{#if $customizeAvailableStore} {/if}
<button </section>
type="submit"
class="selectCharacterSceneFormCustomYourOwnSubmit nes-btn"
on:click|preventDefault={customizeScene}>{$LL.woka.selectWoka.customize()}</button
>
{/if}
</section>
</form>
<style lang="scss"> <style lang="scss">
@import "../../../style/breakpoints.scss"; @import "../../../style/breakpoints.scss";
form.selectCharacterScene { section {
font-family: "Press Start 2P";
color: #ebeeee;
margin: 5px;
&.category {
text-align: center;
margin-top: 8vh;
.category-text {
font-family: "Press Start 2P";
display: inline-block;
width: 65%;
}
}
&.action {
position: absolute;
bottom: 2vh;
width: 100%;
text-align: center;
}
h2 {
font-family: "Press Start 2P";
margin: 1px;
}
&.text-center {
text-align: center;
}
button.selectCharacterButton {
margin: 0;
}
}
button {
font-family: "Press Start 2P"; font-family: "Press Start 2P";
pointer-events: auto; 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;
}
}
}
@include media-breakpoint-up(md) {
form.selectCharacterScene button.selectCharacterButtonLeft {
left: 5vw;
}
form.selectCharacterScene button.selectCharacterButtonRight {
right: 5vw;
}
} }
</style> </style>

View File

@ -363,15 +363,13 @@ class ConnectionManager {
if (locale) { if (locale) {
try { try {
if (locales.indexOf(locale) == -1) { if (locales.indexOf(locale) !== -1) {
locales.forEach((l) => { await setCurrentLocale(locale as Locales);
if (l.startsWith(locale.split("-")[0])) {
setCurrentLocale(l);
return;
}
});
} else { } else {
setCurrentLocale(locale as Locales); const nonRegionSpecificLocale = locales.find((l) => l.startsWith(locale.split("-")[0]));
if (nonRegionSpecificLocale) {
await setCurrentLocale(nonRegionSpecificLocale);
}
} }
} catch (err) { } catch (err) {
console.warn("Could not set locale", err); console.warn("Could not set locale", err);

View File

@ -1,6 +1,7 @@
import type { SignalData } from "simple-peer"; import type { SignalData } from "simple-peer";
import type { RoomConnection } from "./RoomConnection"; import type { RoomConnection } from "./RoomConnection";
import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Phaser/Entity/PlayerTextures";
import { AvailabilityStatus } from "../Messages/ts-proto-generated/protos/messages";
export interface PointInterface { export interface PointInterface {
x: number; x: number;
@ -14,7 +15,7 @@ export interface MessageUserPositionInterface {
name: string; name: string;
characterLayers: BodyResourceDescriptionInterface[]; characterLayers: BodyResourceDescriptionInterface[];
position: PointInterface; position: PointInterface;
away: boolean; status: AvailabilityStatus;
visitCardUrl: string | null; visitCardUrl: string | null;
companion: string | null; companion: string | null;
userUuid: string; userUuid: string;
@ -30,7 +31,7 @@ export interface MessageUserJoined {
name: string; name: string;
characterLayers: BodyResourceDescriptionInterface[]; characterLayers: BodyResourceDescriptionInterface[];
position: PointInterface; position: PointInterface;
away: boolean; status: AvailabilityStatus;
visitCardUrl: string | null; visitCardUrl: string | null;
companion: string | null; companion: string | null;
userUuid: string; userUuid: string;

View File

@ -42,6 +42,7 @@ import {
SetPlayerDetailsMessage as SetPlayerDetailsMessageTsProto, SetPlayerDetailsMessage as SetPlayerDetailsMessageTsProto,
PingMessage as PingMessageTsProto, PingMessage as PingMessageTsProto,
CharacterLayerMessage, CharacterLayerMessage,
AvailabilityStatus,
} from "../Messages/ts-proto-generated/protos/messages"; } from "../Messages/ts-proto-generated/protos/messages";
import { Subject } from "rxjs"; import { Subject } from "rxjs";
import { selectCharacterSceneVisibleStore } from "../Stores/SelectCharacterStore"; import { selectCharacterSceneVisibleStore } from "../Stores/SelectCharacterStore";
@ -302,8 +303,7 @@ export class RoomConnection implements RoomConnection {
} }
default: { default: {
// Security check: if we forget a "case", the line below will catch the error at compile-time. // Security check: if we forget a "case", the line below will catch the error at compile-time.
// eslint-disable-next-line @typescript-eslint/no-unused-vars const _exhaustiveCheck: never = subMessage;
const tmp: never = subMessage;
} }
} }
} }
@ -483,15 +483,20 @@ export class RoomConnection implements RoomConnection {
} }
case "errorScreenMessage": { case "errorScreenMessage": {
this._errorScreenMessageStream.next(message.errorScreenMessage); this._errorScreenMessageStream.next(message.errorScreenMessage);
if (message.errorScreenMessage.code !== "retry") this.closed = true; console.error("An error occurred server side: " + JSON.stringify(message.errorScreenMessage));
console.error("An error occurred server side: " + message.errorScreenMessage.code); if (message.errorScreenMessage.code !== "retry") {
errorScreenStore.setError(message.errorScreenMessage); this.closed = true;
}
if (message.errorScreenMessage.type === "redirect" && message.errorScreenMessage.urlToRedirect) {
window.location.assign(message.errorScreenMessage.urlToRedirect);
} else {
errorScreenStore.setError(message.errorScreenMessage);
}
break; break;
} }
default: { default: {
// Security check: if we forget a "case", the line below will catch the error at compile-time. // Security check: if we forget a "case", the line below will catch the error at compile-time.
// eslint-disable-next-line @typescript-eslint/no-unused-vars const _exhaustiveCheck: never = message;
const tmp: never = message;
} }
} }
}; };
@ -532,9 +537,9 @@ export class RoomConnection implements RoomConnection {
this.socket.send(bytes); this.socket.send(bytes);
} }
public emitPlayerAway(away: boolean): void { public emitPlayerStatusChange(status: AvailabilityStatus): void {
const message = SetPlayerDetailsMessageTsProto.fromPartial({ const message = SetPlayerDetailsMessageTsProto.fromPartial({
away, status,
}); });
const bytes = ClientToServerMessageTsProto.encode({ const bytes = ClientToServerMessageTsProto.encode({
message: { message: {
@ -626,19 +631,6 @@ export class RoomConnection implements RoomConnection {
this.socket.send(bytes); this.socket.send(bytes);
} }
public setSilent(silent: boolean): void {
const bytes = ClientToServerMessageTsProto.encode({
message: {
$case: "silentMessage",
silentMessage: {
silent,
},
},
}).finish();
this.socket.send(bytes);
}
public setViewport(viewport: ViewportInterface): void { public setViewport(viewport: ViewportInterface): void {
const bytes = ClientToServerMessageTsProto.encode({ const bytes = ClientToServerMessageTsProto.encode({
message: { message: {
@ -682,7 +674,7 @@ export class RoomConnection implements RoomConnection {
characterLayers, characterLayers,
visitCardUrl: message.visitCardUrl, visitCardUrl: message.visitCardUrl,
position: ProtobufClientUtils.toPointInterface(position), position: ProtobufClientUtils.toPointInterface(position),
away: message.away, status: message.status,
companion: companion ? companion.name : null, companion: companion ? companion.name : null,
userUuid: message.userUuid, userUuid: message.userUuid,
outlineColor: message.hasOutline ? message.outlineColor : undefined, outlineColor: message.hasOutline ? message.outlineColor : undefined,

View File

@ -61,9 +61,8 @@ export class CustomWokaPreviewer extends Phaser.GameObjects.Container {
this.frame = this.scene.add.graphics(); this.frame = this.scene.add.graphics();
this.turnIcon = this.scene.add this.turnIcon = this.scene.add
.image(this.background.displayWidth * 0.35, this.background.displayHeight * 0.35, "iconTurn") .image(this.background.displayWidth * 0.35, this.background.displayHeight * 0.35, "iconTurn")
.setScale(0.25) .setScale(0.2)
.setTintFill(0xffffff) .setAlpha(0.75);
.setAlpha(0.5);
this.drawFrame(); this.drawFrame();
this.setSize(this.SIZE, this.SIZE); this.setSize(this.SIZE, this.SIZE);
@ -130,11 +129,11 @@ export class CustomWokaPreviewer extends Phaser.GameObjects.Container {
this.changeAnimation(direction, moving); this.changeAnimation(direction, moving);
this.turnIconTween?.stop(); this.turnIconTween?.stop();
this.turnIcon.setScale(0.25); this.turnIcon.setScale(0.2);
this.turnIconTween = this.scene.tweens.add({ this.turnIconTween = this.scene.tweens.add({
targets: [this.turnIcon], targets: [this.turnIcon],
duration: 100, duration: 100,
scale: 0.2, scale: 0.15,
yoyo: true, yoyo: true,
ease: Easing.SineEaseIn, ease: Easing.SineEaseIn,
}); });

View File

@ -1,36 +1,41 @@
import { AvailabilityStatus } from "../../Messages/ts-proto-generated/protos/messages";
import { Easing } from "../../types"; import { Easing } from "../../types";
export class PlayerStatusDot extends Phaser.GameObjects.Container { export class PlayerStatusDot extends Phaser.GameObjects.Container {
private graphics: Phaser.GameObjects.Graphics; private statusImage: Phaser.GameObjects.Image;
private statusImageOutline: Phaser.GameObjects.Image;
private away: boolean; private status: AvailabilityStatus;
private readonly COLORS = { private readonly COLORS: Record<AvailabilityStatus, { filling: number; outline: number }> = {
// online: 0x00ff00, [AvailabilityStatus.AWAY]: { filling: 0xf5931e, outline: 0x875d13 },
// away: 0xffff00, [AvailabilityStatus.ONLINE]: { filling: 0x8cc43f, outline: 0x427a25 },
online: 0x8cc43f, [AvailabilityStatus.SILENT]: { filling: 0xe74c3c, outline: 0xc0392b },
onlineOutline: 0x427a25, [AvailabilityStatus.JITSI]: { filling: 0x8cc43f, outline: 0x427a25 },
away: 0xf5931e, [AvailabilityStatus.UNRECOGNIZED]: { filling: 0xffffff, outline: 0xffffff },
awayOutline: 0x875d13, [AvailabilityStatus.UNCHANGED]: { filling: 0xffffff, outline: 0xffffff },
}; };
constructor(scene: Phaser.Scene, x: number, y: number) { constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y); super(scene, x, y);
this.away = false; this.status = AvailabilityStatus.ONLINE;
this.statusImage = this.scene.add.image(0, 0, "iconStatusIndicatorInside");
this.statusImageOutline = this.scene.add.image(0, 0, "iconStatusIndicatorOutline");
this.add([this.statusImage, this.statusImageOutline]);
this.graphics = this.scene.add.graphics();
this.add(this.graphics);
this.redraw(); this.redraw();
this.scene.add.existing(this); this.scene.add.existing(this);
} }
public setAway(away: boolean = true, instant: boolean = false): void { public setStatus(status: AvailabilityStatus, instant: boolean = false): void {
if (this.away === away) { if (this.status === status || status === AvailabilityStatus.UNCHANGED) {
return; return;
} }
this.away = away; this.status = status;
if (instant) { if (instant) {
this.redraw(); this.redraw();
} else { } else {
@ -56,10 +61,8 @@ export class PlayerStatusDot extends Phaser.GameObjects.Container {
} }
private redraw(): void { private redraw(): void {
this.graphics.clear(); const colors = this.COLORS[this.status];
this.graphics.fillStyle(this.away ? this.COLORS.away : this.COLORS.online); this.statusImage.setTintFill(colors.filling);
this.graphics.lineStyle(1, this.away ? this.COLORS.awayOutline : this.COLORS.onlineOutline); this.statusImageOutline.setTintFill(colors.outline);
this.graphics.fillCircle(0, 0, 3);
this.graphics.strokeCircle(0, 0, 3);
} }
} }

View File

@ -0,0 +1,35 @@
import { GridItem } from "@home-based-studio/phaser3-utils";
export class WokaSlot extends GridItem {
private sprite: Phaser.GameObjects.Sprite;
private selection: Phaser.GameObjects.Rectangle;
private readonly SIZE: number = 50;
constructor(scene: Phaser.Scene, spriteKey: string, id?: string) {
super(scene, id);
this.sprite = this.scene.add.sprite(0, 0, spriteKey);
this.selection = this.scene.add
.rectangle(0, 0, this.SIZE, this.SIZE)
.setStrokeStyle(1, 0xffffff)
.setVisible(false);
this.add([this.selection, this.sprite]);
this.setSize(this.SIZE, this.SIZE);
this.setInteractive({ cursor: "pointer" });
this.scene.input.setDraggable(this);
this.bindEventHandlers();
this.scene.add.existing(this);
}
public getSprite(): Phaser.GameObjects.Sprite {
return this.sprite;
}
public select(select: boolean = true): void {
this.selection.setVisible(select);
}
}

View File

@ -6,6 +6,7 @@ export interface IconButtonConfig {
hover: IconButtonAppearanceConfig; hover: IconButtonAppearanceConfig;
pressed: IconButtonAppearanceConfig; pressed: IconButtonAppearanceConfig;
selected: IconButtonAppearanceConfig; selected: IconButtonAppearanceConfig;
iconScale?: number;
} }
export interface IconButtonAppearanceConfig { export interface IconButtonAppearanceConfig {
@ -34,7 +35,7 @@ export class IconButton extends Phaser.GameObjects.Container {
this.config = config; this.config = config;
this.background = this.scene.add.graphics(); this.background = this.scene.add.graphics();
this.icon = this.scene.add.image(0, 0, this.config.iconTextureKey); this.icon = this.scene.add.image(0, 0, this.config.iconTextureKey).setScale(config.iconScale ?? 1);
this.drawBackground(this.config.idle); this.drawBackground(this.config.idle);
this.add([this.background, this.icon]); this.add([this.background, this.icon]);

View File

@ -9,7 +9,6 @@ import { Companion } from "../Companion/Companion";
import type { GameScene } from "../Game/GameScene"; import type { GameScene } from "../Game/GameScene";
import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes"; import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js"; import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js";
import { isSilentStore } from "../../Stores/MediaStore";
import { lazyLoadPlayerCharacterTextures } from "./PlayerTexturesLoadingManager"; import { lazyLoadPlayerCharacterTextures } from "./PlayerTexturesLoadingManager";
import { TexturesHelper } from "../Helpers/TexturesHelper"; import { TexturesHelper } from "../Helpers/TexturesHelper";
import type { PictureStore } from "../../Stores/PictureStore"; import type { PictureStore } from "../../Stores/PictureStore";
@ -20,6 +19,7 @@ import type CancelablePromise from "cancelable-promise";
import { TalkIcon } from "../Components/TalkIcon"; import { TalkIcon } from "../Components/TalkIcon";
import { Deferred } from "ts-deferred"; import { Deferred } from "ts-deferred";
import { PlayerStatusDot } from "../Components/PlayerStatusDot"; import { PlayerStatusDot } from "../Components/PlayerStatusDot";
import { AvailabilityStatus } from "../../Messages/ts-proto-generated/protos/messages";
const playerNameY = -25; const playerNameY = -25;
const interactiveRadius = 35; const interactiveRadius = 35;
@ -236,8 +236,8 @@ export abstract class Character extends Container implements OutlineableInterfac
this.talkIcon.show(show, forceClose); this.talkIcon.show(show, forceClose);
} }
public setAwayStatus(away: boolean = true, instant: boolean = false): void { public setStatus(status: AvailabilityStatus, instant: boolean = false): void {
this.statusDot.setAway(away, instant); this.statusDot.setStatus(status, instant);
} }
public addCompanion(name: string, texturePromise?: CancelablePromise<string>): void { public addCompanion(name: string, texturePromise?: CancelablePromise<string>): void {
@ -355,13 +355,6 @@ export abstract class Character extends Container implements OutlineableInterfac
super.destroy(); super.destroy();
} }
isSilent() {
isSilentStore.set(true);
}
noSilent() {
isSilentStore.set(false);
}
playEmote(emote: string) { playEmote(emote: string) {
this.cancelPreviousEmote(); this.cancelPreviousEmote();
const emoteY = -45; const emoteY = -45;

View File

@ -36,14 +36,16 @@ export enum PlayerTexturesKey {
} }
export class PlayerTextures { export class PlayerTextures {
private PLAYER_RESOURCES: BodyResourceDescriptionListInterface = {}; private wokaResources: BodyResourceDescriptionListInterface = {};
private COLOR_RESOURCES: BodyResourceDescriptionListInterface = {}; private colorResources: BodyResourceDescriptionListInterface = {};
private EYES_RESOURCES: BodyResourceDescriptionListInterface = {}; private eyesResources: BodyResourceDescriptionListInterface = {};
private HAIR_RESOURCES: BodyResourceDescriptionListInterface = {}; private hairResources: BodyResourceDescriptionListInterface = {};
private CLOTHES_RESOURCES: BodyResourceDescriptionListInterface = {}; private clothesResources: BodyResourceDescriptionListInterface = {};
private HATS_RESOURCES: BodyResourceDescriptionListInterface = {}; private hatsResources: BodyResourceDescriptionListInterface = {};
private ACCESSORIES_RESOURCES: BodyResourceDescriptionListInterface = {}; private accessoriesResources: BodyResourceDescriptionListInterface = {};
private LAYERS: BodyResourceDescriptionListInterface[] = []; private layers: BodyResourceDescriptionListInterface[] = [];
private wokaCollections = new Map<string, BodyResourceDescriptionInterface[]>();
public loadPlayerTexturesMetadata(metadata: WokaList): void { public loadPlayerTexturesMetadata(metadata: WokaList): void {
this.mapTexturesMetadataIntoResources(metadata); this.mapTexturesMetadataIntoResources(metadata);
@ -52,43 +54,53 @@ export class PlayerTextures {
public getTexturesResources(key: PlayerTexturesKey): BodyResourceDescriptionListInterface { public getTexturesResources(key: PlayerTexturesKey): BodyResourceDescriptionListInterface {
switch (key) { switch (key) {
case PlayerTexturesKey.Accessory: case PlayerTexturesKey.Accessory:
return this.ACCESSORIES_RESOURCES; return this.accessoriesResources;
case PlayerTexturesKey.Body: case PlayerTexturesKey.Body:
return this.COLOR_RESOURCES; return this.colorResources;
case PlayerTexturesKey.Clothes: case PlayerTexturesKey.Clothes:
return this.CLOTHES_RESOURCES; return this.clothesResources;
case PlayerTexturesKey.Eyes: case PlayerTexturesKey.Eyes:
return this.EYES_RESOURCES; return this.eyesResources;
case PlayerTexturesKey.Hair: case PlayerTexturesKey.Hair:
return this.HAIR_RESOURCES; return this.hairResources;
case PlayerTexturesKey.Hat: case PlayerTexturesKey.Hat:
return this.HATS_RESOURCES; return this.hatsResources;
case PlayerTexturesKey.Woka: case PlayerTexturesKey.Woka:
return this.PLAYER_RESOURCES; return this.wokaResources;
} }
} }
public getLayers(): BodyResourceDescriptionListInterface[] { public getLayers(): BodyResourceDescriptionListInterface[] {
return this.LAYERS; return this.layers;
}
public getCollectionsKeys(): string[] {
return Array.from(this.wokaCollections.keys());
}
public getWokaCollectionTextures(key: string): BodyResourceDescriptionInterface[] {
return this.wokaCollections.get(key) ?? [];
} }
private mapTexturesMetadataIntoResources(metadata: WokaList): void { private mapTexturesMetadataIntoResources(metadata: WokaList): void {
this.PLAYER_RESOURCES = this.getMappedResources(metadata.woka); this.wokaResources = this.getMappedResources(metadata.woka);
this.COLOR_RESOURCES = this.getMappedResources(metadata.body); this.colorResources = this.getMappedResources(metadata.body);
this.EYES_RESOURCES = this.getMappedResources(metadata.eyes); this.eyesResources = this.getMappedResources(metadata.eyes);
this.HAIR_RESOURCES = this.getMappedResources(metadata.hair); this.hairResources = this.getMappedResources(metadata.hair);
this.CLOTHES_RESOURCES = this.getMappedResources(metadata.clothes); this.clothesResources = this.getMappedResources(metadata.clothes);
this.HATS_RESOURCES = this.getMappedResources(metadata.hat); this.hatsResources = this.getMappedResources(metadata.hat);
this.ACCESSORIES_RESOURCES = this.getMappedResources(metadata.accessory); this.accessoriesResources = this.getMappedResources(metadata.accessory);
this.LAYERS = [ this.layers = [
this.COLOR_RESOURCES, this.colorResources,
this.EYES_RESOURCES, this.eyesResources,
this.HAIR_RESOURCES, this.hairResources,
this.CLOTHES_RESOURCES, this.clothesResources,
this.HATS_RESOURCES, this.hatsResources,
this.ACCESSORIES_RESOURCES, this.accessoriesResources,
]; ];
this.mapWokaCollections(metadata.woka);
} }
private getMappedResources(category: WokaPartType): BodyResourceDescriptionListInterface { private getMappedResources(category: WokaPartType): BodyResourceDescriptionListInterface {
@ -103,6 +115,19 @@ export class PlayerTextures {
} }
return resources; return resources;
} }
private mapWokaCollections(category: WokaPartType): void {
if (!category) {
return;
}
for (const collection of category.collections) {
const textures: BodyResourceDescriptionInterface[] = [];
for (const texture of collection.textures) {
textures.push({ id: texture.id, img: texture.url });
}
this.wokaCollections.set(collection.name, textures);
}
}
} }
export const OBJECTS: BodyResourceDescriptionInterface[] = [ export const OBJECTS: BodyResourceDescriptionInterface[] = [

View File

@ -4,7 +4,7 @@ import { Character } from "../Entity/Character";
import type { GameScene } from "../Game/GameScene"; import type { GameScene } from "../Game/GameScene";
import type { PointInterface } from "../../Connexion/ConnexionModels"; import type { PointInterface } from "../../Connexion/ConnexionModels";
import type { PlayerAnimationDirections } from "../Player/Animation"; import type { PlayerAnimationDirections } from "../Player/Animation";
import type { Unsubscriber } from "svelte/store"; import { get, Unsubscriber } from "svelte/store";
import type { ActivatableInterface } from "../Game/ActivatableInterface"; import type { ActivatableInterface } from "../Game/ActivatableInterface";
import type CancelablePromise from "cancelable-promise"; import type CancelablePromise from "cancelable-promise";
import LL from "../../i18n/i18n-svelte"; import LL from "../../i18n/i18n-svelte";
@ -113,7 +113,7 @@ export class RemotePlayer extends Character implements ActivatableInterface {
const actions: ActionsMenuAction[] = []; const actions: ActionsMenuAction[] = [];
if (this.visitCardUrl) { if (this.visitCardUrl) {
actions.push({ actions.push({
actionName: LL.woka.menu.businessCard(), actionName: get(LL).woka.menu.businessCard(),
protected: true, protected: true,
priority: 1, priority: 1,
callback: () => { callback: () => {
@ -125,8 +125,8 @@ export class RemotePlayer extends Character implements ActivatableInterface {
actions.push({ actions.push({
actionName: blackListManager.isBlackListed(this.userUuid) actionName: blackListManager.isBlackListed(this.userUuid)
? LL.report.block.unblock() ? get(LL).report.block.unblock()
: LL.report.block.block(), : get(LL).report.block.block(),
protected: true, protected: true,
priority: -1, priority: -1,
style: "is-error", style: "is-error",

View File

@ -76,6 +76,7 @@ export class ActivatablesManager {
const currentPlayerPos = this.currentPlayer.getDirectionalActivationPosition( const currentPlayerPos = this.currentPlayer.getDirectionalActivationPosition(
this.directionalActivationPositionShift this.directionalActivationPositionShift
); );
this.activatableObjectsDistances.clear();
for (const object of objects) { for (const object of objects) {
const distance = MathUtils.distanceBetween(currentPlayerPos, object.getPosition()); const distance = MathUtils.distanceBetween(currentPlayerPos, object.getPosition());
this.activatableObjectsDistances.set(object, distance); this.activatableObjectsDistances.set(object, distance);

View File

@ -97,7 +97,6 @@ export class CameraManager extends Phaser.Events.EventEmitter {
}); });
return; return;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this.camera.pan(setTo.x, setTo.y, duration, Easing.SineEaseOut, true, (camera, progress, x, y) => { this.camera.pan(setTo.x, setTo.y, duration, Easing.SineEaseOut, true, (camera, progress, x, y) => {
if (this.cameraMode === CameraMode.Positioned) { if (this.cameraMode === CameraMode.Positioned) {
this.waScaleManager.zoomModifier = currentZoomModifier + progress * zoomModifierChange; this.waScaleManager.zoomModifier = currentZoomModifier + progress * zoomModifierChange;
@ -139,7 +138,6 @@ export class CameraManager extends Phaser.Events.EventEmitter {
this.emit(CameraManagerEvent.CameraUpdate, this.getCameraUpdateEventData()); this.emit(CameraManagerEvent.CameraUpdate, this.getCameraUpdateEventData());
return; return;
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this.camera.pan(focusOn.x, focusOn.y, duration, Easing.SineEaseOut, true, (camera, progress, x, y) => { this.camera.pan(focusOn.x, focusOn.y, duration, Easing.SineEaseOut, true, (camera, progress, x, y) => {
this.waScaleManager.zoomModifier = currentZoomModifier + progress * zoomModifierChange; this.waScaleManager.zoomModifier = currentZoomModifier + progress * zoomModifierChange;
if (progress === 1) { if (progress === 1) {

View File

@ -17,6 +17,7 @@ import { audioManagerFileStore, audioManagerVisibilityStore } from "../../Stores
import { iframeListener } from "../../Api/IframeListener"; import { iframeListener } from "../../Api/IframeListener";
import { Room } from "../../Connexion/Room"; import { Room } from "../../Connexion/Room";
import LL from "../../i18n/i18n-svelte"; import LL from "../../i18n/i18n-svelte";
import { inJitsiStore, silentStore } from "../../Stores/MediaStore";
interface OpenCoWebsite { interface OpenCoWebsite {
actionId: string; actionId: string;
@ -77,6 +78,7 @@ export class GameMapPropertiesListener {
coWebsiteManager.closeCoWebsite(coWebsite); coWebsiteManager.closeCoWebsite(coWebsite);
} }
}); });
inJitsiStore.set(false);
} else { } else {
const openJitsiRoomFunction = () => { const openJitsiRoomFunction = () => {
let addPrefix = true; let addPrefix = true;
@ -119,11 +121,15 @@ export class GameMapPropertiesListener {
uuid: "jitsi", uuid: "jitsi",
type: "message", type: "message",
message: message, message: message,
callback: () => openJitsiRoomFunction(), callback: () => {
openJitsiRoomFunction();
inJitsiStore.set(true);
},
userInputManager: this.scene.userInputManager, userInputManager: this.scene.userInputManager,
}); });
} else { } else {
openJitsiRoomFunction(); openJitsiRoomFunction();
inJitsiStore.set(true);
} }
} }
}); });
@ -160,11 +166,9 @@ export class GameMapPropertiesListener {
this.gameMap.onPropertyChange(GameMapProperties.SILENT, (newValue) => { this.gameMap.onPropertyChange(GameMapProperties.SILENT, (newValue) => {
if (newValue === undefined || newValue === false || newValue === "") { if (newValue === undefined || newValue === false || newValue === "") {
this.scene.connection?.setSilent(false); silentStore.set(false);
this.scene.CurrentPlayer.noSilent();
} else { } else {
this.scene.connection?.setSilent(true); silentStore.set(true);
this.scene.CurrentPlayer.isSilent();
} }
}); });

View File

@ -91,7 +91,7 @@ import { MapStore } from "../../Stores/Utils/MapStore";
import { followUsersColorStore } from "../../Stores/FollowStore"; import { followUsersColorStore } from "../../Stores/FollowStore";
import { GameSceneUserInputHandler } from "../UserInput/GameSceneUserInputHandler"; import { GameSceneUserInputHandler } from "../UserInput/GameSceneUserInputHandler";
import { locale } from "../../i18n/i18n-svelte"; import { locale } from "../../i18n/i18n-svelte";
import { localVolumeStore } from "../../Stores/MediaStore"; import { availabilityStatusStore, localVolumeStore } from "../../Stores/MediaStore";
import { StringUtils } from "../../Utils/StringUtils"; import { StringUtils } from "../../Utils/StringUtils";
import { startLayerNamesStore } from "../../Stores/StartLayerNamesStore"; import { startLayerNamesStore } from "../../Stores/StartLayerNamesStore";
import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite"; import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite";
@ -101,7 +101,6 @@ import CancelablePromise from "cancelable-promise";
import { Deferred } from "ts-deferred"; import { Deferred } from "ts-deferred";
import { SuperLoaderPlugin } from "../Services/SuperLoaderPlugin"; import { SuperLoaderPlugin } from "../Services/SuperLoaderPlugin";
import { PlayerDetailsUpdatedMessage } from "../../Messages/ts-proto-generated/protos/messages"; import { PlayerDetailsUpdatedMessage } from "../../Messages/ts-proto-generated/protos/messages";
import { privacyShutdownStore } from "../../Stores/PrivacyShutdownStore";
export interface GameSceneInitInterface { export interface GameSceneInitInterface {
initPosition: PointInterface | null; initPosition: PointInterface | null;
reconnecting: boolean; reconnecting: boolean;
@ -177,8 +176,6 @@ export class GameScene extends DirtyScene {
private localVolumeStoreUnsubscriber: Unsubscriber | undefined; private localVolumeStoreUnsubscriber: Unsubscriber | undefined;
private followUsersColorStoreUnsubscribe!: Unsubscriber; private followUsersColorStoreUnsubscribe!: Unsubscriber;
private currentPlayerGroupIdStoreUnsubscribe!: Unsubscriber;
private privacyShutdownStoreUnsubscribe!: Unsubscriber;
private userIsJitsiDominantSpeakerStoreUnsubscriber!: Unsubscriber; private userIsJitsiDominantSpeakerStoreUnsubscriber!: Unsubscriber;
private jitsiParticipantsCountStoreUnsubscriber!: Unsubscriber; private jitsiParticipantsCountStoreUnsubscriber!: Unsubscriber;
@ -249,6 +246,8 @@ export class GameScene extends DirtyScene {
this.listenToIframeEvents(); this.listenToIframeEvents();
this.load.image("iconTalk", "/resources/icons/icon_talking.png"); this.load.image("iconTalk", "/resources/icons/icon_talking.png");
this.load.image("iconStatusIndicatorInside", "/resources/icons/icon_status_indicator_inside.png");
this.load.image("iconStatusIndicatorOutline", "/resources/icons/icon_status_indicator_outline.png");
if (touchScreenManager.supportTouchScreen) { if (touchScreenManager.supportTouchScreen) {
this.load.image(joystickBaseKey, joystickBaseImg); this.load.image(joystickBaseKey, joystickBaseImg);
@ -679,6 +678,11 @@ export class GameScene extends DirtyScene {
this.tryChangeShowVoiceIndicatorState(this.jitsiDominantSpeaker && this.jitsiParticipantsCount > 1); this.tryChangeShowVoiceIndicatorState(this.jitsiDominantSpeaker && this.jitsiParticipantsCount > 1);
}); });
availabilityStatusStore.subscribe((status) => {
this.connection?.emitPlayerStatusChange(status);
this.CurrentPlayer.setStatus(status);
});
this.emoteUnsubscribe = emoteStore.subscribe((emote) => { this.emoteUnsubscribe = emoteStore.subscribe((emote) => {
if (emote) { if (emote) {
this.CurrentPlayer?.playEmote(emote.url); this.CurrentPlayer?.playEmote(emote.url);
@ -705,10 +709,6 @@ export class GameScene extends DirtyScene {
} }
}); });
this.privacyShutdownStoreUnsubscribe = privacyShutdownStore.subscribe((away) => {
this.connection?.emitPlayerAway(away);
});
Promise.all([ Promise.all([
this.connectionAnswerPromiseDeferred.promise as Promise<unknown>, this.connectionAnswerPromiseDeferred.promise as Promise<unknown>,
...scriptPromises, ...scriptPromises,
@ -767,7 +767,7 @@ export class GameScene extends DirtyScene {
characterLayers: message.characterLayers, characterLayers: message.characterLayers,
name: message.name, name: message.name,
position: message.position, position: message.position,
away: message.away, status: message.status,
visitCardUrl: message.visitCardUrl, visitCardUrl: message.visitCardUrl,
companion: message.companion, companion: message.companion,
userUuid: message.userUuid, userUuid: message.userUuid,
@ -1106,6 +1106,13 @@ ${escapedMessage}
}) })
); );
this.iframeSubscriptionList.push(
iframeListener.stopSoundStream.subscribe((stopSoundEvent) => {
const url = new URL(stopSoundEvent.url, this.MapUrlFile);
soundManager.stopSound(this.sound, url.toString());
})
);
this.iframeSubscriptionList.push( this.iframeSubscriptionList.push(
iframeListener.addActionsMenuKeyToRemotePlayerStream.subscribe((data) => { iframeListener.addActionsMenuKeyToRemotePlayerStream.subscribe((data) => {
this.MapPlayersByKey.get(data.id)?.registerActionsMenuAction({ this.MapPlayersByKey.get(data.id)?.registerActionsMenuAction({
@ -1556,7 +1563,6 @@ ${escapedMessage}
this.emoteUnsubscribe(); this.emoteUnsubscribe();
this.emoteMenuUnsubscribe(); this.emoteMenuUnsubscribe();
this.followUsersColorStoreUnsubscribe(); this.followUsersColorStoreUnsubscribe();
this.privacyShutdownStoreUnsubscribe();
this.biggestAvailableAreaStoreUnsubscribe(); this.biggestAvailableAreaStoreUnsubscribe();
this.userIsJitsiDominantSpeakerStoreUnsubscriber(); this.userIsJitsiDominantSpeakerStoreUnsubscriber();
this.jitsiParticipantsCountStoreUnsubscriber(); this.jitsiParticipantsCountStoreUnsubscriber();
@ -1696,7 +1702,6 @@ ${escapedMessage}
private createCollisionWithPlayer() { private createCollisionWithPlayer() {
//add collision layer //add collision layer
for (const phaserLayer of this.gameMap.phaserLayers) { for (const phaserLayer of this.gameMap.phaserLayers) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this.physics.add.collider(this.CurrentPlayer, phaserLayer, (object1: GameObject, object2: GameObject) => { this.physics.add.collider(this.CurrentPlayer, phaserLayer, (object1: GameObject, object2: GameObject) => {
//this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name) //this.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name)
}); });
@ -1747,11 +1752,9 @@ ${escapedMessage}
emoteMenuStore.openEmoteMenu(); emoteMenuStore.openEmoteMenu();
} }
}); });
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this.CurrentPlayer.on(Phaser.Input.Events.POINTER_OVER, (pointer: Phaser.Input.Pointer) => { this.CurrentPlayer.on(Phaser.Input.Events.POINTER_OVER, (pointer: Phaser.Input.Pointer) => {
this.CurrentPlayer.pointerOverOutline(0x365dff); this.CurrentPlayer.pointerOverOutline(0x365dff);
}); });
// eslint-disable-next-line @typescript-eslint/no-unused-vars
this.CurrentPlayer.on(Phaser.Input.Events.POINTER_OUT, (pointer: Phaser.Input.Pointer) => { this.CurrentPlayer.on(Phaser.Input.Events.POINTER_OUT, (pointer: Phaser.Input.Pointer) => {
this.CurrentPlayer.pointerOutOutline(); this.CurrentPlayer.pointerOutOutline();
}); });
@ -1853,8 +1856,7 @@ ${escapedMessage}
break; break;
} }
default: { default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars const _exhaustiveCheck: never = event;
const tmp: never = event;
} }
} }
} }
@ -1933,8 +1935,8 @@ ${escapedMessage}
if (addPlayerData.outlineColor !== undefined) { if (addPlayerData.outlineColor !== undefined) {
player.setApiOutlineColor(addPlayerData.outlineColor); player.setApiOutlineColor(addPlayerData.outlineColor);
} }
if (addPlayerData.away !== undefined) { if (addPlayerData.status !== undefined) {
player.setAwayStatus(addPlayerData.away, true); player.setStatus(addPlayerData.status, true);
} }
this.MapPlayers.add(player); this.MapPlayers.add(player);
this.MapPlayersByKey.set(player.userId, player); this.MapPlayersByKey.set(player.userId, player);
@ -2085,8 +2087,8 @@ ${escapedMessage}
if (message.details?.showVoiceIndicator !== undefined) { if (message.details?.showVoiceIndicator !== undefined) {
character.showTalkIcon(message.details?.showVoiceIndicator); character.showTalkIcon(message.details?.showVoiceIndicator);
} }
if (message.details?.away !== undefined) { if (message.details?.status !== undefined) {
character.setAwayStatus(message.details?.away); character.setStatus(message.details?.status);
} }
} }
@ -2130,13 +2132,10 @@ ${escapedMessage}
} }
public enableMediaBehaviors() { public enableMediaBehaviors() {
const silent = this.gameMap.getCurrentProperties().get(GameMapProperties.SILENT);
this.connection?.setSilent(!!silent);
mediaManager.showMyCamera(); mediaManager.showMyCamera();
} }
public disableMediaBehaviors() { public disableMediaBehaviors() {
this.connection?.setSilent(true);
mediaManager.hideMyCamera(); mediaManager.hideMyCamera();
} }

View File

@ -1,3 +1,4 @@
import { AvailabilityStatus } from "../../Messages/ts-proto-generated/protos/messages";
import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures"; import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
export interface PlayerInterface { export interface PlayerInterface {
@ -7,7 +8,7 @@ export interface PlayerInterface {
visitCardUrl: string | null; visitCardUrl: string | null;
companion: string | null; companion: string | null;
userUuid: string; userUuid: string;
away: boolean; status: AvailabilityStatus;
color?: string; color?: string;
outlineColor?: number; outlineColor?: number;
} }

View File

@ -34,7 +34,6 @@ export class PlayerMovement {
const y = const y =
(this.endPosition.y - this.startPosition.y) * ((tick - this.startTick) / (this.endTick - this.startTick)) + (this.endPosition.y - this.startPosition.y) * ((tick - this.startTick) / (this.endTick - this.startTick)) +
this.startPosition.y; this.startPosition.y;
//console.log('Computed position ', x, y)
return { return {
x, x,
y, y,

View File

@ -28,7 +28,7 @@ export class TexturesHelper {
}); });
} catch (error) { } catch (error) {
rt.destroy(); rt.destroy();
throw new Error("Could not get the snapshot"); throw new Error("Could not get the snapshot: " + error);
} }
} }
@ -67,4 +67,20 @@ export class TexturesHelper {
rectangleTexture.generateTexture(textureKey, width, height); rectangleTexture.generateTexture(textureKey, width, height);
rectangleTexture.destroy(); rectangleTexture.destroy();
} }
public static createCircleTexture(
scene: Phaser.Scene,
textureKey: string,
radius: number,
color: number,
outlineColor?: number,
outlineThickness?: number
): void {
const circleTexture = scene.add.graphics().fillStyle(color, 1).fillCircle(radius, radius, radius);
if (outlineColor) {
circleTexture.lineStyle(outlineThickness ?? 1, outlineColor).strokeCircle(radius, radius, radius);
}
circleTexture.generateTexture(textureKey, radius * 2, radius * 2);
circleTexture.destroy();
}
} }

View File

@ -11,4 +11,8 @@ export abstract class AbstractCharacterScene extends ResizableScene {
this.playerTextures = new PlayerTextures(); this.playerTextures = new PlayerTextures();
this.superLoad = new SuperLoaderPlugin(this); this.superLoad = new SuperLoaderPlugin(this);
} }
preload() {
this.input.dragDistanceThreshold = 10;
}
} }

View File

@ -54,7 +54,7 @@ export class CustomizeScene extends AbstractCharacterScene {
} }
public preload(): void { public preload(): void {
this.input.dragDistanceThreshold = 10; super.preload();
this.load.image("iconClothes", "/resources/icons/icon_clothes.png"); this.load.image("iconClothes", "/resources/icons/icon_clothes.png");
this.load.image("iconAccessory", "/resources/icons/icon_accessory.png"); this.load.image("iconAccessory", "/resources/icons/icon_accessory.png");
@ -94,6 +94,7 @@ export class CustomizeScene extends AbstractCharacterScene {
} }
public create(): void { public create(): void {
this.tryLoadLastUsedWokaLayers();
waScaleManager.zoomModifier = 1; waScaleManager.zoomModifier = 1;
this.createSlotBackgroundTextures(); this.createSlotBackgroundTextures();
this.initializeCustomWokaPreviewer(); this.initializeCustomWokaPreviewer();
@ -112,7 +113,6 @@ export class CustomizeScene extends AbstractCharacterScene {
this.onResize(); this.onResize();
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public update(time: number, dt: number): void { public update(time: number, dt: number): void {
this.customWokaPreviewer.update(); this.customWokaPreviewer.update();
} }
@ -150,12 +150,35 @@ export class CustomizeScene extends AbstractCharacterScene {
this.scene.run(SelectCharacterSceneName); this.scene.run(SelectCharacterSceneName);
} }
private tryLoadLastUsedWokaLayers(): void {
try {
const savedWokaLayers = gameManager.getCharacterLayers();
if (savedWokaLayers && savedWokaLayers.length !== 0) {
this.selectedLayers = [];
for (let i = 0; i < savedWokaLayers.length; i += 1) {
this.selectedLayers.push(
this.layers[i].findIndex((item) => item.id === gameManager.getCharacterLayers()[i])
);
}
}
} catch {
console.warn("Cannot load previous WOKA");
}
}
private createSlotBackgroundTextures(): void { private createSlotBackgroundTextures(): void {
for (let i = 0; i < 4; i += 1) { for (let i = 0; i < 4; i += 1) {
if (this.textures.getTextureKeys().includes(`floorTexture${i}`)) { if (this.textures.getTextureKeys().includes(`floorTexture${i}`)) {
continue; continue;
} }
TexturesHelper.createFloorRectangleTexture(this, `floorTexture${i}`, 50, 50, "floorTiles", i); TexturesHelper.createFloorRectangleTexture(
this,
`floorTexture${i}`,
WokaBodyPartSlot.SIZE,
WokaBodyPartSlot.SIZE,
"floorTiles",
i
);
} }
} }
@ -214,15 +237,16 @@ export class CustomizeScene extends AbstractCharacterScene {
), ),
[CustomWokaBodyPart.Body]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconBody")), [CustomWokaBodyPart.Body]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconBody")),
[CustomWokaBodyPart.Clothes]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconClothes")), [CustomWokaBodyPart.Clothes]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconClothes")),
[CustomWokaBodyPart.Eyes]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconEyes")), [CustomWokaBodyPart.Eyes]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconEyes", 0.7)),
[CustomWokaBodyPart.Hair]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconHair")), [CustomWokaBodyPart.Hair]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconHair")),
[CustomWokaBodyPart.Hat]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconHat")), [CustomWokaBodyPart.Hat]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconHat")),
}; };
} }
private getDefaultIconButtonConfig(iconTextureKey: string): IconButtonConfig { private getDefaultIconButtonConfig(iconTextureKey: string, iconScale?: number): IconButtonConfig {
return { return {
iconTextureKey, iconTextureKey,
iconScale,
width: 25, width: 25,
height: 25, height: 25,
idle: { idle: {
@ -328,13 +352,14 @@ export class CustomizeScene extends AbstractCharacterScene {
} }
private handleCustomWokaPreviewerOnResize(): void { private handleCustomWokaPreviewerOnResize(): void {
const ratio = innerHeight / innerWidth;
this.customWokaPreviewer.x = this.cameras.main.worldView.x + this.cameras.main.width / 2; this.customWokaPreviewer.x = this.cameras.main.worldView.x + this.cameras.main.width / 2;
this.customWokaPreviewer.y = this.customWokaPreviewer.displayHeight * 0.5 + 10; this.customWokaPreviewer.y = this.customWokaPreviewer.displayHeight * 0.5 + (ratio > 1.6 ? 40 : 10);
} }
private handleBodyPartButtonsOnResize(): void { private handleBodyPartButtonsOnResize(): void {
const ratio = innerHeight / innerWidth; const ratio = innerHeight / innerWidth;
const slotDimension = 50; const slotDimension = WokaBodyPartSlot.SIZE;
for (const part in this.bodyPartsButtons) { for (const part in this.bodyPartsButtons) {
this.bodyPartsButtons[part as CustomWokaBodyPart].setDisplaySize(slotDimension, slotDimension); this.bodyPartsButtons[part as CustomWokaBodyPart].setDisplaySize(slotDimension, slotDimension);
@ -421,7 +446,7 @@ export class CustomizeScene extends AbstractCharacterScene {
private handleRandomizeButtonOnResize(): void { private handleRandomizeButtonOnResize(): void {
const x = const x =
this.customWokaPreviewer.x + this.customWokaPreviewer.x -
(this.customWokaPreviewer.displayWidth - this.randomizeButton.displayWidth) * 0.5; (this.customWokaPreviewer.displayWidth - this.randomizeButton.displayWidth) * 0.5;
const y = const y =
this.customWokaPreviewer.y + this.customWokaPreviewer.y +
@ -432,7 +457,7 @@ export class CustomizeScene extends AbstractCharacterScene {
private handleFinishButtonOnResize(): void { private handleFinishButtonOnResize(): void {
const x = const x =
this.customWokaPreviewer.x - this.customWokaPreviewer.x +
(this.customWokaPreviewer.displayWidth - this.randomizeButton.displayWidth) * 0.5; (this.customWokaPreviewer.displayWidth - this.randomizeButton.displayWidth) * 0.5;
const y = const y =
this.customWokaPreviewer.y + this.customWokaPreviewer.y +

View File

@ -13,6 +13,5 @@ export class EmptyScene extends Scene {
create() {} create() {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
update(time: number, delta: number): void {} update(time: number, delta: number): void {}
} }

View File

@ -24,7 +24,6 @@ export class EnableCameraScene extends ResizableScene {
public onResize(): void {} public onResize(): void {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
update(time: number, delta: number): void {} update(time: number, delta: number): void {}
public login(): void { public login(): void {

View File

@ -5,6 +5,7 @@ import { waScaleManager } from "../Services/WaScaleManager";
import { ReconnectingTextures } from "../Reconnecting/ReconnectingScene"; import { ReconnectingTextures } from "../Reconnecting/ReconnectingScene";
import { localeDetector } from "../../i18n/locales"; import { localeDetector } from "../../i18n/locales";
import { errorScreenStore } from "../../Stores/ErrorScreenStore"; import { errorScreenStore } from "../../Stores/ErrorScreenStore";
import { isErrorApiData } from "../../Messages/JsonMessages/ErrorApiData";
export const EntrySceneName = "EntryScene"; export const EntrySceneName = "EntryScene";
@ -45,8 +46,11 @@ export class EntryScene extends Scene {
this.scene.start(nextSceneName); this.scene.start(nextSceneName);
}) })
.catch((err) => { .catch((err) => {
if (err.response.data?.code) { const errorType = isErrorApiData.safeParse(err?.response?.data);
errorScreenStore.setError(err.response.data); if (errorType.success) {
if (errorType.data.type === "redirect") {
window.location.assign(errorType.data.urlToRedirect);
} else errorScreenStore.setError(err?.response?.data);
} else { } else {
ErrorScene.showError(err, this.scene); ErrorScene.showError(err, this.scene);
} }

View File

@ -49,7 +49,6 @@ export class LoginScene extends ResizableScene {
loginSceneVisibleStore.set(false); loginSceneVisibleStore.set(false);
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
update(time: number, delta: number): void {} update(time: number, delta: number): void {}
public onResize(): void {} public onResize(): void {}

View File

@ -1,58 +0,0 @@
import { SelectCharacterScene } from "./SelectCharacterScene";
export class SelectCharacterMobileScene extends SelectCharacterScene {
create() {
super.create();
this.onResize();
this.selectedRectangle.destroy();
}
protected defineSetupPlayer(num: number) {
const deltaX = 30;
const deltaY = 2;
let [playerX, playerY] = this.getCharacterPosition();
let playerVisible = true;
let playerScale = 1.5;
let playerOpacity = 1;
if (this.currentSelectUser !== num) {
playerVisible = false;
}
if (num === this.currentSelectUser + 1) {
playerY -= deltaY;
playerX += deltaX;
playerScale = 0.8;
playerOpacity = 0.6;
playerVisible = true;
}
if (num === this.currentSelectUser + 2) {
playerY -= deltaY;
playerX += deltaX * 2;
playerScale = 0.8;
playerOpacity = 0.6;
playerVisible = true;
}
if (num === this.currentSelectUser - 1) {
playerY -= deltaY;
playerX -= deltaX;
playerScale = 0.8;
playerOpacity = 0.6;
playerVisible = true;
}
if (num === this.currentSelectUser - 2) {
playerY -= deltaY;
playerX -= deltaX * 2;
playerScale = 0.8;
playerOpacity = 0.6;
playerVisible = true;
}
return { playerX, playerY, playerScale, playerOpacity, playerVisible };
}
/**
* Returns pixel position by on column and row number
*/
protected getCharacterPosition(): [number, number] {
return [this.game.renderer.width / 2, this.game.renderer.height / 3];
}
}

View File

@ -1,5 +1,4 @@
import { gameManager } from "../Game/GameManager"; import { gameManager } from "../Game/GameManager";
import Rectangle = Phaser.GameObjects.Rectangle;
import { EnableCameraSceneName } from "./EnableCameraScene"; import { EnableCameraSceneName } from "./EnableCameraScene";
import { CustomizeSceneName } from "./CustomizeScene"; import { CustomizeSceneName } from "./CustomizeScene";
import { localUserStore } from "../../Connexion/LocalUserStore"; import { localUserStore } from "../../Connexion/LocalUserStore";
@ -13,25 +12,26 @@ import { PinchManager } from "../UserInput/PinchManager";
import { selectCharacterSceneVisibleStore } from "../../Stores/SelectCharacterStore"; import { selectCharacterSceneVisibleStore } from "../../Stores/SelectCharacterStore";
import { waScaleManager } from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
import { analyticsClient } from "../../Administration/AnalyticsClient"; import { analyticsClient } from "../../Administration/AnalyticsClient";
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
import { PUSHER_URL } from "../../Enum/EnvironmentVariable"; import { PUSHER_URL } from "../../Enum/EnvironmentVariable";
import { customizeAvailableStore } from "../../Stores/SelectCharacterSceneStore"; import { customizeAvailableStore, selectedCollection } from "../../Stores/SelectCharacterSceneStore";
import { DraggableGrid } from "@home-based-studio/phaser3-utils";
import { WokaSlot } from "../Components/SelectWoka/WokaSlot";
import { DraggableGridEvent } from "@home-based-studio/phaser3-utils/lib/utils/gui/containers/grids/DraggableGrid";
import { wokaList } from "../../Messages/JsonMessages/PlayerTextures"; import { wokaList } from "../../Messages/JsonMessages/PlayerTextures";
import { myCameraVisibilityStore } from "../../Stores/MyCameraStoreVisibility";
//todo: put this constants in a dedicated file //todo: put this constants in a dedicated file
export const SelectCharacterSceneName = "SelectCharacterScene"; export const SelectCharacterSceneName = "SelectCharacterScene";
export class SelectCharacterScene extends AbstractCharacterScene { export class SelectCharacterScene extends AbstractCharacterScene {
protected readonly nbCharactersPerRow = 6; protected selectedWoka!: Phaser.GameObjects.Sprite | null; // null if we are selecting the "customize" option
protected selectedPlayer!: Phaser.Physics.Arcade.Sprite | null; // null if we are selecting the "customize" option
protected players: Array<Phaser.Physics.Arcade.Sprite> = new Array<Phaser.Physics.Arcade.Sprite>();
protected playerModels!: BodyResourceDescriptionInterface[]; protected playerModels!: BodyResourceDescriptionInterface[];
protected selectedRectangle!: Rectangle; private charactersDraggableGrid!: DraggableGrid;
private collectionKeys!: string[];
protected currentSelectUser = 0; private selectedCollectionIndex!: number;
protected pointerClicked: boolean = false; private selectedGridItemIndex?: number;
protected pointerTimer: number = 0; private gridRowsCount: number = 1;
protected lazyloadingAttempt = true; //permit to update texture loaded after renderer protected lazyloadingAttempt = true; //permit to update texture loaded after renderer
private loader: Loader; private loader: Loader;
@ -44,7 +44,8 @@ export class SelectCharacterScene extends AbstractCharacterScene {
this.playerTextures = new PlayerTextures(); this.playerTextures = new PlayerTextures();
} }
preload() { public preload() {
super.preload();
const wokaMetadataKey = "woka-list" + gameManager.currentStartedRoom.href; const wokaMetadataKey = "woka-list" + gameManager.currentStartedRoom.href;
this.cache.json.remove(wokaMetadataKey); this.cache.json.remove(wokaMetadataKey);
@ -67,228 +68,237 @@ export class SelectCharacterScene extends AbstractCharacterScene {
} }
) )
.catch((e) => console.error(e)); .catch((e) => console.error(e));
this.playerModels = loadAllDefaultModels(this.load, this.playerTextures);
this.lazyloadingAttempt = false;
//this function must stay at the end of preload function //this function must stay at the end of preload function
this.loader.addLoader(); this.loader.addLoader();
} }
create() { public create() {
waScaleManager.zoomModifier = 1;
this.selectedWoka = null;
this.selectedCollectionIndex = 0;
this.collectionKeys = this.playerTextures.getCollectionsKeys();
selectedCollection.set(this.getSelectedCollectionName());
customizeAvailableStore.set(this.isCustomizationAvailable()); customizeAvailableStore.set(this.isCustomizationAvailable());
selectCharacterSceneVisibleStore.set(true); selectCharacterSceneVisibleStore.set(true);
this.events.addListener("wake", () => {
waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 2 : 1;
selectCharacterSceneVisibleStore.set(true);
});
if (touchScreenManager.supportTouchScreen) { if (touchScreenManager.supportTouchScreen) {
new PinchManager(this); new PinchManager(this);
} }
waScaleManager.saveZoom(); this.charactersDraggableGrid = new DraggableGrid(this, {
waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 2 : 1; position: { x: 0, y: 0 },
maskPosition: { x: 0, y: 0 },
const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16; dimension: { x: 485, y: 165 },
this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xffffff); horizontal: true,
this.selectedRectangle.setDepth(2); repositionToCenter: true,
itemsInRow: 1,
/*create user*/ margin: {
this.createCurrentPlayer(); left: ((innerWidth - 200) / waScaleManager.getActualZoom()) * 0.5,
right: ((innerWidth - 200) / waScaleManager.getActualZoom()) * 0.5,
this.input.keyboard.on("keyup-ENTER", () => { },
return this.nextSceneToCameraScene(); spacing: 5,
debug: {
showDraggableSpace: false,
},
}); });
this.input.keyboard.on("keydown-RIGHT", () => { this.bindEventHandlers();
this.moveToRight();
}); this.onResize();
this.input.keyboard.on("keydown-LEFT", () => {
this.moveToLeft();
});
this.input.keyboard.on("keydown-UP", () => {
this.moveToUp();
});
this.input.keyboard.on("keydown-DOWN", () => {
this.moveToDown();
});
} }
public nextSceneToCameraScene(): void { public nextSceneToCameraScene(): void {
if (this.selectedPlayer !== null && !areCharacterLayersValid([this.selectedPlayer.texture.key])) { if (this.selectedWoka !== null && !areCharacterLayersValid([this.selectedWoka.texture.key])) {
return; return;
} }
if (!this.selectedPlayer) { if (!this.selectedWoka) {
return; return;
} }
analyticsClient.validationWoka("SelectWoka"); analyticsClient.validationWoka("SelectWoka");
gameManager.setCharacterLayers([this.selectedWoka.texture.key]);
this.selectedWoka = null;
this.scene.stop(SelectCharacterSceneName); this.scene.stop(SelectCharacterSceneName);
waScaleManager.restoreZoom();
gameManager.setCharacterLayers([this.selectedPlayer.texture.key]);
gameManager.tryResumingGame(EnableCameraSceneName); gameManager.tryResumingGame(EnableCameraSceneName);
this.players = [];
selectCharacterSceneVisibleStore.set(false); selectCharacterSceneVisibleStore.set(false);
this.events.removeListener("wake"); this.events.removeListener("wake");
} }
public nextSceneToCustomizeScene(): void { public nextSceneToCustomizeScene(): void {
if (this.selectedPlayer !== null && !areCharacterLayersValid([this.selectedPlayer.texture.key])) { if (this.selectedWoka !== null && !areCharacterLayersValid([this.selectedWoka.texture.key])) {
return; return;
} }
this.selectedWoka = null;
myCameraVisibilityStore.set(false);
this.scene.sleep(SelectCharacterSceneName); this.scene.sleep(SelectCharacterSceneName);
waScaleManager.restoreZoom();
this.scene.run(CustomizeSceneName); this.scene.run(CustomizeSceneName);
selectCharacterSceneVisibleStore.set(false); selectCharacterSceneVisibleStore.set(false);
} }
createCurrentPlayer(): void { public update(): void {
for (let i = 0; i < this.playerModels.length; i++) {
const playerResource = this.playerModels[i];
//check already exist texture
if (this.players.find((c) => c.texture.key === playerResource.id)) {
continue;
}
const [middleX, middleY] = this.getCharacterPosition();
const player = this.physics.add.sprite(middleX, middleY, playerResource.id, 0);
this.setUpPlayer(player, i);
this.anims.create({
key: playerResource.id,
frames: this.anims.generateFrameNumbers(playerResource.id, { start: 0, end: 11 }),
frameRate: 8,
repeat: -1,
});
player.setInteractive().on("pointerdown", () => {
if (this.pointerClicked) {
return;
}
if (this.currentSelectUser === i) {
return;
}
//To not trigger two time the pointerdown events :
// We set a boolean to true so that pointerdown events does nothing when the boolean is true
// We set a timer that we decrease in update function to not trigger the pointerdown events twice
this.pointerClicked = true;
this.pointerTimer = 250;
this.currentSelectUser = i;
this.moveUser();
});
this.players.push(player);
}
if (this.currentSelectUser >= this.players.length) {
this.currentSelectUser = 0;
}
this.selectedPlayer = this.players[this.currentSelectUser];
this.selectedPlayer.play(this.playerModels[this.currentSelectUser].id);
}
protected moveUser() {
for (let i = 0; i < this.players.length; i++) {
const player = this.players[i];
this.setUpPlayer(player, i);
}
this.updateSelectedPlayer();
}
public moveToLeft() {
if (this.currentSelectUser === 0) {
return;
}
this.currentSelectUser -= 1;
this.moveUser();
}
public moveToRight() {
if (this.currentSelectUser === this.players.length - 1) {
return;
}
this.currentSelectUser += 1;
this.moveUser();
}
protected moveToUp() {
if (this.currentSelectUser < this.nbCharactersPerRow) {
return;
}
this.currentSelectUser -= this.nbCharactersPerRow;
this.moveUser();
}
protected moveToDown() {
if (this.currentSelectUser + this.nbCharactersPerRow > this.players.length - 1) {
return;
}
this.currentSelectUser += this.nbCharactersPerRow;
this.moveUser();
}
protected defineSetupPlayer(num: number) {
const deltaX = 32;
const deltaY = 32;
let [playerX, playerY] = this.getCharacterPosition(); // player X and player y are middle of the
playerX = playerX - deltaX * 2.5 + deltaX * (num % this.nbCharactersPerRow); // calcul position on line users
playerY = playerY - deltaY * 2 + deltaY * Math.floor(num / this.nbCharactersPerRow); // calcul position on column users
const playerVisible = true;
const playerScale = 1;
const playerOpacity = 1;
// if selected
if (num === this.currentSelectUser) {
this.selectedRectangle.setX(playerX);
this.selectedRectangle.setY(playerY);
}
return { playerX, playerY, playerScale, playerOpacity, playerVisible };
}
protected setUpPlayer(player: Phaser.Physics.Arcade.Sprite, num: number) {
const { playerX, playerY, playerScale, playerOpacity, playerVisible } = this.defineSetupPlayer(num);
player.setBounce(0.2);
player.setCollideWorldBounds(false);
player.setVisible(playerVisible);
player.setScale(playerScale, playerScale);
player.setAlpha(playerOpacity);
player.setX(playerX);
player.setY(playerY);
}
/**
* Returns pixel position by on column and row number
*/
protected getCharacterPosition(): [number, number] {
return [this.game.renderer.width / 2, this.game.renderer.height / 2.5];
}
protected updateSelectedPlayer(): void {
this.selectedPlayer?.anims?.pause(this.selectedPlayer?.anims.currentAnim.frames[0]);
const player = this.players[this.currentSelectUser];
player?.play(this.playerModels[this.currentSelectUser].id);
this.selectedPlayer = player;
localUserStore.setPlayerCharacterIndex(this.currentSelectUser);
}
update(time: number, delta: number): void {
// pointerTimer is set to 250 when pointerdown events is trigger
// After 250ms, pointerClicked is set to false and the pointerdown events can be trigger again
this.pointerTimer -= delta;
if (this.pointerTimer <= 0) {
this.pointerClicked = false;
}
if (this.lazyloadingAttempt) { if (this.lazyloadingAttempt) {
//re-render players list
this.createCurrentPlayer();
this.moveUser();
this.lazyloadingAttempt = false; this.lazyloadingAttempt = false;
} }
} }
public onResize(): void { public onResize(): void {
//move position of user this.handleCharactersGridOnResize();
this.moveUser(); }
public getSelectedCollectionName(): string {
return this.collectionKeys[this.selectedCollectionIndex] ?? "";
}
public getCollectionKeysSize(): number {
return this.playerTextures.getCollectionsKeys().length;
}
public selectPreviousCollection(): void {
this.selectedCollectionIndex = (this.selectedCollectionIndex + 1) % this.collectionKeys.length;
selectedCollection.set(this.getSelectedCollectionName());
this.populateGrid();
}
public selectNextCollection(): void {
if (this.collectionKeys.length === 1) {
return;
}
this.selectedCollectionIndex =
this.selectedCollectionIndex - 1 < 0 ? this.collectionKeys.length - 1 : this.selectedCollectionIndex - 1;
selectedCollection.set(this.getSelectedCollectionName());
this.populateGrid();
}
private handleCharactersGridOnResize(): void {
const ratio = innerHeight / innerWidth;
this.gridRowsCount = ratio > 1 || innerHeight > 900 ? 2 : 1;
const gridHeight = this.gridRowsCount === 2 ? 210 : 105;
const gridWidth = innerWidth / waScaleManager.getActualZoom();
const gridPos = {
x: this.cameras.main.worldView.x + this.cameras.main.width / 2,
y: this.cameras.main.worldView.y + this.cameras.main.height * (ratio > 1 ? 0.5 : 0.575),
};
try {
this.charactersDraggableGrid.changeDraggableSpacePosAndSize(
gridPos,
{ x: gridWidth, y: gridHeight },
gridPos
);
} catch (error) {
console.warn(error);
}
this.charactersDraggableGrid.setItemsInRow(this.gridRowsCount);
this.populateGrid();
}
private populateGrid(): void {
const wokaDimension = 100;
this.selectedWoka = null;
this.charactersDraggableGrid.clearAllItems();
const textures = this.playerTextures.getWokaCollectionTextures(this.getSelectedCollectionName());
for (let i = 0; i < textures.length; i += 1) {
const slot = new WokaSlot(this, textures[i].id).setDisplaySize(wokaDimension, wokaDimension);
this.charactersDraggableGrid.addItem(slot);
}
this.charactersDraggableGrid.moveContentToBeginning();
void this.charactersDraggableGrid.moveContentTo(0.5, textures.length * 50);
}
private bindEventHandlers(): void {
this.bindKeyboardEventHandlers();
this.events.addListener("wake", () => {
selectCharacterSceneVisibleStore.set(true);
});
this.input.keyboard.on("keyup-ENTER", () => {
return this.nextSceneToCameraScene();
});
this.charactersDraggableGrid.on(DraggableGridEvent.ItemClicked, (item: WokaSlot) => {
this.selectGridItem(item);
});
}
private selectGridItem(item: WokaSlot): void {
this.selectedGridItemIndex = this.charactersDraggableGrid.getAllItems().indexOf(item);
if (this.charactersDraggableGrid.getDraggableSpaceWidth() < this.charactersDraggableGrid.getGridSize().x) {
void this.charactersDraggableGrid.centerOnItem(this.selectedGridItemIndex, 500);
}
this.charactersDraggableGrid.getAllItems().forEach((slot) => (slot as WokaSlot).select(false));
this.selectedWoka?.stop()?.setFrame(0);
this.selectedWoka = item.getSprite();
const wokaKey = this.selectedWoka.texture.key;
this.createWokaAnimation(wokaKey);
this.selectedWoka.play(wokaKey);
item.select(true);
}
private bindKeyboardEventHandlers(): void {
this.input.keyboard.on("keyup-SPACE", () => {
this.selectNextCollection();
});
this.input.keyboard.on("keydown-LEFT", () => {
this.selectNextGridItem(true, true);
});
this.input.keyboard.on("keydown-RIGHT", () => {
this.selectNextGridItem(false, true);
});
this.input.keyboard.on("keydown-UP", () => {
this.selectNextGridItem(true, false);
});
this.input.keyboard.on("keydown-DOWN", () => {
this.selectNextGridItem(false, false);
});
this.input.keyboard.on("keydown-W", () => {
this.selectNextGridItem(true, false);
});
this.input.keyboard.on("keydown-S", () => {
this.selectNextGridItem(false, false);
});
this.input.keyboard.on("keydown-A", () => {
this.selectNextGridItem(true, true);
});
this.input.keyboard.on("keydown-D", () => {
this.selectNextGridItem(false, true);
});
}
private selectNextGridItem(previous: boolean = false, horizontally: boolean): void {
if (this.selectedGridItemIndex === undefined) {
this.selectedGridItemIndex = 0;
}
if (
previous
? this.selectedGridItemIndex > 0
: this.selectedGridItemIndex < this.charactersDraggableGrid.getAllItems().length - 1
) {
// NOTE: getItemsInRowCount() not working properly. Fix on lib side needed
const jump = horizontally ? this.gridRowsCount : 1;
const item = this.charactersDraggableGrid.getAllItems()[
this.selectedGridItemIndex + (previous ? -jump : jump)
] as WokaSlot;
if (!item) {
return;
}
this.selectedGridItemIndex += previous ? -1 : 1;
this.selectGridItem(item);
}
}
private createWokaAnimation(key: string): void {
this.anims.create({
key,
frames: this.anims.generateFrameNumbers(key, { start: 0, end: 11 }),
frameRate: 8,
repeat: -1,
});
} }
private isCustomizationAvailable(): boolean { private isCustomizationAvailable(): boolean {

View File

@ -28,7 +28,6 @@ export class Player extends Character {
companionTexturePromise?: CancelablePromise<string> companionTexturePromise?: CancelablePromise<string>
) { ) {
super(Scene, x, y, texturesPromise, name, direction, moving, 1, true, companion, companionTexturePromise); super(Scene, x, y, texturesPromise, name, direction, moving, 1, true, companion, companionTexturePromise);
this.statusDot.setVisible(false);
//the current player model should be push away by other players to prevent conflict //the current player model should be push away by other players to prevent conflict
this.getBody().setImmovable(false); this.getBody().setImmovable(false);
} }

View File

@ -16,7 +16,6 @@ export class GameSceneUserInputHandler implements UserInputHandlerInterface {
gameObjects: Phaser.GameObjects.GameObject[], gameObjects: Phaser.GameObjects.GameObject[],
deltaX: number, deltaX: number,
deltaY: number, deltaY: number,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
deltaZ: number deltaZ: number
): void { ): void {
this.gameScene.zoomByFactor(1 - (deltaY / 53) * 0.1); this.gameScene.zoomByFactor(1 - (deltaY / 53) * 0.1);
@ -51,7 +50,6 @@ export class GameSceneUserInputHandler implements UserInputHandlerInterface {
}); });
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public handlePointerDownEvent(pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]): void {} public handlePointerDownEvent(pointer: Phaser.Input.Pointer, gameObjects: Phaser.GameObjects.GameObject[]): void {}
public handleSpaceKeyUpEvent(event: Event): Event { public handleSpaceKeyUpEvent(event: Event): Event {

View File

@ -87,7 +87,10 @@ function createChatMessagesStore() {
return list; return list;
}); });
}, },
addExternalMessage(authorId: number, text: string) { /**
* @param origin The iframe that originated this message (if triggered from the Scripting API), or undefined otherwise.
*/
addExternalMessage(authorId: number, text: string, origin?: Window) {
update((list) => { update((list) => {
const lastMessage = list[list.length - 1]; const lastMessage = list[list.length - 1];
if ( if (
@ -106,7 +109,7 @@ function createChatMessagesStore() {
}); });
} }
iframeListener.sendUserInputChat(text); iframeListener.sendUserInputChat(text, origin);
return list; return list;
}); });
chatVisibilityStore.set(true); chatVisibilityStore.set(true);

View File

@ -9,7 +9,9 @@ function createErrorScreenStore() {
return { return {
subscribe, subscribe,
setError: (e: ErrorScreenMessage): void => set(e), setError: (e: ErrorScreenMessage): void => {
set(e);
},
}; };
} }

View File

@ -11,6 +11,7 @@ import { peerStore } from "./PeerStore";
import { privacyShutdownStore } from "./PrivacyShutdownStore"; import { privacyShutdownStore } from "./PrivacyShutdownStore";
import { MediaStreamConstraintsError } from "./Errors/MediaStreamConstraintsError"; import { MediaStreamConstraintsError } from "./Errors/MediaStreamConstraintsError";
import { SoundMeter } from "../Phaser/Components/SoundMeter"; import { SoundMeter } from "../Phaser/Components/SoundMeter";
import { AvailabilityStatus } from "../Messages/ts-proto-generated/protos/messages";
import deepEqual from "fast-deep-equal"; import deepEqual from "fast-deep-equal";
/** /**
@ -179,10 +180,19 @@ function createVideoConstraintStore() {
}; };
} }
/** export const inJitsiStore = writable(false);
* A store containing if user is silent, so if he is in silent zone. This permit to show et hide camera of user export const silentStore = writable(false);
*/
export const isSilentStore = writable(false); export const availabilityStatusStore = derived(
[inJitsiStore, silentStore, privacyShutdownStore],
([$inJitsiStore, $silentStore, $privacyShutdownStore]) => {
if ($inJitsiStore) return AvailabilityStatus.JITSI;
if ($silentStore) return AvailabilityStatus.SILENT;
if ($privacyShutdownStore) return AvailabilityStatus.AWAY;
return AvailabilityStatus.ONLINE;
},
AvailabilityStatus.ONLINE
);
export const videoConstraintStore = createVideoConstraintStore(); export const videoConstraintStore = createVideoConstraintStore();
@ -241,7 +251,7 @@ export const mediaStreamConstraintsStore = derived(
audioConstraintStore, audioConstraintStore,
privacyShutdownStore, privacyShutdownStore,
cameraEnergySavingStore, cameraEnergySavingStore,
isSilentStore, availabilityStatusStore,
], ],
( (
[ [
@ -253,7 +263,7 @@ export const mediaStreamConstraintsStore = derived(
$audioConstraintStore, $audioConstraintStore,
$privacyShutdownStore, $privacyShutdownStore,
$cameraEnergySavingStore, $cameraEnergySavingStore,
$isSilentStore, $availabilityStatusStore,
], ],
set set
) => { ) => {
@ -310,7 +320,7 @@ export const mediaStreamConstraintsStore = derived(
//currentAudioConstraint = false; //currentAudioConstraint = false;
} }
if ($isSilentStore === true) { if ($availabilityStatusStore === AvailabilityStatus.SILENT) {
currentVideoConstraint = false; currentVideoConstraint = false;
currentAudioConstraint = false; currentAudioConstraint = false;
} }

View File

@ -2,6 +2,7 @@ import { writable } from "svelte/store";
import type { PlayerInterface } from "../Phaser/Game/PlayerInterface"; import type { PlayerInterface } from "../Phaser/Game/PlayerInterface";
import type { RoomConnection } from "../Connexion/RoomConnection"; import type { RoomConnection } from "../Connexion/RoomConnection";
import { getRandomColor } from "../WebRtc/ColorGenerator"; import { getRandomColor } from "../WebRtc/ColorGenerator";
import { AvailabilityStatus } from "../Messages/ts-proto-generated/protos/messages";
let idCount = 0; let idCount = 0;
@ -28,7 +29,7 @@ function createPlayersStore() {
visitCardUrl: message.visitCardUrl, visitCardUrl: message.visitCardUrl,
companion: message.companion, companion: message.companion,
userUuid: message.userUuid, userUuid: message.userUuid,
away: message.away, status: message.status,
color: getRandomColor(), color: getRandomColor(),
}); });
return users; return users;
@ -58,7 +59,7 @@ function createPlayersStore() {
characterLayers: [], characterLayers: [],
visitCardUrl: null, visitCardUrl: null,
companion: null, companion: null,
away: false, status: AvailabilityStatus.ONLINE,
userUuid: "dummy", userUuid: "dummy",
color: getRandomColor(), color: getRandomColor(),
}); });

View File

@ -2,6 +2,7 @@ import { derived, Readable, readable, writable } from "svelte/store";
import { peerStore } from "./PeerStore"; import { peerStore } from "./PeerStore";
import type { LocalStreamStoreValue } from "./MediaStore"; import type { LocalStreamStoreValue } from "./MediaStore";
import { myCameraVisibilityStore } from "./MyCameraStoreVisibility"; import { myCameraVisibilityStore } from "./MyCameraStoreVisibility";
import type { DesktopCapturerSource } from "@wa-preload-app";
declare const navigator: any; // eslint-disable-line @typescript-eslint/no-explicit-any declare const navigator: any; // eslint-disable-line @typescript-eslint/no-explicit-any
@ -91,6 +92,25 @@ export const screenSharingConstraintsStore = derived(
} as MediaStreamConstraints } as MediaStreamConstraints
); );
async function getDesktopCapturerSources() {
showDesktopCapturerSourcePicker.set(true);
const source = await new Promise<DesktopCapturerSource | null>((resolve) => {
desktopCapturerSourcePromiseResolve = resolve;
});
if (source === null) {
return;
}
return navigator.mediaDevices.getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: "desktop",
chromeMediaSourceId: source.id,
},
},
});
}
/** /**
* A store containing the MediaStream object for ScreenSharing (or null if nothing requested, or Error if an error occurred) * A store containing the MediaStream object for ScreenSharing (or null if nothing requested, or Error if an error occurred)
*/ */
@ -110,7 +130,9 @@ export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstra
} }
let currentStreamPromise: Promise<MediaStream>; let currentStreamPromise: Promise<MediaStream>;
if (navigator.getDisplayMedia) { if (window.WAD?.getDesktopCapturerSources) {
currentStreamPromise = getDesktopCapturerSources();
} else if (navigator.getDisplayMedia) {
currentStreamPromise = navigator.getDisplayMedia({ constraints }); currentStreamPromise = navigator.getDisplayMedia({ constraints });
} else if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) { } else if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
currentStreamPromise = navigator.mediaDevices.getDisplayMedia({ constraints }); currentStreamPromise = navigator.mediaDevices.getDisplayMedia({ constraints });
@ -200,3 +222,7 @@ export const screenSharingLocalMedia = readable<ScreenSharingLocalMedia | null>(
unsubscribe(); unsubscribe();
}; };
}); });
export const showDesktopCapturerSourcePicker = writable(false);
export let desktopCapturerSourcePromiseResolve: ((source: DesktopCapturerSource | null) => void) | undefined;

View File

@ -1,3 +1,5 @@
import { writable } from "svelte/store"; import { writable } from "svelte/store";
export const customizeAvailableStore = writable(false); export const customizeAvailableStore = writable(false);
export const selectedCollection = writable<string>();

View File

@ -14,15 +14,6 @@ export function getColorRgbFromHue(hue: number): { r: number; g: number; b: numb
return hsv_to_rgb(hue, 0.5, 0.95); return hsv_to_rgb(hue, 0.5, 0.95);
} }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function stringToDouble(string: string): number {
let num = 1;
for (const char of string.split("")) {
num *= char.charCodeAt(0);
}
return (num % 255) / 255;
}
//todo: test this. //todo: test this.
function hsv_to_rgb(hue: number, saturation: number, brightness: number): { r: number; g: number; b: number } { function hsv_to_rgb(hue: number, saturation: number, brightness: number): { r: number; g: number; b: number } {
const h_i = Math.floor(hue * 6); const h_i = Math.floor(hue * 6);

View File

@ -1,3 +1 @@
i18n-svelte.ts i18n-*.ts
i18n-types.ts
i18n-util.ts

View File

@ -16,8 +16,6 @@ import trigger from "./trigger";
const de_DE: Translation = { const de_DE: Translation = {
...(en_US as Translation), ...(en_US as Translation),
language: "Deutsch",
country: "Deutschland",
audio, audio,
camera, camera,
chat, chat,

View File

@ -14,8 +14,6 @@ import emoji from "./emoji";
import trigger from "./trigger"; import trigger from "./trigger";
const en_US: BaseTranslation = { const en_US: BaseTranslation = {
language: "English",
country: "United States",
audio, audio,
camera, camera,
chat, chat,

View File

@ -1,8 +1,8 @@
import type { AsyncFormattersInitializer } from "typesafe-i18n"; import type { FormattersInitializer } from "typesafe-i18n";
import type { Locales, Formatters } from "./i18n-types"; import type { Locales, Formatters } from "./i18n-types";
// eslint-disable-next-line @typescript-eslint/require-await // eslint-disable-next-line @typescript-eslint/require-await
export const initFormatters: AsyncFormattersInitializer<Locales, Formatters> = async () => { export const initFormatters: FormattersInitializer<Locales, Formatters> = async () => {
const formatters: Formatters = { const formatters: Formatters = {
// add your formatter functions here // add your formatter functions here
}; };

View File

@ -16,8 +16,6 @@ import trigger from "./trigger";
const fr_FR: Translation = { const fr_FR: Translation = {
...(en_US as Translation), ...(en_US as Translation),
language: "Français",
country: "France",
audio, audio,
camera, camera,
chat, chat,

View File

@ -1,52 +1,44 @@
import { detectLocale, navigatorDetector, initLocalStorageDetector } from "typesafe-i18n/detectors"; import { detectLocale, navigatorDetector, initLocalStorageDetector } from "typesafe-i18n/detectors";
import { FALLBACK_LOCALE } from "../Enum/EnvironmentVariable"; import { FALLBACK_LOCALE } from "../Enum/EnvironmentVariable";
import { initI18n, setLocale } from "./i18n-svelte"; import { setLocale } from "./i18n-svelte";
import type { Locales } from "./i18n-types"; import type { Locales } from "./i18n-types";
import { baseLocale, getTranslationForLocale, locales } from "./i18n-util"; import { baseLocale, locales } from "./i18n-util";
import { loadLocaleAsync } from "./i18n-util.async";
const fallbackLocale = FALLBACK_LOCALE || baseLocale; const fallbackLocale = (FALLBACK_LOCALE || baseLocale) as Locales;
const localStorageProperty = "language"; const localStorageProperty = "language";
export const localeDetector = async () => { export const localeDetector = async () => {
const exist = localStorage.getItem(localStorageProperty); const exist = localStorage.getItem(localStorageProperty);
let detectedLocale: Locales = fallbackLocale as Locales; let detectedLocale: Locales = fallbackLocale;
if (exist) { if (exist) {
const localStorageDetector = initLocalStorageDetector(localStorageProperty); const localStorageDetector = initLocalStorageDetector(localStorageProperty);
detectedLocale = detectLocale(fallbackLocale, locales, localStorageDetector) as Locales; detectedLocale = detectLocale(fallbackLocale, locales, localStorageDetector);
} else { } else {
detectedLocale = detectLocale(fallbackLocale, locales, navigatorDetector) as Locales; detectedLocale = detectLocale(fallbackLocale, locales, navigatorDetector);
} }
await initI18n(detectedLocale); await setCurrentLocale(detectedLocale);
}; };
export const setCurrentLocale = (locale: Locales) => { export const setCurrentLocale = async (locale: Locales) => {
localStorage.setItem(localStorageProperty, locale); localStorage.setItem(localStorageProperty, locale);
setLocale(locale).catch(() => { await loadLocaleAsync(locale);
console.log("Cannot reload the locale!"); setLocale(locale);
});
}; };
export type DisplayableLocale = { id: Locales; language: string; country: string }; export const displayableLocales: { id: Locales; language: string; region: string }[] = locales.map((locale) => {
const [language, region] = locale.split("-");
function getDisplayableLocales() { // backwards compatibility
const localesObject: DisplayableLocale[] = []; if (!Intl.DisplayNames) {
locales.forEach((locale) => { return { id: locale, language, region };
getTranslationForLocale(locale) }
.then((translations) => {
localesObject.push({
id: locale,
language: translations.language,
country: translations.country,
});
})
.catch((error) => {
console.log(error);
});
});
return localesObject; return {
} id: locale,
language: new Intl.DisplayNames(locale, { type: "language" }).of(language),
export const displayableLocales = getDisplayableLocales(); region: new Intl.DisplayNames(locale, { type: "region" }).of(region),
};
});

View File

@ -16,8 +16,6 @@ import trigger from "./trigger";
const zh_CN: Translation = { const zh_CN: Translation = {
...(en_US as Translation), ...(en_US as Translation),
language: "中文",
country: "中国",
audio, audio,
camera, camera,
chat, chat,

View File

@ -17,14 +17,12 @@ 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";
import { desktopApi } from "./Api/desktop/index"; import { desktopApi } from "./Api/desktop/index";
import { SelectCharacterMobileScene } from "./Phaser/Login/SelectCharacterMobileScene";
import { HdpiManager } from "./Phaser/Services/HdpiManager"; import { HdpiManager } from "./Phaser/Services/HdpiManager";
import { waScaleManager } from "./Phaser/Services/WaScaleManager"; import { waScaleManager } from "./Phaser/Services/WaScaleManager";
import { Game } from "./Phaser/Game/Game"; import { Game } from "./Phaser/Game/Game";
import App from "./Components/App.svelte"; import App from "./Components/App.svelte";
import { HtmlUtils } from "./WebRtc/HtmlUtils"; import { HtmlUtils } from "./WebRtc/HtmlUtils";
import WebGLRenderer = Phaser.Renderer.WebGL.WebGLRenderer; import WebGLRenderer = Phaser.Renderer.WebGL.WebGLRenderer;
import { isMediaBreakpointUp } from "./Utils/BreakpointsUtils";
const { width, height } = coWebsiteManager.getGameSize(); const { width, height } = coWebsiteManager.getGameSize();
const valueGameQuality = localUserStore.getGameQualityValue(); const valueGameQuality = localUserStore.getGameQualityValue();
@ -91,7 +89,7 @@ const config: GameConfig = {
scene: [ scene: [
EntryScene, EntryScene,
LoginScene, LoginScene,
isMediaBreakpointUp("md") ? SelectCharacterMobileScene : SelectCharacterScene, SelectCharacterScene,
SelectCompanionScene, SelectCompanionScene,
EnableCameraScene, EnableCameraScene,
ReconnectingScene, ReconnectingScene,

View File

@ -92,7 +92,7 @@
&-buffer { &-buffer {
iframe { iframe {
z-index: 45 !important; z-index: 201 !important;
pointer-events: none !important; pointer-events: none !important;
overflow: hidden; overflow: hidden;
border: 0; border: 0;

View File

@ -8,7 +8,7 @@
"moduleResolution": "node", "moduleResolution": "node",
//"module": "CommonJS", //"module": "CommonJS",
"module": "ESNext", "module": "ESNext",
"target": "ES2017", "target": "ES2020",
"declaration": false, "declaration": false,
"downlevelIteration": true, "downlevelIteration": true,
"jsx": "react", "jsx": "react",

View File

@ -77,10 +77,10 @@
resolved "https://registry.yarnpkg.com/@geprog/vite-plugin-env-config/-/vite-plugin-env-config-4.0.3.tgz#ca04bd9ad9f55fe568917db79266afe8e766e25e" resolved "https://registry.yarnpkg.com/@geprog/vite-plugin-env-config/-/vite-plugin-env-config-4.0.3.tgz#ca04bd9ad9f55fe568917db79266afe8e766e25e"
integrity sha512-2HDCV+6XXJjSuBAhDWLRr111buMQ3bIZrKo3dymIhEJ4oJCC/3yDqg7HDQIn8Y8KKbsM0AtuHMZW4yz2tPBsYg== integrity sha512-2HDCV+6XXJjSuBAhDWLRr111buMQ3bIZrKo3dymIhEJ4oJCC/3yDqg7HDQIn8Y8KKbsM0AtuHMZW4yz2tPBsYg==
"@home-based-studio/phaser3-utils@^0.4.2": "@home-based-studio/phaser3-utils@^0.4.7":
version "0.4.2" version "0.4.7"
resolved "https://registry.yarnpkg.com/@home-based-studio/phaser3-utils/-/phaser3-utils-0.4.2.tgz#b2c1815a6b51321ea8dab027b5badcf714d99fd6" resolved "https://registry.yarnpkg.com/@home-based-studio/phaser3-utils/-/phaser3-utils-0.4.7.tgz#d0464c81cb27328657d3fd048396f6936e200c48"
integrity sha512-S0VkAq3z0Kf0vEUUyCDes911icvc+nkUq7lGp23zD/5lk7LTGM51NswSAfel7Rm/DLY8IBxvDTBJADTf/De82w== integrity sha512-gYt1mkuad85uzYwHK0+wp+mrsGASV4sRZPaHZHnO8A2ofTAnX36S3PcI+BqKchdJ0I7jvBQcfh0yp1Ug0BHT+A==
dependencies: dependencies:
phaser "3.55.1" phaser "3.55.1"
@ -806,10 +806,10 @@ debug@~3.1.0:
dependencies: dependencies:
ms "2.0.0" ms "2.0.0"
deep-copy-ts@^0.5.0: deep-copy-ts@^0.5.4:
version "0.5.0" version "0.5.4"
resolved "https://registry.yarnpkg.com/deep-copy-ts/-/deep-copy-ts-0.5.0.tgz#b9493d8e2bae85ef7d659c16eb707c13efb84499" resolved "https://registry.yarnpkg.com/deep-copy-ts/-/deep-copy-ts-0.5.4.tgz#e81b15797e4075cb3a690a1a7ac30179f2d72562"
integrity sha512-/3cgBcMkznRf5BM8wu6YWz3SQUkHzgh/v1TZFjevztLj9sMjFvNFBtpN4uUtPzw/rA/TldyD6c6LRL1zno4+YA== integrity sha512-YJbPjw0YqdosorpCsa6copy1p/gJsFT9Q6Zq0tLi7D0nXh6Y/usjeIQZfkzV3HVuqY0Hl/5gM7TwgIbIWvEjlA==
deep-equal@^1.0.1: deep-equal@^1.0.1:
version "1.1.1" version "1.1.1"
@ -2991,10 +2991,10 @@ type-fest@^0.21.3:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
typesafe-i18n@^2.59.0: typesafe-i18n@^5.4.0:
version "2.59.0" version "5.4.0"
resolved "https://registry.yarnpkg.com/typesafe-i18n/-/typesafe-i18n-2.59.0.tgz#09a9a32e61711418d927a389fa52e1c06a5fa5c4" resolved "https://registry.yarnpkg.com/typesafe-i18n/-/typesafe-i18n-5.4.0.tgz#cab696160bb144c387d7cbd13f7a728aa8371777"
integrity sha512-Qv3Mrwmb8b73VNzQDPHPECzwymdBRVyDiZ3w2qnp4c2iv/7TGuiJegNHT/l3MooEN7IPbSpc5tbXw2x3MbGtFg== integrity sha512-htewpld3FzZQv3Y1G31w54bofaaKR11MCkDK0FIYuXCpX72y1G6fkXUDslqzZCyVkZWRnIhY8leviNDxLwEzRw==
typescript@*: typescript@*:
version "4.3.2" version "4.3.2"

View File

@ -1,7 +1,7 @@
let menuIframeApi = undefined; let menuIframeApi = undefined;
WA.ui.registerMenuCommand('custom callback menu', () => { WA.ui.registerMenuCommand('custom callback menu', () => {
WA.nav.openTab("https://workadventu.re/"); WA.chat.sendChatMessage('Custom menu clicked', 'Mr Robot');
}) })
WA.ui.registerMenuCommand('custom iframe menu', {iframe: 'customIframeMenu.html'}); WA.ui.registerMenuCommand('custom iframe menu', {iframe: 'customIframeMenu.html'});

Some files were not shown because too many files have changed in this diff Show More