Merge remote-tracking branch 'remotes/workadventure-main/develop' into load-page-api

# Conflicts:
#	front/src/Api/Events/IframeEvent.ts
#	front/src/Api/IframeListener.ts
#	front/src/Phaser/Game/GameScene.ts
#	front/src/iframe_api.ts
This commit is contained in:
jonny 2021-06-15 15:05:49 +02:00
commit 238f333b81
204 changed files with 6943 additions and 4151 deletions

View file

@ -10,6 +10,8 @@ START_ROOM_URL=/_/global/maps.workadventure.localhost/Floor0/floor0.json
# If you are using Coturn, this is the value of the "static-auth-secret" parameter in your coturn config file.
# Keep empty if you are sharing hard coded / clear text credentials.
TURN_STATIC_AUTH_SECRET=
DISABLE_NOTIFICATIONS=true
SKIP_RENDER_OPTIMIZATIONS=false
# The email address used by Let's encrypt to send renewal warnings (compulsory)
ACME_EMAIL=

View file

@ -1,6 +1,8 @@
name: Build, push and deploy Docker image
on:
push:
branches: [master, develop]
release:
types: [created]
pull_request:
@ -14,7 +16,7 @@ env:
jobs:
build-front:
if: ${{ github.event.release || contains(github.event.pull_request.labels.*.name, 'deploy') }}
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
runs-on: ubuntu-latest
steps:
@ -34,11 +36,11 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: thecodingmachine/workadventure-front
tags: ${{ github.event.pull_request && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
add_git_labels: true
build-back:
if: ${{ github.event.release || contains(github.event.pull_request.labels.*.name, 'deploy') }}
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
runs-on: ubuntu-latest
steps:
@ -57,11 +59,11 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: thecodingmachine/workadventure-back
tags: ${{ github.event.pull_request && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
add_git_labels: true
build-pusher:
if: ${{ github.event.release || contains(github.event.pull_request.labels.*.name, 'deploy') }}
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
runs-on: ubuntu-latest
steps:
@ -80,11 +82,11 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: thecodingmachine/workadventure-pusher
tags: ${{ github.event.pull_request && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
add_git_labels: true
build-uploader:
if: ${{ github.event.release || contains(github.event.pull_request.labels.*.name, 'deploy') }}
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
runs-on: ubuntu-latest
steps:
@ -103,11 +105,11 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: thecodingmachine/workadventure-uploader
tags: ${{ github.event.pull_request && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
add_git_labels: true
build-maps:
if: ${{ github.event.release || contains(github.event.pull_request.labels.*.name, 'deploy') }}
if: ${{ github.event_name == 'push' || github.event_name == 'release' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
runs-on: ubuntu-latest
steps:
@ -127,7 +129,7 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: thecodingmachine/workadventure-maps
tags: ${{ github.event.pull_request && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
tags: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
add_git_labels: true
deeploy:
@ -138,7 +140,7 @@ jobs:
- build-maps
- build-uploader
runs-on: ubuntu-latest
if: ${{ contains(github.event.pull_request.labels.*.name, 'deploy') }}
if: ${{ github.event_name == 'push' || github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'deploy') }}
steps:
- name: Checkout
@ -147,6 +149,37 @@ jobs:
# Create a slugified value of the branch
- uses: rlespinasse/github-slug-action@3.1.0
- name: Write certificate
run: echo "${CERTS_PRIVATE_KEY}" > secret.key && chmod 0600 secret.key
env:
CERTS_PRIVATE_KEY: ${{ secrets.CERTS_PRIVATE_KEY }}
- name: Download certificate
run: mkdir secrets && scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i secret.key ubuntu@cert.workadventu.re:./config/live/workadventu.re/* secrets/
- name: Create namespace
uses: steebchen/kubectl@v1.0.0
env:
KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_FILE_BASE64 }}
with:
args: create namespace workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
continue-on-error: true
- name: Delete old certificates in namespace
uses: steebchen/kubectl@v1.0.0
env:
KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_FILE_BASE64 }}
with:
args: -n workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} delete secret certificate-tls
continue-on-error: true
- name: Install certificates in namespace
uses: steebchen/kubectl@v1.0.0
env:
KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_FILE_BASE64 }}
with:
args: -n workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }} create secret tls certificate-tls --key="secrets/privkey.pem" --cert="secrets/fullchain.pem"
- name: Deploy
uses: thecodingmachine/deeployer-action@master
env:
@ -156,14 +189,14 @@ jobs:
JITSI_URL: ${{ secrets.JITSI_URL }}
SECRET_JITSI_KEY: ${{ secrets.SECRET_JITSI_KEY }}
TURN_STATIC_AUTH_SECRET: ${{ secrets.TURN_STATIC_AUTH_SECRET }}
DEPLOY_REF: ${{ env.GITHUB_HEAD_REF_SLUG }}
DEPLOY_REF: ${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
with:
namespace: workadventure-${{ env.GITHUB_HEAD_REF_SLUG }}
namespace: workadventure-${{ github.event_name == 'pull_request' && env.GITHUB_HEAD_REF_SLUG || env.GITHUB_REF_SLUG }}
- name: Add a comment in PR
uses: unsplash/comment-on-pr@v1.2.0
if: ${{ github.event_name == 'pull_request' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
msg: Environment deployed at https://play.${{ env.GITHUB_HEAD_REF_SLUG }}.test.workadventu.re
check_for_duplicate_msg: true
msg: Environment deployed at https://play-${{ env.GITHUB_HEAD_REF_SLUG }}.test.workadventu.re

View file

@ -49,7 +49,7 @@ jobs:
- name: "Build"
run: yarn run build
env:
API_URL: "localhost:8080"
PUSHER_URL: "//localhost:8080"
working-directory: "front"
- name: "Lint"

View file

@ -1,12 +1,38 @@
## Version 1.3.9 - in dev
### BREAKING CHANGES
- Scripting API:
- Changed function names: `restorePlayerControl` => `restorePlayerControls`, `disablePlayerControl` => `disablePlayerControls`.
Please keep in mind that the scripting API is still experimental. Some breaking changes can occur in it until we mark it as stable.
### Updates
- Added the emote feature to WorkAdventure. (@Kharhamel, @Tabascoeye)
- The emote menu can be opened by clicking on your character.
- Clicking on one of its element will close the menu and play an emote above your character.
- This emote can be seen by other players.
- Player names were improved. (@Kharhamel)
- We now create a GameObject.Text instead of GameObject.BitmapText
- now use the 'Press Start 2P' font family and added an outline
- As a result, we can now allow non-standard letters like french accents or chinese characters!
- Added the contact card feature. (@Kharhamel)
- Click on another player to see its contact info.
- Premium-only feature unfortunately. I need to find a way to make it available for all.
- If no contact data is found (either because the user is anonymous or because no admin backend), display an error card.
- Mobile support has been improved
- WorkAdventure automatically sets the zoom level based on the viewport size to ensure a sensible size of the map is visible, whatever the viewport used
- Mouse wheel support to zoom in / out
- Pinch support on mobile to zoom in / out
- Improved virtual joystick size (adapts to the zoom level)
- WorkAdventure automatically sets the zoom level based on the viewport size to ensure a sensible size of the map is visible, whatever the viewport used
- Mouse wheel support to zoom in / out
- Pinch support on mobile to zoom in / out
- Improved virtual joystick size (adapts to the zoom level)
- Redesigned intermediate scenes
- Redesigned Select Companion scene
- Redesigned Enter Your Name scene
- Added a new `DISPLAY_TERMS_OF_USE` environment variable to trigger the display of terms of use
- New scripting API features:
- Use `WA.loadSound(): Sound` to load / play / stop a sound
### Bug Fixes

View file

@ -1,9 +1,6 @@
import {
BatchMessage,
PusherToBackMessage,
ServerToAdminClientMessage,
ServerToClientMessage,
SubMessage, UserJoinedRoomMessage, UserLeftRoomMessage
UserJoinedRoomMessage, UserLeftRoomMessage
} from "../Messages/generated/messages_pb";
import {AdminSocket} from "../RoomManager";

View file

@ -2,12 +2,12 @@ import {PointInterface} from "./Websocket/PointInterface";
import {Group} from "./Group";
import {User, UserSocket} from "./User";
import {PositionInterface} from "_Model/PositionInterface";
import {EntersCallback, LeavesCallback, MovesCallback} from "_Model/Zone";
import {EmoteCallback, EntersCallback, LeavesCallback, MovesCallback} from "_Model/Zone";
import {PositionNotifier} from "./PositionNotifier";
import {Movable} from "_Model/Movable";
import {extractDataFromPrivateRoomId, extractRoomSlugPublicRoomId, isRoomAnonymous} from "./RoomIdentifier";
import {arrayIntersect} from "../Services/ArrayHelper";
import {JoinRoomMessage} from "../Messages/generated/messages_pb";
import {EmoteEventMessage, JoinRoomMessage} from "../Messages/generated/messages_pb";
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
import {ZoneSocket} from "src/RoomManager";
import {Admin} from "../Model/Admin";
@ -51,8 +51,9 @@ export class GameRoom {
groupRadius: number,
onEnters: EntersCallback,
onMoves: MovesCallback,
onLeaves: LeavesCallback)
{
onLeaves: LeavesCallback,
onEmote: EmoteCallback,
) {
this.roomId = roomId;
if (isRoomAnonymous(roomId)) {
@ -74,7 +75,7 @@ export class GameRoom {
this.minDistance = minDistance;
this.groupRadius = groupRadius;
// A zone is 10 sprites wide.
this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves);
this.positionNotifier = new PositionNotifier(320, 320, onEnters, onMoves, onLeaves, onEmote);
}
public getGroups(): Group[] {
@ -88,7 +89,10 @@ export class GameRoom {
public getUserByUuid(uuid: string): User|undefined {
return this.usersByUuid.get(uuid);
}
public getUserById(id: number): User|undefined {
return this.users.get(id);
}
public join(socket : UserSocket, joinRoomMessage: JoinRoomMessage): User {
const positionMessage = joinRoomMessage.getPositionmessage();
if (positionMessage === undefined) {
@ -104,6 +108,7 @@ export class GameRoom {
this.positionNotifier,
socket,
joinRoomMessage.getTagList(),
joinRoomMessage.getVisitcardurl(),
joinRoomMessage.getName(),
ProtobufUtils.toCharacterLayerObjects(joinRoomMessage.getCharacterlayerList()),
joinRoomMessage.getCompanion()
@ -325,4 +330,8 @@ export class GameRoom {
this.versionNumber++
return this.versionNumber;
}
public emitEmoteEvent(user: User, emoteEventMessage: EmoteEventMessage) {
this.positionNotifier.emitEmoteEvent(user, emoteEventMessage);
}
}

View file

@ -8,10 +8,12 @@
* The PositionNotifier is important for performance. It allows us to send the position of players only to a restricted
* number of players around the current player.
*/
import {EntersCallback, LeavesCallback, MovesCallback, Zone} from "./Zone";
import {EmoteCallback, EntersCallback, LeavesCallback, MovesCallback, Zone} from "./Zone";
import {Movable} from "_Model/Movable";
import {PositionInterface} from "_Model/PositionInterface";
import {ZoneSocket} from "../RoomManager";
import {User} from "_Model/User";
import {EmoteEventMessage} from "../Messages/generated/messages_pb";
interface ZoneDescriptor {
i: number;
@ -24,7 +26,7 @@ export class PositionNotifier {
private zones: Zone[][] = [];
constructor(private zoneWidth: number, private zoneHeight: number, private onUserEnters: EntersCallback, private onUserMoves: MovesCallback, private onUserLeaves: LeavesCallback) {
constructor(private zoneWidth: number, private zoneHeight: number, private onUserEnters: EntersCallback, private onUserMoves: MovesCallback, private onUserLeaves: LeavesCallback, private onEmote: EmoteCallback) {
}
private getZoneDescriptorFromCoordinates(x: number, y: number): ZoneDescriptor {
@ -77,7 +79,7 @@ export class PositionNotifier {
let zone = this.zones[j][i];
if (zone === undefined) {
zone = new Zone(this.onUserEnters, this.onUserMoves, this.onUserLeaves, i, j);
zone = new Zone(this.onUserEnters, this.onUserMoves, this.onUserLeaves, this.onEmote, i, j);
this.zones[j][i] = zone;
}
return zone;
@ -93,4 +95,11 @@ export class PositionNotifier {
const zone = this.getZone(x, y);
zone.removeListener(call);
}
public emitEmoteEvent(user: User, emoteEventMessage: EmoteEventMessage) {
const zoneDesc = this.getZoneDescriptorFromCoordinates(user.getPosition().x, user.getPosition().y);
const zone = this.getZone(zoneDesc.i, zoneDesc.j);
zone.emitEmoteEvent(emoteEventMessage);
}
}

View file

@ -22,6 +22,7 @@ export class User implements Movable {
private positionNotifier: PositionNotifier,
public readonly socket: UserSocket,
public readonly tags: string[],
public readonly visitCardUrl: string|null,
public readonly name: string,
public readonly characterLayers: CharacterLayer[],
public readonly companion?: CompanionMessage

View file

@ -3,21 +3,19 @@ import {PositionInterface} from "_Model/PositionInterface";
import {Movable} from "./Movable";
import {Group} from "./Group";
import {ZoneSocket} from "../RoomManager";
import {EmoteEventMessage} from "../Messages/generated/messages_pb";
export type EntersCallback = (thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => void;
export type MovesCallback = (thing: Movable, position: PositionInterface, listener: ZoneSocket) => void;
export type LeavesCallback = (thing: Movable, newZone: Zone|null, listener: ZoneSocket) => void;
export type EmoteCallback = (emoteEventMessage: EmoteEventMessage, listener: ZoneSocket) => void;
export class Zone {
private things: Set<Movable> = new Set<Movable>();
private listeners: Set<ZoneSocket> = new Set<ZoneSocket>();
/**
* @param x For debugging purpose only
* @param y For debugging purpose only
*/
constructor(private onEnters: EntersCallback, private onMoves: MovesCallback, private onLeaves: LeavesCallback, public readonly x: number, public readonly y: number) {
}
constructor(private onEnters: EntersCallback, private onMoves: MovesCallback, private onLeaves: LeavesCallback, private onEmote: EmoteCallback, public readonly x: number, public readonly y: number) { }
/**
* A user/thing leaves the zone
@ -41,9 +39,7 @@ export class Zone {
*/
private notifyLeft(thing: Movable, newZone: Zone|null) {
for (const listener of this.listeners) {
//if (listener !== thing && (newZone === null || !listener.listenedZones.has(newZone))) {
this.onLeaves(thing, newZone, listener);
//}
this.onLeaves(thing, newZone, listener);
}
}
@ -57,15 +53,6 @@ export class Zone {
*/
private notifyEnter(thing: Movable, oldZone: Zone|null, position: PositionInterface) {
for (const listener of this.listeners) {
/*if (listener === thing) {
continue;
}
if (oldZone === null || !listener.listenedZones.has(oldZone)) {
this.onEnters(thing, listener);
} else {
this.onMoves(thing, position, listener);
}*/
this.onEnters(thing, oldZone, listener);
}
}
@ -85,28 +72,6 @@ export class Zone {
}
}
/*public startListening(listener: User): void {
for (const thing of this.things) {
if (thing !== listener) {
this.onEnters(thing, listener);
}
}
this.listeners.add(listener);
listener.listenedZones.add(this);
}
public stopListening(listener: User): void {
for (const thing of this.things) {
if (thing !== listener) {
this.onLeaves(thing, listener);
}
}
this.listeners.delete(listener);
listener.listenedZones.delete(this);
}*/
public getThings(): Set<Movable> {
return this.things;
}
@ -119,4 +84,11 @@ export class Zone {
public removeListener(socket: ZoneSocket): void {
this.listeners.delete(socket);
}
public emitEmoteEvent(emoteEventMessage: EmoteEventMessage) {
for (const listener of this.listeners) {
this.onEmote(emoteEventMessage, listener);
}
}
}

View file

@ -5,6 +5,7 @@ import {
AdminPusherToBackMessage,
AdminRoomMessage,
BanMessage,
EmotePromptMessage,
EmptyMessage,
ItemEventMessage,
JoinRoomMessage,
@ -71,6 +72,8 @@ const roomManager: IRoomManagerServer = {
socketManager.emitPlayGlobalMessage(room, message.getPlayglobalmessage() as PlayGlobalMessage);
} else if (message.hasQueryjitsijwtmessage()){
socketManager.handleQueryJitsiJwtMessage(user, message.getQueryjitsijwtmessage() as QueryJitsiJwtMessage);
} else if (message.hasEmotepromptmessage()){
socketManager.handleEmoteEventMessage(room, user, message.getEmotepromptmessage() as EmotePromptMessage);
}else if (message.hasSendusermessage()) {
const sendUserMessage = message.getSendusermessage();
if(sendUserMessage !== undefined) {

View file

@ -26,7 +26,8 @@ import {
GroupLeftZoneMessage,
WorldFullWarningMessage,
UserLeftZoneMessage,
BanUserMessage, RefreshRoomMessage,
EmoteEventMessage,
BanUserMessage, RefreshRoomMessage, EmotePromptMessage,
} from "../Messages/generated/messages_pb";
import {User, UserSocket} from "../Model/User";
import {ProtobufUtils} from "../Model/Websocket/ProtobufUtils";
@ -67,6 +68,7 @@ export class SocketManager {
private rooms: Map<string, GameRoom> = new Map<string, GameRoom>();
constructor() {
clientEventsEmitter.registerToClientJoin((clientUUid: string, roomId: string) => {
gaugeManager.incNbClientPerRoomGauge(roomId);
});
@ -263,7 +265,8 @@ export class SocketManager {
GROUP_RADIUS,
(thing: Movable, fromZone: Zone|null, listener: ZoneSocket) => this.onZoneEnter(thing, fromZone, listener),
(thing: Movable, position:PositionInterface, listener: ZoneSocket) => this.onClientMove(thing, position, listener),
(thing: Movable, newZone: Zone|null, listener: ZoneSocket) => this.onClientLeave(thing, newZone, listener)
(thing: Movable, newZone: Zone|null, listener: ZoneSocket) => this.onClientLeave(thing, newZone, listener),
(emoteEventMessage:EmoteEventMessage, listener: ZoneSocket) => this.onEmote(emoteEventMessage, listener),
);
gaugeManager.incNbRoomGauge();
this.rooms.set(roomId, world);
@ -296,6 +299,9 @@ export class SocketManager {
userJoinedZoneMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
userJoinedZoneMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
userJoinedZoneMessage.setFromzone(this.toProtoZone(fromZone));
if (thing.visitCardUrl) {
userJoinedZoneMessage.setVisitcardurl(thing.visitCardUrl);
}
userJoinedZoneMessage.setCompanion(thing.companion);
const subMessage = new SubToPusherMessage();
@ -339,6 +345,14 @@ export class SocketManager {
}
}
private onEmote(emoteEventMessage: EmoteEventMessage, client: ZoneSocket) {
const subMessage = new SubToPusherMessage();
subMessage.setEmoteeventmessage(emoteEventMessage);
emitZoneMessage(subMessage, client);
}
private emitCreateUpdateGroupEvent(client: ZoneSocket, fromZone: Zone|null, group: Group): void {
const position = group.getPosition();
const pointMessage = new PointMessage();
@ -593,6 +607,9 @@ export class SocketManager {
userJoinedMessage.setName(thing.name);
userJoinedMessage.setCharacterlayersList(ProtobufUtils.toCharacterLayerMessages(thing.characterLayers));
userJoinedMessage.setPosition(ProtobufUtils.toPositionMessage(thing.getPosition()));
if (thing.visitCardUrl) {
userJoinedMessage.setVisitcardurl(thing.visitCardUrl);
}
userJoinedMessage.setCompanion(thing.companion);
const subMessage = new SubToPusherMessage();
@ -751,6 +768,13 @@ export class SocketManager {
recipient.socket.write(clientMessage);
});
}
handleEmoteEventMessage(room: GameRoom, user: User, emotePromptMessage: EmotePromptMessage) {
const emoteEventMessage = new EmoteEventMessage();
emoteEventMessage.setEmote(emotePromptMessage.getEmote());
emoteEventMessage.setActoruserid(user.id);
room.emitEmoteEvent(user, emoteEventMessage);
}
}
export const socketManager = new SocketManager();

View file

@ -5,6 +5,7 @@ import {Group} from "../src/Model/Group";
import {User, UserSocket} from "_Model/User";
import {JoinRoomMessage, PositionMessage} from "../src/Messages/generated/messages_pb";
import Direction = PositionMessage.Direction;
import {EmoteCallback} from "_Model/Zone";
function createMockUser(userId: number): User {
return {
@ -33,6 +34,8 @@ function createJoinRoomMessage(uuid: string, x: number, y: number): JoinRoomMess
return joinRoomMessage;
}
const emote: EmoteCallback = (emoteEventMessage, listener): void => {}
describe("GameRoom", () => {
it("should connect user1 and user2", () => {
let connectCalledNumber: number = 0;
@ -43,7 +46,8 @@ describe("GameRoom", () => {
}
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {});
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote);
@ -72,7 +76,7 @@ describe("GameRoom", () => {
}
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {});
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote);
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));
@ -101,7 +105,7 @@ describe("GameRoom", () => {
disconnectCallNumber++;
}
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {});
const world = new GameRoom('_/global/test.json', connect, disconnect, 160, 160, () => {}, () => {}, () => {}, emote);
const user1 = world.join(createMockUserSocket(), createJoinRoomMessage('1', 100, 100));

View file

@ -1,10 +1,6 @@
import "jasmine";
import {GameRoom, ConnectCallback, DisconnectCallback } from "_Model/GameRoom";
import {Point} from "../src/Model/Websocket/MessageUserPosition";
import { Group } from "../src/Model/Group";
import {PositionNotifier} from "../src/Model/PositionNotifier";
import {User, UserSocket} from "../src/Model/User";
import {PointInterface} from "../src/Model/Websocket/PointInterface";
import {Zone} from "_Model/Zone";
import {Movable} from "_Model/Movable";
import {PositionInterface} from "_Model/PositionInterface";
@ -23,21 +19,21 @@ describe("PositionNotifier", () => {
moveTriggered = true;
}, (thing: Movable) => {
leaveTriggered = true;
});
}, () => {});
const user1 = new User(1, 'test', '10.0.0.2', {
x: 500,
y: 500,
moving: false,
direction: 'down'
}, false, positionNotifier, {} as UserSocket, [], 'foo', []);
}, false, positionNotifier, {} as UserSocket, [], null, 'foo', []);
const user2 = new User(2, 'test', '10.0.0.2', {
x: -9999,
y: -9999,
moving: false,
direction: 'down'
}, false, positionNotifier, {} as UserSocket, [], 'foo', []);
}, false, positionNotifier, {} as UserSocket, [], null, 'foo', []);
positionNotifier.addZoneListener({} as ZoneSocket, 0, 0);
positionNotifier.addZoneListener({} as ZoneSocket, 0, 1);
@ -98,21 +94,21 @@ describe("PositionNotifier", () => {
moveTriggered = true;
}, (thing: Movable) => {
leaveTriggered = true;
});
}, () => {});
const user1 = new User(1, 'test', '10.0.0.2', {
x: 500,
y: 500,
moving: false,
direction: 'down'
}, false, positionNotifier, {} as UserSocket, [], 'foo', []);
}, false, positionNotifier, {} as UserSocket, [], null, 'foo', []);
const user2 = new User(2, 'test', '10.0.0.2', {
x: 0,
y: 0,
moving: false,
direction: 'down'
}, false, positionNotifier, {} as UserSocket, [], 'foo', []);
}, false, positionNotifier, {} as UserSocket, [], null, 'foo', []);
const listener = {} as ZoneSocket;
positionNotifier.addZoneListener(listener, 0, 0);

View file

@ -1251,9 +1251,9 @@ has-values@^1.0.0:
kind-of "^4.0.0"
hosted-git-info@^2.1.4:
version "2.8.8"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
http-errors@1.7.2:
version "1.7.2"
@ -1704,9 +1704,9 @@ lodash.once@^4.0.0:
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
long@~3:
version "3.2.0"

View file

@ -209,9 +209,9 @@
}
},
"glob-parent": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz",
"integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==",
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"requires": {
"is-glob": "^4.0.1"
}
@ -230,9 +230,9 @@
}
},
"hosted-git-info": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz",
"integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg=="
"version": "2.8.9",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw=="
},
"indent-string": {
"version": "2.1.0",

View file

@ -148,8 +148,8 @@ get-stdin@^4.0.1:
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
glob-parent@~5.1.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229"
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
dependencies:
is-glob "^4.0.1"
@ -169,8 +169,8 @@ graceful-fs@^4.1.2:
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb"
hosted-git-info@^2.1.4:
version "2.8.8"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
indent-string@^2.1.0:
version "2.1.0"

View file

@ -2,7 +2,7 @@
local env = std.extVar("env"),
local namespace = env.DEPLOY_REF,
local tag = namespace,
local url = if namespace == "master" then "workadventu.re" else namespace+".test.workadventu.re",
local url = namespace+".test.workadventu.re",
// develop branch does not use admin because of issue with SSL certificate of admin as of now.
local adminUrl = if namespace == "master" || namespace == "develop" || std.startsWith(namespace, "admin") then "https://"+url else null,
"$schema": "https://raw.githubusercontent.com/thecodingmachine/deeployer/master/deeployer.schema.json",
@ -11,8 +11,7 @@
"back1": {
"image": "thecodingmachine/workadventure-back:"+tag,
"host": {
"url": "api1."+url,
"https": "enable",
"url": "api1-"+url,
"containerPort": 8080
},
"ports": [8080, 50051],
@ -25,16 +24,12 @@
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
} + (if adminUrl != null then {
"ADMIN_API_URL": adminUrl,
} else {}) + if namespace != "master" then {
// Absolutely ugly WorkAround to circumvent broken certificates on the K8S test cluster. Don't do this in production kids!
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
}
} else {})
},
"back2": {
"image": "thecodingmachine/workadventure-back:"+tag,
"host": {
"url": "api2."+url,
"https": "enable",
"url": "api2-"+url,
"containerPort": 8080
},
"ports": [8080, 50051],
@ -47,17 +42,13 @@
"TURN_STATIC_AUTH_SECRET": env.TURN_STATIC_AUTH_SECRET,
} + (if adminUrl != null then {
"ADMIN_API_URL": adminUrl,
} else {}) + if namespace != "master" then {
// Absolutely ugly WorkAround to circumvent broken certificates on the K8S test cluster. Don't do this in production kids!
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
}
} else {})
},
"pusher": {
"replicas": 2,
"image": "thecodingmachine/workadventure-pusher:"+tag,
"host": {
"url": "pusher."+url,
"https": "enable"
"url": "pusher-"+url,
},
"ports": [8080],
"env": {
@ -69,35 +60,30 @@
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
} + (if adminUrl != null then {
"ADMIN_API_URL": adminUrl,
} else {}) + if namespace != "master" then {
// Absolutely ugly WorkAround to circumvent broken certificates on the K8S test cluster. Don't do this in production kids!
"NODE_TLS_REJECT_UNAUTHORIZED": "0"
}
} else {})
},
"front": {
"image": "thecodingmachine/workadventure-front:"+tag,
"host": {
"url": "play."+url,
"https": "enable"
"url": "play-"+url,
},
"ports": [80],
"env": {
"PUSHER_URL": "//pusher."+url,
"UPLOADER_URL": "//uploader."+url,
"PUSHER_URL": "//pusher-"+url,
"UPLOADER_URL": "//uploader-"+url,
"ADMIN_URL": "//"+url,
"JITSI_URL": env.JITSI_URL,
"SECRET_JITSI_KEY": env.SECRET_JITSI_KEY,
"TURN_SERVER": "turn:coturn.workadventu.re:443,turns:coturn.workadventu.re:443",
"JITSI_PRIVATE_MODE": if env.SECRET_JITSI_KEY != '' then "true" else "false",
"START_ROOM_URL": "/_/global/maps."+url+"/Floor0/floor0.json"
"START_ROOM_URL": "/_/global/maps-"+url+"/Floor0/floor0.json"
//"GA_TRACKING_ID": "UA-10196481-11"
}
},
"uploader": {
"image": "thecodingmachine/workadventure-uploader:"+tag,
"host": {
"url": "uploader."+url,
"https": "enable",
"url": "uploader-"+url,
"containerPort": 8080
},
"ports": [8080],
@ -107,16 +93,12 @@
"maps": {
"image": "thecodingmachine/workadventure-maps:"+tag,
"host": {
"url": "maps."+url,
"https": "enable"
"url": "maps-"+url
},
"ports": [80]
},
},
"config": {
"https": {
"mail": "d.negrier@thecodingmachine.com"
},
k8sextension(k8sConf)::
k8sConf + {
back1+: {
@ -131,6 +113,14 @@
}
}
}
},
ingress+: {
spec+: {
tls+: [{
hosts: ["api1-"+url],
secretName: "certificate-tls"
}]
}
}
},
back2+: {
@ -145,6 +135,14 @@
}
}
}
},
ingress+: {
spec+: {
tls+: [{
hosts: ["api2-"+url],
secretName: "certificate-tls"
}]
}
}
},
pusher+: {
@ -159,8 +157,46 @@
}
}
}
}
}
},
ingress+: {
spec+: {
tls+: [{
hosts: ["pusher-"+url],
secretName: "certificate-tls"
}]
}
}
},
front+: {
ingress+: {
spec+: {
tls+: [{
hosts: ["play-"+url],
secretName: "certificate-tls"
}]
}
}
},
uploader+: {
ingress+: {
spec+: {
tls+: [{
hosts: ["uploader-"+url],
secretName: "certificate-tls"
}]
}
}
},
maps+: {
ingress+: {
spec+: {
tls+: [{
hosts: ["maps-"+url],
secretName: "certificate-tls"
}]
}
}
},
}
}
}

View file

@ -33,6 +33,8 @@ services:
STARTUP_COMMAND_1: ./templater.sh
STARTUP_COMMAND_2: yarn install
TURN_SERVER: "turn:localhost:3478,turns:localhost:5349"
DISABLE_NOTIFICATIONS: "$DISABLE_NOTIFICATIONS"
SKIP_RENDER_OPTIMIZATIONS: "$SKIP_RENDER_OPTIMIZATIONS"
# Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials.
# Advice: you should instead use Coturn REST API along the TURN_STATIC_AUTH_SECRET in the Back container
TURN_USER: ""

View file

@ -33,6 +33,8 @@ services:
STARTUP_COMMAND_2: yarn install
STUN_SERVER: "stun:stun.l.google.com:19302"
TURN_SERVER: "turn:coturn.workadventure.localhost:3478,turns:coturn.workadventure.localhost:5349"
DISABLE_NOTIFICATIONS: "$DISABLE_NOTIFICATIONS"
SKIP_RENDER_OPTIMIZATIONS: "$SKIP_RENDER_OPTIMIZATIONS"
# Use TURN_USER/TURN_PASSWORD if your Coturn server is secured via hard coded credentials.
# Advice: you should instead use Coturn REST API along the TURN_STATIC_AUTH_SECRET in the Back container
TURN_USER: ""

237
docs/maps/api-reference.md Normal file
View file

@ -0,0 +1,237 @@
{.section-title.accent.text-primary}
# API Reference
### Sending a message in the chat
```
sendChatMessage(message: string, author: string): void
```
Sends a message in the chat. The message is only visible in the browser of the current user.
* **message**: the message to be displayed in the chat
* **author**: the name displayed for the author of the message. It does not have to be a real user.
Example:
```javascript
WA.sendChatMessage('Hello world', 'Mr Robot');
```
### Listening to messages from the chat
```javascript
onChatMessage(callback: (message: string) => void): void
```
Listens to messages typed by the current user and calls the callback. Messages from other users in the chat cannot be listened to.
* **callback**: the function that will be called when a message is received. It contains the message typed by the user.
Example:
```javascript
WA.onChatMessage((message => {
console.log('The user typed a message', message);
}));
```
### Detecting when the user enters/leaves a zone
```
onEnterZone(name: string, callback: () => void): void
onLeaveZone(name: string, callback: () => void): void
```
Listens to the position of the current user. The event is triggered when the user enters or leaves a given zone. The name of the zone is stored in the map, on a dedicated layer with the `zone` property.
<div>
<figure class="figure">
<img src="https://workadventu.re/img/docs/trigger_event.png" class="figure-img img-fluid rounded" alt="" />
<figcaption class="figure-caption">The `zone` property, applied on a layer</figcaption>
</figure>
</div>
* **name**: the name of the zone, as defined in the `zone` property.
* **callback**: the function that will be called when a user enters or leaves the zone.
Example:
```javascript
WA.onEnterZone('myZone', () => {
WA.sendChatMessage("Hello!", 'Mr Robot');
})
WA.onLeaveZone('myZone', () => {
WA.sendChatMessage("Goodbye!", 'Mr Robot');
})
```
### Opening a popup
In order to open a popup window, you must first define the position of the popup on your map.
You can position this popup by using a "rectangle" object in Tiled that you will place on an "object" layer.
<div class="row">
<div class="col">
<img src="https://workadventu.re/img/docs/screen_popup_tiled.png" class="figure-img img-fluid rounded" alt="" />
</div>
<div class="col">
<img src="https://workadventu.re/img/docs/screen_popup_in_game.png" class="figure-img img-fluid rounded" alt="" />
</div>
</div>
```
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup
```
* **targetObject**: the name of the rectangle object defined in Tiled.
* **message**: the message to display in the popup.
* **buttons**: an array of action buttons defined underneath the popup.
Action buttons are `ButtonDescriptor` objects containing these properties.
* **label (_string_)**: The label of the button.
* **className (_string_)**: The visual type of the button. Can be one of "normal", "primary", "success", "warning", "error", "disabled".
* **callback (_(popup: Popup)=>void_)**: Callback called when the button is pressed.
Please note that `openPopup` returns an object of the `Popup` class. Also, the callback called when a button is clicked is passed a `Popup` object.
The `Popup` class that represents an open popup contains a single method: `close()`. This will obviously close the popup when called.
```javascript
class Popup {
/**
* Closes the popup
*/
close() {};
}
```
Example:
```javascript
let helloWorldPopup;
// Open the popup when we enter a given zone
helloWorldPopup = WA.onEnterZone('myZone', () => {
WA.openPopup("popupRectangle", 'Hello world!', [{
label: "Close",
className: "primary",
callback: (popup) => {
// Close the popup when the "Close" button is pressed.
popup.close();
}
});
}]);
// Close the popup when we leave the zone.
WA.onLeaveZone('myZone', () => {
helloWorldPopup.close();
});
```
### Disabling / restoring controls
```
disablePlayerControls(): void
restorePlayerControls(): void
```
These 2 methods can be used to completely disable player controls and to enable them again.
When controls are disabled, the user cannot move anymore using keyboard input. This can be useful in a "First Time User Experience" part, to display an important message to a user before letting him/her move again.
Example:
```javascript
WA.onEnterZone('myZone', () => {
WA.disablePlayerControls();
WA.openPopup("popupRectangle", 'This is an imporant message!', [{
label: "Got it!",
className: "primary",
callback: (popup) => {
WA.restorePlayerControls();
popup.close();
}
}]);
});
```
### Opening a web page in a new tab
```
openTab(url: string): void
```
Opens the webpage at "url" in your browser, in a new tab.
Example:
```javascript
WA.openTab('https://www.wikipedia.org/');
```
### Opening a web page in the current tab
```
goToPage(url: string): void
```
Opens the webpage at "url" in your browser in place of WorkAdventure. WorkAdventure will be completely unloaded.
Example:
```javascript
WA.goToPage('https://www.wikipedia.org/');
```
### Opening/closing a web page in an iFrame
```
openCoWebSite(url: string): void
closeCoWebSite(): void
```
Opens the webpage at "url" in an iFrame (on the right side of the screen) or close that iFrame.
Example:
```javascript
WA.openCoWebSite('https://www.wikipedia.org/');
// ...
WA.closeCoWebSite();
```
### Load a sound from an url
```
loadSound(url: string): Sound
```
Load a sound from an url
Please note that `loadSound` returns an object of the `Sound` class
The `Sound` class that represents a loaded sound contains two methods: `play(soundConfig : SoundConfig|undefined)` and `stop()`
The parameter soundConfig is optional, if you call play without a Sound config the sound will be played with the basic configuration.
Example:
```javascript
var mySound = WA.loadSound("Sound.ogg");
var config = {
volume : 0.5,
loop : false,
rate : 1,
detune : 1,
delay : 0,
seek : 0,
mute : false
}
mySound.play(config);
// ...
mySound.stop();
```

117
docs/maps/scripting.md Normal file
View file

@ -0,0 +1,117 @@
{.alert.alert-danger style="width:80%"}
This feature is "_experimental_". We may apply changes in the near future to the way it works when we gather some feedback.
{.section-title.accent.text-primary}
# Scripting WorkAdventure maps
Do you want to add a bit of intelligence to your map? Scripts allow you to create maps with special features.
You can for instance:
* Create FTUE (First Time User Experience) scenarios where a first-time user will be displayed a notification popup.
* Create NPC (non playing characters) and interact with those characters using the chat.
* Organize interactions between an iframe and your map (for instance, walking on a special zone might add a product in the cart of an eCommerce website...)
* etc...
Please note that scripting in WorkAdventure is at an early stage of development and that more features might be added in the future. You can actually voice your opinion about useful features by adding [an issue on Github](https://github.com/thecodingmachine/workadventure/issues).
{.alert.alert-warning}
**Beware:** Scripts are executed in the browser of the current user only. Generally speaking, scripts cannot be used to trigger a change that will be displayed on other users screen.
## Scripting language
Client-side scripting is done in **Javascript** (or any language that transpiles to Javascript like _Typescript_).
There are 2 ways you can use the scripting language:
* **In the map**: By directly referring a Javascript file inside your map, in the `script` property of your map.
* **In an iFrame**: By placing your Javascript script into an iFrame, your script can communicate with the WorkAdventure game
## Adding a script in the map
Create a `script` property in your map.
In Tiled, in order to access your map properties, you can click on _"Map > Map properties"_.
<div>
<figure class="figure">
<img src="https://workadventu.re/img/docs/admin/map_properties.png" class="figure-img img-fluid rounded" alt="" />
<figcaption class="figure-caption">The Map properties menu</figcaption>
</figure>
</div>
Create a `script` property (a "string"), and put the URL of your script.
You can put relative URLs. If your script file is next to your map, you can simply write the name of the script file here.
<div>
<figure class="figure">
<img src="https://workadventu.re/img/docs/script_property.png" class="figure-img img-fluid rounded" alt="" />
<figcaption class="figure-caption">The script property</figcaption>
</figure>
</div>
Start by testing this with a simple message sent to the chat.
**script.js**
```javascript
WA.sendChatMessage('Hello world', 'Mr Robot');
```
The `WA` objects contains a number of useful methods enabling you to interact with the WorkAdventure game. For instance, `WA.sendChatMessage` opens the chat and adds a message in it.
In your browser console, when you open the map, the chat message should be displayed right away.
## Adding a script in an iFrame
In WorkAdventure, you can easily [open an iFrame using the `openWebsite` property on a layer](special-zones). However, by default, the iFrame is not allowed to communicate with WorkAdventure.
This is done to improve security. In order to be able to execute a script that communicates with WorkAdventure inside an iFrame, you have to **explicitly allow the iFrame to use the "iFrame API"**.
In order to allow communication with WorkAdventure, you need to add an additional property: `openWebsiteAllowApi`. This property must be _boolean_ and you must set it to "true".
<div>
<figure class="figure">
<img src="https://workadventu.re/img/docs/open_website_allow_api.png" class="figure-img img-fluid rounded" alt="" />
<figcaption class="figure-caption">The `openWebsiteAllowApi` property</figcaption>
</figure>
</div>
In your iFrame HTML page, you now need to import the _WorkAdventure client API Javascript library_. This library contains the `WA` object that you can use to communicate with WorkAdventure.
The library is available at `https://play.workadventu.re/iframe_api.js`.
_Note:_ if you are using a self-hosted version of WorkAdventure, use `https://[front_domain]/iframe_api.js`
**iframe.html**
```html
<!doctype html>
<html lang="en">
<head>
<script src="https://play.workadventu.re/iframe_api.js"></script>
</head>
<body>
</body>
</html>
```
You can now start by testing this with a simple message sent to the chat.
**iframe.html**
```html
...
<script>
WA.sendChatMessage('Hello world', 'Mr Robot');
</script>
...
```
Let's now review the complete list of methods available in this `WA` object.
## Using Typescript
View the dedicated page about [using Typescript with the scripting API](using-typescript).
## Available features in the client API
The list of available functions and features is [available in the API Reference page, with examples](api-reference).

View file

@ -25,6 +25,15 @@
],
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-explicit-any": "error"
"@typescript-eslint/no-explicit-any": "error",
// TODO: remove those ignored rules and write a stronger code!
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/restrict-plus-operands": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/restrict-template-expressions": "off"
}
}

View file

@ -29,7 +29,6 @@
<base href="/">
<link href="https://fonts.googleapis.com/css?family=Press+Start+2P" rel="stylesheet">
<link href="https://unpkg.com/nes.css@2.3.0/css/nes.min.css" rel="stylesheet" />
<title>WorkAdventure</title>
@ -38,6 +37,8 @@
<div class="main-container" id="main-container">
<!-- Create the editor container -->
<div id="game" class="game">
<div id="svelte-overlay">
</div>
<div id="game-overlay" class="game-overlay">
<div id="main-section" class="main-section">
</div>
@ -45,33 +46,6 @@
</aside>
<div id="chat-mode" class="chat-mode three-col" style="display: none;">
</div>
<div id="activeCam" class="activeCam">
<div id="div-myCamVideo" class="video-container">
<video id="myCamVideo" autoplay muted></video>
<div id="mySoundMeter" class="sound-progress">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
<div class="btn-cam-action">
<div id="btn-monitor" class="btn-monitor">
<img id="monitor" src="resources/logos/monitor.svg">
<img id="monitor-close" src="resources/logos/monitor-close.svg">
</div>
<div id="btn-video" class="btn-video">
<img id="cinema" src="resources/logos/cinema.svg">
<img id="cinema-close" src="resources/logos/cinema-close.svg">
</div>
<div id="btn-micro" class="btn-micro">
<img id="microphone" src="resources/logos/microphone.svg">
<img id="microphone-close" src="resources/logos/microphone-close.svg">
</div>
</div>
</div>
</div>
<div id="cowebsite" class="cowebsite hidden">
@ -106,30 +80,17 @@
</div>
</div>
<div class="audioplayer">
<label id="label-audioplayer_decrease_while_talking" for="audiooplayer_decrease_while_talking" title="decrease background volume by 50% when entering conversations">
<label id="label-audioplayer_decrease_while_talking" for="audioplayer_decrease_while_talking" title="decrease background volume by 50% when entering conversations">
reduce in conversations
<input type="checkbox" id="audioplayer_decrease_while_talking" checked />
</label>
<div id="audioplayer" style="visibility: hidden"></div>
</div>
</div>
<div class="audio-playing">
<img src="/resources/logos/megaphone.svg" />
</div>
</div>
<div id="activeScreenSharing" class="active-screen-sharing active">
</div>
<div id="webRtcSetup" class="webrtcsetup">
<img id="webRtcSetupNoVideo" class="background-img" src="resources/logos/cinema-close.svg">
<video id="myCamVideoSetup" autoplay muted></video>
</div>
<audio id="audio-webrtc-in">
<source src="/resources/objects/webrtc-in.mp3" type="audio/mp3">
</audio>
<audio id="audio-webrtc-out">
<source src="/resources/objects/webrtc-out.mp3" type="audio/mp3">
</audio>
<audio id="report-message">
<source src="/resources/objects/report-message.mp3" type="audio/mp3">
</audio>

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

5
front/dist/resources/fonts/fonts.css vendored Normal file
View file

@ -0,0 +1,5 @@
/*This file is a workaround to allow phaser to load directly this font */
@font-face {
font-family: "Press Start 2P";
src: url("/fonts/press-start-2p-latin-400-normal.woff2") format('woff2');
}

View file

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

View file

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

View file

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

View file

@ -1,7 +1,4 @@
<style>
*{
font-family: PixelFont-7,monospace!important;
}
#gameMenu main{
margin-top: 15px;
}
@ -67,7 +64,7 @@
<button id="adminConsoleButton">Admin console</button>
</section>
<section id="socialLinks" hidden>
<a class="not-button" href="https://www.facebook.com/workadventurebytcm" target="_blank"><img class="not-button" src="/resources/objects/facebook-icon.png"/></a>
<a class="not-button" href="https://www.facebook.com/workadventure.WA" target="_blank"><img class="not-button" src="/resources/objects/facebook-icon.png"/></a>
<a class="not-button" href="https://twitter.com/Workadventure_" target="_blank"><img class="not-button" src="/resources/objects/twitter-icon.png"/></a>
</section>
</main>

View file

@ -1,7 +1,4 @@
<style>
*{
font-family: PixelFont-7,monospace!important;
}
#menuIcon button {
background-color: black;
color: white;

View file

@ -1,7 +1,4 @@
<style>
*{
font-family: PixelFont-7,monospace!important;
}
#gameQuality {
background: #eceeee;
border: 1px solid #42464b;

View file

@ -1,7 +1,4 @@
<style>
*{
font-family: PixelFont-7,monospace!important;
}
#gameReport {
background: #eceeee;
border: 1px solid #42464b;

View file

@ -1,7 +1,4 @@
<style>
*{
font-family: PixelFont-7,monospace!important;
}
#gameShare {
background: #eceeee;
border: 1px solid #42464b;

View file

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

View file

@ -1,120 +0,0 @@
<style>
*{
font-family: PixelFont-7,monospace!important;
}
#loginScene {
background: #000000;
/*border: 1px solid #ebeeee;*/
border-radius: 6px;
margin: 20px auto 0;
width: 90%;
max-width: 200px;
color: #ebeeee;
max-height: 40vh;
overflow: hidden;
}
#loginScene h1 {
background-image: linear-gradient(top, #f1f3f3, #d4dae0);
border-bottom: 1px solid #a6abaf;
border-radius: 6px 6px 0 0;
box-sizing: border-box;
color: #727678;
display: block;
height: 43px;
padding-top: 10px;
margin: 0;
text-align: center;
text-shadow: 0 -1px 0 rgba(0,0,0,0.2), 0 1px 0 #fff;
}
#loginScene input {
font-size: 70%;
background: linear-gradient(top, #d6d7d7, #dee0e0);
border: 1px solid #a1a3a3;
border-radius: 4px;
box-shadow: 0 1px #fff;
box-sizing: border-box;
color: #696969;
height: 30px;
transition: box-shadow 0.3s;
width: 100%;
text-align: center;
}
#loginScene section {
margin: 10px;
}
#loginScene section.action{
text-align: center;
margin: 0;
}
#loginScene button {
margin: 10px;
background-color: black;;
color: #ebeeee;
border-radius: 7px;
padding-bottom: 4px;
width: 100px;
}
#loginScene button#loginSceneFormCancel {
background-color: #c7c7c700;
color: #292929;
}
#loginScene section h6,
#loginScene section h5{
margin: 1px;
}
#loginScene section.text-center{
text-align: center;
}
#loginScene section a{
font-size: 8px;
text-decoration: underline;
color: #ebeeee;
}
#loginScene section a:hover{
font-weight: 700;
}
#loginScene section p{
text-align: left;
font-size: 8px;
margin: 10px 10px;
}
#loginScene section p.err{
color: red;
text-align: center;
}
#loginScene section p.info{
display: none;
text-align: center;
}
#loginScene section input#loginSceneLink{
background-color: #a1a3a3;
}
#loginScene section img{
width: 160px;
margin: 20px 0;
}
@media only screen and (max-width: 800px),
only screen and (max-height: 600px) {
#loginScene{
overflow-y: scroll;
}
}
</style>
<form id="loginScene" hidden>
<section class="text-center">
<img src="resources/logos/logo.png">
</section>
<section class="text-center">
<h5>Enter your name</h5>
<p class="info">9 chars maximum</p>
<p class="err" id="errorLoginScene"></p>
</section>
<section>
<input type="text" name="email" id="loginSceneName">
<p>By continuing, you are agreeing our <a href="https://workadventu.re/terms-of-use" target="_blank">terms of use</a>, <a href="https://workadventu.re/privacy-policy" target="_blank">privacy policy</a> and <a href="https://workadventu.re/cookie-policy" target="_blank">cookie policy</a>.</p>
</section>
<section class="action">
<button type="submit" id="loginSceneFormSubmit">Continue</button>
</section>
</form>

View file

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

View file

@ -1,7 +1,4 @@
<style>
*{
font-family: PixelFont-7,monospace!important;
}
#warningMain {
border-radius: 5px;
height: 100px;

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 B

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View file

@ -1,4 +0,0 @@
@import "cowebsite.scss";
@import "cowebsite-mobile.scss";
@import "style.css";
@import "mobile-style.scss";

View file

@ -4,27 +4,37 @@
"main": "index.js",
"license": "SEE LICENSE IN LICENSE.txt",
"devDependencies": {
"@tsconfig/svelte": "^1.0.10",
"@types/google-protobuf": "^3.7.3",
"@types/jasmine": "^3.5.10",
"@types/mini-css-extract-plugin": "^1.4.3",
"@types/node": "^15.3.0",
"@types/quill": "^1.3.7",
"@typescript-eslint/eslint-plugin": "^2.26.0",
"@typescript-eslint/parser": "^2.26.0",
"css-loader": "^5.1.3",
"eslint": "^6.8.0",
"html-webpack-plugin": "^4.3.0",
"@types/webpack-dev-server": "^3.11.4",
"@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0",
"css-loader": "^5.2.4",
"eslint": "^7.26.0",
"fork-ts-checker-webpack-plugin": "^6.2.9",
"html-webpack-plugin": "^5.3.1",
"jasmine": "^3.5.0",
"mini-css-extract-plugin": "^1.3.9",
"sass": "^1.32.8",
"sass-loader": "10.1.1",
"ts-loader": "^6.2.2",
"ts-node": "^8.10.2",
"typescript": "^3.8.3",
"webpack": "^4.42.1",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3",
"webpack-merge": "^4.2.2"
"mini-css-extract-plugin": "^1.6.0",
"node-polyfill-webpack-plugin": "^1.1.2",
"sass": "^1.32.12",
"sass-loader": "^11.1.0",
"svelte": "^3.38.2",
"svelte-loader": "^3.1.1",
"svelte-preprocess": "^4.7.3",
"ts-loader": "^9.1.2",
"ts-node": "^9.1.1",
"tsconfig-paths": "^3.9.0",
"typescript": "^4.2.4",
"webpack": "^5.37.0",
"webpack-cli": "^4.7.0",
"webpack-dev-server": "^3.11.2"
},
"dependencies": {
"@fontsource/press-start-2p": "^4.3.0",
"@types/simple-peer": "^9.6.0",
"@types/socket.io-client": "^1.4.32",
"axios": "^0.21.1",
@ -37,12 +47,12 @@
"rxjs": "^6.6.3",
"simple-peer": "^9.6.2",
"socket.io-client": "^2.3.0",
"webpack-require-http": "^0.4.3"
"standardized-audio-context": "^25.2.4"
},
"scripts": {
"start": "webpack-dev-server --open",
"build": "webpack --config webpack.prod.js",
"test": "ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
"start": "TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" webpack serve --open",
"build": "TS_NODE_PROJECT=\"tsconfig-for-webpack.json\" NODE_ENV=production webpack",
"test": "TS_NODE_PROJECT=\"tsconfig-for-jasmine.json\" ts-node node_modules/jasmine/bin/jasmine --config=jasmine.json",
"lint": "node_modules/.bin/eslint src/ . --ext .ts",
"fix": "node_modules/.bin/eslint --fix src/ . --ext .ts"
}

View file

@ -1,7 +1,7 @@
import {HtmlUtils} from "../WebRtc/HtmlUtils";
import {UserInputManager} from "../Phaser/UserInput/UserInputManager";
import {RoomConnection} from "../Connexion/RoomConnection";
import {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels";
import type {UserInputManager} from "../Phaser/UserInput/UserInputManager";
import type {RoomConnection} from "../Connexion/RoomConnection";
import type {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels";
import {AdminMessageEventTypes} from "../Connexion/AdminMessagesService";
export const CLASS_CONSOLE_MESSAGE = 'main-console';

View file

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

View file

@ -1,4 +1,4 @@
import {TypeMessageInterface} from "./UserMessageManager";
import type {TypeMessageInterface} from "./UserMessageManager";
import {HtmlUtils} from "../WebRtc/HtmlUtils";
let modalTimeOut : NodeJS.Timeout;
@ -44,7 +44,13 @@ export class TypeMessageExt implements TypeMessageInterface{
mainSectionDiv.appendChild(div);
const reportMessageAudio = HtmlUtils.getElementByIdOrFail<HTMLAudioElement>('report-message');
reportMessageAudio.play();
// FIXME: this will fail on iOS
// We should move the sound playing into the GameScene and listen to the event of a report using a store
try {
reportMessageAudio.play();
} catch (e) {
console.error(e);
}
this.nbSecond = this.maxNbSecond;
setTimeout((c) => {
@ -86,4 +92,4 @@ export class Banned extends TypeMessageExt {
showMessage(message: string){
super.showMessage(message, false);
}
}
}

View file

@ -1,15 +1,17 @@
import { ButtonClickedEvent } from './ButtonClickedEvent';
import { ChatEvent } from './ChatEvent';
import { ClosePopupEvent } from './ClosePopupEvent';
import { EnterLeaveEvent } from './EnterLeaveEvent';
import { GoToPageEvent } from './GoToPageEvent';
import { LoadPageEvent } from './LoadPageEvent';
import { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent';
import { OpenPopupEvent } from './OpenPopupEvent';
import { OpenTabEvent } from './OpenTabEvent';
import { UserInputChatEvent } from './UserInputChatEvent';
import type { ButtonClickedEvent } from './ButtonClickedEvent';
import type { ChatEvent } from './ChatEvent';
import type { ClosePopupEvent } from './ClosePopupEvent';
import type { EnterLeaveEvent } from './EnterLeaveEvent';
import type { GoToPageEvent } from './GoToPageEvent';
import type { LoadPageEvent } from './LoadPageEvent';
import type { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent';
import type { OpenPopupEvent } from './OpenPopupEvent';
import type { OpenTabEvent } from './OpenTabEvent';
import type { UserInputChatEvent } from './UserInputChatEvent';
import type { LoadSoundEvent} from "./LoadSoundEvent";
import type {PlaySoundEvent} from "./PlaySoundEvent";
export interface TypedMessageEvent<T> extends MessageEvent {
@ -27,10 +29,13 @@ export type IframeEventMap = {
goToPage: GoToPageEvent
openCoWebSite: OpenCoWebSiteEvent
closeCoWebSite: null
disablePlayerControl: null
restorePlayerControl: null
disablePlayerControls: null
restorePlayerControls: null
displayBubble: null
removeBubble: null
loadSound: LoadSoundEvent
playSound: PlaySoundEvent
stopSound: null
}
export interface IframeEvent<T extends keyof IframeEventMap> {
type: T;
@ -54,4 +59,4 @@ export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeResponseEventWrapper = (event: { type?: string }): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === 'string';
export const isIframeResponseEventWrapper = (event: { type?: string }): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === 'string';

View file

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isLoadSoundEvent =
new tg.IsInterface().withProperties({
url: tg.isString,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type LoadSoundEvent = tg.GuardedType<typeof isLoadSoundEvent>;

View file

@ -0,0 +1,24 @@
import * as tg from "generic-type-guard";
const isSoundConfig =
new tg.IsInterface().withProperties({
volume: tg.isOptional(tg.isNumber),
loop: tg.isOptional(tg.isBoolean),
mute: tg.isOptional(tg.isBoolean),
rate: tg.isOptional(tg.isNumber),
detune: tg.isOptional(tg.isNumber),
seek: tg.isOptional(tg.isNumber),
delay: tg.isOptional(tg.isNumber)
}).get();
export const isPlaySoundEvent =
new tg.IsInterface().withProperties({
url: tg.isString,
config : tg.isOptional(isSoundConfig),
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type PlaySoundEvent = tg.GuardedType<typeof isPlaySoundEvent>;

View file

@ -0,0 +1,11 @@
import * as tg from "generic-type-guard";
export const isStopSoundEvent =
new tg.IsInterface().withProperties({
url: tg.isString,
}).get();
/**
* A message sent from the iFrame to the game to add a message in the chat.
*/
export type StopSoundEvent = tg.GuardedType<typeof isStopSoundEvent>;

View file

@ -1,20 +1,21 @@
import { Subject } from "rxjs";
import { ChatEvent, isChatEvent } from "./Events/ChatEvent";
import * as crypto from "crypto";
import { HtmlUtils } from "../WebRtc/HtmlUtils";
import { EnterLeaveEvent } from "./Events/EnterLeaveEvent";
import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent";
import { isOpenPopupEvent, OpenPopupEvent } from "./Events/OpenPopupEvent";
import { isOpenTabEvent, OpenTabEvent } from "./Events/OpenTabEvent";
import { ButtonClickedEvent } from "./Events/ButtonClickedEvent";
import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent";
import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent";
import { scriptUtils } from "./ScriptUtils";
import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent";
import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent";
import { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent";
import { UserInputChatEvent } from "./Events/UserInputChatEvent";
import type { UserInputChatEvent } from "./Events/UserInputChatEvent";
import { isLoadPageEvent } from './Events/LoadPageEvent';
import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent";
import {isStopSoundEvent, StopSoundEvent} from "./Events/StopSoundEvent";
import {isLoadSoundEvent, LoadSoundEvent} from "./Events/LoadSoundEvent";
/**
* Listens to messages from iframes and turn those messages into easy to use observables.
* Also allows to send messages to those iframes.
@ -57,6 +58,15 @@ class IframeListener {
private readonly _removeBubbleStream: Subject<void> = new Subject();
public readonly removeBubbleStream = this._removeBubbleStream.asObservable();
private readonly _playSoundStream: Subject<PlaySoundEvent> = new Subject();
public readonly playSoundStream = this._playSoundStream.asObservable();
private readonly _stopSoundStream: Subject<StopSoundEvent> = new Subject();
public readonly stopSoundStream = this._stopSoundStream.asObservable();
private readonly _loadSoundStream: Subject<LoadSoundEvent> = new Subject();
public readonly loadSoundStream = this._loadSoundStream.asObservable();
private readonly iframes = new Set<HTMLIFrameElement>();
private readonly scripts = new Map<string, HTMLIFrameElement>();
@ -65,15 +75,23 @@ class IframeListener {
// Do we trust the sender of this message?
// Let's only accept messages from the iframe that are allowed.
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
let foundSrc: string | null = null;
for (const iframe of this.iframes) {
if (iframe.contentWindow === message.source) {
foundSrc = iframe.src;
break;
let foundSrc: string | undefined;
foundSrc = [...this.scripts.keys()].find(key => {
return this.scripts.get(key)?.contentWindow == message.source
});
if (foundSrc === undefined) {
for (const iframe of this.iframes) {
if (iframe.contentWindow === message.source) {
foundSrc = iframe.src;
break;
}
}
if (foundSrc === undefined) {
return;
}
}
if (!foundSrc) {
return;
}
const payload = message.data;
@ -91,20 +109,27 @@ class IframeListener {
else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) {
scriptUtils.goToPage(payload.data.url);
}
else if (payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) {
const scriptUrl = [...this.scripts.keys()].find(key => {
return this.scripts.get(key)?.contentWindow == message.source
})
scriptUtils.openCoWebsite(payload.data.url, scriptUrl || foundSrc);
else if (payload.type === 'playSound' && isPlaySoundEvent(payload.data)) {
this._playSoundStream.next(payload.data);
}
else if (payload.type === 'stopSound' && isStopSoundEvent(payload.data)) {
this._stopSoundStream.next(payload.data);
}
else if (payload.type === 'loadSound' && isLoadSoundEvent(payload.data)) {
this._loadSoundStream.next(payload.data);
}
else if (payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) {
scriptUtils.openCoWebsite(payload.data.url, foundSrc);
}
else if (payload.type === 'closeCoWebSite') {
scriptUtils.closeCoWebSite();
}
else if (payload.type === 'disablePlayerControl') {
else if (payload.type === 'disablePlayerControls') {
this._disablePlayerControlStream.next();
}
else if (payload.type === 'restorePlayerControl') {
else if (payload.type === 'restorePlayerControls') {
this._enablePlayerControlStream.next();
}
else if (payload.type === 'displayBubble') {
@ -183,7 +208,7 @@ class IframeListener {
}
private getIFrameId(scriptUrl: string): string {
return 'script' + crypto.createHash('md5').update(scriptUrl).digest("hex");
return 'script' + btoa(scriptUrl);
}
unregisterScript(scriptUrl: string): void {

View file

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

View file

@ -0,0 +1,61 @@
<script lang="typescript">
import {requestedScreenSharingState, screenSharingAvailableStore} from "../Stores/ScreenSharingStore";
import {requestedCameraState, requestedMicrophoneState} from "../Stores/MediaStore";
import monitorImg from "./images/monitor.svg";
import monitorCloseImg from "./images/monitor-close.svg";
import cinemaImg from "./images/cinema.svg";
import cinemaCloseImg from "./images/cinema-close.svg";
import microphoneImg from "./images/microphone.svg";
import microphoneCloseImg from "./images/microphone-close.svg";
function screenSharingClick(): void {
if ($requestedScreenSharingState === true) {
requestedScreenSharingState.disableScreenSharing();
} else {
requestedScreenSharingState.enableScreenSharing();
}
}
function cameraClick(): void {
if ($requestedCameraState === true) {
requestedCameraState.disableWebcam();
} else {
requestedCameraState.enableWebcam();
}
}
function microphoneClick(): void {
if ($requestedMicrophoneState === true) {
requestedMicrophoneState.disableMicrophone();
} else {
requestedMicrophoneState.enableMicrophone();
}
}
</script>
<div>
<div class="btn-cam-action">
<div class="btn-monitor" on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore} class:enabled={$requestedScreenSharingState}>
{#if $requestedScreenSharingState}
<img src={monitorImg} alt="Start screen sharing">
{:else}
<img src={monitorCloseImg} alt="Stop screen sharing">
{/if}
</div>
<div class="btn-video" on:click={cameraClick} class:disabled={!$requestedCameraState}>
{#if $requestedCameraState}
<img src={cinemaImg} alt="Turn on webcam">
{:else}
<img src={cinemaCloseImg} alt="Turn off webcam">
{/if}
</div>
<div class="btn-micro" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState}>
{#if $requestedMicrophoneState}
<img src={microphoneImg} alt="Turn on microphone">
{:else}
<img src={microphoneCloseImg} alt="Turn off microphone">
{/if}
</div>
</div>
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,33 @@
<script lang="typescript">
</script>
<main class="menuIcon">
<section>
<button>
<img src="/static/images/menu.svg" alt="Open menu">
</button>
</section>
</main>
<style lang="scss">
.menuIcon button {
background-color: black;
color: white;
border-radius: 7px;
padding: 2px 8px;
img {
width: 14px;
padding-top: 0;
/*cursor: url('/resources/logos/cursor_pointer.png'), pointer;*/
}
}
.menuIcon section {
margin: 10px;
}
@media only screen and (max-height: 700px) {
.menuIcon section {
margin: 2px;
}
}
</style>

View file

@ -0,0 +1,46 @@
<script lang="typescript">
import {localStreamStore} from "../Stores/MediaStore";
import SoundMeterWidget from "./SoundMeterWidget.svelte";
import {onDestroy} from "svelte";
function srcObject(node, stream) {
node.srcObject = stream;
return {
update(newStream) {
if (node.srcObject != newStream) {
node.srcObject = newStream
}
}
}
}
let stream : MediaStream|null;
/*$: {
if ($localStreamStore.type === 'success') {
stream = $localStreamStore.stream;
} else {
stream = null;
}
}*/
const unsubscribe = localStreamStore.subscribe(value => {
if (value.type === 'success') {
stream = value.stream;
} else {
stream = null;
}
});
onDestroy(unsubscribe);
</script>
<div>
<div class="video-container div-myCamVideo" class:hide={!$localStreamStore.constraints.video}>
<video class="myCamVideo" use:srcObject={$localStreamStore.stream} autoplay muted playsinline></video>
<!-- {#if stream}
<SoundMeterWidget stream={stream}></SoundMeterWidget>
{/if} -->
</div>
</div>

View file

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

View file

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

View file

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

View file

@ -0,0 +1,48 @@
<script lang="ts">
import {errorStore} from "../../Stores/ErrorStore";
function close(): boolean {
errorStore.clearMessages();
return false;
}
</script>
<div class="error-div nes-container is-dark is-rounded" open>
<p class="nes-text is-error title">Error</p>
<div class="body">
{#each $errorStore as error}
<p>{error}</p>
{/each}
</div>
<div class="button-bar">
<button class="nes-btn is-error" on:click={close}>Close</button>
</div>
</div>
<style lang="scss">
div.error-div {
pointer-events: auto;
margin-top: 10vh;
margin-right: auto;
margin-left: auto;
width: max-content;
max-width: 80vw;
.button-bar {
text-align: center;
}
.body {
max-height: 50vh;
}
p {
font-family: "Press Start 2P";
&.title {
text-align: center;
}
}
}
</style>

View file

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

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,84 @@
<script lang="typescript">
import { fly } from 'svelte/transition';
import {requestVisitCardsStore} from "../../Stores/GameStore";
import {onMount} from "svelte";
export let visitCardUrl: string;
let w = '500px';
let h = '250px';
let hidden = true;
let cvIframe;
function closeCard() {
requestVisitCardsStore.set(null);
}
function handleIframeMessage(message:any) {
if (message.data.type === 'cvIframeSize') {
w = (message.data.data.w) + 'px';
h = (message.data.data.h) + 'px';
}
}
onMount(() => {
cvIframe.onload = () => hidden = false
cvIframe.onerror = () => hidden = false
})
</script>
<style lang="scss">
.loader {
border: 16px solid #f3f3f3; /* Light grey */
border-top: 16px solid #3498db; /* Blue */
border-radius: 50%;
width: 120px;
height: 120px;
margin:auto;
animation: spin 2s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.visitCard {
pointer-events: all;
margin-left: auto;
margin-right: auto;
margin-top: 200px;
max-width: 80vw;
iframe {
border: 0;
max-width: 80vw;
overflow: hidden;
&.hidden {
visibility: hidden;
position: absolute;
}
}
button {
float: right;
}
}
</style>
<section class="visitCard" transition:fly="{{ y: -200, duration: 1000 }}" style="width: {w}">
{#if hidden}
<div class="loader"></div>
{/if}
<iframe title="visitCard" src={visitCardUrl} allow="clipboard-read; clipboard-write self {visitCardUrl}" style="width: {w}; height: {h}" class:hidden={hidden} bind:this={cvIframe}></iframe>
{#if !hidden}
<div class="buttonContainer">
<button class="nes-btn is-popUpElement" on:click={closeCard}>Close</button>
</div>
{/if}
</section>
<svelte:window on:message={handleIframeMessage}/>

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 332.8 332.8" style="enable-background:new 0 0 332.8 332.8;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<g>
<g>
<g>
<path class="st0" d="M330.8,171c-3.6-6.4-12-8.8-18.8-4.8l-45.6,26.4l-11.6,6.8v63.2l10.8,6.4c0.4,0,0.4,0.4,0.8,0.4l44.8,26
c2,1.6,4.8,2.4,7.6,2.4c7.6,0,13.6-6,13.6-13.6v-53.6l0.4-52.8C332.8,175.4,332.4,173,330.8,171z"/>
<path class="st0" d="M193.2,150.6c35.6,0,64.4-28.8,64.4-64.4s-28.8-64.4-64.4-64.4s-64.4,28.8-64.4,64.4
C128.8,121.8,157.6,150.6,193.2,150.6z M193.2,59.8c14.8,0,26.4,12,26.4,26.4c0,14.8-12,26.4-26.4,26.4s-26.4-12-26.4-26.4
C166.8,71.4,178.4,59.8,193.2,59.8z"/>
</g>
</g>
</g>
<g>
<g>
</g>
</g>
<rect x="134.8" y="-45.3" transform="matrix(-0.7402 0.6723 -0.6723 -0.7402 376.0669 224.8258)" class="st0" width="19.6" height="460.7"/>
<path class="st0" d="M90.6,83.3c-0.2-2.2-1.3-8.9-6.7-14.9c-5.4-5.9-11.9-7.6-14.1-8.1C59.7,49.2,49.5,38,39.4,26.8
c24.3-9.8,52-4.4,70.2,13.6c19.9,19.7,24.7,50.8,11.5,76.4C110.9,105.6,100.8,94.5,90.6,83.3z"/>
<path class="st0" d="M10.1,51.6c9.4,10.2,18.8,20.4,28.2,30.6c-0.2,1.8-1.4,11.7,5.5,20.5c8.2,10.3,20.7,10.2,22.1,10.1
c9.2,10.3,18.5,20.6,27.7,30.8c-4.8,2.3-24.6,11.2-48.3,4.1c-6-1.8-20.7-7.3-32.1-22C-0.3,108.1-0.2,89.1,0.1,83.4
C0.8,68,6.8,56.8,10.1,51.6z"/>
<g>
<path class="st0" d="M243.4,178.2c0.1,24.5,0.2,49,0.2,73.5c-30.7-33.8-61.3-67.7-92-101.5c5.9,3.9,20.9,12.4,41.6,12.4
c16,0,28.2-5.2,34.4-8.4c2.5,1.5,7,4.6,10.7,10.3C242,170,243,175.4,243.4,178.2z"/>
<g>
<path class="st0" d="M211.2,311C150.8,258.7,90.4,206.5,30,154.2c6.1,3.1,18.2,8.4,34.4,8.4c18.1,0,31.5-6.5,37.5-9.9
c44.5,49,89.1,98.1,133.6,147.1c-1.8,2.1-5.3,5.5-10.6,8.1C219.2,310.6,214,311,211.2,311z"/>
<path class="st0" d="M46.8,311C36,267.7,25.2,224.3,14.4,181c0.1-3.2,0.7-11.3,6.5-18.8c3.1-4.1,6.7-6.6,9.1-8
C90.4,206.5,150.8,258.7,211.2,311C156.4,311,101.6,311,46.8,311z"/>
<path class="st0" d="M14.4,278.6L14.4,278.6c0-32.5,0-65.1,0-97.6c10.8,43.3,21.6,86.7,32.4,130c-2.6,0-12.7-0.4-21.5-8.1
C14.7,293.5,14.4,280.7,14.4,278.6z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;}
</style>
<rect x="257" y="-47.9" transform="matrix(-0.7402 0.6723 -0.6723 -0.7402 643.9641 283.6469)" class="st0" width="20.4" height="628.3"/>
<g>
<g>
<path class="st0" d="M333.6,250.3c-52.6-43.9-105.1-87.9-157.7-131.8c0-17.9,0-35.8,0-53.6c6.5-38.6,40.3-67,79.3-66.8
c38.6,0.2,71.9,28.5,78.4,66.8C333.6,126.7,333.6,188.5,333.6,250.3z"/>
<path class="st0" d="M322.6,279.9c-48.9-53.8-97.8-107.6-146.6-161.4l0,0c52.6,43.9,105.1,87.9,157.7,131.8
c-0.2,1.6-0.5,3.3-0.9,5C330.5,265.2,326.6,273.5,322.6,279.9z"/>
</g>
<path class="st0" d="M292.5,308.1c-2.3,1.2-39.5,20.3-76.7-1c-36.4-20.8-39.4-61.2-39.6-64.1c-0.1-21-0.1-42.1-0.2-63.1
C214.8,222.6,253.6,265.3,292.5,308.1z"/>
</g>
<path class="st0" d="M431.6,238.5c-0.9-8.4-8.5-14.4-16.6-13.5c-7.9,0.9-13.9,8.1-13.2,16.3c-0.1,13.3-2.2,34.6-12.6,57.9
c-6.3,14.2-14,25.2-20.6,33.1c6.8,7.5,13.6,14.9,20.3,22.4c9.5-10.9,23.4-29.7,32.8-56.3C430.3,273.9,431.8,252.5,431.6,238.5z"/>
<line class="st0" x1="354.5" y1="347.2" x2="374.6" y2="369.4"/>
<path class="st0" d="M338.5,359.9c6.8,7.4,13.5,14.9,20.3,22.3c-52.6,37.6-121.5,43.7-179.2,15.8c-60.3-29.1-98.9-90.7-99.3-158.2
c0-8.2,6.8-15,15-15s15,6.8,15,15c0.1,13.5,2.4,54.4,32.4,91.6c4.2,5.2,45.1,54.1,113.3,54.1C297,385.6,326.7,367.9,338.5,359.9z"/>
<rect x="241" y="409.6" class="st0" width="29.9" height="102.3"/>
<path class="st0" d="M304.2,511.9h-97.1c-8-0.4-14.3-7.1-14.3-15c0-8.1,6.7-14.9,15-15c31.7,0,63.4,0.1,95.1,0.1
c8.9-0.6,16.3,6.5,16.3,14.9C319.2,504.8,312.6,511.7,304.2,511.9z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

Before

Width:  |  Height:  |  Size: 937 B

After

Width:  |  Height:  |  Size: 937 B

View file

Before

Width:  |  Height:  |  Size: 884 B

After

Width:  |  Height:  |  Size: 884 B

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

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

View file

@ -1,5 +1,5 @@
import {Subject} from "rxjs";
import {BanUserMessage, SendUserMessage} from "../Messages/generated/messages_pb";
import type {BanUserMessage, SendUserMessage} from "../Messages/generated/messages_pb";
export enum AdminMessageEventTypes {
admin = 'message',
@ -19,11 +19,11 @@ interface AdminMessageEvent {
class AdminMessagesService {
private _messageStream: Subject<AdminMessageEvent> = new Subject();
public messageStream = this._messageStream.asObservable();
constructor() {
this.messageStream.subscribe((event) => console.log('message', event))
}
onSendusermessage(message: SendUserMessage|BanUserMessage) {
this._messageStream.next({
type: message.getType() as unknown as AdminMessageEventTypes,
@ -32,4 +32,4 @@ class AdminMessagesService {
}
}
export const adminMessagesService = new AdminMessagesService();
export const adminMessagesService = new AdminMessagesService();

View file

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

View file

@ -1,8 +1,6 @@
import {PlayerAnimationDirections} from "../Phaser/Player/Animation";
import {UserSimplePeerInterface} from "../WebRtc/SimplePeer";
import {SignalData} from "simple-peer";
import {RoomConnection} from "./RoomConnection";
import {BodyResourceDescriptionInterface} from "../Phaser/Entity/PlayerTextures";
import type {SignalData} from "simple-peer";
import type {RoomConnection} from "./RoomConnection";
import type {BodyResourceDescriptionInterface} from "../Phaser/Entity/PlayerTextures";
export enum EventMessage{
CONNECT = "connect",
@ -47,6 +45,7 @@ export interface MessageUserPositionInterface {
name: string;
characterLayers: BodyResourceDescriptionInterface[];
position: PointInterface;
visitCardUrl: string|null;
companion: string|null;
}
@ -60,6 +59,7 @@ export interface MessageUserJoined {
name: string;
characterLayers: BodyResourceDescriptionInterface[];
position: PointInterface;
visitCardUrl: string | null;
companion: string|null;
}
@ -85,11 +85,6 @@ export interface WebRtcSignalReceivedMessageInterface {
webRtcPassword: string | undefined
}
export interface StartMapInterface {
mapUrlStart: string,
startInstance: string
}
export interface ViewportInterface {
left: number,
top: number,

View file

@ -0,0 +1,19 @@
import {Subject} from "rxjs";
interface EmoteEvent {
userId: number,
emoteName: string,
}
class EmoteEventStream {
private _stream:Subject<EmoteEvent> = new Subject();
public stream = this._stream.asObservable();
fire(userId: number, emoteName:string) {
this._stream.next({userId, emoteName});
}
}
export const emoteEventStream = new EmoteEventStream();

View file

@ -9,9 +9,8 @@ export interface CharacterTexture {
export const maxUserNameLength: number = MAX_USERNAME_LENGTH;
export function isUserNameValid(value: string): boolean {
const regexp = new RegExp('^[A-Za-z0-9]{1,'+maxUserNameLength+'}$');
return regexp.test(value);
export function isUserNameValid(value: unknown): boolean {
return typeof value === "string" && value.length > 0 && value.length <= maxUserNameLength && value.indexOf(' ') === -1;
}
export function areCharacterLayersValid(value: string[] | null): boolean {
@ -25,6 +24,6 @@ export function areCharacterLayersValid(value: string[] | null): boolean {
}
export class LocalUser {
constructor(public readonly uuid:string, public readonly jwtToken: string, public readonly textures: CharacterTexture[]) {
constructor(public readonly uuid:string, public readonly jwtToken: string, public textures: CharacterTexture[]) {
}
}

View file

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

View file

@ -27,11 +27,13 @@ import {
SendJitsiJwtMessage,
CharacterLayerMessage,
PingMessage,
SendUserMessage,
BanUserMessage
EmoteEventMessage,
EmotePromptMessage,
SendUserMessage,
BanUserMessage,
} from "../Messages/generated/messages_pb"
import {UserSimplePeerInterface} from "../WebRtc/SimplePeer";
import type {UserSimplePeerInterface} from "../WebRtc/SimplePeer";
import Direction = PositionMessage.Direction;
import {ProtobufClientUtils} from "../Network/ProtobufClientUtils";
import {
@ -42,11 +44,12 @@ import {
ViewportInterface, WebRtcDisconnectMessageInterface,
WebRtcSignalReceivedMessageInterface,
} from "./ConnexionModels";
import {BodyResourceDescriptionInterface} from "../Phaser/Entity/PlayerTextures";
import type {BodyResourceDescriptionInterface} from "../Phaser/Entity/PlayerTextures";
import {adminMessagesService} from "./AdminMessagesService";
import {worldFullMessageStream} from "./WorldFullMessageStream";
import {worldFullWarningStream} from "./WorldFullWarningStream";
import {connectionManager} from "./ConnectionManager";
import {emoteEventStream} from "./EmoteEventStream";
const manualPingDelay = 20000;
@ -86,7 +89,7 @@ export class RoomConnection implements RoomConnection {
url += '&bottom='+Math.floor(viewport.bottom);
url += '&left='+Math.floor(viewport.left);
url += '&right='+Math.floor(viewport.right);
if (typeof companion === 'string') {
url += '&companion='+encodeURIComponent(companion);
}
@ -124,7 +127,7 @@ export class RoomConnection implements RoomConnection {
if (message.hasBatchmessage()) {
for (const subMessage of (message.getBatchmessage() as BatchMessage).getPayloadList()) {
let event: string;
let event: string|null = null;
let payload;
if (subMessage.hasUsermovedmessage()) {
event = EventMessage.USER_MOVED;
@ -144,11 +147,16 @@ export class RoomConnection implements RoomConnection {
} else if (subMessage.hasItemeventmessage()) {
event = EventMessage.ITEM_EVENT;
payload = subMessage.getItemeventmessage();
} else if (subMessage.hasEmoteeventmessage()) {
const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage;
emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
} else {
throw new Error('Unexpected batch message type');
}
this.dispatch(event, payload);
if (event) {
this.dispatch(event, payload);
}
}
} else if (message.hasRoomjoinedmessage()) {
const roomJoinedMessage = message.getRoomjoinedmessage() as RoomJoinedMessage;
@ -336,6 +344,7 @@ export class RoomConnection implements RoomConnection {
userId: message.getUserid(),
name: message.getName(),
characterLayers,
visitCardUrl: message.getVisitcardurl(),
position: ProtobufClientUtils.toPointInterface(position),
companion: companion ? companion.getName() : null
}
@ -599,4 +608,14 @@ export class RoomConnection implements RoomConnection {
public isAdmin(): boolean {
return this.hasTag('admin');
}
public emitEmoteEvent(emoteName: string): void {
const emoteMessage = new EmotePromptMessage();
emoteMessage.setEmote(emoteName)
const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setEmotepromptmessage(emoteMessage);
this.socket.send(clientToServerMessage.serializeBinary().buffer);
}
}

View file

@ -1,11 +1,11 @@
const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true";
const START_ROOM_URL : string = process.env.START_ROOM_URL || '/_/global/maps.workadventure.localhost/Floor0/floor0.json';
// For compatibility reasons with older versions, API_URL is the old host name of PUSHER_URL
const PUSHER_URL = process.env.PUSHER_URL || (process.env.API_URL ? '//'+process.env.API_URL : "//pusher.workadventure.localhost");
const PUSHER_URL = process.env.PUSHER_URL || '//pusher.workadventure.localhost';
const UPLOADER_URL = process.env.UPLOADER_URL || '//uploader.workadventure.localhost';
const ADMIN_URL = process.env.ADMIN_URL || "//workadventure.localhost";
const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302";
const TURN_SERVER: string = process.env.TURN_SERVER || "";
const SKIP_RENDER_OPTIMIZATIONS: boolean = process.env.SKIP_RENDER_OPTIMIZATIONS == "true";
const DISABLE_NOTIFICATIONS: boolean = process.env.DISABLE_NOTIFICATIONS == "true";
const TURN_USER: string = process.env.TURN_USER || '';
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || '';
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL;
@ -14,15 +14,17 @@ const POSITION_DELAY = 200; // Wait 200ms between sending position events
const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player
export const MAX_USERNAME_LENGTH = parseInt(process.env.MAX_USERNAME_LENGTH || '') || 8;
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || '4');
export const DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == 'true';
export const isMobile = ():boolean => ( ( window.innerWidth <= 800 ) || ( window.innerHeight <= 600 ) );
export {
DEBUG_MODE,
START_ROOM_URL,
SKIP_RENDER_OPTIMIZATIONS,
DISABLE_NOTIFICATIONS,
PUSHER_URL,
UPLOADER_URL,
ADMIN_URL,
POSITION_DELAY,
MAX_EXTRAPOLATION_TIME,
STUN_SERVER,

View file

@ -1,6 +1,6 @@
import {PositionMessage} from "../Messages/generated/messages_pb";
import Direction = PositionMessage.Direction;
import {PointInterface} from "../Connexion/ConnexionModels";
import type {PointInterface} from "../Connexion/ConnexionModels";
export class ProtobufClientUtils {

View file

@ -1,6 +1,5 @@
import Sprite = Phaser.GameObjects.Sprite;
import Container = Phaser.GameObjects.Container;
import { lazyLoadCompanionResource } from "./CompanionTexturesLoadingManager";
import { PlayerAnimationDirections, PlayerAnimationTypes } from "../Player/Animation";
export interface CompanionStatus {
@ -25,7 +24,7 @@ export class Companion extends Container {
constructor(scene: Phaser.Scene, x: number, y: number, name: string, texturePromise: Promise<string>) {
super(scene, x + 14, y + 4);
this.sprites = new Map<string, Sprite>();
this.delta = 0;
@ -104,7 +103,7 @@ export class Companion extends Container {
}
}
}
this.setDepth(this.y);
this.playAnimation(this.direction, this.animationType);
}
@ -137,7 +136,7 @@ export class Companion extends Container {
this.getAnimations(resource).forEach(animation => {
this.scene.anims.create(animation);
});
this.scene.sys.updateList.add(sprite);
this.sprites.set(resource, sprite);
}

View file

@ -1,3 +1,5 @@
import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
export class ChatModeIcon extends Phaser.GameObjects.Sprite {
constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y, 'layout_modes', 3);
@ -6,6 +8,6 @@ export class ChatModeIcon extends Phaser.GameObjects.Sprite {
this.setOrigin(0, 1);
this.setInteractive();
this.setVisible(false);
this.setDepth(99999);
this.setDepth(DEPTH_INGAME_TEXT_INDEX);
}
}

View file

@ -1,6 +1,6 @@
import VirtualJoystick from 'phaser3-rex-plugins/plugins/virtualjoystick.js';
import ScaleManager = Phaser.Scale.ScaleManager;
import {waScaleManager} from "../Services/WaScaleManager";
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes";
//the assets were found here: https://hannemann.itch.io/virtual-joystick-pack-free
export const joystickBaseKey = 'joystickBase';
@ -20,21 +20,21 @@ export class MobileJoystick extends VirtualJoystick {
x: -1000,
y: -1000,
radius: radius * window.devicePixelRatio,
base: scene.add.image(0, 0, joystickBaseKey).setDisplaySize(baseSize * window.devicePixelRatio, baseSize * window.devicePixelRatio).setDepth(99999),
thumb: scene.add.image(0, 0, joystickThumbKey).setDisplaySize(thumbSize * window.devicePixelRatio, thumbSize * window.devicePixelRatio).setDepth(99999),
base: scene.add.image(0, 0, joystickBaseKey).setDisplaySize(baseSize * window.devicePixelRatio, baseSize * window.devicePixelRatio).setDepth(DEPTH_INGAME_TEXT_INDEX),
thumb: scene.add.image(0, 0, joystickThumbKey).setDisplaySize(thumbSize * window.devicePixelRatio, thumbSize * window.devicePixelRatio).setDepth(DEPTH_INGAME_TEXT_INDEX),
enable: true,
dir: "8dir",
});
this.visible = false;
this.enable = false;
this.scene.input.on('pointerdown', (pointer: { x: number; y: number; wasTouch: boolean; event: TouchEvent }) => {
this.scene.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
if (!pointer.wasTouch) {
return;
}
// Let's only display the joystick if there is one finger on the screen
if (pointer.event.touches.length === 1) {
if ((pointer.event as TouchEvent).touches.length === 1) {
this.x = pointer.x;
this.y = pointer.y;
this.visible = true;

View file

@ -1,4 +1,5 @@
import {discussionManager} from "../../WebRtc/DiscussionManager";
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes";
export const openChatIconName = 'openChatIcon';
export class OpenChatIcon extends Phaser.GameObjects.Image {
@ -9,7 +10,7 @@ export class OpenChatIcon extends Phaser.GameObjects.Image {
this.setOrigin(0, 1);
this.setInteractive();
this.setVisible(false);
this.setDepth(99999);
this.setDepth(DEPTH_INGAME_TEXT_INDEX);
this.on("pointerup", () => discussionManager.showDiscussionPart());
}

View file

@ -1,3 +1,5 @@
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes";
export class PresentationModeIcon extends Phaser.GameObjects.Sprite {
constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y, 'layout_modes', 0);
@ -6,6 +8,6 @@ export class PresentationModeIcon extends Phaser.GameObjects.Sprite {
this.setOrigin(0, 1);
this.setInteractive();
this.setVisible(false);
this.setDepth(99999);
this.setDepth(DEPTH_INGAME_TEXT_INDEX);
}
}

View file

@ -0,0 +1,74 @@
import Sprite = Phaser.GameObjects.Sprite;
import {DEPTH_UI_INDEX} from "../Game/DepthIndexes";
import {waScaleManager} from "../Services/WaScaleManager";
export interface RadialMenuItem {
image: string,
name: string,
}
export const RadialMenuClickEvent = 'radialClick';
export class RadialMenu extends Phaser.GameObjects.Container {
private resizeCallback: OmitThisParameter<() => void>;
constructor(scene: Phaser.Scene, x: number, y: number, private items: RadialMenuItem[]) {
super(scene, x, y);
this.setDepth(DEPTH_UI_INDEX)
this.scene.add.existing(this);
this.initItems();
this.resize();
this.resizeCallback = this.resize.bind(this);
this.scene.scale.on(Phaser.Scale.Events.RESIZE, this.resizeCallback);
}
private initItems() {
const itemsNumber = this.items.length;
const menuRadius = 70 + (waScaleManager.uiScalingFactor - 1) * 20;
this.items.forEach((item, index) => this.createRadialElement(item, index, itemsNumber, menuRadius))
}
private createRadialElement(item: RadialMenuItem, index: number, itemsNumber: number, menuRadius: number) {
const image = new Sprite(this.scene, 0, menuRadius, item.image);
this.add(image);
this.scene.sys.updateList.add(image);
const scalingFactor = waScaleManager.uiScalingFactor * 0.075;
image.setScale(scalingFactor)
image.setInteractive({
useHandCursor: true,
});
image.on('pointerdown', () => this.emit(RadialMenuClickEvent, item));
image.on('pointerover', () => {
this.scene.tweens.add({
targets: image,
props: {
scale: 2 * scalingFactor,
},
duration: 500,
ease: 'Power3',
})
});
image.on('pointerout', () => {
this.scene.tweens.add({
targets: image,
props: {
scale: scalingFactor,
},
duration: 500,
ease: 'Power3',
})
});
const angle = 2 * Math.PI * index / itemsNumber;
Phaser.Actions.RotateAroundDistance([image], {x: 0, y: 0}, angle, menuRadius);
}
private resize() {
this.setScale(waScaleManager.uiScalingFactor);
}
public destroy() {
this.scene.scale.removeListener(Phaser.Scale.Events.RESIZE, this.resizeCallback);
super.destroy();
}
}

View file

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

View file

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

View file

@ -1,7 +1,5 @@
import {ITiledMapObject} from "../Map/ITiledMap";
import Text = Phaser.GameObjects.Text;
import {GameScene} from "../Game/GameScene";
import TextStyle = Phaser.GameObjects.TextStyle;
import type {ITiledMapObject} from "../Map/ITiledMap";
import type {GameScene} from "../Game/GameScene";
export class TextUtils {
public static createTextFromITiledMapObject(scene: GameScene, object: ITiledMapObject): void {

View file

@ -1,10 +1,15 @@
import {PlayerAnimationDirections, PlayerAnimationTypes} from "../Player/Animation";
import {SpeechBubble} from "./SpeechBubble";
import BitmapText = Phaser.GameObjects.BitmapText;
import Text = Phaser.GameObjects.Text;
import Container = Phaser.GameObjects.Container;
import Sprite = Phaser.GameObjects.Sprite;
import {TextureError} from "../../Exception/TextureError";
import {Companion} from "../Companion/Companion";
import type {GameScene} from "../Game/GameScene";
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes";
import {waScaleManager} from "../Services/WaScaleManager";
const playerNameY = - 25;
interface AnimationData {
key: string;
@ -14,24 +19,31 @@ interface AnimationData {
frames : number[]
}
const interactiveRadius = 35;
export abstract class Character extends Container {
private bubble: SpeechBubble|null = null;
private readonly playerName: BitmapText;
private readonly playerName: Text;
public PlayerValue: string;
public sprites: Map<string, Sprite>;
private lastDirection: PlayerAnimationDirections = PlayerAnimationDirections.Down;
//private teleportation: Sprite;
private invisible: boolean;
public companion?: Companion;
private emote: Phaser.GameObjects.Sprite | null = null;
private emoteTween: Phaser.Tweens.Tween|null = null;
constructor(scene: Phaser.Scene,
constructor(scene: GameScene,
x: number,
y: number,
texturesPromise: Promise<string[]>,
name: string,
direction: PlayerAnimationDirections,
moving: boolean,
frame?: string | number
frame: string | number,
isClickable: boolean,
companion: string|null,
companionTexturePromise?: Promise<string>
) {
super(scene, x, y/*, texture, frame*/);
this.PlayerValue = name;
@ -45,19 +57,18 @@ export abstract class Character extends Container {
this.invisible = false
})
/*this.teleportation = new Sprite(scene, -20, -10, 'teleportation', 3);
this.teleportation.setInteractive();
this.teleportation.visible = false;
this.teleportation.on('pointerup', () => {
this.report.visible = false;
this.teleportation.visible = false;
});
this.add(this.teleportation);*/
this.playerName = new BitmapText(scene, 0, - 25, 'main_font', name, 7);
this.playerName.setOrigin(0.5).setCenterAlign().setDepth(99999);
this.playerName = new Text(scene, 0, playerNameY, name, {fontFamily: '"Press Start 2P"', fontSize: '8px', strokeThickness: 2, stroke: "gray"});
this.playerName.setOrigin(0.5).setDepth(DEPTH_INGAME_TEXT_INDEX);
this.add(this.playerName);
if (isClickable) {
this.setInteractive({
hitArea: new Phaser.Geom.Circle(0, 0, interactiveRadius),
hitAreaCallback: Phaser.Geom.Circle.Contains, //eslint-disable-line @typescript-eslint/unbound-method
useHandCursor: true,
});
}
scene.add.existing(this);
this.scene.physics.world.enableBody(this);
@ -69,6 +80,10 @@ export abstract class Character extends Container {
this.setDepth(-1);
this.playAnimation(direction, moving);
if (typeof companion === 'string') {
this.addCompanion(companion, companionTexturePromise);
}
}
public addCompanion(name: string, texturePromise?: Promise<string>): void {
@ -79,11 +94,10 @@ export abstract class Character extends Container {
public addTextures(textures: string[], frame?: string | number): void {
for (const texture of textures) {
if(!this.scene.textures.exists(texture)){
if(this.scene && !this.scene.textures.exists(texture)){
throw new TextureError('texture not found');
}
const sprite = new Sprite(this.scene, 0, 0, texture, frame);
sprite.setInteractive({useHandCursor: true});
this.add(sprite);
this.getPlayerAnimations(texture).forEach(d => {
this.scene.anims.create({
@ -225,7 +239,84 @@ export abstract class Character extends Container {
this.scene.sys.updateList.remove(sprite);
}
}
this.list.forEach(objectContaining => objectContaining.destroy())
super.destroy();
this.playerName.destroy();
}
playEmote(emoteKey: string) {
this.cancelPreviousEmote();
const scalingFactor = waScaleManager.uiScalingFactor * 0.05;
const emoteY = -30 - scalingFactor * 10;
this.playerName.setVisible(false);
this.emote = new Sprite(this.scene, 0, 0, emoteKey);
this.emote.setAlpha(0);
this.emote.setScale(0.1 * scalingFactor);
this.add(this.emote);
this.scene.sys.updateList.add(this.emote);
this.createStartTransition(scalingFactor, emoteY);
}
private createStartTransition(scalingFactor: number, emoteY: number) {
this.emoteTween = this.scene?.tweens.add({
targets: this.emote,
props: {
scale: scalingFactor,
alpha: 1,
y: emoteY,
},
ease: 'Power2',
duration: 500,
onComplete: () => {
this.startPulseTransition(emoteY, scalingFactor);
}
});
}
private startPulseTransition(emoteY: number, scalingFactor: number) {
this.emoteTween = this.scene?.tweens.add({
targets: this.emote,
props: {
y: emoteY * 1.3,
scale: scalingFactor * 1.1
},
duration: 250,
yoyo: true,
repeat: 1,
completeDelay: 200,
onComplete: () => {
this.startExitTransition(emoteY);
}
});
}
private startExitTransition(emoteY: number) {
this.emoteTween = this.scene?.tweens.add({
targets: this.emote,
props: {
alpha: 0,
y: 2 * emoteY,
},
ease: 'Power2',
duration: 500,
onComplete: () => {
this.destroyEmote();
}
});
}
cancelPreviousEmote() {
if (!this.emote) return;
this.emoteTween?.remove();
this.destroyEmote()
}
private destroyEmote() {
this.emote?.destroy();
this.emote = null;
this.playerName.setVisible(true);
}
}

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