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

View File

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

View File

@ -144,9 +144,8 @@ export class GameRoom {
joinRoomMessage.getUseruuid(),
joinRoomMessage.getIpaddress(),
position,
false,
this.positionNotifier,
joinRoomMessage.getAway(),
joinRoomMessage.getStatus(),
socket,
joinRoomMessage.getTagList(),
joinRoomMessage.getVisitcardurl(),
@ -208,6 +207,9 @@ export class GameRoom {
updatePlayerDetails(user: User, playerDetailsMessage: SetPlayerDetailsMessage) {
user.updateDetails(playerDetailsMessage);
if (user.group !== undefined && user.silent) {
this.leaveGroup(user);
}
}
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.
*
@ -572,11 +559,7 @@ export class GameRoom {
return {
mapUrl,
policy_type: 1,
tags: [],
authenticationMandatory: null,
roomSlug: null,
contactPage: null,
group: null,
};
}

View File

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

View File

@ -71,7 +71,6 @@ export class Zone {
/**
* 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) {
for (const listener of this.listeners) {
this.onEnters(thing, oldZone, listener);

View File

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

View File

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

View File

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

View File

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

View File

@ -30,7 +30,7 @@ ADMIN_API_URL=
DATA_DIR=./wa
# 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,
# 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
- MAX_PER_GROUP
- STORE_VARIABLES_FOR_LOCAL_MAPS
- REDIS_HOST=redis
labels:
- "traefik.http.routers.back.rule=Host(`${BACK_HOST}`)"
- "traefik.http.routers.back.entryPoints=web"
@ -117,3 +118,11 @@ services:
- "traefik.http.routers.icon-ssl.service=icon"
- "traefik.http.routers.icon-ssl.tls=true"
- "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
owner: thecodingmachine
repo: workadventure
vPrefixedTagName: false
releaseType: draft
releaseType: release

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

View File

@ -10,7 +10,7 @@ import { setLogLevel } from "./log";
import "./serve"; // prepare custom url scheme
import { loadShortcuts } from "./shortcuts";
function init() {
async function init() {
const appLock = app.requestSingleInstanceLock();
if (!appLock) {
@ -21,7 +21,7 @@ function init() {
app.on("second-instance", () => {
// re-create window if closed
createWindow();
void createWindow();
const mainWindow = getWindow();
@ -36,15 +36,15 @@ function init() {
});
// This method will be called when Electron has finished loading
app.whenReady().then(async () => {
await app.whenReady().then(async () => {
await settings.init();
setLogLevel(settings.get("log_level") || "info");
autoUpdater.init();
await autoUpdater.init();
// enable auto launch
updateAutoLaunch();
await updateAutoLaunch();
// load ipc handler
ipc();
@ -72,7 +72,7 @@ function init() {
// 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.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
void createWindow();
}
});

View File

@ -38,32 +38,35 @@ export async function manualRequestUpdateCheck() {
isManualRequestedUpdate = false;
}
function init() {
async function init() {
autoUpdater.logger = log;
autoUpdater.on("update-downloaded", ({ releaseNotes, releaseName }) => {
(async () => {
const dialogOpts = {
type: "question",
buttons: ["Install and Restart", "Install Later"],
defaultId: 0,
title: "WorkAdventure - Update",
message: process.platform === "win32" ? releaseNotes : releaseName,
detail: "A new version has been downloaded. Restart the application to apply the updates.",
};
autoUpdater.on(
"update-downloaded",
({ releaseNotes, releaseName }: { releaseNotes: string; releaseName: string }) => {
void (async () => {
const dialogOpts = {
type: "question",
buttons: ["Install and Restart", "Install Later"],
defaultId: 0,
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);
if (response === 0) {
await sleep(1000);
const { response } = await dialog.showMessageBox(dialogOpts);
if (response === 0) {
await sleep(1000);
autoUpdater.quitAndInstall();
autoUpdater.quitAndInstall();
// Force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
// app.confirmedExitPrompt = true;
app.quit();
}
})();
});
// Force app to quit. This is just a workaround, ideally autoUpdater.quitAndInstall() should relaunch the app.
// app.confirmedExitPrompt = true;
app.quit();
}
})();
}
);
if (process.platform === "linux" && !process.env.APPIMAGE) {
autoUpdater.autoDownload = false;
@ -85,7 +88,7 @@ function init() {
}
});
checkForUpdates();
await checkForUpdates();
// run update check every hour again
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 { createAndShowNotification } from "./notification";
import { Server } from "./preload-local-app/types";
@ -30,10 +30,18 @@ export default () => {
ipcMain.handle("get-version", () => (electronIsDev ? "dev" : app.getVersion()));
// app ipc
ipcMain.on("app:notify", (event, txt) => {
ipcMain.on("app:notify", (event, txt: string) => {
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
ipcMain.handle("local-app:showLocalApp", () => {
hideAppView();
@ -43,7 +51,7 @@ export default () => {
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 selectedServer = servers.find((s) => s._id === serverId);
@ -51,7 +59,7 @@ export default () => {
return new Error("Server not found");
}
showAppView(selectedServer.url);
await showAppView(selectedServer.url);
return true;
});

View File

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

View File

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

View File

@ -8,6 +8,7 @@ const api: WorkAdventureDesktopApi = {
notify: (txt) => ipcRenderer.send("app:notify", txt),
onMuteToggle: (callback) => ipcRenderer.on("app:on-mute-toggle", callback),
onCameraToggle: (callback) => ipcRenderer.on("app:on-camera-toggle", callback),
getDesktopCapturerSources: (options) => ipcRenderer.invoke("app:getDesktopCapturerSources", options),
};
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 = {
desktop: boolean;
isDevelopment: () => Promise<boolean>;
@ -5,4 +17,5 @@ export type WorkAdventureDesktopApi = {
notify: (txt: string) => void;
onMuteToggle: (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",
async click() {
await autoUpdater.manualRequestUpdateCheck();
click() {
void autoUpdater.manualRequestUpdateCheck();
},
},
{
label: "Open Logs",
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) {
throw new Error("App view not found");
}
@ -130,7 +130,7 @@ export function showAppView(url?: string) {
mainWindow.addBrowserView(appView);
if (url && url !== appViewUrl) {
appView.webContents.loadURL(url);
await appView.webContents.loadURL(url);
appViewUrl = url;
}

File diff suppressed because it is too large Load Diff

View File

@ -166,7 +166,8 @@ return [
],
[
'title' => 'Troubleshooting',
'url' => '/map-building/troubleshooting',
'view' => 'content.map.troubleshooting'
'url' => '/map-building/troubleshooting.md',
'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}
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}
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

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"],
"@typescript-eslint/no-explicit-any": "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!
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/restrict-plus-operands": "off",

View File

@ -1,5 +1,3 @@
src/Messages/generated
src/Messages/JsonMessages
src/i18n/i18n-svelte.ts
src/i18n/i18n-types.ts
src/i18n/i18n-util.ts
src/i18n/i18n-*.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",
"adapter": "svelte"
}

View File

@ -5,7 +5,7 @@
"license": "SEE LICENSE IN LICENSE.txt",
"devDependencies": {
"@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",
"@tsconfig/svelte": "^1.0.10",
"@types/google-protobuf": "^3.7.3",
@ -42,7 +42,7 @@
"buffer": "^6.0.3",
"cancelable-promise": "^4.2.1",
"cross-env": "^7.0.3",
"deep-copy-ts": "^0.5.0",
"deep-copy-ts": "^0.5.4",
"easystarjs": "^0.4.4",
"fast-deep-equal": "^3.1.3",
"google-protobuf": "^3.13.0",
@ -60,7 +60,7 @@
"standardized-audio-context": "^25.2.4",
"ts-deferred": "^1.0.4",
"ts-proto": "^1.96.0",
"typesafe-i18n": "^2.59.0",
"typesafe-i18n": "^5.4.0",
"uuidv4": "^6.2.10",
"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") {
this._cameraFollowPlayerStream.next(iframeEvent.data);
} else if (iframeEvent.type === "chat") {
scriptUtils.sendAnonymousChat(iframeEvent.data);
scriptUtils.sendAnonymousChat(iframeEvent.data, iframe.contentWindow ?? undefined);
} else if (iframeEvent.type === "openPopup") {
this._openPopupStream.next(iframeEvent.data);
} else if (iframeEvent.type === "closePopup") {
@ -294,7 +294,6 @@ class IframeListener {
handleMenuUnregisterEvent(iframeEvent.data.name);
} else {
// 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;
}
}
@ -400,13 +399,20 @@ class IframeListener {
this.scripts.delete(scriptUrl);
}
sendUserInputChat(message: string) {
this.postMessage({
type: "userInputChat",
data: {
message: message,
} as UserInputChatEvent,
});
/**
* @param message The message to dispatch
* @param exceptOrigin Don't dispatch the message to exceptOrigin (to avoid infinite loops)
*/
sendUserInputChat(message: string, exceptOrigin?: Window) {
this.postMessage(
{
type: "userInputChat",
data: {
message: message,
} as UserInputChatEvent,
},
exceptOrigin
);
}
sendEnterEvent(name: string) {
@ -522,8 +528,11 @@ class IframeListener {
/**
* 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) {
if (exceptOrigin === iframe.contentWindow) {
continue;
}
iframe.contentWindow?.postMessage(message, "*");
}
}

View File

@ -11,9 +11,9 @@ class ScriptUtils {
window.location.href = url;
}
public sendAnonymousChat(chatEvent: ChatEvent) {
public sendAnonymousChat(chatEvent: ChatEvent, origin?: Window) {
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 { WorkAdventureDesktopApi } from "@wa-preload-app";
declare global {
interface Window {
WAD: WorkAdventureDesktopApi;
WAD?: WorkAdventureDesktopApi;
}
}
@ -36,8 +36,8 @@ class DesktopApi {
}
});
isSilentStore.subscribe((value) => {
this.isSilent = value;
silentStore.subscribe((silent) => {
this.isSilent = silent;
});
}
}

View File

@ -88,7 +88,6 @@ export function createState(target: "global" | "player"): WorkadventureStateComm
}
return target.loadVariable(p.toString());
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
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.
// User must use WA.state.saveVariable to have error message.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -88,16 +88,11 @@
}
}
function translateMenuName(menu: MenuItem) {
if (menu.type === "scripting") {
return menu.label;
}
// 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();
}
$: subMenuTranslations = $subMenusStore.map((subMenu) =>
subMenu.type === "scripting" ? subMenu.label : $LL.menu.sub[subMenu.key]()
);
$: activeSubMenuTranslation =
activeSubMenu.type === "scripting" ? activeSubMenu.label : $LL.menu.sub[activeSubMenu.key]();
</script>
<svelte:window on:keydown={onKeyDown} />
@ -106,20 +101,20 @@
<div class="menu-nav-sidebar nes-container is-rounded" transition:fly={{ x: -1000, duration: 500 }}>
<h2>{$LL.menu.title()}</h2>
<nav>
{#each $subMenusStore as submenu}
{#each $subMenusStore as submenu, i}
<button
type="button"
class="nes-btn {activeSubMenu === submenu ? 'is-disabled' : ''}"
on:click|preventDefault={() => void switchMenu(submenu)}
>
{translateMenuName(submenu)}
{subMenuTranslations[i]}
</button>
{/each}
</nav>
</div>
<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>
<h2>{translateMenuName(activeSubMenu)}</h2>
<h2>{activeSubMenuTranslation}</h2>
<svelte:component this={activeComponent} {...props} />
</div>
</div>

View File

@ -28,12 +28,12 @@
let previewCameraPrivacySettings = valueCameraPrivacySettings;
let previewMicrophonePrivacySettings = valueMicrophonePrivacySettings;
function saveSetting() {
async function saveSetting() {
let change = false;
if (valueLocale !== previewValueLocale) {
previewValueLocale = valueLocale;
setCurrentLocale(valueLocale as Locales);
await setCurrentLocale(valueLocale as Locales);
}
if (valueVideo !== previewValueVideo) {
@ -174,7 +174,7 @@
<div class="nes-select is-dark">
<select class="languages-switcher" bind:value={valueLocale}>
{#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}
</select>
</div>

View File

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

View File

@ -2,47 +2,46 @@
import { fly } from "svelte/transition";
import { errorScreenStore } from "../../Stores/ErrorScreenStore";
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";
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 external from "../images/external-link.png";
import { get } from "svelte/store";
let errorScreen = get(errorScreenStore);
function click() {
if (errorScreen.urlToRedirect) window.location.replace(errorScreen.urlToRedirect);
else if (errorScreen.type === "redirect" && window.history.length > 2) history.back();
if (errorScreen.type === "unauthorized") void connectionManager.logout();
else window.location.reload();
}
let details = errorScreen.details;
let timeVar = errorScreen.timeToRetry ?? 0;
if (errorScreen.type === "retry") {
setInterval(() => {
let interval = setInterval(() => {
if (timeVar <= 1000) click();
timeVar -= 1000;
}, 1000);
onDestroy(() => clearInterval(interval));
}
$: detailsStylized = details.replace("{time}", `${timeVar / 1000}`);
$: detailsStylized = (details ?? "").replace("{time}", `${timeVar / 1000}`);
</script>
<main class="errorScreen" transition:fly={{ y: -200, duration: 500 }}>
<div style="width: 90%;">
<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}
<p>{$errorScreenStore.subtitle}</p>
{#if $errorScreenStore.type !== "retry"}<p class="code">Code : {$errorScreenStore.code}</p>{/if}
<p class="details">
{detailsStylized}{#if $errorScreenStore.type === "retry"}<div class="loading" />{/if}
</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}>
<img src={$errorScreenStore.type === "retry" ? reload : external} alt="" class="reload" />
{#if $errorScreenStore.type === "retry"}<img src={reload} alt="" class="reload" />{/if}
{$errorScreenStore.buttonTitle}
</button>
{/if}
@ -74,10 +73,14 @@
.logo {
width: 50%;
margin-bottom: 50px;
max-height: 25vh;
max-width: 50vw;
}
.icon {
height: 125px;
margin-bottom: 25px;
max-height: 25vh;
max-width: 50vw;
}
h2 {
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 { SelectCharacterScene, SelectCharacterSceneName } from "../../Phaser/Login/SelectCharacterScene";
import LL from "../../i18n/i18n-svelte";
import { customizeAvailableStore } from "../../Stores/SelectCharacterSceneStore";
import { customizeAvailableStore, selectedCollection } from "../../Stores/SelectCharacterSceneStore";
export let game: Game;
const selectCharacterScene = game.scene.getScene(SelectCharacterSceneName) as SelectCharacterScene;
const showArrows = selectCharacterScene.getCollectionKeysSize() > 1;
function selectLeft() {
selectCharacterScene.moveToLeft();
selectCharacterScene.selectPreviousCollection();
}
function selectRight() {
selectCharacterScene.moveToRight();
selectCharacterScene.selectNextCollection();
}
function cameraScene() {
@ -25,83 +26,72 @@
}
</script>
<form class="selectCharacterScene">
<section class="text-center">
<h2>{$LL.woka.selectWoka.title()}</h2>
<button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}>
&lt;
</button>
<button class="selectCharacterButton selectCharacterButtonRight nes-btn" on:click|preventDefault={selectRight}>
&gt;
</button>
</section>
<section class="action">
<section class="text-center">
<h2>{$LL.woka.selectWoka.title()}</h2>
</section>
<section class="category">
{#if showArrows}
<button class="selectCharacterButton nes-btn" on:click|preventDefault={selectLeft}> &lt; </button>
<strong class="category-text">{$selectedCollection}</strong>
<button class="selectCharacterButton nes-btn" on:click|preventDefault={selectRight}> &gt; </button>
{/if}
</section>
<section class="action">
<button
type="submit"
class="selectCharacterSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={cameraScene}>{$LL.woka.selectWoka.continue()}</button
>
{#if $customizeAvailableStore}
<button
type="submit"
class="selectCharacterSceneFormSubmit nes-btn is-primary"
on:click|preventDefault={cameraScene}>{$LL.woka.selectWoka.continue()}</button
class="selectCharacterSceneFormCustomYourOwnSubmit nes-btn"
on:click|preventDefault={customizeScene}>{$LL.woka.selectWoka.customize()}</button
>
{#if $customizeAvailableStore}
<button
type="submit"
class="selectCharacterSceneFormCustomYourOwnSubmit nes-btn"
on:click|preventDefault={customizeScene}>{$LL.woka.selectWoka.customize()}</button
>
{/if}
</section>
</form>
{/if}
</section>
<style lang="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";
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>

View File

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

View File

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

View File

@ -42,6 +42,7 @@ import {
SetPlayerDetailsMessage as SetPlayerDetailsMessageTsProto,
PingMessage as PingMessageTsProto,
CharacterLayerMessage,
AvailabilityStatus,
} from "../Messages/ts-proto-generated/protos/messages";
import { Subject } from "rxjs";
import { selectCharacterSceneVisibleStore } from "../Stores/SelectCharacterStore";
@ -302,8 +303,7 @@ export class RoomConnection implements RoomConnection {
}
default: {
// 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 tmp: never = subMessage;
const _exhaustiveCheck: never = subMessage;
}
}
}
@ -483,15 +483,20 @@ export class RoomConnection implements RoomConnection {
}
case "errorScreenMessage": {
this._errorScreenMessageStream.next(message.errorScreenMessage);
if (message.errorScreenMessage.code !== "retry") this.closed = true;
console.error("An error occurred server side: " + message.errorScreenMessage.code);
errorScreenStore.setError(message.errorScreenMessage);
console.error("An error occurred server side: " + JSON.stringify(message.errorScreenMessage));
if (message.errorScreenMessage.code !== "retry") {
this.closed = true;
}
if (message.errorScreenMessage.type === "redirect" && message.errorScreenMessage.urlToRedirect) {
window.location.assign(message.errorScreenMessage.urlToRedirect);
} else {
errorScreenStore.setError(message.errorScreenMessage);
}
break;
}
default: {
// 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 tmp: never = message;
const _exhaustiveCheck: never = message;
}
}
};
@ -532,9 +537,9 @@ export class RoomConnection implements RoomConnection {
this.socket.send(bytes);
}
public emitPlayerAway(away: boolean): void {
public emitPlayerStatusChange(status: AvailabilityStatus): void {
const message = SetPlayerDetailsMessageTsProto.fromPartial({
away,
status,
});
const bytes = ClientToServerMessageTsProto.encode({
message: {
@ -626,19 +631,6 @@ export class RoomConnection implements RoomConnection {
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 {
const bytes = ClientToServerMessageTsProto.encode({
message: {
@ -682,7 +674,7 @@ export class RoomConnection implements RoomConnection {
characterLayers,
visitCardUrl: message.visitCardUrl,
position: ProtobufClientUtils.toPointInterface(position),
away: message.away,
status: message.status,
companion: companion ? companion.name : null,
userUuid: message.userUuid,
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.turnIcon = this.scene.add
.image(this.background.displayWidth * 0.35, this.background.displayHeight * 0.35, "iconTurn")
.setScale(0.25)
.setTintFill(0xffffff)
.setAlpha(0.5);
.setScale(0.2)
.setAlpha(0.75);
this.drawFrame();
this.setSize(this.SIZE, this.SIZE);
@ -130,11 +129,11 @@ export class CustomWokaPreviewer extends Phaser.GameObjects.Container {
this.changeAnimation(direction, moving);
this.turnIconTween?.stop();
this.turnIcon.setScale(0.25);
this.turnIcon.setScale(0.2);
this.turnIconTween = this.scene.tweens.add({
targets: [this.turnIcon],
duration: 100,
scale: 0.2,
scale: 0.15,
yoyo: true,
ease: Easing.SineEaseIn,
});

View File

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

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;
pressed: IconButtonAppearanceConfig;
selected: IconButtonAppearanceConfig;
iconScale?: number;
}
export interface IconButtonAppearanceConfig {
@ -34,7 +35,7 @@ export class IconButton extends Phaser.GameObjects.Container {
this.config = config;
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.add([this.background, this.icon]);

View File

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

View File

@ -36,14 +36,16 @@ export enum PlayerTexturesKey {
}
export class PlayerTextures {
private PLAYER_RESOURCES: BodyResourceDescriptionListInterface = {};
private COLOR_RESOURCES: BodyResourceDescriptionListInterface = {};
private EYES_RESOURCES: BodyResourceDescriptionListInterface = {};
private HAIR_RESOURCES: BodyResourceDescriptionListInterface = {};
private CLOTHES_RESOURCES: BodyResourceDescriptionListInterface = {};
private HATS_RESOURCES: BodyResourceDescriptionListInterface = {};
private ACCESSORIES_RESOURCES: BodyResourceDescriptionListInterface = {};
private LAYERS: BodyResourceDescriptionListInterface[] = [];
private wokaResources: BodyResourceDescriptionListInterface = {};
private colorResources: BodyResourceDescriptionListInterface = {};
private eyesResources: BodyResourceDescriptionListInterface = {};
private hairResources: BodyResourceDescriptionListInterface = {};
private clothesResources: BodyResourceDescriptionListInterface = {};
private hatsResources: BodyResourceDescriptionListInterface = {};
private accessoriesResources: BodyResourceDescriptionListInterface = {};
private layers: BodyResourceDescriptionListInterface[] = [];
private wokaCollections = new Map<string, BodyResourceDescriptionInterface[]>();
public loadPlayerTexturesMetadata(metadata: WokaList): void {
this.mapTexturesMetadataIntoResources(metadata);
@ -52,43 +54,53 @@ export class PlayerTextures {
public getTexturesResources(key: PlayerTexturesKey): BodyResourceDescriptionListInterface {
switch (key) {
case PlayerTexturesKey.Accessory:
return this.ACCESSORIES_RESOURCES;
return this.accessoriesResources;
case PlayerTexturesKey.Body:
return this.COLOR_RESOURCES;
return this.colorResources;
case PlayerTexturesKey.Clothes:
return this.CLOTHES_RESOURCES;
return this.clothesResources;
case PlayerTexturesKey.Eyes:
return this.EYES_RESOURCES;
return this.eyesResources;
case PlayerTexturesKey.Hair:
return this.HAIR_RESOURCES;
return this.hairResources;
case PlayerTexturesKey.Hat:
return this.HATS_RESOURCES;
return this.hatsResources;
case PlayerTexturesKey.Woka:
return this.PLAYER_RESOURCES;
return this.wokaResources;
}
}
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 {
this.PLAYER_RESOURCES = this.getMappedResources(metadata.woka);
this.COLOR_RESOURCES = this.getMappedResources(metadata.body);
this.EYES_RESOURCES = this.getMappedResources(metadata.eyes);
this.HAIR_RESOURCES = this.getMappedResources(metadata.hair);
this.CLOTHES_RESOURCES = this.getMappedResources(metadata.clothes);
this.HATS_RESOURCES = this.getMappedResources(metadata.hat);
this.ACCESSORIES_RESOURCES = this.getMappedResources(metadata.accessory);
this.wokaResources = this.getMappedResources(metadata.woka);
this.colorResources = this.getMappedResources(metadata.body);
this.eyesResources = this.getMappedResources(metadata.eyes);
this.hairResources = this.getMappedResources(metadata.hair);
this.clothesResources = this.getMappedResources(metadata.clothes);
this.hatsResources = this.getMappedResources(metadata.hat);
this.accessoriesResources = this.getMappedResources(metadata.accessory);
this.LAYERS = [
this.COLOR_RESOURCES,
this.EYES_RESOURCES,
this.HAIR_RESOURCES,
this.CLOTHES_RESOURCES,
this.HATS_RESOURCES,
this.ACCESSORIES_RESOURCES,
this.layers = [
this.colorResources,
this.eyesResources,
this.hairResources,
this.clothesResources,
this.hatsResources,
this.accessoriesResources,
];
this.mapWokaCollections(metadata.woka);
}
private getMappedResources(category: WokaPartType): BodyResourceDescriptionListInterface {
@ -103,6 +115,19 @@ export class PlayerTextures {
}
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[] = [

View File

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

View File

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

View File

@ -97,7 +97,6 @@ export class CameraManager extends Phaser.Events.EventEmitter {
});
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) => {
if (this.cameraMode === CameraMode.Positioned) {
this.waScaleManager.zoomModifier = currentZoomModifier + progress * zoomModifierChange;
@ -139,7 +138,6 @@ export class CameraManager extends Phaser.Events.EventEmitter {
this.emit(CameraManagerEvent.CameraUpdate, this.getCameraUpdateEventData());
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.waScaleManager.zoomModifier = currentZoomModifier + progress * zoomModifierChange;
if (progress === 1) {

View File

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

View File

@ -91,7 +91,7 @@ import { MapStore } from "../../Stores/Utils/MapStore";
import { followUsersColorStore } from "../../Stores/FollowStore";
import { GameSceneUserInputHandler } from "../UserInput/GameSceneUserInputHandler";
import { locale } from "../../i18n/i18n-svelte";
import { localVolumeStore } from "../../Stores/MediaStore";
import { availabilityStatusStore, localVolumeStore } from "../../Stores/MediaStore";
import { StringUtils } from "../../Utils/StringUtils";
import { startLayerNamesStore } from "../../Stores/StartLayerNamesStore";
import { JitsiCoWebsite } from "../../WebRtc/CoWebsite/JitsiCoWebsite";
@ -101,7 +101,6 @@ import CancelablePromise from "cancelable-promise";
import { Deferred } from "ts-deferred";
import { SuperLoaderPlugin } from "../Services/SuperLoaderPlugin";
import { PlayerDetailsUpdatedMessage } from "../../Messages/ts-proto-generated/protos/messages";
import { privacyShutdownStore } from "../../Stores/PrivacyShutdownStore";
export interface GameSceneInitInterface {
initPosition: PointInterface | null;
reconnecting: boolean;
@ -177,8 +176,6 @@ export class GameScene extends DirtyScene {
private localVolumeStoreUnsubscriber: Unsubscriber | undefined;
private followUsersColorStoreUnsubscribe!: Unsubscriber;
private currentPlayerGroupIdStoreUnsubscribe!: Unsubscriber;
private privacyShutdownStoreUnsubscribe!: Unsubscriber;
private userIsJitsiDominantSpeakerStoreUnsubscriber!: Unsubscriber;
private jitsiParticipantsCountStoreUnsubscriber!: Unsubscriber;
@ -249,6 +246,8 @@ export class GameScene extends DirtyScene {
this.listenToIframeEvents();
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) {
this.load.image(joystickBaseKey, joystickBaseImg);
@ -679,6 +678,11 @@ export class GameScene extends DirtyScene {
this.tryChangeShowVoiceIndicatorState(this.jitsiDominantSpeaker && this.jitsiParticipantsCount > 1);
});
availabilityStatusStore.subscribe((status) => {
this.connection?.emitPlayerStatusChange(status);
this.CurrentPlayer.setStatus(status);
});
this.emoteUnsubscribe = emoteStore.subscribe((emote) => {
if (emote) {
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([
this.connectionAnswerPromiseDeferred.promise as Promise<unknown>,
...scriptPromises,
@ -767,7 +767,7 @@ export class GameScene extends DirtyScene {
characterLayers: message.characterLayers,
name: message.name,
position: message.position,
away: message.away,
status: message.status,
visitCardUrl: message.visitCardUrl,
companion: message.companion,
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(
iframeListener.addActionsMenuKeyToRemotePlayerStream.subscribe((data) => {
this.MapPlayersByKey.get(data.id)?.registerActionsMenuAction({
@ -1556,7 +1563,6 @@ ${escapedMessage}
this.emoteUnsubscribe();
this.emoteMenuUnsubscribe();
this.followUsersColorStoreUnsubscribe();
this.privacyShutdownStoreUnsubscribe();
this.biggestAvailableAreaStoreUnsubscribe();
this.userIsJitsiDominantSpeakerStoreUnsubscriber();
this.jitsiParticipantsCountStoreUnsubscriber();
@ -1696,7 +1702,6 @@ ${escapedMessage}
private createCollisionWithPlayer() {
//add collision layer
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.CurrentPlayer.say("Collision with layer : "+ (object2 as Tile).layer.name)
});
@ -1747,11 +1752,9 @@ ${escapedMessage}
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.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.pointerOutOutline();
});
@ -1853,8 +1856,7 @@ ${escapedMessage}
break;
}
default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const tmp: never = event;
const _exhaustiveCheck: never = event;
}
}
}
@ -1933,8 +1935,8 @@ ${escapedMessage}
if (addPlayerData.outlineColor !== undefined) {
player.setApiOutlineColor(addPlayerData.outlineColor);
}
if (addPlayerData.away !== undefined) {
player.setAwayStatus(addPlayerData.away, true);
if (addPlayerData.status !== undefined) {
player.setStatus(addPlayerData.status, true);
}
this.MapPlayers.add(player);
this.MapPlayersByKey.set(player.userId, player);
@ -2085,8 +2087,8 @@ ${escapedMessage}
if (message.details?.showVoiceIndicator !== undefined) {
character.showTalkIcon(message.details?.showVoiceIndicator);
}
if (message.details?.away !== undefined) {
character.setAwayStatus(message.details?.away);
if (message.details?.status !== undefined) {
character.setStatus(message.details?.status);
}
}
@ -2130,13 +2132,10 @@ ${escapedMessage}
}
public enableMediaBehaviors() {
const silent = this.gameMap.getCurrentProperties().get(GameMapProperties.SILENT);
this.connection?.setSilent(!!silent);
mediaManager.showMyCamera();
}
public disableMediaBehaviors() {
this.connection?.setSilent(true);
mediaManager.hideMyCamera();
}

View File

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

View File

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

View File

@ -28,7 +28,7 @@ export class TexturesHelper {
});
} catch (error) {
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.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.superLoad = new SuperLoaderPlugin(this);
}
preload() {
this.input.dragDistanceThreshold = 10;
}
}

View File

@ -54,7 +54,7 @@ export class CustomizeScene extends AbstractCharacterScene {
}
public preload(): void {
this.input.dragDistanceThreshold = 10;
super.preload();
this.load.image("iconClothes", "/resources/icons/icon_clothes.png");
this.load.image("iconAccessory", "/resources/icons/icon_accessory.png");
@ -94,6 +94,7 @@ export class CustomizeScene extends AbstractCharacterScene {
}
public create(): void {
this.tryLoadLastUsedWokaLayers();
waScaleManager.zoomModifier = 1;
this.createSlotBackgroundTextures();
this.initializeCustomWokaPreviewer();
@ -112,7 +113,6 @@ export class CustomizeScene extends AbstractCharacterScene {
this.onResize();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public update(time: number, dt: number): void {
this.customWokaPreviewer.update();
}
@ -150,12 +150,35 @@ export class CustomizeScene extends AbstractCharacterScene {
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 {
for (let i = 0; i < 4; i += 1) {
if (this.textures.getTextureKeys().includes(`floorTexture${i}`)) {
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.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.Hat]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconHat")),
};
}
private getDefaultIconButtonConfig(iconTextureKey: string): IconButtonConfig {
private getDefaultIconButtonConfig(iconTextureKey: string, iconScale?: number): IconButtonConfig {
return {
iconTextureKey,
iconScale,
width: 25,
height: 25,
idle: {
@ -328,13 +352,14 @@ export class CustomizeScene extends AbstractCharacterScene {
}
private handleCustomWokaPreviewerOnResize(): void {
const ratio = innerHeight / innerWidth;
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 {
const ratio = innerHeight / innerWidth;
const slotDimension = 50;
const slotDimension = WokaBodyPartSlot.SIZE;
for (const part in this.bodyPartsButtons) {
this.bodyPartsButtons[part as CustomWokaBodyPart].setDisplaySize(slotDimension, slotDimension);
@ -421,7 +446,7 @@ export class CustomizeScene extends AbstractCharacterScene {
private handleRandomizeButtonOnResize(): void {
const x =
this.customWokaPreviewer.x +
this.customWokaPreviewer.x -
(this.customWokaPreviewer.displayWidth - this.randomizeButton.displayWidth) * 0.5;
const y =
this.customWokaPreviewer.y +
@ -432,7 +457,7 @@ export class CustomizeScene extends AbstractCharacterScene {
private handleFinishButtonOnResize(): void {
const x =
this.customWokaPreviewer.x -
this.customWokaPreviewer.x +
(this.customWokaPreviewer.displayWidth - this.randomizeButton.displayWidth) * 0.5;
const y =
this.customWokaPreviewer.y +

View File

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

View File

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

View File

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

View File

@ -49,7 +49,6 @@ export class LoginScene extends ResizableScene {
loginSceneVisibleStore.set(false);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
update(time: number, delta: number): 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 Rectangle = Phaser.GameObjects.Rectangle;
import { EnableCameraSceneName } from "./EnableCameraScene";
import { CustomizeSceneName } from "./CustomizeScene";
import { localUserStore } from "../../Connexion/LocalUserStore";
@ -13,25 +12,26 @@ import { PinchManager } from "../UserInput/PinchManager";
import { selectCharacterSceneVisibleStore } from "../../Stores/SelectCharacterStore";
import { waScaleManager } from "../Services/WaScaleManager";
import { analyticsClient } from "../../Administration/AnalyticsClient";
import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils";
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 { myCameraVisibilityStore } from "../../Stores/MyCameraStoreVisibility";
//todo: put this constants in a dedicated file
export const SelectCharacterSceneName = "SelectCharacterScene";
export class SelectCharacterScene extends AbstractCharacterScene {
protected readonly nbCharactersPerRow = 6;
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 selectedWoka!: Phaser.GameObjects.Sprite | null; // null if we are selecting the "customize" option
protected playerModels!: BodyResourceDescriptionInterface[];
protected selectedRectangle!: Rectangle;
protected currentSelectUser = 0;
protected pointerClicked: boolean = false;
protected pointerTimer: number = 0;
private charactersDraggableGrid!: DraggableGrid;
private collectionKeys!: string[];
private selectedCollectionIndex!: number;
private selectedGridItemIndex?: number;
private gridRowsCount: number = 1;
protected lazyloadingAttempt = true; //permit to update texture loaded after renderer
private loader: Loader;
@ -44,7 +44,8 @@ export class SelectCharacterScene extends AbstractCharacterScene {
this.playerTextures = new PlayerTextures();
}
preload() {
public preload() {
super.preload();
const wokaMetadataKey = "woka-list" + gameManager.currentStartedRoom.href;
this.cache.json.remove(wokaMetadataKey);
@ -67,228 +68,237 @@ export class SelectCharacterScene extends AbstractCharacterScene {
}
)
.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.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());
selectCharacterSceneVisibleStore.set(true);
this.events.addListener("wake", () => {
waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 2 : 1;
selectCharacterSceneVisibleStore.set(true);
});
if (touchScreenManager.supportTouchScreen) {
new PinchManager(this);
}
waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 2 : 1;
const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16;
this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xffffff);
this.selectedRectangle.setDepth(2);
/*create user*/
this.createCurrentPlayer();
this.input.keyboard.on("keyup-ENTER", () => {
return this.nextSceneToCameraScene();
this.charactersDraggableGrid = new DraggableGrid(this, {
position: { x: 0, y: 0 },
maskPosition: { x: 0, y: 0 },
dimension: { x: 485, y: 165 },
horizontal: true,
repositionToCenter: true,
itemsInRow: 1,
margin: {
left: ((innerWidth - 200) / waScaleManager.getActualZoom()) * 0.5,
right: ((innerWidth - 200) / waScaleManager.getActualZoom()) * 0.5,
},
spacing: 5,
debug: {
showDraggableSpace: false,
},
});
this.input.keyboard.on("keydown-RIGHT", () => {
this.moveToRight();
});
this.input.keyboard.on("keydown-LEFT", () => {
this.moveToLeft();
});
this.input.keyboard.on("keydown-UP", () => {
this.moveToUp();
});
this.input.keyboard.on("keydown-DOWN", () => {
this.moveToDown();
});
this.bindEventHandlers();
this.onResize();
}
public nextSceneToCameraScene(): void {
if (this.selectedPlayer !== null && !areCharacterLayersValid([this.selectedPlayer.texture.key])) {
if (this.selectedWoka !== null && !areCharacterLayersValid([this.selectedWoka.texture.key])) {
return;
}
if (!this.selectedPlayer) {
if (!this.selectedWoka) {
return;
}
analyticsClient.validationWoka("SelectWoka");
gameManager.setCharacterLayers([this.selectedWoka.texture.key]);
this.selectedWoka = null;
this.scene.stop(SelectCharacterSceneName);
waScaleManager.restoreZoom();
gameManager.setCharacterLayers([this.selectedPlayer.texture.key]);
gameManager.tryResumingGame(EnableCameraSceneName);
this.players = [];
selectCharacterSceneVisibleStore.set(false);
this.events.removeListener("wake");
}
public nextSceneToCustomizeScene(): void {
if (this.selectedPlayer !== null && !areCharacterLayersValid([this.selectedPlayer.texture.key])) {
if (this.selectedWoka !== null && !areCharacterLayersValid([this.selectedWoka.texture.key])) {
return;
}
this.selectedWoka = null;
myCameraVisibilityStore.set(false);
this.scene.sleep(SelectCharacterSceneName);
waScaleManager.restoreZoom();
this.scene.run(CustomizeSceneName);
selectCharacterSceneVisibleStore.set(false);
}
createCurrentPlayer(): 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;
}
public update(): void {
if (this.lazyloadingAttempt) {
//re-render players list
this.createCurrentPlayer();
this.moveUser();
this.lazyloadingAttempt = false;
}
}
public onResize(): void {
//move position of user
this.moveUser();
this.handleCharactersGridOnResize();
}
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 {

View File

@ -28,7 +28,6 @@ export class Player extends Character {
companionTexturePromise?: CancelablePromise<string>
) {
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
this.getBody().setImmovable(false);
}

View File

@ -16,7 +16,6 @@ export class GameSceneUserInputHandler implements UserInputHandlerInterface {
gameObjects: Phaser.GameObjects.GameObject[],
deltaX: number,
deltaY: number,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
deltaZ: number
): void {
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 handleSpaceKeyUpEvent(event: Event): Event {

View File

@ -87,7 +87,10 @@ function createChatMessagesStore() {
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) => {
const lastMessage = list[list.length - 1];
if (
@ -106,7 +109,7 @@ function createChatMessagesStore() {
});
}
iframeListener.sendUserInputChat(text);
iframeListener.sendUserInputChat(text, origin);
return list;
});
chatVisibilityStore.set(true);

View File

@ -9,7 +9,9 @@ function createErrorScreenStore() {
return {
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 { MediaStreamConstraintsError } from "./Errors/MediaStreamConstraintsError";
import { SoundMeter } from "../Phaser/Components/SoundMeter";
import { AvailabilityStatus } from "../Messages/ts-proto-generated/protos/messages";
import deepEqual from "fast-deep-equal";
/**
@ -179,10 +180,19 @@ function createVideoConstraintStore() {
};
}
/**
* 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 isSilentStore = writable(false);
export const inJitsiStore = writable(false);
export const silentStore = 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();
@ -241,7 +251,7 @@ export const mediaStreamConstraintsStore = derived(
audioConstraintStore,
privacyShutdownStore,
cameraEnergySavingStore,
isSilentStore,
availabilityStatusStore,
],
(
[
@ -253,7 +263,7 @@ export const mediaStreamConstraintsStore = derived(
$audioConstraintStore,
$privacyShutdownStore,
$cameraEnergySavingStore,
$isSilentStore,
$availabilityStatusStore,
],
set
) => {
@ -310,7 +320,7 @@ export const mediaStreamConstraintsStore = derived(
//currentAudioConstraint = false;
}
if ($isSilentStore === true) {
if ($availabilityStatusStore === AvailabilityStatus.SILENT) {
currentVideoConstraint = false;
currentAudioConstraint = false;
}

View File

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

View File

@ -2,6 +2,7 @@ import { derived, Readable, readable, writable } from "svelte/store";
import { peerStore } from "./PeerStore";
import type { LocalStreamStoreValue } from "./MediaStore";
import { myCameraVisibilityStore } from "./MyCameraStoreVisibility";
import type { DesktopCapturerSource } from "@wa-preload-app";
declare const navigator: any; // eslint-disable-line @typescript-eslint/no-explicit-any
@ -91,6 +92,25 @@ export const screenSharingConstraintsStore = derived(
} 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)
*/
@ -110,7 +130,9 @@ export const screenSharingLocalStreamStore = derived<Readable<MediaStreamConstra
}
let currentStreamPromise: Promise<MediaStream>;
if (navigator.getDisplayMedia) {
if (window.WAD?.getDesktopCapturerSources) {
currentStreamPromise = getDesktopCapturerSources();
} else if (navigator.getDisplayMedia) {
currentStreamPromise = navigator.getDisplayMedia({ constraints });
} else if (navigator.mediaDevices && navigator.mediaDevices.getDisplayMedia) {
currentStreamPromise = navigator.mediaDevices.getDisplayMedia({ constraints });
@ -200,3 +222,7 @@ export const screenSharingLocalMedia = readable<ScreenSharingLocalMedia | null>(
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";
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);
}
// 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.
function hsv_to_rgb(hue: number, saturation: number, brightness: number): { r: number; g: number; b: number } {
const h_i = Math.floor(hue * 6);

View File

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

View File

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

View File

@ -14,8 +14,6 @@ import emoji from "./emoji";
import trigger from "./trigger";
const en_US: BaseTranslation = {
language: "English",
country: "United States",
audio,
camera,
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";
// 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 = {
// add your formatter functions here
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@
"moduleResolution": "node",
//"module": "CommonJS",
"module": "ESNext",
"target": "ES2017",
"target": "ES2020",
"declaration": false,
"downlevelIteration": true,
"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"
integrity sha512-2HDCV+6XXJjSuBAhDWLRr111buMQ3bIZrKo3dymIhEJ4oJCC/3yDqg7HDQIn8Y8KKbsM0AtuHMZW4yz2tPBsYg==
"@home-based-studio/phaser3-utils@^0.4.2":
version "0.4.2"
resolved "https://registry.yarnpkg.com/@home-based-studio/phaser3-utils/-/phaser3-utils-0.4.2.tgz#b2c1815a6b51321ea8dab027b5badcf714d99fd6"
integrity sha512-S0VkAq3z0Kf0vEUUyCDes911icvc+nkUq7lGp23zD/5lk7LTGM51NswSAfel7Rm/DLY8IBxvDTBJADTf/De82w==
"@home-based-studio/phaser3-utils@^0.4.7":
version "0.4.7"
resolved "https://registry.yarnpkg.com/@home-based-studio/phaser3-utils/-/phaser3-utils-0.4.7.tgz#d0464c81cb27328657d3fd048396f6936e200c48"
integrity sha512-gYt1mkuad85uzYwHK0+wp+mrsGASV4sRZPaHZHnO8A2ofTAnX36S3PcI+BqKchdJ0I7jvBQcfh0yp1Ug0BHT+A==
dependencies:
phaser "3.55.1"
@ -806,10 +806,10 @@ debug@~3.1.0:
dependencies:
ms "2.0.0"
deep-copy-ts@^0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/deep-copy-ts/-/deep-copy-ts-0.5.0.tgz#b9493d8e2bae85ef7d659c16eb707c13efb84499"
integrity sha512-/3cgBcMkznRf5BM8wu6YWz3SQUkHzgh/v1TZFjevztLj9sMjFvNFBtpN4uUtPzw/rA/TldyD6c6LRL1zno4+YA==
deep-copy-ts@^0.5.4:
version "0.5.4"
resolved "https://registry.yarnpkg.com/deep-copy-ts/-/deep-copy-ts-0.5.4.tgz#e81b15797e4075cb3a690a1a7ac30179f2d72562"
integrity sha512-YJbPjw0YqdosorpCsa6copy1p/gJsFT9Q6Zq0tLi7D0nXh6Y/usjeIQZfkzV3HVuqY0Hl/5gM7TwgIbIWvEjlA==
deep-equal@^1.0.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"
integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==
typesafe-i18n@^2.59.0:
version "2.59.0"
resolved "https://registry.yarnpkg.com/typesafe-i18n/-/typesafe-i18n-2.59.0.tgz#09a9a32e61711418d927a389fa52e1c06a5fa5c4"
integrity sha512-Qv3Mrwmb8b73VNzQDPHPECzwymdBRVyDiZ3w2qnp4c2iv/7TGuiJegNHT/l3MooEN7IPbSpc5tbXw2x3MbGtFg==
typesafe-i18n@^5.4.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/typesafe-i18n/-/typesafe-i18n-5.4.0.tgz#cab696160bb144c387d7cbd13f7a728aa8371777"
integrity sha512-htewpld3FzZQv3Y1G31w54bofaaKR11MCkDK0FIYuXCpX72y1G6fkXUDslqzZCyVkZWRnIhY8leviNDxLwEzRw==
typescript@*:
version "4.3.2"

View File

@ -1,7 +1,7 @@
let menuIframeApi = undefined;
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'});

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