Merging with develop

This commit is contained in:
David Négrier 2021-07-23 16:41:38 +02:00
parent 3d5c222957
commit cdd61bdb2c
135 changed files with 4313 additions and 2128 deletions

View file

@ -4,10 +4,11 @@ export const isGameStateEvent = new tg.IsInterface()
.withProperties({
roomId: tg.isString,
mapUrl: tg.isString,
nickname: tg.isUnion(tg.isString, tg.isNull),
nickname: tg.isString,
uuid: tg.isUnion(tg.isString, tg.isUndefined),
startLayerName: tg.isUnion(tg.isString, tg.isNull),
tags: tg.isArray(tg.isString),
variables: tg.isObject,
})
.get();
/**

View file

@ -1,3 +1,4 @@
import * as tg from "generic-type-guard";
import type { GameStateEvent } from "./GameStateEvent";
import type { ButtonClickedEvent } from "./ButtonClickedEvent";
import type { ChatEvent } from "./ChatEvent";
@ -9,7 +10,7 @@ import type { OpenCoWebSiteEvent } from "./OpenCoWebSiteEvent";
import type { OpenPopupEvent } from "./OpenPopupEvent";
import type { OpenTabEvent } from "./OpenTabEvent";
import type { UserInputChatEvent } from "./UserInputChatEvent";
import type { DataLayerEvent } from "./DataLayerEvent";
import type { MapDataEvent } from "./MapDataEvent";
import type { LayerEvent } from "./LayerEvent";
import type { SetPropertyEvent } from "./setPropertyEvent";
import type { LoadSoundEvent } from "./LoadSoundEvent";
@ -24,6 +25,11 @@ import type {
triggerMessage,
TriggerMessageEvent,
} from "./ui/TriggerMessageEvent";
import type { SetVariableEvent } from "./SetVariableEvent";
import { isGameStateEvent } from "./GameStateEvent";
import { isMapDataEvent } from "./MapDataEvent";
import { isSetVariableEvent } from "./SetVariableEvent";
import { isMessageReferenceEvent, isTriggerMessageEvent } from "./ui/TriggerMessageEvent";
export interface TypedMessageEvent<T> extends MessageEvent {
data: T;
@ -49,7 +55,6 @@ export type IframeEventMap = {
showLayer: LayerEvent;
hideLayer: LayerEvent;
setProperty: SetPropertyEvent;
getDataLayer: undefined;
loadSound: LoadSoundEvent;
playSound: PlaySoundEvent;
stopSound: null;
@ -75,8 +80,8 @@ export interface IframeResponseEventMap {
leaveEvent: EnterLeaveEvent;
buttonClickedEvent: ButtonClickedEvent;
hasPlayerMoved: HasPlayerMovedEvent;
dataLayer: DataLayerEvent;
menuItemClicked: MenuItemClickedEvent;
setVariable: SetVariableEvent;
messageTriggered: MessageReferenceEvent;
}
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
@ -90,22 +95,40 @@ export const isIframeResponseEventWrapper = (event: {
}): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === "string";
/**
* List event types sent from an iFrame to WorkAdventure that expect a unique answer from WorkAdventure along the type for the answer from WorkAdventure to the iFrame
* List event types sent from an iFrame to WorkAdventure that expect a unique answer from WorkAdventure along the type for the answer from WorkAdventure to the iFrame.
* Types are defined using Type guards that will actually bused to enforce and check types.
*/
export type IframeQueryMap = {
export const iframeQueryMapTypeGuards = {
getState: {
query: undefined;
answer: GameStateEvent;
};
query: tg.isUndefined,
answer: isGameStateEvent,
},
getMapData: {
query: tg.isUndefined,
answer: isMapDataEvent,
},
setVariable: {
query: isSetVariableEvent,
answer: tg.isUndefined,
},
triggerMessage: {
query: isTriggerMessageEvent,
answer: tg.isUndefined,
},
removeTriggerMessage: {
query: isMessageReferenceEvent,
answer: tg.isUndefined,
},
};
[triggerMessage]: {
query: TriggerMessageEvent;
answer: void;
};
type GuardedType<T> = T extends (x: unknown) => x is infer T ? T : never;
type IframeQueryMapTypeGuardsType = typeof iframeQueryMapTypeGuards;
type UnknownToVoid<T> = undefined extends T ? void : T;
[removeTriggerMessage]: {
query: MessageReferenceEvent;
answer: void;
export type IframeQueryMap = {
[key in keyof IframeQueryMapTypeGuardsType]: {
query: GuardedType<IframeQueryMapTypeGuardsType[key]["query"]>;
answer: UnknownToVoid<GuardedType<IframeQueryMapTypeGuardsType[key]["answer"]>>;
};
};
@ -119,8 +142,21 @@ export interface IframeQueryWrapper<T extends keyof IframeQueryMap> {
query: IframeQuery<T>;
}
export const isIframeQueryKey = (type: string): type is keyof IframeQueryMap => {
return type in iframeQueryMapTypeGuards;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeQuery = (event: any): event is IframeQuery<keyof IframeQueryMap> => typeof event.type === "string";
export const isIframeQuery = (event: any): event is IframeQuery<keyof IframeQueryMap> => {
const type = event.type;
if (typeof type !== "string") {
return false;
}
if (!isIframeQueryKey(type)) {
return false;
}
return iframeQueryMapTypeGuards[type].query(event.data);
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const isIframeQueryWrapper = (event: any): event is IframeQueryWrapper<keyof IframeQueryMap> =>

View file

@ -1,6 +1,6 @@
import * as tg from "generic-type-guard";
export const isDataLayerEvent = new tg.IsInterface()
export const isMapDataEvent = new tg.IsInterface()
.withProperties({
data: tg.isObject,
})
@ -9,4 +9,4 @@ export const isDataLayerEvent = new tg.IsInterface()
/**
* A message sent from the game to the iFrame when the data of the layers change after the iFrame send a message to the game that it want to listen to the data of the layers
*/
export type DataLayerEvent = tg.GuardedType<typeof isDataLayerEvent>;
export type MapDataEvent = tg.GuardedType<typeof isMapDataEvent>;

View file

@ -5,7 +5,7 @@ export const isSetTilesEvent = tg.isArray(
.withProperties({
x: tg.isNumber,
y: tg.isNumber,
tile: tg.isUnion(tg.isNumber, tg.isString),
tile: tg.isUnion(tg.isUnion(tg.isNumber, tg.isString), tg.isNull),
layer: tg.isString,
})
.get()

View file

@ -0,0 +1,20 @@
import * as tg from "generic-type-guard";
import { isMenuItemRegisterEvent } from "./ui/MenuItemRegisterEvent";
export const isSetVariableEvent = new tg.IsInterface()
.withProperties({
key: tg.isString,
value: tg.isUnknown,
})
.get();
/**
* A message sent from the iFrame to the game to change the value of the property of the layer
*/
export type SetVariableEvent = tg.GuardedType<typeof isSetVariableEvent>;
export const isSetVariableIframeEvent = new tg.IsInterface()
.withProperties({
type: tg.isSingletonString("setVariable"),
data: isSetVariableEvent,
})
.get();

View file

@ -14,7 +14,6 @@ import {
IframeErrorAnswerEvent,
IframeEvent,
IframeEventMap,
IframeQuery,
IframeQueryMap,
IframeResponseEvent,
IframeResponseEventMap,
@ -29,22 +28,27 @@ import { isLoadSoundEvent, LoadSoundEvent } from "./Events/LoadSoundEvent";
import { isSetPropertyEvent, SetPropertyEvent } from "./Events/setPropertyEvent";
import { isLayerEvent, LayerEvent } from "./Events/LayerEvent";
import { isMenuItemRegisterEvent } from "./Events/ui/MenuItemRegisterEvent";
import type { DataLayerEvent } from "./Events/DataLayerEvent";
import type { MapDataEvent } from "./Events/MapDataEvent";
import type { GameStateEvent } from "./Events/GameStateEvent";
import type { HasPlayerMovedEvent } from "./Events/HasPlayerMovedEvent";
import { isLoadPageEvent } from "./Events/LoadPageEvent";
import { handleMenuItemRegistrationEvent, isMenuItemRegisterIframeEvent } from "./Events/ui/MenuItemRegisterEvent";
import { SetTilesEvent, isSetTilesEvent } from "./Events/SetTilesEvent";
import type { SetVariableEvent } from "./Events/SetVariableEvent";
type AnswererCallback<T extends keyof IframeQueryMap> = (
query: IframeQueryMap[T]["query"]
) => IframeQueryMap[T]["answer"] | Promise<IframeQueryMap[T]["answer"]>;
query: IframeQueryMap[T]["query"],
source: MessageEventSource | null
) => IframeQueryMap[T]["answer"] | PromiseLike<IframeQueryMap[T]["answer"]>;
/**
* Listens to messages from iframes and turn those messages into easy to use observables.
* Also allows to send messages to those iframes.
*/
class IframeListener {
private readonly _readyStream: Subject<HTMLIFrameElement> = new Subject();
public readonly readyStream = this._readyStream.asObservable();
private readonly _chatStream: Subject<ChatEvent> = new Subject();
public readonly chatStream = this._chatStream.asObservable();
@ -90,9 +94,6 @@ class IframeListener {
private readonly _setPropertyStream: Subject<SetPropertyEvent> = new Subject();
public readonly setPropertyStream = this._setPropertyStream.asObservable();
private readonly _dataLayerChangeStream: Subject<void> = new Subject();
public readonly dataLayerChangeStream = this._dataLayerChangeStream.asObservable();
private readonly _registerMenuCommandStream: Subject<string> = new Subject();
public readonly registerMenuCommandStream = this._registerMenuCommandStream.asObservable();
@ -116,16 +117,15 @@ class IframeListener {
private readonly scripts = new Map<string, HTMLIFrameElement>();
private sendPlayerMove: boolean = false;
// Note: we are forced to type this in unknown and later cast with "as" because of https://github.com/microsoft/TypeScript/issues/31904
private answerers: {
[key in keyof IframeQueryMap]?: AnswererCallback<key>;
[str in keyof IframeQueryMap]?: unknown;
} = {};
init() {
window.addEventListener(
"message",
<T extends keyof IframeEventMap, U extends keyof IframeQueryMap>(
message: TypedMessageEvent<IframeEvent<T | U>>
) => {
(message: MessageEvent<unknown>) => {
// 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).
@ -157,9 +157,9 @@ class IframeListener {
if (isIframeQueryWrapper(payload)) {
const queryId = payload.id;
const query = payload.query as IframeQuery<U>;
const query = payload.query;
const answerer = this.answerers[query.type] as AnswererCallback<U> | undefined;
const answerer = this.answerers[query.type] as AnswererCallback<keyof IframeQueryMap> | undefined;
if (answerer === undefined) {
const errorMsg =
'The iFrame sent a message of type "' +
@ -177,35 +177,43 @@ class IframeListener {
return;
}
Promise.resolve(answerer(query.data))
.then((value) => {
iframe?.contentWindow?.postMessage(
{
id: queryId,
type: query.type,
data: value,
},
"*"
);
})
.catch((reason) => {
console.error("An error occurred while responding to an iFrame query.", reason);
let reasonMsg: string;
if (reason instanceof Error) {
reasonMsg = reason.message;
} else {
reasonMsg = reason.toString();
}
const errorHandler = (reason: unknown) => {
console.error("An error occurred while responding to an iFrame query.", reason);
let reasonMsg: string = "";
if (reason instanceof Error) {
reasonMsg = reason.message;
} else if (typeof reason === "object") {
reasonMsg = reason ? reason.toString() : "";
} else if (typeof reason === "string") {
reasonMsg = reason;
}
iframe?.contentWindow?.postMessage(
{
id: queryId,
type: query.type,
error: reasonMsg,
} as IframeErrorAnswerEvent,
"*"
);
});
iframe?.contentWindow?.postMessage(
{
id: queryId,
type: query.type,
error: reasonMsg,
} as IframeErrorAnswerEvent,
"*"
);
};
try {
Promise.resolve(answerer(query.data, message.source))
.then((value) => {
iframe?.contentWindow?.postMessage(
{
id: queryId,
type: query.type,
data: value,
},
"*"
);
})
.catch(errorHandler);
} catch (reason) {
errorHandler(reason);
}
} else if (isIframeEventWrapper(payload)) {
if (payload.type === "showLayer" && isLayerEvent(payload.data)) {
this._showLayerStream.next(payload.data);
@ -250,8 +258,6 @@ class IframeListener {
this._removeBubbleStream.next();
} else if (payload.type == "onPlayerMove") {
this.sendPlayerMove = true;
} else if (payload.type == "getDataLayer") {
this._dataLayerChangeStream.next();
} else if (isMenuItemRegisterIframeEvent(payload)) {
const data = payload.data.menutItem;
// @ts-ignore
@ -268,13 +274,6 @@ class IframeListener {
);
}
sendDataLayerEvent(dataLayerEvent: DataLayerEvent) {
this.postMessage({
type: "dataLayer",
data: dataLayerEvent,
});
}
/**
* Allows the passed iFrame to send/receive messages via the API.
*/
@ -414,6 +413,13 @@ class IframeListener {
});
}
setVariable(setVariableEvent: SetVariableEvent) {
this.postMessage({
type: "setVariable",
data: setVariableEvent,
});
}
/**
* Sends the message... to all allowed iframes.
*/
@ -431,17 +437,31 @@ class IframeListener {
* @param key The "type" of the query we are answering
* @param callback
*/
public registerAnswerer<T extends keyof IframeQueryMap, Guard extends tg.TypeGuard<IframeQueryMap[T]["query"]>>(
key: T,
callback: AnswererCallback<T>,
typeChecker?: Guard
): void {
this.answerers[key] = callback as never;
public registerAnswerer<T extends keyof IframeQueryMap>(key: T, callback: AnswererCallback<T>): void {
this.answerers[key] = callback;
}
public unregisterAnswerer(key: keyof IframeQueryMap): void {
delete this.answerers[key];
}
dispatchVariableToOtherIframes(key: string, value: unknown, source: MessageEventSource | null) {
// Let's dispatch the message to the other iframes
for (const iframe of this.iframes) {
if (iframe.contentWindow !== source) {
iframe.contentWindow?.postMessage(
{
type: "setVariable",
data: {
key,
value,
},
},
"*"
);
}
}
}
}
export const iframeListener = new IframeListener();

View file

@ -6,6 +6,24 @@ import { isHasPlayerMovedEvent } from "../Events/HasPlayerMovedEvent";
const moveStream = new Subject<HasPlayerMovedEvent>();
let playerName: string | undefined;
export const setPlayerName = (name: string) => {
playerName = name;
};
let tags: string[] | undefined;
export const setTags = (_tags: string[]) => {
tags = _tags;
};
let uuid: string | undefined;
export const setUuid = (_uuid: string | undefined) => {
uuid = _uuid;
};
export class WorkadventurePlayerCommands extends IframeApiContribution<WorkadventurePlayerCommands> {
callbacks = [
apiCallback({
@ -24,6 +42,31 @@ export class WorkadventurePlayerCommands extends IframeApiContribution<Workadven
data: null,
});
}
get name(): string {
if (playerName === undefined) {
throw new Error(
"Player name not initialized yet. You should call WA.player.name within a WA.onInit callback."
);
}
return playerName;
}
get tags(): string[] {
if (tags === undefined) {
throw new Error("Tags not initialized yet. You should call WA.player.tags within a WA.onInit callback.");
}
return tags;
}
get id(): string | undefined {
// Note: this is not a type, we are checking if playerName is undefined because playerName cannot be undefined
// while uuid could.
if (playerName === undefined) {
throw new Error("Player id not initialized yet. You should call WA.player.id within a WA.onInit callback.");
}
return uuid;
}
}
export default new WorkadventurePlayerCommands();

View file

@ -1,56 +1,33 @@
import { Subject } from "rxjs";
import { Observable, Subject } from "rxjs";
import { isDataLayerEvent } from "../Events/DataLayerEvent";
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
import { isGameStateEvent } from "../Events/GameStateEvent";
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks";
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
import type { DataLayerEvent } from "../Events/DataLayerEvent";
import type { GameStateEvent } from "../Events/GameStateEvent";
const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
const dataLayerResolver = new Subject<DataLayerEvent>();
const stateResolvers = new Subject<GameStateEvent>();
let immutableDataPromise: Promise<GameStateEvent> | undefined = undefined;
interface Room {
id: string;
mapUrl: string;
map: ITiledMap;
startLayer: string | null;
}
interface User {
id: string | undefined;
nickName: string | null;
tags: string[];
}
interface TileDescriptor {
x: number;
y: number;
tile: number | string;
tile: number | string | null;
layer: string;
}
function getGameState(): Promise<GameStateEvent> {
if (immutableDataPromise === undefined) {
immutableDataPromise = queryWorkadventure({ type: "getState", data: undefined });
}
return immutableDataPromise;
}
let roomId: string | undefined;
function getDataLayer(): Promise<DataLayerEvent> {
return new Promise<DataLayerEvent>((resolver, thrower) => {
dataLayerResolver.subscribe(resolver);
sendToWorkadventure({ type: "getDataLayer", data: null });
});
}
export const setRoomId = (id: string) => {
roomId = id;
};
let mapURL: string | undefined;
export const setMapURL = (url: string) => {
mapURL = url;
};
export class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> {
callbacks = [
@ -68,13 +45,6 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
leaveStreams.get(payloadData.name)?.next();
},
}),
apiCallback({
type: "dataLayer",
typeChecker: isDataLayerEvent,
callback: (payloadData) => {
dataLayerResolver.next(payloadData);
},
}),
];
onEnterZone(name: string, callback: () => void): void {
@ -109,22 +79,9 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
},
});
}
getCurrentRoom(): Promise<Room> {
return getGameState().then((gameState) => {
return getDataLayer().then((mapJson) => {
return {
id: gameState.roomId,
map: mapJson.data as ITiledMap,
mapUrl: gameState.mapUrl,
startLayer: gameState.startLayerName,
};
});
});
}
getCurrentUser(): Promise<User> {
return getGameState().then((gameState) => {
return { id: gameState.uuid, nickName: gameState.nickname, tags: gameState.tags };
});
async getTiledMap(): Promise<ITiledMap> {
const event = await queryWorkadventure({ type: "getMapData", data: undefined });
return event.data as ITiledMap;
}
setTiles(tiles: TileDescriptor[]) {
sendToWorkadventure({
@ -132,6 +89,22 @@ export class WorkadventureRoomCommands extends IframeApiContribution<Workadventu
data: tiles,
});
}
get id(): string {
if (roomId === undefined) {
throw new Error("Room id not initialized yet. You should call WA.room.id within a WA.onInit callback.");
}
return roomId;
}
get mapURL(): string {
if (mapURL === undefined) {
throw new Error(
"mapURL is not initialized yet. You should call WA.room.mapURL within a WA.onInit callback."
);
}
return mapURL;
}
}
export default new WorkadventureRoomCommands();

View file

@ -0,0 +1,90 @@
import { Observable, Subject } from "rxjs";
import { EnterLeaveEvent, isEnterLeaveEvent } from "../Events/EnterLeaveEvent";
import { IframeApiContribution, queryWorkadventure, sendToWorkadventure } from "./IframeApiContribution";
import { apiCallback } from "./registeredCallbacks";
import { isSetVariableEvent, SetVariableEvent } from "../Events/SetVariableEvent";
import type { ITiledMap } from "../../Phaser/Map/ITiledMap";
const setVariableResolvers = new Subject<SetVariableEvent>();
const variables = new Map<string, unknown>();
const variableSubscribers = new Map<string, Subject<unknown>>();
export const initVariables = (_variables: Map<string, unknown>): void => {
for (const [name, value] of _variables.entries()) {
// In case the user already decided to put values in the variables (before onInit), let's make sure onInit does not override this.
if (!variables.has(name)) {
variables.set(name, value);
}
}
};
setVariableResolvers.subscribe((event) => {
const oldValue = variables.get(event.key);
// If we are setting the same value, no need to do anything.
// No need to do this check since it is already performed in SharedVariablesManager
/*if (JSON.stringify(oldValue) === JSON.stringify(event.value)) {
return;
}*/
variables.set(event.key, event.value);
const subject = variableSubscribers.get(event.key);
if (subject !== undefined) {
subject.next(event.value);
}
});
export class WorkadventureStateCommands extends IframeApiContribution<WorkadventureStateCommands> {
callbacks = [
apiCallback({
type: "setVariable",
typeChecker: isSetVariableEvent,
callback: (payloadData) => {
setVariableResolvers.next(payloadData);
},
}),
];
saveVariable(key: string, value: unknown): Promise<void> {
variables.set(key, value);
return queryWorkadventure({
type: "setVariable",
data: {
key,
value,
},
});
}
loadVariable(key: string): unknown {
return variables.get(key);
}
onVariableChange(key: string): Observable<unknown> {
let subject = variableSubscribers.get(key);
if (subject === undefined) {
subject = new Subject<unknown>();
variableSubscribers.set(key, subject);
}
return subject.asObservable();
}
}
const proxyCommand = new Proxy(new WorkadventureStateCommands(), {
get(target: WorkadventureStateCommands, p: PropertyKey, receiver: unknown): unknown {
if (p in target) {
return Reflect.get(target, p, receiver);
}
return target.loadVariable(p.toString());
},
set(target: WorkadventureStateCommands, p: PropertyKey, value: unknown, receiver: unknown): boolean {
// Note: when using "set", there is no way to wait, so we ignore the return of the promise.
// User must use WA.state.saveVariable to have error message.
target.saveVariable(p.toString(), value);
return true;
},
}) as WorkadventureStateCommands & { [key: string]: unknown };
export default proxyCommand;

View file

@ -10,12 +10,14 @@
import {errorStore} from "../Stores/ErrorStore";
import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte";
import LoginScene from "./Login/LoginScene.svelte";
import Chat from "./Chat/Chat.svelte";
import {loginSceneVisibleStore} from "../Stores/LoginSceneStore";
import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte";
import VisitCard from "./VisitCard/VisitCard.svelte";
import {requestVisitCardsStore} from "../Stores/GameStore";
import type {Game} from "../Phaser/Game/Game";
import {chatVisibilityStore} from "../Stores/ChatStore";
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
import AudioPlaying from "./UI/AudioPlaying.svelte";
@ -61,14 +63,6 @@
<AudioPlaying url={$soundPlayingStore} />
</div>
{/if}
<!--
{#if $menuIconVisible}
<div>
<MenuIcon />
</div>
{/if}
-->
{#if $gameOverlayVisibilityStore}
<div>
<VideoOverlay></VideoOverlay>
@ -94,4 +88,7 @@
<ErrorDialog></ErrorDialog>
</div>
{/if}
{#if $chatVisibilityStore}
<Chat></Chat>
{/if}
</div>

View file

@ -0,0 +1,101 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import { chatMessagesStore, chatVisibilityStore } from "../../Stores/ChatStore";
import ChatMessageForm from './ChatMessageForm.svelte';
import ChatElement from './ChatElement.svelte';
import { afterUpdate, beforeUpdate } from "svelte";
let listDom: HTMLElement;
let autoscroll: boolean;
beforeUpdate(() => {
autoscroll = listDom && (listDom.offsetHeight + listDom.scrollTop) > (listDom.scrollHeight - 20);
});
afterUpdate(() => {
if (autoscroll) listDom.scrollTo(0, listDom.scrollHeight);
});
function closeChat() {
chatVisibilityStore.set(false);
}
function onKeyDown(e:KeyboardEvent) {
if (e.key === 'Escape') {
closeChat();
}
}
</script>
<svelte:window on:keydown={onKeyDown}/>
<aside class="chatWindow" transition:fly="{{ x: -1000, duration: 500 }}">
<p class="close-icon" on:click={closeChat}>&times</p>
<section class="messagesList" bind:this={listDom}>
<ul>
<li><p class="system-text">Here is your chat history: </p></li>
{#each $chatMessagesStore as message, i}
<li><ChatElement message={message} line={i}></ChatElement></li>
{/each}
</ul>
</section>
<section class="messageForm">
<ChatMessageForm></ChatMessageForm>
</section>
</aside>
<style lang="scss">
p.close-icon {
position: absolute;
padding: 4px;
right: 12px;
font-size: 30px;
line-height: 25px;
cursor: pointer;
}
p.system-text {
border-radius: 8px;
margin-bottom: 10px;
padding:6px;
overflow-wrap: break-word;
max-width: 100%;
background: gray;
display: inline-block;
}
aside.chatWindow {
z-index:100;
pointer-events: auto;
position: absolute;
top: 0;
left: 0;
height: 100vh;
width:30vw;
min-width: 350px;
background: rgb(5, 31, 51, 0.9);
color: whitesmoke;
display: flex;
flex-direction: column;
padding: 10px;
border-bottom-right-radius: 16px;
border-top-right-radius: 16px;
.messagesList {
margin-top: 35px;
overflow-y: auto;
flex: auto;
ul {
list-style-type: none;
padding-left: 0;
}
}
.messageForm {
flex: 0 70px;
padding-top: 15px;
}
}
</style>

View file

@ -0,0 +1,83 @@
<script lang="ts">
import {ChatMessageTypes} from "../../Stores/ChatStore";
import type {ChatMessage} from "../../Stores/ChatStore";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import ChatPlayerName from './ChatPlayerName.svelte';
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
export let message: ChatMessage;
export let line: number;
$: author = message.author as PlayerInterface;
$: targets = message.targets || [];
$: texts = message.text || [];
function urlifyText(text: string): string {
return HtmlUtils.urlify(text)
}
function renderDate(date: Date) {
return date.toLocaleTimeString(navigator.language, {
hour: '2-digit',
minute:'2-digit'
});
}
function isLastIteration(index: number) {
return targets.length -1 === index;
}
</script>
<div class="chatElement">
<div class="messagePart">
{#if message.type === ChatMessageTypes.userIncoming}
&gt;&gt; {#each targets as target, index}<ChatPlayerName player={target} line={line}></ChatPlayerName>{#if !isLastIteration(index)}, {/if}{/each} entered <span class="date">({renderDate(message.date)})</span>
{:else if message.type === ChatMessageTypes.userOutcoming}
&lt;&lt; {#each targets as target, index}<ChatPlayerName player={target} line={line}></ChatPlayerName>{#if !isLastIteration(index)}, {/if}{/each} left <span class="date">({renderDate(message.date)})</span>
{:else if message.type === ChatMessageTypes.me}
<h4>Me: <span class="date">({renderDate(message.date)})</span></h4>
{#each texts as text}
<div><p class="my-text">{@html urlifyText(text)}</p></div>
{/each}
{:else}
<h4><ChatPlayerName player={author} line={line}></ChatPlayerName>: <span class="date">({renderDate(message.date)})</span></h4>
{#each texts as text}
<div><p class="other-text">{@html urlifyText(text)}</p></div>
{/each}
{/if}
</div>
</div>
<style lang="scss">
h4, p {
font-family: Lato;
}
div.chatElement {
display: flex;
margin-bottom: 20px;
.messagePart {
flex-grow:1;
max-width: 100%;
span.date {
font-size: 80%;
color: gray;
}
div > p {
border-radius: 8px;
margin-bottom: 10px;
padding:6px;
overflow-wrap: break-word;
max-width: 100%;
display: inline-block;
&.other-text {
background: gray;
}
&.my-text {
background: #6489ff;
}
}
}
}
</style>

View file

@ -0,0 +1,56 @@
<script lang="ts">
import {chatMessagesStore, chatInputFocusStore} from "../../Stores/ChatStore";
let newMessageText = '';
function onFocus() {
chatInputFocusStore.set(true);
}
function onBlur() {
chatInputFocusStore.set(false);
}
function saveMessage() {
if (!newMessageText) return;
chatMessagesStore.addPersonnalMessage(newMessageText);
newMessageText = '';
}
</script>
<form on:submit|preventDefault={saveMessage}>
<input type="text" bind:value={newMessageText} placeholder="Enter your message..." on:focus={onFocus} on:blur={onBlur} >
<button type="submit">
<img src="/static/images/send.png" alt="Send" width="20">
</button>
</form>
<style lang="scss">
form {
display: flex;
padding-left: 4px;
padding-right: 4px;
input {
flex: auto;
background-color: #254560;
color: white;
border-bottom-left-radius: 4px;
border-top-left-radius: 4px;
border: none;
font-size: 22px;
font-family: Lato;
padding-left: 6px;
min-width: 0; //Needed so that the input doesn't overflow the container in firefox
outline: none;
}
button {
background-color: #254560;
border-bottom-right-radius: 4px;
border-top-right-radius: 4px;
border: none;
border-left: solid white 1px;
font-size: 16px;
}
}
</style>

View file

@ -0,0 +1,51 @@
<script lang="ts">
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
import {chatSubMenuVisbilityStore} from "../../Stores/ChatStore";
import {onDestroy, onMount} from "svelte";
import type {Unsubscriber} from "svelte/store";
import ChatSubMenu from "./ChatSubMenu.svelte";
export let player: PlayerInterface;
export let line: number;
let isSubMenuOpen: boolean;
let chatSubMenuVisivilytUnsubcribe: Unsubscriber;
function openSubMenu() {
chatSubMenuVisbilityStore.openSubMenu(player.name, line);
}
onMount(() => {
chatSubMenuVisivilytUnsubcribe = chatSubMenuVisbilityStore.subscribe((newValue) => {
isSubMenuOpen = (newValue === player.name + line);
})
})
onDestroy(() => {
chatSubMenuVisivilytUnsubcribe();
})
</script>
<span class="subMenu">
<span class="chatPlayerName" style="color: {player.color || 'white'}" on:click={openSubMenu}>
{player.name}
</span>
{#if isSubMenuOpen}
<ChatSubMenu player={player}/>
{/if}
</span>
<style lang="scss">
span.subMenu {
display: inline-block;
}
span.chatPlayerName {
margin-left: 3px;
}
.chatPlayerName:hover {
text-decoration: underline;
cursor: pointer;
}
</style>

View file

@ -0,0 +1,33 @@
<script lang="ts">
import type {PlayerInterface} from "../../Phaser/Game/PlayerInterface";
import {requestVisitCardsStore} from "../../Stores/GameStore";
export let player: PlayerInterface;
function openVisitCard() {
if (player.visitCardUrl) {
requestVisitCardsStore.set(player.visitCardUrl);
}
}
</script>
<ul class="selectMenu" style="border-top: {player.color || 'whitesmoke'} 5px solid">
<li><button class="text-btn" disabled={!player.visitCardUrl} on:click={openVisitCard}>Visit card</button></li>
<li><button class="text-btn" disabled>Add friend</button></li>
</ul>
<style lang="scss">
ul.selectMenu {
background-color: whitesmoke;
position: absolute;
padding: 5px;
border-radius: 4px;
list-style-type: none;
li {
text-align: center;
}
}
</style>

View file

@ -37,9 +37,7 @@
<img alt="Report this user" src={reportImg}>
<span>Report/Block</span>
</button>
{#if $streamStore }
<video use:srcObject={$streamStore} autoplay playsinline on:click={() => videoFocusStore.toggleFocus(peer)}></video>
{/if}
<img src={blockSignImg} class="block-logo" alt="Block" />
{#if $constraintStore && $constraintStore.audio !== false}
<SoundMeterWidget stream={$streamStore}></SoundMeterWidget>

View file

@ -1,3 +1,6 @@
import type { UserSimplePeerInterface } from "../../WebRtc/SimplePeer";
import { STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER } from "../../Enum/EnvironmentVariable";
export function getColorByString(str: string): string | null {
let hash = 0;
if (str.length === 0) {
@ -15,7 +18,7 @@ export function getColorByString(str: string): string | null {
return color;
}
export function srcObject(node: HTMLVideoElement, stream: MediaStream) {
export function srcObject(node: HTMLVideoElement, stream: MediaStream | null) {
node.srcObject = stream;
return {
update(newStream: MediaStream) {
@ -25,3 +28,19 @@ export function srcObject(node: HTMLVideoElement, stream: MediaStream) {
},
};
}
export function getIceServersConfig(user: UserSimplePeerInterface): RTCIceServer[] {
const config: RTCIceServer[] = [
{
urls: STUN_SERVER.split(","),
},
];
if (TURN_SERVER !== "") {
config.push({
urls: TURN_SERVER.split(","),
username: user.webRtcUser || TURN_USER,
credential: user.webRtcPassword || TURN_PASSWORD,
});
}
return config;
}

View file

@ -45,8 +45,9 @@
.visitCard {
pointer-events: all;
margin-left: auto;
margin-right: auto;
position: absolute;
left: 50%;
transform: translate(-50%, 0);
margin-top: 200px;
max-width: 80vw;

View file

@ -1,92 +1,107 @@
import Axios from "axios";
import {PUSHER_URL, START_ROOM_URL} from "../Enum/EnvironmentVariable";
import {RoomConnection} from "./RoomConnection";
import type {OnConnectInterface, PositionInterface, ViewportInterface} from "./ConnexionModels";
import {GameConnexionTypes, urlManager} from "../Url/UrlManager";
import {localUserStore} from "./LocalUserStore";
import {CharacterTexture, LocalUser} from "./LocalUser";
import {Room} from "./Room";
import { PUSHER_URL, START_ROOM_URL } from "../Enum/EnvironmentVariable";
import { RoomConnection } from "./RoomConnection";
import type { OnConnectInterface, PositionInterface, ViewportInterface } from "./ConnexionModels";
import { GameConnexionTypes, urlManager } from "../Url/UrlManager";
import { localUserStore } from "./LocalUserStore";
import { CharacterTexture, LocalUser } from "./LocalUser";
import { Room } from "./Room";
class ConnectionManager {
private localUser!:LocalUser;
private localUser!: LocalUser;
private connexionType?: GameConnexionTypes
private reconnectingTimeout: NodeJS.Timeout|null = null;
private _unloading:boolean = false;
private connexionType?: GameConnexionTypes;
private reconnectingTimeout: NodeJS.Timeout | null = null;
private _unloading: boolean = false;
get unloading () {
get unloading() {
return this._unloading;
}
constructor() {
window.addEventListener('beforeunload', () => {
window.addEventListener("beforeunload", () => {
this._unloading = true;
if (this.reconnectingTimeout) clearTimeout(this.reconnectingTimeout)
})
if (this.reconnectingTimeout) clearTimeout(this.reconnectingTimeout);
});
}
/**
* Tries to login to the node server and return the starting map url to be loaded
*/
public async initGameConnexion(): Promise<Room> {
const connexionType = urlManager.getGameConnexionType();
this.connexionType = connexionType;
if(connexionType === GameConnexionTypes.register) {
const organizationMemberToken = urlManager.getOrganizationToken();
const data = await Axios.post(`${PUSHER_URL}/register`, {organizationMemberToken}).then(res => res.data);
if (connexionType === GameConnexionTypes.register) {
const organizationMemberToken = urlManager.getOrganizationToken();
const data = await Axios.post(`${PUSHER_URL}/register`, { organizationMemberToken }).then(
(res) => res.data
);
this.localUser = new LocalUser(data.userUuid, data.authToken, data.textures);
localUserStore.saveUser(this.localUser);
const organizationSlug = data.organizationSlug;
const worldSlug = data.worldSlug;
const roomSlug = data.roomSlug;
const roomUrl = data.roomUrl;
const room = new Room('/@/'+organizationSlug+'/'+worldSlug+'/'+roomSlug + window.location.search + window.location.hash);
const room = await Room.createRoom(
new URL(
window.location.protocol +
"//" +
window.location.host +
roomUrl +
window.location.search +
window.location.hash
)
);
urlManager.pushRoomIdToUrl(room);
return Promise.resolve(room);
} else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) {
} else if (
connexionType === GameConnexionTypes.organization ||
connexionType === GameConnexionTypes.anonymous ||
connexionType === GameConnexionTypes.empty
) {
let localUser = localUserStore.getLocalUser();
if (localUser && localUser.jwtToken && localUser.uuid && localUser.textures) {
this.localUser = localUser;
try {
await this.verifyToken(localUser.jwtToken);
} catch(e) {
} catch (e) {
// If the token is invalid, let's generate an anonymous one.
console.error('JWT token invalid. Did it expire? Login anonymously instead.');
console.error("JWT token invalid. Did it expire? Login anonymously instead.");
await this.anonymousLogin();
}
}else{
} else {
await this.anonymousLogin();
}
localUser = localUserStore.getLocalUser();
if(!localUser){
if (!localUser) {
throw "Error to store local user data";
}
let roomId: string;
let roomPath: string;
if (connexionType === GameConnexionTypes.empty) {
roomId = START_ROOM_URL;
roomPath = window.location.protocol + "//" + window.location.host + START_ROOM_URL;
} else {
roomId = window.location.pathname + window.location.search + window.location.hash;
roomPath =
window.location.protocol +
"//" +
window.location.host +
window.location.pathname +
window.location.search +
window.location.hash;
}
//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) {
const room = await Room.createRoom(new URL(roomPath));
if (room.textures != undefined && room.textures.length > 0) {
//check if texture was changed
if(localUser.textures.length === 0){
localUser.textures = mapDetail.textures;
}else{
mapDetail.textures.forEach((newTexture) => {
if (localUser.textures.length === 0) {
localUser.textures = room.textures;
} else {
room.textures.forEach((newTexture) => {
const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id);
if(localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1){
if (localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1) {
return;
}
localUser?.textures.push(newTexture)
localUser?.textures.push(newTexture);
});
}
this.localUser = localUser;
@ -95,55 +110,79 @@ class ConnectionManager {
return Promise.resolve(room);
}
return Promise.reject(new Error('Invalid URL'));
return Promise.reject(new Error("Invalid URL"));
}
private async verifyToken(token: string): Promise<void> {
await Axios.get(`${PUSHER_URL}/verify`, {params: {token}});
await Axios.get(`${PUSHER_URL}/verify`, { params: { token } });
}
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then(res => res.data);
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data);
this.localUser = new LocalUser(data.userUuid, data.authToken, []);
if (!isBenchmark) { // In benchmark, we don't have a local storage.
if (!isBenchmark) {
// In benchmark, we don't have a local storage.
localUserStore.saveUser(this.localUser);
}
}
public initBenchmark(): void {
this.localUser = new LocalUser('', 'test', []);
this.localUser = new LocalUser("", "test", []);
}
public connectToRoomSocket(roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null): Promise<OnConnectInterface> {
public connectToRoomSocket(
roomUrl: string,
name: string,
characterLayers: string[],
position: PositionInterface,
viewport: ViewportInterface,
companion: string | null
): Promise<OnConnectInterface> {
return new Promise<OnConnectInterface>((resolve, reject) => {
const connection = new RoomConnection(this.localUser.jwtToken, roomId, name, characterLayers, position, viewport, companion);
const connection = new RoomConnection(
this.localUser.jwtToken,
roomUrl,
name,
characterLayers,
position,
viewport,
companion
);
connection.onConnectError((error: object) => {
console.log('An error occurred while connecting to socket server. Retrying');
console.log("An error occurred while connecting to socket server. Retrying");
reject(error);
});
connection.onConnectingError((event: CloseEvent) => {
console.log('An error occurred while connecting to socket server. Retrying');
reject(new Error('An error occurred while connecting to socket server. Retrying. Code: '+event.code+', Reason: '+event.reason));
console.log("An error occurred while connecting to socket server. Retrying");
reject(
new Error(
"An error occurred while connecting to socket server. Retrying. Code: " +
event.code +
", Reason: " +
event.reason
)
);
});
connection.onConnect((connect: OnConnectInterface) => {
resolve(connect);
});
}).catch((err) => {
// Let's retry in 4-6 seconds
return new Promise<OnConnectInterface>((resolve, reject) => {
this.reconnectingTimeout = setTimeout(() => {
//todo: allow a way to break recursion?
//todo: find a way to avoid recursive function. Otherwise, the call stack will grow indefinitely.
this.connectToRoomSocket(roomId, name, characterLayers, position, viewport, companion).then((connection) => resolve(connection));
}, 4000 + Math.floor(Math.random() * 2000) );
this.connectToRoomSocket(roomUrl, name, characterLayers, position, viewport, companion).then(
(connection) => resolve(connection)
);
}, 4000 + Math.floor(Math.random() * 2000));
});
});
}
get getConnexionType(){
get getConnexionType() {
return this.connexionType;
}
}

View file

@ -1,8 +1,8 @@
import type {SignalData} from "simple-peer";
import type {RoomConnection} from "./RoomConnection";
import type {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{
export enum EventMessage {
CONNECT = "connect",
WEBRTC_SIGNAL = "webrtc-signal",
WEBRTC_SCREEN_SHARING_SIGNAL = "webrtc-screen-sharing-signal",
@ -17,7 +17,7 @@ export enum EventMessage{
GROUP_CREATE_UPDATE = "group-create-update",
GROUP_DELETE = "group-delete",
SET_PLAYER_DETAILS = "set-player-details", // Send the name and character to the server (on connect), receive back the id.
ITEM_EVENT = 'item-event',
ITEM_EVENT = "item-event",
CONNECT_ERROR = "connect_error",
CONNECTING_ERROR = "connecting_error",
@ -31,12 +31,13 @@ export enum EventMessage{
TELEPORT = "teleport",
USER_MESSAGE = "user-message",
START_JITSI_ROOM = "start-jitsi-room",
SET_VARIABLE = "set-variable",
}
export interface PointInterface {
x: number;
y: number;
direction : string;
direction: string;
moving: boolean;
}
@ -45,8 +46,9 @@ export interface MessageUserPositionInterface {
name: string;
characterLayers: BodyResourceDescriptionInterface[];
position: PointInterface;
visitCardUrl: string|null;
companion: string|null;
visitCardUrl: string | null;
companion: string | null;
userUuid: string;
}
export interface MessageUserMovedInterface {
@ -60,58 +62,60 @@ export interface MessageUserJoined {
characterLayers: BodyResourceDescriptionInterface[];
position: PointInterface;
visitCardUrl: string | null;
companion: string|null;
companion: string | null;
userUuid: string;
}
export interface PositionInterface {
x: number,
y: number
x: number;
y: number;
}
export interface GroupCreatedUpdatedMessageInterface {
position: PositionInterface,
groupId: number,
groupSize: number
position: PositionInterface;
groupId: number;
groupSize: number;
}
export interface WebRtcDisconnectMessageInterface {
userId: number
userId: number;
}
export interface WebRtcSignalReceivedMessageInterface {
userId: number,
signal: SignalData,
webRtcUser: string | undefined,
webRtcPassword: string | undefined
userId: number;
signal: SignalData;
webRtcUser: string | undefined;
webRtcPassword: string | undefined;
}
export interface ViewportInterface {
left: number,
top: number,
right: number,
bottom: number,
left: number;
top: number;
right: number;
bottom: number;
}
export interface ItemEventMessageInterface {
itemId: number,
event: string,
state: unknown,
parameters: unknown
itemId: number;
event: string;
state: unknown;
parameters: unknown;
}
export interface RoomJoinedMessageInterface {
//users: MessageUserPositionInterface[],
//groups: GroupCreatedUpdatedMessageInterface[],
items: { [itemId: number] : unknown }
items: { [itemId: number]: unknown };
variables: Map<string, unknown>;
}
export interface PlayGlobalMessageInterface {
id: string
type: string
message: string
id: string;
type: string;
message: string;
}
export interface OnConnectInterface {
connection: RoomConnection,
room: RoomJoinedMessageInterface
connection: RoomConnection;
room: RoomJoinedMessageInterface;
}

View file

@ -6,18 +6,20 @@ export class MapDetail {
constructor(public readonly mapUrl: string, public readonly textures: CharacterTexture[] | undefined) {}
}
export interface RoomRedirect {
redirectUrl: string;
}
export class Room {
public readonly id: string;
public readonly isPublic: boolean;
private mapUrl: string | undefined;
private textures: CharacterTexture[] | undefined;
private _mapUrl: string | undefined;
private _textures: CharacterTexture[] | undefined;
private instance: string | undefined;
private _search: URLSearchParams;
private readonly _search: URLSearchParams;
constructor(id: string) {
const url = new URL(id, "https://example.com");
this.id = url.pathname;
private constructor(private roomUrl: URL) {
this.id = roomUrl.pathname;
if (this.id.startsWith("/")) {
this.id = this.id.substr(1);
@ -30,74 +32,74 @@ export class Room {
throw new Error("Invalid room ID");
}
this._search = new URLSearchParams(url.search);
this._search = new URLSearchParams(roomUrl.search);
}
public static getIdFromIdentifier(
identifier: string,
baseUrl: string,
currentInstance: string
): { roomId: string; hash: string | null } {
let roomId = "";
let hash = null;
if (!identifier.startsWith("/_/") && !identifier.startsWith("/@/")) {
//relative file link
//Relative identifier can be deep enough to rewrite the base domain, so we cannot use the variable 'baseUrl' as the actual base url for the URL objects.
//We instead use 'workadventure' as a dummy base value.
const baseUrlObject = new URL(baseUrl);
const absoluteExitSceneUrl = new URL(
identifier,
"http://workadventure/_/" + currentInstance + "/" + baseUrlObject.hostname + baseUrlObject.pathname
);
roomId = absoluteExitSceneUrl.pathname; //in case of a relative url, we need to create a public roomId
roomId = roomId.substring(1); //remove the leading slash
hash = absoluteExitSceneUrl.hash;
hash = hash.substring(1); //remove the leading diese
if (!hash.length) {
hash = null;
}
} else {
//absolute room Id
const parts = identifier.split("#");
roomId = parts[0];
roomId = roomId.substring(1); //remove the leading slash
if (parts.length > 1) {
hash = parts[1];
/**
* Creates a "Room" object representing the room.
* This method will follow room redirects if necessary, so the instance returned is a "real" room.
*/
public static async createRoom(roomUrl: URL): Promise<Room> {
let redirectCount = 0;
while (redirectCount < 32) {
const room = new Room(roomUrl);
const result = await room.getMapDetail();
if (result instanceof MapDetail) {
return room;
}
redirectCount++;
roomUrl = new URL(result.redirectUrl);
}
return { roomId, hash };
throw new Error("Room resolving seems stuck in a redirect loop after 32 redirect attempts");
}
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;
}
public static getRoomPathFromExitUrl(exitUrl: string, currentRoomUrl: string): URL {
const url = new URL(exitUrl, currentRoomUrl);
return url;
}
if (this.isPublic) {
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(new MapDetail(this.mapUrl, this.textures));
return;
} else {
// We have a private ID, we need to query the map URL from the server.
const urlParts = this.parsePrivateUrl(this.id);
/**
* @deprecated USage of exitSceneUrl is deprecated and therefore, this method is deprecated too.
*/
public static getRoomPathFromExitSceneUrl(
exitSceneUrl: string,
currentRoomUrl: string,
currentMapUrl: string
): URL {
const absoluteExitSceneUrl = new URL(exitSceneUrl, currentMapUrl);
const baseUrl = new URL(currentRoomUrl);
Axios.get(`${PUSHER_URL}/map`, {
params: urlParts,
})
.then(({ data }) => {
console.log("Map ", this.id, " resolves to URL ", data.mapUrl);
resolve(data);
return;
})
.catch((reason) => {
reject(reason);
});
}
const currentRoom = new Room(baseUrl);
let instance: string = "global";
if (currentRoom.isPublic) {
instance = currentRoom.instance as string;
}
baseUrl.pathname = "/_/" + instance + "/" + absoluteExitSceneUrl.host + absoluteExitSceneUrl.pathname;
if (absoluteExitSceneUrl.hash) {
baseUrl.hash = absoluteExitSceneUrl.hash;
}
return baseUrl;
}
private async getMapDetail(): Promise<MapDetail | RoomRedirect> {
const result = await Axios.get(`${PUSHER_URL}/map`, {
params: {
playUri: this.roomUrl.toString(),
},
});
const data = result.data;
if (data.redirectUrl) {
return {
redirectUrl: data.redirectUrl as string,
};
}
console.log("Map ", this.id, " resolves to URL ", data.mapUrl);
this._mapUrl = data.mapUrl;
this._textures = data.textures;
return new MapDetail(data.mapUrl, data.textures);
}
/**
@ -123,6 +125,9 @@ export class Room {
}
}
/**
* @deprecated
*/
private parsePrivateUrl(url: string): { organizationSlug: string; worldSlug: string; roomSlug?: string } {
const regex = /@\/([^/]+)\/([^/]+)(?:\/([^/]*))?/gm;
const match = regex.exec(url);
@ -150,4 +155,33 @@ export class Room {
public get search(): URLSearchParams {
return this._search;
}
/**
* 2 rooms are equal if they share the same path (but not necessarily the same hash)
* @param room
*/
public isEqual(room: Room): boolean {
return room.key === this.key;
}
/**
* A key representing this room
*/
public get key(): string {
const newUrl = new URL(this.roomUrl.toString());
newUrl.search = "";
newUrl.hash = "";
return newUrl.toString();
}
get textures(): CharacterTexture[] | undefined {
return this._textures;
}
get mapUrl(): string {
if (!this._mapUrl) {
throw new Error("Map URL not fetched yet");
}
return this._mapUrl;
}
}

View file

@ -32,6 +32,8 @@ import {
EmotePromptMessage,
SendUserMessage,
BanUserMessage,
VariableMessage,
ErrorMessage,
} from "../Messages/generated/messages_pb";
import type { UserSimplePeerInterface } from "../WebRtc/SimplePeer";
@ -75,11 +77,11 @@ export class RoomConnection implements RoomConnection {
/**
*
* @param token A JWT token containing the UUID of the user
* @param roomId The ID of the room in the form "_/[instance]/[map_url]" or "@/[org]/[event]/[map]"
* @param roomUrl The URL of the room in the form "https://example.com/_/[instance]/[map_url]" or "https://example.com/@/[org]/[event]/[map]"
*/
public constructor(
token: string | null,
roomId: string,
roomUrl: string,
name: string,
characterLayers: string[],
position: PositionInterface,
@ -92,7 +94,7 @@ export class RoomConnection implements RoomConnection {
url += "/";
}
url += "room";
url += "?roomId=" + (roomId ? encodeURIComponent(roomId) : "");
url += "?roomId=" + encodeURIComponent(roomUrl);
url += "&token=" + (token ? encodeURIComponent(token) : "");
url += "&name=" + encodeURIComponent(name);
for (const layer of characterLayers) {
@ -164,6 +166,12 @@ export class RoomConnection implements RoomConnection {
} else if (subMessage.hasEmoteeventmessage()) {
const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage;
emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
} else if (subMessage.hasErrormessage()) {
const errorMessage = subMessage.getErrormessage() as ErrorMessage;
console.error("An error occurred server side: " + errorMessage.getMessage());
} else if (subMessage.hasVariablemessage()) {
event = EventMessage.SET_VARIABLE;
payload = subMessage.getVariablemessage();
} else {
throw new Error("Unexpected batch message type");
}
@ -180,6 +188,22 @@ export class RoomConnection implements RoomConnection {
items[item.getItemid()] = JSON.parse(item.getStatejson());
}
const variables = new Map<string, unknown>();
for (const variable of roomJoinedMessage.getVariableList()) {
try {
variables.set(variable.getName(), JSON.parse(variable.getValue()));
} catch (e) {
console.error(
'Unable to unserialize value received from server for variable "' +
variable.getName() +
'". Value received: "' +
variable.getValue() +
'". Error: ',
e
);
}
}
this.userId = roomJoinedMessage.getCurrentuserid();
this.tags = roomJoinedMessage.getTagList();
@ -187,6 +211,7 @@ export class RoomConnection implements RoomConnection {
connection: this,
room: {
items,
variables,
} as RoomJoinedMessageInterface,
});
} else if (message.hasWorldfullmessage()) {
@ -365,6 +390,7 @@ export class RoomConnection implements RoomConnection {
visitCardUrl: message.getVisitcardurl(),
position: ProtobufClientUtils.toPointInterface(position),
companion: companion ? companion.getName() : null,
userUuid: message.getUseruuid(),
};
}
@ -466,7 +492,6 @@ export class RoomConnection implements RoomConnection {
this.onMessage(EventMessage.WEBRTC_START, (message: WebRtcStartMessage) => {
callback({
userId: message.getUserid(),
name: message.getName(),
initiator: message.getInitiator(),
webRtcUser: message.getWebrtcusername() ?? undefined,
webRtcPassword: message.getWebrtcpassword() ?? undefined,
@ -536,6 +561,17 @@ export class RoomConnection implements RoomConnection {
this.socket.send(clientToServerMessage.serializeBinary().buffer);
}
emitSetVariableEvent(name: string, value: unknown): void {
const variableMessage = new VariableMessage();
variableMessage.setName(name);
variableMessage.setValue(JSON.stringify(value));
const clientToServerMessage = new ClientToServerMessage();
clientToServerMessage.setVariablemessage(variableMessage);
this.socket.send(clientToServerMessage.serializeBinary().buffer);
}
onActionableEvent(callback: (message: ItemEventMessageInterface) => void): void {
this.onMessage(EventMessage.ITEM_EVENT, (message: ItemEventMessage) => {
callback({
@ -592,9 +628,9 @@ export class RoomConnection implements RoomConnection {
this.socket.send(clientToServerMessage.serializeBinary().buffer);
}
public emitReportPlayerMessage(reportedUserId: number, reportComment: string): void {
public emitReportPlayerMessage(reportedUserUuid: string, reportComment: string): void {
const reportPlayerMessage = new ReportPlayerMessage();
reportPlayerMessage.setReporteduserid(reportedUserId);
reportPlayerMessage.setReporteduseruuid(reportedUserUuid);
reportPlayerMessage.setReportcomment(reportComment);
const clientToServerMessage = new ClientToServerMessage();
@ -622,6 +658,29 @@ export class RoomConnection implements RoomConnection {
});
}
public onSetVariable(callback: (name: string, value: unknown) => void): void {
this.onMessage(EventMessage.SET_VARIABLE, (message: VariableMessage) => {
const name = message.getName();
const serializedValue = message.getValue();
let value: unknown = undefined;
if (serializedValue) {
try {
value = JSON.parse(serializedValue);
} catch (e) {
console.error(
'Unable to unserialize value received from server for variable "' +
name +
'". Value received: "' +
serializedValue +
'". Error: ',
e
);
}
}
callback(name, value);
});
}
public hasTag(tag: string): boolean {
return this.tags.includes(tag);
}

View file

@ -1,7 +1,7 @@
import {discussionManager} from "../../WebRtc/DiscussionManager";
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes";
import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
import { chatVisibilityStore } from "../../Stores/ChatStore";
export const openChatIconName = 'openChatIcon';
export const openChatIconName = "openChatIcon";
export class OpenChatIcon extends Phaser.GameObjects.Image {
constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y, openChatIconName, 3);
@ -9,9 +9,9 @@ export class OpenChatIcon extends Phaser.GameObjects.Image {
this.setScrollFactor(0, 0);
this.setOrigin(0, 1);
this.setInteractive();
this.setVisible(false);
//this.setVisible(false);
this.setDepth(DEPTH_INGAME_TEXT_INDEX);
this.on("pointerup", () => discussionManager.showDiscussionPart());
this.on("pointerup", () => chatVisibilityStore.set(true));
}
}
}

View file

@ -101,7 +101,6 @@ export const createLoadingPromise = (
frameConfig: FrameConfig
) => {
return new Promise<BodyResourceDescriptionInterface>((res, rej) => {
console.log("count", loadPlugin.listenerCount("loaderror"));
if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
return res(playerResourceDescriptor);
}

View file

@ -1,11 +1,6 @@
import type {PointInterface} from "../../Connexion/ConnexionModels";
import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
import type { PointInterface } from "../../Connexion/ConnexionModels";
import type { PlayerInterface } from "./PlayerInterface";
export interface AddPlayerInterface {
userId: number;
name: string;
characterLayers: BodyResourceDescriptionInterface[];
export interface AddPlayerInterface extends PlayerInterface {
position: PointInterface;
visitCardUrl: string|null;
companion: string|null;
}

View file

@ -28,7 +28,7 @@ export class GameManager {
public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise<string> {
this.startRoom = await connectionManager.initGameConnexion();
await this.loadMap(this.startRoom, scenePlugin);
this.loadMap(this.startRoom, scenePlugin);
if (!this.playerName) {
return LoginSceneName;
@ -68,20 +68,19 @@ export class GameManager {
return this.companion;
}
public async loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin): Promise<void> {
const roomID = room.id;
const mapDetail = await room.getMapDetail();
public loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin) {
const roomID = room.key;
const gameIndex = scenePlugin.getIndex(roomID);
if (gameIndex === -1) {
const game: Phaser.Scene = new GameScene(room, mapDetail.mapUrl);
const game: Phaser.Scene = new GameScene(room, room.mapUrl);
scenePlugin.add(roomID, game, false);
}
}
public goToStartingMap(scenePlugin: Phaser.Scenes.ScenePlugin): void {
console.log("starting " + (this.currentGameSceneName || this.startRoom.id));
scenePlugin.start(this.currentGameSceneName || this.startRoom.id);
console.log("starting " + (this.currentGameSceneName || this.startRoom.key));
scenePlugin.start(this.currentGameSceneName || this.startRoom.key);
scenePlugin.launch(MenuSceneName);
if (

View file

@ -1,4 +1,4 @@
import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty } from "../Map/ITiledMap";
import type { ITiledMap, ITiledMapLayer, ITiledMapProperty } from "../Map/ITiledMap";
import { flattenGroupLayersMap } from "../Map/LayersFlattener";
import TilemapLayer = Phaser.Tilemaps.TilemapLayer;
import { DEPTH_OVERLAY_INDEX } from "./DepthIndexes";
@ -19,7 +19,7 @@ export class GameMap {
private callbacks = new Map<string, Array<PropertyChangeCallback>>();
private tileNameMap = new Map<string, number>();
private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapLayerProperty> } = {};
private tileSetPropertyMap: { [tile_index: number]: Array<ITiledMapProperty> } = {};
public readonly flatLayers: ITiledMapLayer[];
public readonly phaserLayers: TilemapLayer[] = [];
@ -61,7 +61,7 @@ export class GameMap {
}
}
public getPropertiesForIndex(index: number): Array<ITiledMapLayerProperty> {
public getPropertiesForIndex(index: number): Array<ITiledMapProperty> {
if (this.tileSetPropertyMap[index]) {
return this.tileSetPropertyMap[index];
}
@ -151,8 +151,11 @@ export class GameMap {
return this.map;
}
private getTileProperty(index: number): Array<ITiledMapLayerProperty> {
return this.tileSetPropertyMap[index];
private getTileProperty(index: number): Array<ITiledMapProperty> {
if (this.tileSetPropertyMap[index]) {
return this.tileSetPropertyMap[index];
}
return [];
}
private trigger(
@ -189,6 +192,10 @@ export class GameMap {
return this.phaserLayers.find((layer) => layer.layer.name === layerName);
}
public findPhaserLayers(groupName: string): TilemapLayer[] {
return this.phaserLayers.filter((l) => l.layer.name.includes(groupName));
}
public addTerrain(terrain: Phaser.Tilemaps.Tileset): void {
for (const phaserLayer of this.phaserLayers) {
phaserLayer.tileset.push(terrain);
@ -198,37 +205,45 @@ export class GameMap {
private putTileInFlatLayer(index: number, x: number, y: number, layer: string): void {
const fLayer = this.findLayer(layer);
if (fLayer == undefined) {
console.error("The layer that you want to change doesn't exist.");
console.error("The layer '" + layer + "' that you want to change doesn't exist.");
return;
}
if (fLayer.type !== "tilelayer") {
console.error("The layer that you want to change is not a tilelayer. Tile can only be put in tilelayer.");
console.error(
"The layer '" +
layer +
"' that you want to change is not a tilelayer. Tile can only be put in tilelayer."
);
return;
}
if (typeof fLayer.data === "string") {
console.error("Data of the layer that you want to change is only readable.");
console.error("Data of the layer '" + layer + "' that you want to change is only readable.");
return;
}
fLayer.data[x + y * fLayer.height] = index;
fLayer.data[x + y * fLayer.width] = index;
}
public putTile(tile: string | number, x: number, y: number, layer: string): void {
public putTile(tile: string | number | null, x: number, y: number, layer: string): void {
const phaserLayer = this.findPhaserLayer(layer);
if (phaserLayer) {
if (tile === null) {
phaserLayer.putTileAt(-1, x, y);
return;
}
const tileIndex = this.getIndexForTileType(tile);
if (tileIndex !== undefined) {
this.putTileInFlatLayer(tileIndex, x, y, layer);
const phaserTile = phaserLayer.putTileAt(tileIndex, x, y);
for (const property of this.getTileProperty(tileIndex)) {
if (property.name === "collides" && property.value === "true") {
if (property.name === "collides" && property.value) {
phaserTile.setCollision(true);
}
}
} else {
console.error("The tile that you want to place doesn't exist.");
console.error("The tile '" + tile + "' that you want to place doesn't exist.");
}
} else {
console.error("The layer that you want to change is not a tilelayer. Tile can only be put in tilelayer.");
console.error("The layer '" + layer + "' does not exist (or is not a tilelaye).");
}
}

View file

@ -47,13 +47,7 @@ import { RemotePlayer } from "../Entity/RemotePlayer";
import type { ActionableItem } from "../Items/ActionableItem";
import type { ItemFactoryInterface } from "../Items/ItemFactoryInterface";
import { SelectCharacterScene, SelectCharacterSceneName } from "../Login/SelectCharacterScene";
import type {
ITiledMap,
ITiledMapLayer,
ITiledMapLayerProperty,
ITiledMapObject,
ITiledTileSet,
} from "../Map/ITiledMap";
import type { ITiledMap, ITiledMapLayer, ITiledMapProperty, ITiledMapObject, ITiledTileSet } from "../Map/ITiledMap";
import { MenuScene, MenuSceneName } from "../Menu/MenuScene";
import { PlayerAnimationDirections } from "../Player/Animation";
import { hasMovedEventName, Player, requestEmoteEventName } from "../Player/Player";
@ -92,6 +86,9 @@ import { peerStore, screenSharingPeerStore } from "../../Stores/PeerStore";
import { videoFocusStore } from "../../Stores/VideoFocusStore";
import { biggestAvailableAreaStore } from "../../Stores/BiggestAvailableAreaStore";
import { isMessageReferenceEvent, isTriggerMessageEvent } from "../../Api/Events/ui/TriggerMessageEvent";
import { SharedVariablesManager } from "./SharedVariablesManager";
import { playersStore } from "../../Stores/PlayersStore";
import { chatVisibilityStore } from "../../Stores/ChatStore";
export interface GameSceneInitInterface {
initPosition: PointInterface | null;
@ -169,9 +166,10 @@ export class GameScene extends DirtyScene {
private createPromiseResolve!: (value?: void | PromiseLike<void>) => void;
private iframeSubscriptionList!: Array<Subscription>;
private peerStoreUnsubscribe!: () => void;
private chatVisibilityUnsubscribe!: () => void;
private biggestAvailableAreaStoreUnsubscribe!: () => void;
MapUrlFile: string;
RoomId: string;
roomUrl: string;
instance: string;
currentTick!: number;
@ -200,18 +198,19 @@ export class GameScene extends DirtyScene {
private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time.
private emoteManager!: EmoteManager;
private preloading: boolean = true;
startPositionCalculator!: StartPositionCalculator;
private startPositionCalculator!: StartPositionCalculator;
private sharedVariablesManager!: SharedVariablesManager;
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
super({
key: customKey ?? room.id,
key: customKey ?? room.key,
});
this.Terrains = [];
this.groups = new Map<number, Sprite>();
this.instance = room.getInstance();
this.MapUrlFile = MapUrlFile;
this.RoomId = room.id;
this.roomUrl = room.key;
this.createPromise = new Promise<void>((resolve, reject): void => {
this.createPromiseResolve = resolve;
@ -463,11 +462,13 @@ export class GameScene extends DirtyScene {
if (layer.type === "tilelayer") {
const exitSceneUrl = this.getExitSceneUrl(layer);
if (exitSceneUrl !== undefined) {
this.loadNextGame(exitSceneUrl);
this.loadNextGame(
Room.getRoomPathFromExitSceneUrl(exitSceneUrl, window.location.toString(), this.MapUrlFile)
);
}
const exitUrl = this.getExitUrl(layer);
if (exitUrl !== undefined) {
this.loadNextGame(exitUrl);
this.loadNextGameFromExitUrl(exitUrl);
}
}
if (layer.type === "objectgroup") {
@ -480,7 +481,7 @@ export class GameScene extends DirtyScene {
}
this.gameMap.exitUrls.forEach((exitUrl) => {
this.loadNextGame(exitUrl);
this.loadNextGameFromExitUrl(exitUrl);
});
this.startPositionCalculator = new StartPositionCalculator(
@ -571,6 +572,10 @@ export class GameScene extends DirtyScene {
}
oldPeerNumber = newPeerNumber;
});
this.chatVisibilityUnsubscribe = chatVisibilityStore.subscribe((v) => {
this.openChatIcon.setVisible(!v);
});
}
/**
@ -581,7 +586,7 @@ export class GameScene extends DirtyScene {
connectionManager
.connectToRoomSocket(
this.RoomId,
this.roomUrl,
this.playerName,
this.characterLayers,
{
@ -598,6 +603,8 @@ export class GameScene extends DirtyScene {
.then((onConnect: OnConnectInterface) => {
this.connection = onConnect.connection;
playersStore.connectToRoomConnection(this.connection);
this.connection.onUserJoins((message: MessageUserJoined) => {
const userMessage: AddPlayerInterface = {
userId: message.userId,
@ -606,6 +613,7 @@ export class GameScene extends DirtyScene {
position: message.position,
visitCardUrl: message.visitCardUrl,
companion: message.companion,
userUuid: message.userUuid,
};
this.addPlayer(userMessage);
});
@ -689,12 +697,12 @@ export class GameScene extends DirtyScene {
const self = this;
this.simplePeer.registerPeerConnectionListener({
onConnect(peer) {
self.openChatIcon.setVisible(true);
//self.openChatIcon.setVisible(true);
audioManager.decreaseVolume();
},
onDisconnect(userId: number) {
if (self.simplePeer.getNbConnections() === 0) {
self.openChatIcon.setVisible(false);
//self.openChatIcon.setVisible(false);
audioManager.restoreVolume();
}
},
@ -707,6 +715,13 @@ export class GameScene extends DirtyScene {
this.gameMap.setPosition(event.x, event.y);
});
// Set up variables manager
this.sharedVariablesManager = new SharedVariablesManager(
this.connection,
this.gameMap,
onConnect.room.variables
);
//this.initUsersPosition(roomJoinedMessage.users);
this.connectionAnswerPromiseResolve(onConnect.room);
// Analyze tags to find if we are admin. If yes, show console.
@ -766,10 +781,13 @@ export class GameScene extends DirtyScene {
private triggerOnMapLayerPropertyChange() {
this.gameMap.onPropertyChange("exitSceneUrl", (newValue, oldValue) => {
if (newValue) this.onMapExit(newValue as string);
if (newValue)
this.onMapExit(
Room.getRoomPathFromExitSceneUrl(newValue as string, window.location.toString(), this.MapUrlFile)
);
});
this.gameMap.onPropertyChange("exitUrl", (newValue, oldValue) => {
if (newValue) this.onMapExit(newValue as string);
if (newValue) this.onMapExit(Room.getRoomPathFromExitUrl(newValue as string, window.location.toString()));
});
this.gameMap.onPropertyChange("openWebsite", (newValue, oldValue, allProps) => {
if (newValue === undefined) {
@ -994,9 +1012,9 @@ ${escapedMessage}
);
this.iframeSubscriptionList.push(
iframeListener.loadPageStream.subscribe((url: string) => {
this.loadNextGame(url).then(() => {
this.loadNextGameFromExitUrl(url).then(() => {
this.events.once(EVENT_TYPE.POST_UPDATE, () => {
this.onMapExit(url);
this.onMapExit(Room.getRoomPathFromExitUrl(url, window.location.toString()));
});
});
})
@ -1039,20 +1057,24 @@ ${escapedMessage}
})
);
this.iframeSubscriptionList.push(
iframeListener.dataLayerChangeStream.subscribe(() => {
iframeListener.sendDataLayerEvent({ data: this.gameMap.getMap() });
})
);
iframeListener.registerAnswerer("getMapData", () => {
return {
data: this.gameMap.getMap(),
};
});
iframeListener.registerAnswerer("getState", () => {
iframeListener.registerAnswerer("getState", async () => {
// The sharedVariablesManager is not instantiated before the connection is established. So we need to wait
// for the connection to send back the answer.
await this.connectionAnswerPromise;
return {
mapUrl: this.MapUrlFile,
startLayerName: this.startPositionCalculator.startLayerName,
uuid: localUserStore.getLocalUser()?.uuid,
nickname: localUserStore.getName(),
roomId: this.RoomId,
nickname: this.playerName,
roomId: this.roomUrl,
tags: this.connection ? this.connection.getAllTags() : [],
variables: this.sharedVariablesManager.variables,
};
});
this.iframeSubscriptionList.push(
@ -1076,17 +1098,12 @@ ${escapedMessage}
},
this.userInputManager
);
}),
isTriggerMessageEvent
})
);
iframeListener.registerAnswerer(
"removeTriggerMessage",
(message) => {
layoutManager.removeActionButton(message.uuid, this.userInputManager);
},
isMessageReferenceEvent
);
iframeListener.registerAnswerer("removeTriggerMessage", (message) => {
layoutManager.removeActionButton(message.uuid, this.userInputManager);
});
}
private setPropertyLayer(
@ -1099,53 +1116,86 @@ ${escapedMessage}
console.warn('Could not find layer "' + layerName + '" when calling setProperty');
return;
}
if (propertyName === "exitUrl" && typeof propertyValue === "string") {
this.loadNextGameFromExitUrl(propertyValue);
}
if (layer.properties === undefined) {
layer.properties = [];
}
const property = layer.properties.find((property) => property.name === propertyName);
if (property === undefined) {
if (propertyValue === undefined) {
return;
}
layer.properties.push({ name: propertyName, type: typeof propertyValue, value: propertyValue });
return;
}
if (propertyValue === undefined) {
const index = layer.properties.indexOf(property);
layer.properties.splice(index, 1);
}
property.value = propertyValue;
}
private setLayerVisibility(layerName: string, visible: boolean): void {
const phaserLayer = this.gameMap.findPhaserLayer(layerName);
if (phaserLayer === undefined) {
console.warn('Could not find layer "' + layerName + '" when calling WA.hideLayer / WA.showLayer');
return;
if (phaserLayer != undefined) {
phaserLayer.setVisible(visible);
phaserLayer.setCollisionByProperty({ collides: true }, visible);
} else {
const phaserLayers = this.gameMap.findPhaserLayers(layerName + "/");
if (phaserLayers === []) {
console.warn(
'Could not find layer with name that contains "' +
layerName +
'" when calling WA.hideLayer / WA.showLayer'
);
return;
}
for (let i = 0; i < phaserLayers.length; i++) {
phaserLayers[i].setVisible(visible);
phaserLayers[i].setCollisionByProperty({ collides: true }, visible);
}
}
phaserLayer.setVisible(visible);
this.dirty = true;
this.markDirty();
}
private getMapDirUrl(): string {
return this.MapUrlFile.substr(0, this.MapUrlFile.lastIndexOf("/"));
}
private onMapExit(exitKey: string) {
private async onMapExit(roomUrl: URL) {
if (this.mapTransitioning) return;
this.mapTransitioning = true;
const { roomId, hash } = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance);
if (!roomId) throw new Error("Could not find the room from its exit key: " + exitKey);
if (hash) {
urlManager.pushStartLayerNameToUrl(hash);
let targetRoom: Room;
try {
targetRoom = await Room.createRoom(roomUrl);
} catch (e: unknown) {
console.error('Error while fetching new room "' + roomUrl.toString() + '"', e);
this.mapTransitioning = false;
return;
}
if (roomUrl.hash) {
urlManager.pushStartLayerNameToUrl(roomUrl.hash);
}
const menuScene: MenuScene = this.scene.get(MenuSceneName) as MenuScene;
menuScene.reset();
if (roomId !== this.scene.key) {
if (this.scene.get(roomId) === null) {
console.error("next room not loaded", exitKey);
if (!targetRoom.isEqual(this.room)) {
if (this.scene.get(targetRoom.key) === null) {
console.error("next room not loaded", targetRoom.key);
return;
}
this.cleanupClosingScene();
this.scene.stop();
this.scene.start(targetRoom.key);
this.scene.remove(this.scene.key);
this.scene.start(roomId);
} else {
//if the exit points to the current map, we simply teleport the user back to the startLayer
this.startPositionCalculator.initPositionFromLayerName(hash, hash);
this.startPositionCalculator.initPositionFromLayerName(roomUrl.hash, roomUrl.hash);
this.CurrentPlayer.x = this.startPositionCalculator.startPosition.x;
this.CurrentPlayer.y = this.startPositionCalculator.startPosition.y;
setTimeout(() => (this.mapTransitioning = false), 500);
@ -1172,8 +1222,13 @@ ${escapedMessage}
this.pinchManager?.destroy();
this.emoteManager.destroy();
this.peerStoreUnsubscribe();
this.chatVisibilityUnsubscribe();
this.biggestAvailableAreaStoreUnsubscribe();
iframeListener.unregisterAnswerer("getMapData");
iframeListener.unregisterAnswerer("getState");
iframeListener.unregisterAnswerer("triggerMessage");
iframeListener.unregisterAnswerer("removeTriggerMessage");
this.sharedVariablesManager?.close();
mediaManager.hideGameOverlay();
@ -1213,12 +1268,12 @@ ${escapedMessage}
}
private getProperty(layer: ITiledMapLayer | ITiledMap, name: string): string | boolean | number | undefined {
const properties: ITiledMapLayerProperty[] | undefined = layer.properties;
const properties: ITiledMapProperty[] | undefined = layer.properties;
if (!properties) {
return undefined;
}
const obj = properties.find(
(property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase()
(property: ITiledMapProperty) => property.name.toLowerCase() === name.toLowerCase()
);
if (obj === undefined) {
return undefined;
@ -1227,20 +1282,27 @@ ${escapedMessage}
}
private getProperties(layer: ITiledMapLayer | ITiledMap, name: string): (string | number | boolean | undefined)[] {
const properties: ITiledMapLayerProperty[] | undefined = layer.properties;
const properties: ITiledMapProperty[] | undefined = layer.properties;
if (!properties) {
return [];
}
return properties
.filter((property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase())
.filter((property: ITiledMapProperty) => property.name.toLowerCase() === name.toLowerCase())
.map((property) => property.value);
}
private loadNextGameFromExitUrl(exitUrl: string): Promise<void> {
return this.loadNextGame(Room.getRoomPathFromExitUrl(exitUrl, window.location.toString()));
}
//todo: push that into the gameManager
private loadNextGame(exitSceneIdentifier: string): Promise<void> {
const { roomId, hash } = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance);
const room = new Room(roomId);
return gameManager.loadMap(room, this.scene).catch(() => {});
private async loadNextGame(exitRoomPath: URL): Promise<void> {
try {
const room = await Room.createRoom(exitRoomPath);
return gameManager.loadMap(room, this.scene);
} catch (e: unknown) {
console.warn('Error while pre-loading exit room "' + exitRoomPath.toString() + '"', e);
}
}
//todo: in a dedicated class/function?
@ -1683,7 +1745,7 @@ ${escapedMessage}
this.scene.start(ErrorSceneName, {
title: "Banned",
subTitle: "You were banned from WorkAdventure",
message: "If you want more information, you may contact us at: workadventure@thecodingmachine.com",
message: "If you want more information, you may contact us at: hello@workadventu.re",
});
}
@ -1698,14 +1760,14 @@ ${escapedMessage}
this.scene.start(ErrorSceneName, {
title: "Connection rejected",
subTitle: "The world you are trying to join is full. Try again later.",
message: "If you want more information, you may contact us at: workadventure@thecodingmachine.com",
message: "If you want more information, you may contact us at: hello@workadventu.re",
});
} else {
this.scene.start(ErrorSceneName, {
title: "Connection rejected",
subTitle: "You cannot join the World. Try again later. \n\r \n\r Error: " + message + ".",
message:
"If you want more information, you may contact administrator or contact us at: workadventure@thecodingmachine.com",
"If you want more information, you may contact administrator or contact us at: hello@workadventu.re",
});
}
}

View file

@ -0,0 +1,11 @@
import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
export interface PlayerInterface {
userId: number;
name: string;
characterLayers: BodyResourceDescriptionInterface[];
visitCardUrl: string | null;
companion: string | null;
userUuid: string;
color?: string;
}

View file

@ -0,0 +1,167 @@
import type { RoomConnection } from "../../Connexion/RoomConnection";
import { iframeListener } from "../../Api/IframeListener";
import type { Subscription } from "rxjs";
import type { GameMap } from "./GameMap";
import type { ITile, ITiledMapObject } from "../Map/ITiledMap";
import type { Var } from "svelte/types/compiler/interfaces";
import { init } from "svelte/internal";
interface Variable {
defaultValue: unknown;
readableBy?: string;
writableBy?: string;
}
/**
* Stores variables and provides a bridge between scripts and the pusher server.
*/
export class SharedVariablesManager {
private _variables = new Map<string, unknown>();
private variableObjects: Map<string, Variable>;
constructor(
private roomConnection: RoomConnection,
private gameMap: GameMap,
serverVariables: Map<string, unknown>
) {
// We initialize the list of variable object at room start. The objects cannot be edited later
// (otherwise, this would cause a security issue if the scripting API can edit this list of objects)
this.variableObjects = SharedVariablesManager.findVariablesInMap(gameMap);
// Let's initialize default values
for (const [name, variableObject] of this.variableObjects.entries()) {
if (variableObject.readableBy && !this.roomConnection.hasTag(variableObject.readableBy)) {
// Do not initialize default value for variables that are not readable
continue;
}
this._variables.set(name, variableObject.defaultValue);
}
// Override default values with the variables from the server:
for (const [name, value] of serverVariables) {
this._variables.set(name, value);
}
roomConnection.onSetVariable((name, value) => {
this._variables.set(name, value);
// On server change, let's notify the iframes
iframeListener.setVariable({
key: name,
value: value,
});
});
// When a variable is modified from an iFrame
iframeListener.registerAnswerer("setVariable", (event, source) => {
const key = event.key;
const object = this.variableObjects.get(key);
if (object === undefined) {
const errMsg =
'A script is trying to modify variable "' +
key +
'" but this variable is not defined in the map.' +
'There should be an object in the map whose name is "' +
key +
'" and whose type is "variable"';
console.error(errMsg);
throw new Error(errMsg);
}
if (object.writableBy && !this.roomConnection.hasTag(object.writableBy)) {
const errMsg =
'A script is trying to modify variable "' +
key +
'" but this variable is only writable for users with tag "' +
object.writableBy +
'".';
console.error(errMsg);
throw new Error(errMsg);
}
// Let's stop any propagation of the value we set is the same as the existing value.
if (JSON.stringify(event.value) === JSON.stringify(this._variables.get(key))) {
return;
}
this._variables.set(key, event.value);
// Dispatch to the room connection.
this.roomConnection.emitSetVariableEvent(key, event.value);
// Dispatch to other iframes
iframeListener.dispatchVariableToOtherIframes(key, event.value, source);
});
}
private static findVariablesInMap(gameMap: GameMap): Map<string, Variable> {
const objects = new Map<string, Variable>();
for (const layer of gameMap.getMap().layers) {
if (layer.type === "objectgroup") {
for (const object of layer.objects) {
if (object.type === "variable") {
if (object.template) {
console.warn(
'Warning, a variable object is using a Tiled "template". WorkAdventure does not support objects generated from Tiled templates.'
);
}
// We store a copy of the object (to make it immutable)
objects.set(object.name, this.iTiledObjectToVariable(object));
}
}
}
}
return objects;
}
private static iTiledObjectToVariable(object: ITiledMapObject): Variable {
const variable: Variable = {
defaultValue: undefined,
};
if (object.properties) {
for (const property of object.properties) {
const value = property.value;
switch (property.name) {
case "default":
variable.defaultValue = value;
break;
case "writableBy":
if (typeof value !== "string") {
throw new Error(
'The writableBy property of variable "' + object.name + '" must be a string'
);
}
if (value) {
variable.writableBy = value;
}
break;
case "readableBy":
if (typeof value !== "string") {
throw new Error(
'The readableBy property of variable "' + object.name + '" must be a string'
);
}
if (value) {
variable.readableBy = value;
}
break;
}
}
}
return variable;
}
public close(): void {
iframeListener.unregisterAnswerer("setVariable");
}
get variables(): Map<string, unknown> {
return this._variables;
}
}

View file

@ -1,5 +1,5 @@
import type { PositionInterface } from "../../Connexion/ConnexionModels";
import type { ITiledMap, ITiledMapLayer, ITiledMapLayerProperty, ITiledMapTileLayer } from "../Map/ITiledMap";
import type { ITiledMap, ITiledMapLayer, ITiledMapProperty, ITiledMapTileLayer } from "../Map/ITiledMap";
import type { GameMap } from "./GameMap";
const defaultStartLayerName = "start";
@ -112,12 +112,12 @@ export class StartPositionCalculator {
}
private getProperty(layer: ITiledMapLayer | ITiledMap, name: string): string | boolean | number | undefined {
const properties: ITiledMapLayerProperty[] | undefined = layer.properties;
const properties: ITiledMapProperty[] | undefined = layer.properties;
if (!properties) {
return undefined;
}
const obj = properties.find(
(property: ITiledMapLayerProperty) => property.name.toLowerCase() === name.toLowerCase()
(property: ITiledMapProperty) => property.name.toLowerCase() === name.toLowerCase()
);
if (obj === undefined) {
return undefined;

View file

@ -1,8 +1,8 @@
import {gameManager} from "../Game/GameManager";
import {Scene} from "phaser";
import {ErrorScene} from "../Reconnecting/ErrorScene";
import {WAError} from "../Reconnecting/WAError";
import {waScaleManager} from "../Services/WaScaleManager";
import { gameManager } from "../Game/GameManager";
import { Scene } from "phaser";
import { ErrorScene } from "../Reconnecting/ErrorScene";
import { WAError } from "../Reconnecting/WAError";
import { waScaleManager } from "../Services/WaScaleManager";
export const EntrySceneName = "EntryScene";
@ -13,26 +13,32 @@ export const EntrySceneName = "EntryScene";
export class EntryScene extends Scene {
constructor() {
super({
key: EntrySceneName
key: EntrySceneName,
});
}
create() {
gameManager.init(this.scene).then((nextSceneName) => {
// Let's rescale before starting the game
// We can do it at this stage.
waScaleManager.applyNewSize();
this.scene.start(nextSceneName);
}).catch((err) => {
if (err.response && err.response.status == 404) {
ErrorScene.showError(new WAError(
'Access link incorrect',
'Could not find map. Please check your access link.',
'If you want more information, you may contact administrator or contact us at: workadventure@thecodingmachine.com'), this.scene);
} else {
ErrorScene.showError(err, this.scene);
}
});
gameManager
.init(this.scene)
.then((nextSceneName) => {
// Let's rescale before starting the game
// We can do it at this stage.
waScaleManager.applyNewSize();
this.scene.start(nextSceneName);
})
.catch((err) => {
if (err.response && err.response.status == 404) {
ErrorScene.showError(
new WAError(
"Access link incorrect",
"Could not find map. Please check your access link.",
"If you want more information, you may contact administrator or contact us at: hello@workadventu.re"
),
this.scene
);
} else {
ErrorScene.showError(err, this.scene);
}
});
}
}

View file

@ -1,25 +1,25 @@
import {gameManager} from "../Game/GameManager";
import { gameManager } from "../Game/GameManager";
import Rectangle = Phaser.GameObjects.Rectangle;
import {EnableCameraSceneName} from "./EnableCameraScene";
import {CustomizeSceneName} from "./CustomizeScene";
import {localUserStore} from "../../Connexion/LocalUserStore";
import {loadAllDefaultModels} from "../Entity/PlayerTexturesLoadingManager";
import {addLoader} from "../Components/Loader";
import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
import {AbstractCharacterScene} from "./AbstractCharacterScene";
import {areCharacterLayersValid} from "../../Connexion/LocalUser";
import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager";
import {selectCharacterSceneVisibleStore} from "../../Stores/SelectCharacterStore";
import {waScaleManager} from "../Services/WaScaleManager";
import {isMobile} from "../../Enum/EnvironmentVariable";
import { EnableCameraSceneName } from "./EnableCameraScene";
import { CustomizeSceneName } from "./CustomizeScene";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { loadAllDefaultModels } from "../Entity/PlayerTexturesLoadingManager";
import { addLoader } from "../Components/Loader";
import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures";
import { AbstractCharacterScene } from "./AbstractCharacterScene";
import { areCharacterLayersValid } from "../../Connexion/LocalUser";
import { touchScreenManager } from "../../Touch/TouchScreenManager";
import { PinchManager } from "../UserInput/PinchManager";
import { selectCharacterSceneVisibleStore } from "../../Stores/SelectCharacterStore";
import { waScaleManager } from "../Services/WaScaleManager";
import { isMobile } from "../../Enum/EnvironmentVariable";
//todo: put this constants in a dedicated file
export const SelectCharacterSceneName = "SelectCharacterScene";
export class SelectCharacterScene extends AbstractCharacterScene {
protected readonly nbCharactersPerRow = 6;
protected selectedPlayer!: Phaser.Physics.Arcade.Sprite|null; // null if we are selecting the "customize" option
protected selectedPlayer!: Phaser.Physics.Arcade.Sprite | null; // null if we are selecting the "customize" option
protected players: Array<Phaser.Physics.Arcade.Sprite> = new Array<Phaser.Physics.Arcade.Sprite>();
protected playerModels!: BodyResourceDescriptionInterface[];
@ -38,7 +38,6 @@ export class SelectCharacterScene extends AbstractCharacterScene {
}
preload() {
this.loadSelectSceneCharacters().then((bodyResourceDescriptions) => {
bodyResourceDescriptions.forEach((bodyResourceDescription) => {
this.playerModels.push(bodyResourceDescription);
@ -54,7 +53,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
create() {
selectCharacterSceneVisibleStore.set(true);
this.events.addListener('wake', () => {
this.events.addListener("wake", () => {
waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 2 : 1;
selectCharacterSceneVisibleStore.set(true);
@ -68,26 +67,26 @@ export class SelectCharacterScene extends AbstractCharacterScene {
waScaleManager.zoomModifier = isMobile() ? 2 : 1;
const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16;
this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xFFFFFF);
this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xffffff);
this.selectedRectangle.setDepth(2);
/*create user*/
this.createCurrentPlayer();
this.input.keyboard.on('keyup-ENTER', () => {
this.input.keyboard.on("keyup-ENTER", () => {
return this.nextSceneToCameraScene();
});
this.input.keyboard.on('keydown-RIGHT', () => {
this.input.keyboard.on("keydown-RIGHT", () => {
this.moveToRight();
});
this.input.keyboard.on('keydown-LEFT', () => {
this.input.keyboard.on("keydown-LEFT", () => {
this.moveToLeft();
});
this.input.keyboard.on('keydown-UP', () => {
this.input.keyboard.on("keydown-UP", () => {
this.moveToUp();
});
this.input.keyboard.on('keydown-DOWN', () => {
this.input.keyboard.on("keydown-DOWN", () => {
this.moveToDown();
});
}
@ -96,7 +95,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
if (this.selectedPlayer !== null && !areCharacterLayersValid([this.selectedPlayer.texture.key])) {
return;
}
if(!this.selectedPlayer){
if (!this.selectedPlayer) {
return;
}
this.scene.stop(SelectCharacterSceneName);
@ -105,7 +104,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
gameManager.tryResumingGame(this, EnableCameraSceneName);
this.players = [];
selectCharacterSceneVisibleStore.set(false);
this.events.removeListener('wake');
this.events.removeListener("wake");
}
public nextSceneToCustomizeScene(): void {
@ -119,11 +118,11 @@ export class SelectCharacterScene extends AbstractCharacterScene {
}
createCurrentPlayer(): void {
for (let i = 0; i <this.playerModels.length; i++) {
for (let i = 0; i < this.playerModels.length; i++) {
const playerResource = this.playerModels[i];
//check already exist texture
if(this.players.find((c) => c.texture.key === playerResource.name)){
if (this.players.find((c) => c.texture.key === playerResource.name)) {
continue;
}
@ -132,9 +131,9 @@ export class SelectCharacterScene extends AbstractCharacterScene {
this.setUpPlayer(player, i);
this.anims.create({
key: playerResource.name,
frames: this.anims.generateFrameNumbers(playerResource.name, {start: 0, end: 11}),
frames: this.anims.generateFrameNumbers(playerResource.name, { start: 0, end: 11 }),
frameRate: 8,
repeat: -1
repeat: -1,
});
player.setInteractive().on("pointerdown", () => {
if (this.pointerClicked) {
@ -153,77 +152,79 @@ export class SelectCharacterScene extends AbstractCharacterScene {
});
this.players.push(player);
}
if (this.currentSelectUser >= this.players.length) {
this.currentSelectUser = 0;
}
this.selectedPlayer = this.players[this.currentSelectUser];
this.selectedPlayer.play(this.playerModels[this.currentSelectUser].name);
}
protected moveUser(){
for(let i = 0; i < this.players.length; i++){
protected moveUser() {
for (let i = 0; i < this.players.length; i++) {
const player = this.players[i];
this.setUpPlayer(player, i);
}
this.updateSelectedPlayer();
}
public moveToLeft(){
if(this.currentSelectUser === 0){
public moveToLeft() {
if (this.currentSelectUser === 0) {
return;
}
this.currentSelectUser -= 1;
this.moveUser();
}
public moveToRight(){
if(this.currentSelectUser === (this.players.length - 1)){
public moveToRight() {
if (this.currentSelectUser === this.players.length - 1) {
return;
}
this.currentSelectUser += 1;
this.moveUser();
}
protected moveToUp(){
if(this.currentSelectUser < this.nbCharactersPerRow){
protected moveToUp() {
if (this.currentSelectUser < this.nbCharactersPerRow) {
return;
}
this.currentSelectUser -= this.nbCharactersPerRow;
this.moveUser();
}
protected moveToDown(){
if((this.currentSelectUser + this.nbCharactersPerRow) > (this.players.length - 1)){
protected moveToDown() {
if (this.currentSelectUser + this.nbCharactersPerRow > this.players.length - 1) {
return;
}
this.currentSelectUser += this.nbCharactersPerRow;
this.moveUser();
}
protected defineSetupPlayer(num: number){
protected defineSetupPlayer(num: number) {
const deltaX = 32;
const deltaY = 32;
let [playerX, playerY] = this.getCharacterPosition(); // player X and player y are middle of the
playerX = ( (playerX - (deltaX * 2.5)) + ((deltaX) * (num % this.nbCharactersPerRow)) ); // calcul position on line users
playerY = ( (playerY - (deltaY * 2)) + ((deltaY) * ( Math.floor(num / this.nbCharactersPerRow) )) ); // calcul position on column users
playerX = playerX - deltaX * 2.5 + deltaX * (num % this.nbCharactersPerRow); // calcul position on line users
playerY = playerY - deltaY * 2 + deltaY * Math.floor(num / this.nbCharactersPerRow); // calcul position on column users
const playerVisible = true;
const playerScale = 1;
const playerOpacity = 1;
// if selected
if( num === this.currentSelectUser ){
if (num === this.currentSelectUser) {
this.selectedRectangle.setX(playerX);
this.selectedRectangle.setY(playerY);
}
return {playerX, playerY, playerScale, playerOpacity, playerVisible}
return { playerX, playerY, playerScale, playerOpacity, playerVisible };
}
protected setUpPlayer(player: Phaser.Physics.Arcade.Sprite, num: number){
const {playerX, playerY, playerScale, playerOpacity, playerVisible} = this.defineSetupPlayer(num);
protected setUpPlayer(player: Phaser.Physics.Arcade.Sprite, num: number) {
const { playerX, playerY, playerScale, playerOpacity, playerVisible } = this.defineSetupPlayer(num);
player.setBounce(0.2);
player.setCollideWorldBounds(false);
player.setVisible( playerVisible );
player.setVisible(playerVisible);
player.setScale(playerScale, playerScale);
player.setAlpha(playerOpacity);
player.setX(playerX);
@ -234,10 +235,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
* Returns pixel position by on column and row number
*/
protected getCharacterPosition(): [number, number] {
return [
this.game.renderer.width / 2,
this.game.renderer.height / 2.5
];
return [this.game.renderer.width / 2, this.game.renderer.height / 2.5];
}
protected updateSelectedPlayer(): void {
@ -256,7 +254,7 @@ export class SelectCharacterScene extends AbstractCharacterScene {
this.pointerClicked = false;
}
if(this.lazyloadingAttempt){
if (this.lazyloadingAttempt) {
//re-render players list
this.createCurrentPlayer();
this.moveUser();

View file

@ -16,7 +16,7 @@ export interface ITiledMap {
* Map orientation (orthogonal)
*/
orientation: string;
properties?: ITiledMapLayerProperty[];
properties?: ITiledMapProperty[];
/**
* Render order (right-down)
@ -33,7 +33,7 @@ export interface ITiledMap {
type?: string;
}
export interface ITiledMapLayerProperty {
export interface ITiledMapProperty {
name: string;
type: string;
value: string | boolean | number | undefined;
@ -51,7 +51,7 @@ export interface ITiledMapGroupLayer {
id?: number;
name: string;
opacity: number;
properties?: ITiledMapLayerProperty[];
properties?: ITiledMapProperty[];
type: "group";
visible: boolean;
@ -69,7 +69,7 @@ export interface ITiledMapTileLayer {
height: number;
name: string;
opacity: number;
properties?: ITiledMapLayerProperty[];
properties?: ITiledMapProperty[];
encoding?: string;
compression?: string;
@ -91,7 +91,7 @@ export interface ITiledMapObjectLayer {
height: number;
name: string;
opacity: number;
properties?: ITiledMapLayerProperty[];
properties?: ITiledMapProperty[];
encoding?: string;
compression?: string;
@ -117,7 +117,7 @@ export interface ITiledMapObject {
gid: number;
height: number;
name: string;
properties: { [key: string]: string };
properties?: ITiledMapProperty[];
rotation: number;
type: string;
visible: boolean;
@ -141,6 +141,7 @@ export interface ITiledMapObject {
polyline: { x: number; y: number }[];
text?: ITiledText;
template?: string;
}
export interface ITiledText {
@ -163,7 +164,7 @@ export interface ITiledTileSet {
imagewidth: number;
margin: number;
name: string;
properties: { [key: string]: string };
properties?: ITiledMapProperty[];
spacing: number;
tilecount: number;
tileheight: number;
@ -182,7 +183,7 @@ export interface ITile {
id: number;
type?: string;
properties?: Array<ITiledMapLayerProperty>;
properties?: ITiledMapProperty[];
}
export interface ITiledMapTerrain {

View file

@ -18,6 +18,9 @@ import { registerMenuCommandStream } from "../../Api/Events/ui/MenuItemRegisterE
import { sendMenuClickedEvent } from "../../Api/iframe/Ui/MenuItem";
import { consoleGlobalMessageManagerVisibleStore } from "../../Stores/ConsoleGlobalMessageManagerStore";
import { get } from "svelte/store";
import { playersStore } from "../../Stores/PlayersStore";
import { mediaManager } from "../../WebRtc/MediaManager";
import { chatVisibilityStore } from "../../Stores/ChatStore";
export const MenuSceneName = "MenuScene";
const gameMenuKey = "gameMenu";
@ -97,6 +100,10 @@ export class MenuScene extends Phaser.Scene {
this.menuElement.setOrigin(0);
MenuScene.revealMenusAfterInit(this.menuElement, "gameMenu");
if (mediaManager.hasNotification()) {
HtmlUtils.getElementByIdOrFail("enableNotification").hidden = true;
}
const middleX = window.innerWidth / 3 - 298;
this.gameQualityMenuElement = this.add.dom(middleX, -400).createFromCache(gameSettingsMenuKey);
MenuScene.revealMenusAfterInit(this.gameQualityMenuElement, "gameQuality");
@ -120,7 +127,11 @@ export class MenuScene extends Phaser.Scene {
showReportScreenStore.subscribe((user) => {
if (user !== null) {
this.closeAll();
this.gameReportElement.open(user.userId, user.userName);
const uuid = playersStore.getPlayerById(user.userId)?.userUuid;
if (uuid === undefined) {
throw new Error("Could not find UUID for user with ID " + user.userId);
}
this.gameReportElement.open(uuid, user.userName);
}
});
@ -137,6 +148,9 @@ export class MenuScene extends Phaser.Scene {
this.menuElement.on("click", this.onMenuClick.bind(this));
worldFullWarningStream.stream.subscribe(() => this.showWorldCapacityWarning());
chatVisibilityStore.subscribe((v) => {
this.menuButton.setVisible(!v);
});
}
//todo put this method in a parent menuElement class
@ -352,6 +366,9 @@ export class MenuScene extends Phaser.Scene {
case "toggleFullscreen":
this.toggleFullscreen();
break;
case "enableNotification":
this.enableNotification();
break;
case "adminConsoleButton":
if (get(consoleGlobalMessageManagerVisibleStore)) {
consoleGlobalMessageManagerVisibleStore.set(false);
@ -414,4 +431,12 @@ export class MenuScene extends Phaser.Scene {
public isDirty(): boolean {
return false;
}
private enableNotification() {
mediaManager.requestNotification().then(() => {
if (mediaManager.hasNotification()) {
HtmlUtils.getElementByIdOrFail("enableNotification").hidden = true;
}
});
}
}

View file

@ -1,15 +1,16 @@
import {MenuScene} from "./MenuScene";
import {gameManager} from "../Game/GameManager";
import {blackListManager} from "../../WebRtc/BlackListManager";
import { MenuScene } from "./MenuScene";
import { gameManager } from "../Game/GameManager";
import { blackListManager } from "../../WebRtc/BlackListManager";
import { playersStore } from "../../Stores/PlayersStore";
export const gameReportKey = 'gameReport';
export const gameReportRessource = 'resources/html/gameReport.html';
export const gameReportKey = "gameReport";
export const gameReportRessource = "resources/html/gameReport.html";
export class ReportMenu extends Phaser.GameObjects.DOMElement {
private opened: boolean = false;
private userId!: number;
private userName!: string|undefined;
private userUuid!: string;
private userName!: string | undefined;
private anonymous: boolean;
constructor(scene: Phaser.Scene, anonymous: boolean) {
@ -18,46 +19,46 @@ export class ReportMenu extends Phaser.GameObjects.DOMElement {
this.createFromCache(gameReportKey);
if (this.anonymous) {
const divToHide = this.getChildByID('reportSection') as HTMLElement;
const divToHide = this.getChildByID("reportSection") as HTMLElement;
divToHide.hidden = true;
const textToHide = this.getChildByID('askActionP') as HTMLElement;
const textToHide = this.getChildByID("askActionP") as HTMLElement;
textToHide.hidden = true;
}
scene.add.existing(this);
MenuScene.revealMenusAfterInit(this, gameReportKey);
this.addListener('click');
this.on('click', (event:MouseEvent) => {
this.addListener("click");
this.on("click", (event: MouseEvent) => {
event.preventDefault();
if ((event?.target as HTMLInputElement).id === 'gameReportFormSubmit') {
if ((event?.target as HTMLInputElement).id === "gameReportFormSubmit") {
this.submitReport();
} else if((event?.target as HTMLInputElement).id === 'gameReportFormCancel') {
} else if ((event?.target as HTMLInputElement).id === "gameReportFormCancel") {
this.close();
} else if((event?.target as HTMLInputElement).id === 'toggleBlockButton') {
} else if ((event?.target as HTMLInputElement).id === "toggleBlockButton") {
this.toggleBlock();
}
});
}
public open(userId: number, userName: string|undefined): void {
public open(userUuid: string, userName: string | undefined): void {
if (this.opened) {
this.close();
return;
}
this.userId = userId;
this.userUuid = userUuid;
this.userName = userName;
const mainEl = this.getChildByID('gameReport') as HTMLElement;
const mainEl = this.getChildByID("gameReport") as HTMLElement;
this.x = this.getCenteredX(mainEl);
this.y = this.getHiddenY(mainEl);
const gameTitleReport = this.getChildByID('nameReported') as HTMLElement;
gameTitleReport.innerText = userName || '';
const gameTitleReport = this.getChildByID("nameReported") as HTMLElement;
gameTitleReport.innerText = userName || "";
const blockButton = this.getChildByID('toggleBlockButton') as HTMLElement;
blockButton.innerText = blackListManager.isBlackListed(this.userId) ? 'Unblock this user' : 'Block this user';
const blockButton = this.getChildByID("toggleBlockButton") as HTMLElement;
blockButton.innerText = blackListManager.isBlackListed(this.userUuid) ? "Unblock this user" : "Block this user";
this.opened = true;
@ -67,19 +68,19 @@ export class ReportMenu extends Phaser.GameObjects.DOMElement {
targets: this,
y: this.getCenteredY(mainEl),
duration: 1000,
ease: 'Power3'
ease: "Power3",
});
}
public close(): void {
gameManager.getCurrentGameScene(this.scene).userInputManager.restoreControls();
this.opened = false;
const mainEl = this.getChildByID('gameReport') as HTMLElement;
const mainEl = this.getChildByID("gameReport") as HTMLElement;
this.scene.tweens.add({
targets: this,
y: this.getHiddenY(mainEl),
duration: 1000,
ease: 'Power3'
ease: "Power3",
});
}
@ -88,31 +89,32 @@ export class ReportMenu extends Phaser.GameObjects.DOMElement {
return window.innerWidth / 4 - mainEl.clientWidth / 2;
}
private getHiddenY(mainEl: HTMLElement): number {
return - mainEl.clientHeight - 50;
return -mainEl.clientHeight - 50;
}
private getCenteredY(mainEl: HTMLElement): number {
return window.innerHeight / 4 - mainEl.clientHeight / 2;
}
private toggleBlock(): void {
!blackListManager.isBlackListed(this.userId) ? blackListManager.blackList(this.userId) : blackListManager.cancelBlackList(this.userId);
!blackListManager.isBlackListed(this.userUuid)
? blackListManager.blackList(this.userUuid)
: blackListManager.cancelBlackList(this.userUuid);
this.close();
}
private submitReport(): void{
const gamePError = this.getChildByID('gameReportErr') as HTMLParagraphElement;
gamePError.innerText = '';
gamePError.style.display = 'none';
const gameTextArea = this.getChildByID('gameReportInput') as HTMLInputElement;
if(!gameTextArea || !gameTextArea.value){
gamePError.innerText = 'Report message cannot to be empty.';
gamePError.style.display = 'block';
private submitReport(): void {
const gamePError = this.getChildByID("gameReportErr") as HTMLParagraphElement;
gamePError.innerText = "";
gamePError.style.display = "none";
const gameTextArea = this.getChildByID("gameReportInput") as HTMLInputElement;
if (!gameTextArea || !gameTextArea.value) {
gamePError.innerText = "Report message cannot to be empty.";
gamePError.style.display = "block";
return;
}
gameManager.getCurrentGameScene(this.scene).connection?.emitReportPlayerMessage(
this.userId,
gameTextArea.value
);
gameManager
.getCurrentGameScene(this.scene)
.connection?.emitReportPlayerMessage(this.userUuid, gameTextArea.value);
this.close();
}
}

View file

@ -1,14 +1,14 @@
import {TextField} from "../Components/TextField";
import { TextField } from "../Components/TextField";
import Image = Phaser.GameObjects.Image;
import Sprite = Phaser.GameObjects.Sprite;
import Text = Phaser.GameObjects.Text;
import ScenePlugin = Phaser.Scenes.ScenePlugin;
import {WAError} from "./WAError";
import { WAError } from "./WAError";
export const ErrorSceneName = "ErrorScene";
enum Textures {
icon = "icon",
mainFont = "main_font"
mainFont = "main_font",
}
export class ErrorScene extends Phaser.Scene {
@ -23,25 +23,21 @@ export class ErrorScene extends Phaser.Scene {
constructor() {
super({
key: ErrorSceneName
key: ErrorSceneName,
});
}
init({title, subTitle, message}: { title?: string, subTitle?: string, message?: string }) {
this.title = title ? title : '';
this.subTitle = subTitle ? subTitle : '';
this.message = message ? message : '';
init({ title, subTitle, message }: { title?: string; subTitle?: string; message?: string }) {
this.title = title ? title : "";
this.subTitle = subTitle ? subTitle : "";
this.message = message ? message : "";
}
preload() {
this.load.image(Textures.icon, "resources/logos/tcm_full.png");
this.load.image(Textures.icon, "static/images/favicons/favicon-32x32.png");
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
this.load.bitmapFont(Textures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
this.load.spritesheet(
'cat',
'resources/characters/pipoya/Cat 01-1.png',
{frameWidth: 32, frameHeight: 32}
);
this.load.bitmapFont(Textures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
this.load.spritesheet("cat", "resources/characters/pipoya/Cat 01-1.png", { frameWidth: 32, frameHeight: 32 });
}
create() {
@ -50,15 +46,25 @@ export class ErrorScene extends Phaser.Scene {
this.titleField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2, this.title);
this.subTitleField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2 + 24, this.subTitle);
this.subTitleField = new TextField(
this,
this.game.renderer.width / 2,
this.game.renderer.height / 2 + 24,
this.subTitle
);
this.messageField = this.add.text(this.game.renderer.width / 2, this.game.renderer.height / 2 + 48, this.message, {
fontFamily: 'Georgia, "Goudy Bookletter 1911", Times, serif',
fontSize: '10px'
});
this.messageField = this.add.text(
this.game.renderer.width / 2,
this.game.renderer.height / 2 + 48,
this.message,
{
fontFamily: 'Georgia, "Goudy Bookletter 1911", Times, serif',
fontSize: "10px",
}
);
this.messageField.setOrigin(0.5, 0.5);
this.cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, 'cat', 6);
this.cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, "cat", 6);
this.cat.flipY = true;
}
@ -69,38 +75,38 @@ export class ErrorScene extends Phaser.Scene {
public static showError(error: any, scene: ScenePlugin): void {
console.error(error);
if (typeof error === 'string' || error instanceof String) {
if (typeof error === "string" || error instanceof String) {
scene.start(ErrorSceneName, {
title: 'An error occurred',
subTitle: error
title: "An error occurred",
subTitle: error,
});
} else if (error instanceof WAError) {
scene.start(ErrorSceneName, {
title: error.title,
subTitle: error.subTitle,
message: error.details
message: error.details,
});
} else if (error.response) {
// Axios HTTP error
// client received an error response (5xx, 4xx)
scene.start(ErrorSceneName, {
title: 'HTTP ' + error.response.status + ' - ' + error.response.statusText,
subTitle: 'An error occurred while accessing URL:',
message: error.response.config.url
title: "HTTP " + error.response.status + " - " + error.response.statusText,
subTitle: "An error occurred while accessing URL:",
message: error.response.config.url,
});
} else if (error.request) {
// Axios HTTP error
// client never received a response, or request never left
scene.start(ErrorSceneName, {
title: 'Network error',
subTitle: error.message
title: "Network error",
subTitle: error.message,
});
} else if (error instanceof Error) {
// Error
scene.start(ErrorSceneName, {
title: 'An error occurred',
title: "An error occurred",
subTitle: error.name,
message: error.message
message: error.message,
});
} else {
throw error;
@ -114,7 +120,7 @@ export class ErrorScene extends Phaser.Scene {
scene.start(ErrorSceneName, {
title,
subTitle,
message
message,
});
}
}

View file

@ -1,11 +1,11 @@
import {TextField} from "../Components/TextField";
import { TextField } from "../Components/TextField";
import Image = Phaser.GameObjects.Image;
import Sprite = Phaser.GameObjects.Sprite;
export const ReconnectingSceneName = "ReconnectingScene";
enum ReconnectingTextures {
icon = "icon",
mainFont = "main_font"
mainFont = "main_font",
}
export class ReconnectingScene extends Phaser.Scene {
@ -14,35 +14,40 @@ export class ReconnectingScene extends Phaser.Scene {
constructor() {
super({
key: ReconnectingSceneName
key: ReconnectingSceneName,
});
}
preload() {
this.load.image(ReconnectingTextures.icon, "resources/logos/tcm_full.png");
this.load.image(ReconnectingTextures.icon, "static/images/favicons/favicon-32x32.png");
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
this.load.bitmapFont(ReconnectingTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
this.load.spritesheet(
'cat',
'resources/characters/pipoya/Cat 01-1.png',
{frameWidth: 32, frameHeight: 32}
);
this.load.bitmapFont(ReconnectingTextures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
this.load.spritesheet("cat", "resources/characters/pipoya/Cat 01-1.png", { frameWidth: 32, frameHeight: 32 });
}
create() {
this.logo = new Image(this, this.game.renderer.width - 30, this.game.renderer.height - 20, ReconnectingTextures.icon);
this.logo = new Image(
this,
this.game.renderer.width - 30,
this.game.renderer.height - 20,
ReconnectingTextures.icon
);
this.add.existing(this.logo);
this.reconnectingField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height / 2, "Connection lost. Reconnecting...");
this.reconnectingField = new TextField(
this,
this.game.renderer.width / 2,
this.game.renderer.height / 2,
"Connection lost. Reconnecting..."
);
const cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, 'cat');
const cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, "cat");
this.anims.create({
key: 'right',
frames: this.anims.generateFrameNumbers('cat', { start: 6, end: 8 }),
key: "right",
frames: this.anims.generateFrameNumbers("cat", { start: 6, end: 8 }),
frameRate: 10,
repeat: -1
repeat: -1,
});
cat.play('right');
cat.play("right");
}
}

View file

@ -0,0 +1,119 @@
import { writable } from "svelte/store";
import { playersStore } from "./PlayersStore";
import type { PlayerInterface } from "../Phaser/Game/PlayerInterface";
export const chatVisibilityStore = writable(false);
export const chatInputFocusStore = writable(false);
export const newChatMessageStore = writable<string | null>(null);
export enum ChatMessageTypes {
text = 1,
me,
userIncoming,
userOutcoming,
}
export interface ChatMessage {
type: ChatMessageTypes;
date: Date;
author?: PlayerInterface;
targets?: PlayerInterface[];
text?: string[];
}
function getAuthor(authorId: number): PlayerInterface {
const author = playersStore.getPlayerById(authorId);
if (!author) {
throw "Could not find data for author " + authorId;
}
return author;
}
function createChatMessagesStore() {
const { subscribe, update } = writable<ChatMessage[]>([]);
return {
subscribe,
addIncomingUser(authorId: number) {
update((list) => {
const lastMessage = list[list.length - 1];
if (lastMessage && lastMessage.type === ChatMessageTypes.userIncoming && lastMessage.targets) {
lastMessage.targets.push(getAuthor(authorId));
} else {
list.push({
type: ChatMessageTypes.userIncoming,
targets: [getAuthor(authorId)],
date: new Date(),
});
}
return list;
});
},
addOutcomingUser(authorId: number) {
update((list) => {
const lastMessage = list[list.length - 1];
if (lastMessage && lastMessage.type === ChatMessageTypes.userOutcoming && lastMessage.targets) {
lastMessage.targets.push(getAuthor(authorId));
} else {
list.push({
type: ChatMessageTypes.userOutcoming,
targets: [getAuthor(authorId)],
date: new Date(),
});
}
return list;
});
},
addPersonnalMessage(text: string) {
newChatMessageStore.set(text);
update((list) => {
const lastMessage = list[list.length - 1];
if (lastMessage && lastMessage.type === ChatMessageTypes.me && lastMessage.text) {
lastMessage.text.push(text);
} else {
list.push({
type: ChatMessageTypes.me,
text: [text],
date: new Date(),
});
}
return list;
});
},
addExternalMessage(authorId: number, text: string) {
update((list) => {
const lastMessage = list[list.length - 1];
if (lastMessage && lastMessage.type === ChatMessageTypes.text && lastMessage.text) {
lastMessage.text.push(text);
} else {
list.push({
type: ChatMessageTypes.text,
text: [text],
author: getAuthor(authorId),
date: new Date(),
});
}
return list;
});
chatVisibilityStore.set(true);
},
};
}
export const chatMessagesStore = createChatMessagesStore();
function createChatSubMenuVisibilityStore() {
const { subscribe, update } = writable<string>("");
return {
subscribe,
openSubMenu(playerName: string, index: number) {
const id = playerName + index;
update((oldValue) => {
return oldValue === id ? "" : id;
});
},
};
}
export const chatSubMenuVisbilityStore = createChatSubMenuVisibilityStore();

View file

@ -0,0 +1,69 @@
import { writable } from "svelte/store";
import type { PlayerInterface } from "../Phaser/Game/PlayerInterface";
import type { RoomConnection } from "../Connexion/RoomConnection";
import { getRandomColor } from "../WebRtc/ColorGenerator";
let idCount = 0;
/**
* A store that contains the list of players currently known.
*/
function createPlayersStore() {
let players = new Map<number, PlayerInterface>();
const { subscribe, set, update } = writable(players);
return {
subscribe,
connectToRoomConnection: (roomConnection: RoomConnection) => {
players = new Map<number, PlayerInterface>();
set(players);
roomConnection.onUserJoins((message) => {
update((users) => {
users.set(message.userId, {
userId: message.userId,
name: message.name,
characterLayers: message.characterLayers,
visitCardUrl: message.visitCardUrl,
companion: message.companion,
userUuid: message.userUuid,
color: getRandomColor(),
});
return users;
});
});
roomConnection.onUserLeft((userId) => {
update((users) => {
users.delete(userId);
return users;
});
});
},
getPlayerById(userId: number): PlayerInterface | undefined {
return players.get(userId);
},
addFacticePlayer(name: string): number {
let userId: number | null = null;
players.forEach((p) => {
if (p.name === name) userId = p.userId;
});
if (userId) return userId;
const newUserId = idCount--;
update((users) => {
users.set(newUserId, {
userId: newUserId,
name,
characterLayers: [],
visitCardUrl: null,
companion: null,
userUuid: "dummy",
color: getRandomColor(),
});
return users;
});
return newUserId;
},
};
}
export const playersStore = createPlayersStore();

View file

@ -1,10 +1,11 @@
import {derived} from "svelte/store";
import {consoleGlobalMessageManagerFocusStore} from "./ConsoleGlobalMessageManagerStore";
import { derived } from "svelte/store";
import { consoleGlobalMessageManagerFocusStore } from "./ConsoleGlobalMessageManagerStore";
import { chatInputFocusStore } from "./ChatStore";
//derived from the focus on Menu, ConsoleGlobal, Chat and ...
export const enableUserInputsStore = derived(
consoleGlobalMessageManagerFocusStore,
($consoleGlobalMessageManagerFocusStore) => {
return !$consoleGlobalMessageManagerFocusStore;
[consoleGlobalMessageManagerFocusStore, chatInputFocusStore],
([$consoleGlobalMessageManagerFocusStore, $chatInputFocusStore]) => {
return !$consoleGlobalMessageManagerFocusStore && !$chatInputFocusStore;
}
);
);

View file

@ -1,24 +1,27 @@
import {Subject} from 'rxjs';
import { Subject } from "rxjs";
class BlackListManager {
private list: number[] = [];
public onBlockStream: Subject<number> = new Subject();
public onUnBlockStream: Subject<number> = new Subject();
isBlackListed(userId: number): boolean {
return this.list.find((data) => data === userId) !== undefined;
}
blackList(userId: number): void {
if (this.isBlackListed(userId)) return;
this.list.push(userId);
this.onBlockStream.next(userId);
private list: string[] = [];
public onBlockStream: Subject<string> = new Subject();
public onUnBlockStream: Subject<string> = new Subject();
isBlackListed(userUuid: string): boolean {
return this.list.find((data) => data === userUuid) !== undefined;
}
cancelBlackList(userId: number): void {
this.list.splice(this.list.findIndex(data => data === userId), 1);
this.onUnBlockStream.next(userId);
blackList(userUuid: string): void {
if (this.isBlackListed(userUuid)) return;
this.list.push(userUuid);
this.onBlockStream.next(userUuid);
}
cancelBlackList(userUuid: string): void {
this.list.splice(
this.list.findIndex((data) => data === userUuid),
1
);
this.onUnBlockStream.next(userUuid);
}
}
export const blackListManager = new BlackListManager();
export const blackListManager = new BlackListManager();

View file

@ -0,0 +1,52 @@
export function getRandomColor(): string {
const golden_ratio_conjugate = 0.618033988749895;
let hue = Math.random();
hue += golden_ratio_conjugate;
hue %= 1;
return hsv_to_rgb(hue, 0.5, 0.95);
}
//todo: test this.
function hsv_to_rgb(hue: number, saturation: number, brightness: number): string {
const h_i = Math.floor(hue * 6);
const f = hue * 6 - h_i;
const p = brightness * (1 - saturation);
const q = brightness * (1 - f * saturation);
const t = brightness * (1 - (1 - f) * saturation);
let r: number, g: number, b: number;
switch (h_i) {
case 0:
r = brightness;
g = t;
b = p;
break;
case 1:
r = q;
g = brightness;
b = p;
break;
case 2:
r = p;
g = brightness;
b = t;
break;
case 3:
r = p;
g = q;
b = brightness;
break;
case 4:
r = t;
g = p;
b = brightness;
break;
case 5:
r = brightness;
g = p;
b = q;
break;
default:
throw "h_i cannot be " + h_i;
}
return "#" + Math.floor(r * 256).toString(16) + Math.floor(g * 256).toString(16) + Math.floor(b * 256).toString(16);
}

View file

@ -1,232 +1,13 @@
import { HtmlUtils } from "./HtmlUtils";
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
import { connectionManager } from "../Connexion/ConnectionManager";
import { GameConnexionTypes } from "../Url/UrlManager";
import { iframeListener } from "../Api/IframeListener";
import { showReportScreenStore } from "../Stores/ShowReportScreenStore";
export type SendMessageCallback = (message: string) => void;
import { chatMessagesStore } from "../Stores/ChatStore";
import { playersStore } from "../Stores/PlayersStore";
export class DiscussionManager {
private mainContainer: HTMLDivElement;
private divDiscuss?: HTMLDivElement;
private divParticipants?: HTMLDivElement;
private nbpParticipants?: HTMLParagraphElement;
private divMessages?: HTMLParagraphElement;
private participants: Map<number | string, HTMLDivElement> = new Map<number | string, HTMLDivElement>();
private activeDiscussion: boolean = false;
private sendMessageCallBack: Map<number | string, SendMessageCallback> = new Map<
number | string,
SendMessageCallback
>();
private userInputManager?: UserInputManager;
constructor() {
this.mainContainer = HtmlUtils.getElementByIdOrFail<HTMLDivElement>("main-container");
this.createDiscussPart(""); //todo: why do we always use empty string?
iframeListener.chatStream.subscribe((chatEvent) => {
this.addMessage(chatEvent.author, chatEvent.message, false);
this.showDiscussion();
const userId = playersStore.addFacticePlayer(chatEvent.author);
chatMessagesStore.addExternalMessage(userId, chatEvent.message);
});
this.onSendMessageCallback("iframe_listener", (message) => {
iframeListener.sendUserInputChat(message);
});
}
private createDiscussPart(name: string) {
this.divDiscuss = document.createElement("div");
this.divDiscuss.classList.add("discussion");
const buttonCloseDiscussion: HTMLButtonElement = document.createElement("button");
buttonCloseDiscussion.classList.add("close-btn");
buttonCloseDiscussion.innerHTML = `<img src="resources/logos/close.svg"/>`;
buttonCloseDiscussion.addEventListener("click", () => {
this.hideDiscussion();
});
this.divDiscuss.appendChild(buttonCloseDiscussion);
const myName: HTMLParagraphElement = document.createElement("p");
myName.innerText = name.toUpperCase();
this.nbpParticipants = document.createElement("p");
this.nbpParticipants.innerText = "PARTICIPANTS (1)";
this.divParticipants = document.createElement("div");
this.divParticipants.classList.add("participants");
this.divMessages = document.createElement("div");
this.divMessages.classList.add("messages");
this.divMessages.innerHTML = "<h2>Local messages</h2>";
this.divDiscuss.appendChild(myName);
this.divDiscuss.appendChild(this.nbpParticipants);
this.divDiscuss.appendChild(this.divParticipants);
this.divDiscuss.appendChild(this.divMessages);
const sendDivMessage: HTMLDivElement = document.createElement("div");
sendDivMessage.classList.add("send-message");
const inputMessage: HTMLInputElement = document.createElement("input");
inputMessage.onfocus = () => {
if (this.userInputManager) {
this.userInputManager.disableControls();
}
};
inputMessage.onblur = () => {
if (this.userInputManager) {
this.userInputManager.restoreControls();
}
};
inputMessage.type = "text";
inputMessage.addEventListener("keyup", (event: KeyboardEvent) => {
if (event.key === "Enter") {
event.preventDefault();
if (inputMessage.value === null || inputMessage.value === "" || inputMessage.value === undefined) {
return;
}
this.addMessage(name, inputMessage.value, true);
for (const callback of this.sendMessageCallBack.values()) {
callback(inputMessage.value);
}
inputMessage.value = "";
}
});
sendDivMessage.appendChild(inputMessage);
this.divDiscuss.appendChild(sendDivMessage);
//append in main container
this.mainContainer.appendChild(this.divDiscuss);
this.addParticipant("me", "Moi", undefined, true);
}
public addParticipant(
userId: number | "me",
name: string | undefined,
img?: string | undefined,
isMe: boolean = false
) {
const divParticipant: HTMLDivElement = document.createElement("div");
divParticipant.classList.add("participant");
divParticipant.id = `participant-${userId}`;
const divImgParticipant: HTMLImageElement = document.createElement("img");
divImgParticipant.src = "resources/logos/boy.svg";
if (img !== undefined) {
divImgParticipant.src = img;
}
const divPParticipant: HTMLParagraphElement = document.createElement("p");
if (!name) {
name = "Anonymous";
}
divPParticipant.innerText = name;
divParticipant.appendChild(divImgParticipant);
divParticipant.appendChild(divPParticipant);
if (
!isMe &&
connectionManager.getConnexionType &&
connectionManager.getConnexionType !== GameConnexionTypes.anonymous &&
userId !== "me"
) {
const reportBanUserAction: HTMLButtonElement = document.createElement("button");
reportBanUserAction.classList.add("report-btn");
reportBanUserAction.innerText = "Report";
reportBanUserAction.addEventListener("click", () => {
showReportScreenStore.set({ userId: userId, userName: name ? name : "" });
});
divParticipant.appendChild(reportBanUserAction);
}
this.divParticipants?.appendChild(divParticipant);
this.participants.set(userId, divParticipant);
this.updateParticipant(this.participants.size);
}
public updateParticipant(nb: number) {
if (!this.nbpParticipants) {
return;
}
this.nbpParticipants.innerText = `PARTICIPANTS (${nb})`;
}
public addMessage(name: string, message: string, isMe: boolean = false) {
const divMessage: HTMLDivElement = document.createElement("div");
divMessage.classList.add("message");
if (isMe) {
divMessage.classList.add("me");
}
const pMessage: HTMLParagraphElement = document.createElement("p");
const date = new Date();
if (isMe) {
name = "Me";
} else {
name = HtmlUtils.escapeHtml(name);
}
pMessage.innerHTML = `<span style="font-weight: bold">${name}</span>
<span style="color:#bac2cc;display:inline-block;font-size:12px;">
${date.getHours()}:${date.getMinutes()}
</span>`;
divMessage.appendChild(pMessage);
const userMessage: HTMLParagraphElement = document.createElement("p");
userMessage.innerHTML = HtmlUtils.urlify(message);
userMessage.classList.add("body");
divMessage.appendChild(userMessage);
this.divMessages?.appendChild(divMessage);
//automatic scroll when there are new message
setTimeout(() => {
this.divMessages?.scroll({
top: this.divMessages?.scrollTop + divMessage.getBoundingClientRect().y,
behavior: "smooth",
});
}, 200);
}
public removeParticipant(userId: number | string) {
const element = this.participants.get(userId);
if (element) {
element.remove();
this.participants.delete(userId);
}
//if all participant leave, hide discussion button
this.sendMessageCallBack.delete(userId);
}
public onSendMessageCallback(userId: string | number, callback: SendMessageCallback): void {
this.sendMessageCallBack.set(userId, callback);
}
get activatedDiscussion() {
return this.activeDiscussion;
}
private showDiscussion() {
this.activeDiscussion = true;
this.divDiscuss?.classList.add("active");
}
private hideDiscussion() {
this.activeDiscussion = false;
this.divDiscuss?.classList.remove("active");
}
public setUserInputManager(userInputManager: UserInputManager) {
this.userInputManager = userInputManager;
}
public showDiscussionPart() {
this.showDiscussion();
}
}

View file

@ -2,9 +2,9 @@ export class HtmlUtils {
public static getElementByIdOrFail<T extends HTMLElement>(id: string): T {
const elem = document.getElementById(id);
if (HtmlUtils.isHtmlElement<T>(elem)) {
return elem;
return elem;
}
throw new Error("Cannot find HTML element with id '"+id+"'");
throw new Error("Cannot find HTML element with id '" + id + "'");
}
public static querySelectorOrFail<T extends HTMLElement>(selector: string): T {
@ -12,7 +12,7 @@ export class HtmlUtils {
if (HtmlUtils.isHtmlElement<T>(elem)) {
return elem;
}
throw new Error("Cannot find HTML element with selector '"+selector+"'");
throw new Error("Cannot find HTML element with selector '" + selector + "'");
}
public static removeElementByIdOrFail<T extends HTMLElement>(id: string): T {
@ -21,12 +21,12 @@ export class HtmlUtils {
elem.remove();
return elem;
}
throw new Error("Cannot find HTML element with id '"+id+"'");
throw new Error("Cannot find HTML element with id '" + id + "'");
}
public static escapeHtml(html: string): string {
const text = document.createTextNode(html);
const p = document.createElement('p');
const text = document.createTextNode(html.replace(/(\r\n|\r|\n)/g, "<br/>"));
const p = document.createElement("p");
p.appendChild(text);
return p.innerHTML;
}
@ -35,7 +35,7 @@ export class HtmlUtils {
const urlRegex = /(https?:\/\/[^\s]+)/g;
text = HtmlUtils.escapeHtml(text);
return text.replace(urlRegex, (url: string) => {
const link = document.createElement('a');
const link = document.createElement("a");
link.href = url;
link.target = "_blank";
const text = document.createTextNode(url);

View file

@ -1,16 +1,10 @@
import { DivImportance, layoutManager } from "./LayoutManager";
import { layoutManager } from "./LayoutManager";
import { HtmlUtils } from "./HtmlUtils";
import { discussionManager, SendMessageCallback } from "./DiscussionManager";
import type { UserInputManager } from "../Phaser/UserInput/UserInputManager";
import { localUserStore } from "../Connexion/LocalUserStore";
import type { UserSimplePeerInterface } from "./SimplePeer";
import { SoundMeter } from "../Phaser/Components/SoundMeter";
import { DISABLE_NOTIFICATIONS } from "../Enum/EnvironmentVariable";
import { localStreamStore } from "../Stores/MediaStore";
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
import { helpCameraSettingsVisibleStore } from "../Stores/HelpCameraSettingsStore";
export type UpdatedLocalStreamCallback = (media: MediaStream | null) => void;
export type StartScreenSharingCallback = (media: MediaStream) => void;
export type StopScreenSharingCallback = (media: MediaStream) => void;
@ -21,16 +15,11 @@ export class MediaManager {
startScreenSharingCallBacks: Set<StartScreenSharingCallback> = new Set<StartScreenSharingCallback>();
stopScreenSharingCallBacks: Set<StopScreenSharingCallback> = new Set<StopScreenSharingCallback>();
private focused: boolean = true;
private triggerCloseJistiFrame: Map<String, Function> = new Map<String, Function>();
private userInputManager?: UserInputManager;
constructor() {
//Check of ask notification navigator permission
this.getNotification();
localStreamStore.subscribe((result) => {
if (result.type === "error") {
console.error(result.error);
@ -182,67 +171,35 @@ export class MediaManager {
}
}
public addNewMessage(name: string, message: string, isMe: boolean = false) {
discussionManager.addMessage(name, message, isMe);
//when there are new message, show discussion
if (!discussionManager.activatedDiscussion) {
discussionManager.showDiscussionPart();
}
}
public addSendMessageCallback(userId: string | number, callback: SendMessageCallback) {
discussionManager.onSendMessageCallback(userId, callback);
}
public setUserInputManager(userInputManager: UserInputManager) {
this.userInputManager = userInputManager;
discussionManager.setUserInputManager(userInputManager);
}
public getNotification() {
//Get notification
if (!DISABLE_NOTIFICATIONS && window.Notification && Notification.permission !== "granted") {
if (this.checkNotificationPromise()) {
Notification.requestPermission().catch((err) => {
console.error(`Notification permission error`, err);
});
} else {
Notification.requestPermission();
}
}
public hasNotification(): boolean {
return Notification.permission === "granted";
}
/**
* Return true if the browser supports the modern version of the Notification API (which is Promise based) or false
* if we are on Safari...
*
* See https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API/Using_the_Notifications_API
*/
private checkNotificationPromise(): boolean {
try {
Notification.requestPermission().then();
} catch (e) {
return false;
public requestNotification() {
if (window.Notification && Notification.permission !== "granted") {
return Notification.requestPermission();
} else {
return Promise.reject();
}
return true;
}
public createNotification(userName: string) {
if (this.focused) {
if (document.hasFocus()) {
return;
}
if (window.Notification && Notification.permission === "granted") {
const title = "WorkAdventure";
if (this.hasNotification()) {
const title = `${userName} wants to discuss with you`;
const options = {
body: `Hi! ${userName} wants to discuss with you, don't be afraid!`,
icon: "/resources/logos/logo-WA-min.png",
image: "/resources/logos/logo-WA-min.png",
badge: "/resources/logos/logo-WA-min.png",
};
new Notification(title, options);
//new Notification(`Hi! ${userName} wants to discuss with you, don't be afraid!`);
}
}
}

View file

@ -1,11 +1,10 @@
import type * as SimplePeerNamespace from "simple-peer";
import { mediaManager } from "./MediaManager";
import { STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER } from "../Enum/EnvironmentVariable";
import type { RoomConnection } from "../Connexion/RoomConnection";
import { MESSAGE_TYPE_CONSTRAINT, PeerStatus } from "./VideoPeer";
import type { UserSimplePeerInterface } from "./SimplePeer";
import { Readable, readable, writable, Writable } from "svelte/store";
import { Readable, readable } from "svelte/store";
import { videoFocusStore } from "../Stores/VideoFocusStore";
import { getIceServersConfig } from "../Components/Video/utils";
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
@ -32,21 +31,9 @@ export class ScreenSharingPeer extends Peer {
stream: MediaStream | null
) {
super({
initiator: initiator ? initiator : false,
//reconnectTimer: 10000,
initiator,
config: {
iceServers: [
{
urls: STUN_SERVER.split(","),
},
TURN_SERVER !== ""
? {
urls: TURN_SERVER.split(","),
username: user.webRtcUser || TURN_USER,
credential: user.webRtcPassword || TURN_PASSWORD,
}
: undefined,
].filter((value) => value !== undefined),
iceServers: getIceServersConfig(user),
},
});

View file

@ -11,10 +11,11 @@ import { get } from "svelte/store";
import { localStreamStore, LocalStreamStoreValue, obtainedMediaConstraintStore } from "../Stores/MediaStore";
import { screenSharingLocalStreamStore } from "../Stores/ScreenSharingStore";
import { discussionManager } from "./DiscussionManager";
import { playersStore } from "../Stores/PlayersStore";
import { newChatMessageStore } from "../Stores/ChatStore";
export interface UserSimplePeerInterface {
userId: number;
name?: string;
initiator?: boolean;
webRtcUser?: string | undefined;
webRtcPassword?: string | undefined;
@ -153,32 +154,13 @@ export class SimplePeer {
}
}
let name = user.name;
if (!name) {
name = this.getName(user.userId);
}
discussionManager.removeParticipant(user.userId);
const name = this.getName(user.userId);
this.lastWebrtcUserName = user.webRtcUser;
this.lastWebrtcPassword = user.webRtcPassword;
const peer = new VideoPeer(user, user.initiator ? user.initiator : false, name, this.Connection, localStream);
//permit to send message
mediaManager.addSendMessageCallback(user.userId, (message: string) => {
peer.write(
new Buffer(
JSON.stringify({
type: MESSAGE_TYPE_MESSAGE,
name: this.myName.toUpperCase(),
userId: this.userId,
message: message,
})
)
);
});
peer.toClose = false;
// When a connection is established to a video stream, and if a screen sharing is taking place,
// the user sharing screen should also initiate a connection to the remote user!
@ -191,7 +173,7 @@ export class SimplePeer {
//Create a notification for first user in circle discussion
if (this.PeerConnectionArray.size === 0) {
mediaManager.createNotification(user.name ?? "");
mediaManager.createNotification(name);
}
this.PeerConnectionArray.set(user.userId, peer);
@ -202,12 +184,7 @@ export class SimplePeer {
}
private getName(userId: number): string {
const userSearch = this.Users.find((userSearch: UserSimplePeerInterface) => userSearch.userId === userId);
if (userSearch) {
return userSearch.name || "";
} else {
return "";
}
return playersStore.getPlayerById(userId)?.name || "";
}
/**
@ -372,7 +349,8 @@ export class SimplePeer {
}
private receiveWebrtcScreenSharingSignal(data: WebRtcSignalReceivedMessageInterface) {
if (blackListManager.isBlackListed(data.userId)) return;
const uuid = playersStore.getPlayerById(data.userId)?.userUuid || "";
if (blackListManager.isBlackListed(uuid)) return;
console.log("receiveWebrtcScreenSharingSignal", data);
const streamResult = get(screenSharingLocalStreamStore);
let stream: MediaStream | null = null;
@ -473,7 +451,8 @@ export class SimplePeer {
}
private sendLocalScreenSharingStreamToUser(userId: number, localScreenCapture: MediaStream): void {
if (blackListManager.isBlackListed(userId)) return;
const uuid = playersStore.getPlayerById(userId)?.userUuid || "";
if (blackListManager.isBlackListed(uuid)) return;
// If a connection already exists with user (because it is already sharing a screen with us... let's use this connection)
if (this.PeerScreenSharingConnectionArray.has(userId)) {
this.pushScreenSharingToRemoteUser(userId, localScreenCapture);

View file

@ -1,13 +1,14 @@
import type * as SimplePeerNamespace from "simple-peer";
import { mediaManager } from "./MediaManager";
import { STUN_SERVER, TURN_PASSWORD, TURN_SERVER, TURN_USER } from "../Enum/EnvironmentVariable";
import type { RoomConnection } from "../Connexion/RoomConnection";
import { blackListManager } from "./BlackListManager";
import type { Subscription } from "rxjs";
import type { UserSimplePeerInterface } from "./SimplePeer";
import { get, readable, Readable } from "svelte/store";
import { get, readable, Readable, Unsubscriber } from "svelte/store";
import { obtainedMediaConstraintStore } from "../Stores/MediaStore";
import { discussionManager } from "./DiscussionManager";
import { playersStore } from "../Stores/PlayersStore";
import { chatMessagesStore, chatVisibilityStore, newChatMessageStore } from "../Stores/ChatStore";
import { getIceServersConfig } from "../Components/Video/utils";
const Peer: SimplePeerNamespace.SimplePeer = require("simple-peer");
@ -26,12 +27,15 @@ export class VideoPeer extends Peer {
private remoteStream!: MediaStream;
private blocked: boolean = false;
public readonly userId: number;
public readonly userUuid: string;
public readonly uniqueId: string;
private onBlockSubscribe: Subscription;
private onUnBlockSubscribe: Subscription;
public readonly streamStore: Readable<MediaStream | null>;
public readonly statusStore: Readable<PeerStatus>;
public readonly constraintsStore: Readable<MediaStreamConstraints | null>;
private newMessageunsubscriber: Unsubscriber | null = null;
private closing: Boolean = false; //this is used to prevent destroy() from being called twice
constructor(
public user: UserSimplePeerInterface,
@ -41,25 +45,14 @@ export class VideoPeer extends Peer {
localStream: MediaStream | null
) {
super({
initiator: initiator ? initiator : false,
//reconnectTimer: 10000,
initiator,
config: {
iceServers: [
{
urls: STUN_SERVER.split(","),
},
TURN_SERVER !== ""
? {
urls: TURN_SERVER.split(","),
username: user.webRtcUser || TURN_USER,
credential: user.webRtcPassword || TURN_PASSWORD,
}
: undefined,
].filter((value) => value !== undefined),
iceServers: getIceServersConfig(user),
},
});
this.userId = user.userId;
this.userUuid = playersStore.getPlayerById(this.userId)?.userUuid || "";
this.uniqueId = "video_" + this.userId;
this.streamStore = readable<MediaStream | null>(null, (set) => {
@ -144,6 +137,20 @@ export class VideoPeer extends Peer {
this.on("connect", () => {
this._connected = true;
chatMessagesStore.addIncomingUser(this.userId);
this.newMessageunsubscriber = newChatMessageStore.subscribe((newMessage) => {
if (!newMessage) return;
this.write(
new Buffer(
JSON.stringify({
type: MESSAGE_TYPE_MESSAGE,
message: newMessage,
})
)
); //send more data
newChatMessageStore.set(null); //This is to prevent a newly created SimplePeer to send an old message a 2nd time. Is there a better way?
});
});
this.on("data", (chunk: Buffer) => {
@ -161,8 +168,8 @@ export class VideoPeer extends Peer {
mediaManager.disabledVideoByUserId(this.userId);
}
} else if (message.type === MESSAGE_TYPE_MESSAGE) {
if (!blackListManager.isBlackListed(message.userId)) {
mediaManager.addNewMessage(message.name, message.message);
if (!blackListManager.isBlackListed(this.userUuid)) {
chatMessagesStore.addExternalMessage(this.userId, message.message);
}
} else if (message.type === MESSAGE_TYPE_BLOCKED) {
//FIXME when A blacklists B, the output stream from A is muted in B's js client. This is insecure since B can manipulate the code to unmute A stream.
@ -181,20 +188,20 @@ export class VideoPeer extends Peer {
});
this.pushVideoToRemoteUser(localStream);
this.onBlockSubscribe = blackListManager.onBlockStream.subscribe((userId) => {
if (userId === this.userId) {
this.onBlockSubscribe = blackListManager.onBlockStream.subscribe((userUuid) => {
if (userUuid === this.userUuid) {
this.toggleRemoteStream(false);
this.sendBlockMessage(true);
}
});
this.onUnBlockSubscribe = blackListManager.onUnBlockStream.subscribe((userId) => {
if (userId === this.userId) {
this.onUnBlockSubscribe = blackListManager.onUnBlockStream.subscribe((userUuid) => {
if (userUuid === this.userUuid) {
this.toggleRemoteStream(true);
this.sendBlockMessage(false);
}
});
if (blackListManager.isBlackListed(this.userId)) {
if (blackListManager.isBlackListed(this.userUuid)) {
this.sendBlockMessage(true);
}
}
@ -231,7 +238,7 @@ export class VideoPeer extends Peer {
private stream(stream: MediaStream) {
try {
this.remoteStream = stream;
if (blackListManager.isBlackListed(this.userId) || this.blocked) {
if (blackListManager.isBlackListed(this.userUuid) || this.blocked) {
this.toggleRemoteStream(false);
}
} catch (err) {
@ -242,18 +249,18 @@ export class VideoPeer extends Peer {
/**
* This is triggered twice. Once by the server, and once by a remote client disconnecting
*/
public destroy(error?: Error): void {
public destroy(): void {
try {
this._connected = false;
if (!this.toClose) {
if (!this.toClose || this.closing) {
return;
}
this.closing = true;
this.onBlockSubscribe.unsubscribe();
this.onUnBlockSubscribe.unsubscribe();
discussionManager.removeParticipant(this.userId);
// FIXME: I don't understand why "Closing connection with" message is displayed TWICE before "Nb users in peerConnectionArray"
// I do understand the method closeConnection is called twice, but I don't understand how they manage to run in parallel.
super.destroy(error);
if (this.newMessageunsubscriber) this.newMessageunsubscriber();
chatMessagesStore.addOutcomingUser(this.userId);
super.destroy();
} catch (err) {
console.error("VideoPeer::destroy", err);
}

View file

@ -13,12 +13,26 @@ import nav from "./Api/iframe/nav";
import controls from "./Api/iframe/controls";
import ui from "./Api/iframe/ui";
import sound from "./Api/iframe/sound";
import room from "./Api/iframe/room";
import player from "./Api/iframe/player";
import room, { setMapURL, setRoomId } from "./Api/iframe/room";
import state, { initVariables } from "./Api/iframe/state";
import player, { setPlayerName, setTags, setUuid } from "./Api/iframe/player";
import type { ButtonDescriptor } from "./Api/iframe/Ui/ButtonDescriptor";
import type { Popup } from "./Api/iframe/Ui/Popup";
import type { Sound } from "./Api/iframe/Sound/Sound";
import { answerPromises, sendToWorkadventure } from "./Api/iframe/IframeApiContribution";
import { answerPromises, queryWorkadventure, sendToWorkadventure } from "./Api/iframe/IframeApiContribution";
// Notify WorkAdventure that we are ready to receive data
const initPromise = queryWorkadventure({
type: "getState",
data: undefined,
}).then((state) => {
setPlayerName(state.nickname);
setRoomId(state.roomId);
setMapURL(state.mapUrl);
setTags(state.tags);
setUuid(state.uuid);
initVariables(state.variables as Map<string, unknown>);
});
const wa = {
ui,
@ -28,6 +42,11 @@ const wa = {
sound,
room,
player,
state,
onInit(): Promise<void> {
return initPromise;
},
// All methods below are deprecated and should not be used anymore.
// They are kept here for backward compatibility.
@ -125,7 +144,7 @@ const wa = {
},
/**
* @deprecated Use WA.controls.restorePlayerControls instead
* @deprecated Use WA.ui.openPopup instead
*/
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
console.warn("Method WA.openPopup is deprecated. Please use WA.ui.openPopup instead");
@ -173,9 +192,20 @@ window.addEventListener(
}
const payload = message.data;
console.debug(payload);
//console.debug(payload);
if (isIframeAnswerEvent(payload)) {
if (isIframeErrorAnswerEvent(payload)) {
const queryId = payload.id;
const payloadError = payload.error;
const resolver = answerPromises.get(queryId);
if (resolver === undefined) {
throw new Error("In Iframe API, got an error answer for a question that we have no track of.");
}
resolver.reject(new Error(payloadError));
answerPromises.delete(queryId);
} else if (isIframeAnswerEvent(payload)) {
const queryId = payload.id;
const payloadData = payload.data;
@ -185,17 +215,6 @@ window.addEventListener(
}
resolver.resolve(payloadData);
answerPromises.delete(queryId);
} else if (isIframeErrorAnswerEvent(payload)) {
const queryId = payload.id;
const payloadError = payload.error;
const resolver = answerPromises.get(queryId);
if (resolver === undefined) {
throw new Error("In Iframe API, got an error answer for a question that we have no track of.");
}
resolver.reject(payloadError);
answerPromises.delete(queryId);
} else if (isIframeResponseEventWrapper(payload)) {
const payloadData = payload.data;

View file

@ -1,35 +1,34 @@
import 'phaser';
import "phaser";
import GameConfig = Phaser.Types.Core.GameConfig;
import "../style/index.scss";
import {DEBUG_MODE, isMobile} from "./Enum/EnvironmentVariable";
import {LoginScene} from "./Phaser/Login/LoginScene";
import {ReconnectingScene} from "./Phaser/Reconnecting/ReconnectingScene";
import {SelectCharacterScene} from "./Phaser/Login/SelectCharacterScene";
import {SelectCompanionScene} from "./Phaser/Login/SelectCompanionScene";
import {EnableCameraScene} from "./Phaser/Login/EnableCameraScene";
import {CustomizeScene} from "./Phaser/Login/CustomizeScene";
import WebFontLoaderPlugin from 'phaser3-rex-plugins/plugins/webfontloader-plugin.js';
import OutlinePipelinePlugin from 'phaser3-rex-plugins/plugins/outlinepipeline-plugin.js';
import {EntryScene} from "./Phaser/Login/EntryScene";
import {coWebsiteManager} from "./WebRtc/CoWebsiteManager";
import {MenuScene} from "./Phaser/Menu/MenuScene";
import {localUserStore} from "./Connexion/LocalUserStore";
import {ErrorScene} from "./Phaser/Reconnecting/ErrorScene";
import {iframeListener} from "./Api/IframeListener";
import { SelectCharacterMobileScene } from './Phaser/Login/SelectCharacterMobileScene';
import {HdpiManager} from "./Phaser/Services/HdpiManager";
import {waScaleManager} from "./Phaser/Services/WaScaleManager";
import {Game} from "./Phaser/Game/Game";
import App from './Components/App.svelte';
import {HtmlUtils} from "./WebRtc/HtmlUtils";
import { DEBUG_MODE, isMobile } from "./Enum/EnvironmentVariable";
import { LoginScene } from "./Phaser/Login/LoginScene";
import { ReconnectingScene } from "./Phaser/Reconnecting/ReconnectingScene";
import { SelectCharacterScene } from "./Phaser/Login/SelectCharacterScene";
import { SelectCompanionScene } from "./Phaser/Login/SelectCompanionScene";
import { EnableCameraScene } from "./Phaser/Login/EnableCameraScene";
import { CustomizeScene } from "./Phaser/Login/CustomizeScene";
import WebFontLoaderPlugin from "phaser3-rex-plugins/plugins/webfontloader-plugin.js";
import OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js";
import { EntryScene } from "./Phaser/Login/EntryScene";
import { coWebsiteManager } from "./WebRtc/CoWebsiteManager";
import { MenuScene } from "./Phaser/Menu/MenuScene";
import { localUserStore } from "./Connexion/LocalUserStore";
import { ErrorScene } from "./Phaser/Reconnecting/ErrorScene";
import { iframeListener } from "./Api/IframeListener";
import { SelectCharacterMobileScene } from "./Phaser/Login/SelectCharacterMobileScene";
import { HdpiManager } from "./Phaser/Services/HdpiManager";
import { waScaleManager } from "./Phaser/Services/WaScaleManager";
import { Game } from "./Phaser/Game/Game";
import App from "./Components/App.svelte";
import { HtmlUtils } from "./WebRtc/HtmlUtils";
import WebGLRenderer = Phaser.Renderer.WebGL.WebGLRenderer;
const {width, height} = coWebsiteManager.getGameSize();
const { width, height } = coWebsiteManager.getGameSize();
const valueGameQuality = localUserStore.getGameQualityValue();
const fps : Phaser.Types.Core.FPSConfig = {
const fps: Phaser.Types.Core.FPSConfig = {
/**
* The minimum acceptable rendering rate, in frames per second.
*/
@ -53,30 +52,30 @@ const fps : Phaser.Types.Core.FPSConfig = {
/**
* Apply delta smoothing during the game update to help avoid spikes?
*/
smoothStep: false
}
smoothStep: false,
};
// the ?phaserMode=canvas parameter can be used to force Canvas usage
const params = new URLSearchParams(document.location.search.substring(1));
const phaserMode = params.get("phaserMode");
let mode: number;
switch (phaserMode) {
case 'auto':
case "auto":
case null:
mode = Phaser.AUTO;
break;
case 'canvas':
case "canvas":
mode = Phaser.CANVAS;
break;
case 'webgl':
case "webgl":
mode = Phaser.WEBGL;
break;
default:
throw new Error('phaserMode parameter must be one of "auto", "canvas" or "webgl"');
}
const hdpiManager = new HdpiManager(640*480, 196*196);
const { game: gameSize, real: realSize } = hdpiManager.getOptimalGameSize({width, height});
const hdpiManager = new HdpiManager(640 * 480, 196 * 196);
const { game: gameSize, real: realSize } = hdpiManager.getOptimalGameSize({ width, height });
const config: GameConfig = {
type: mode,
@ -87,9 +86,10 @@ const config: GameConfig = {
height: gameSize.height,
zoom: realSize.width / gameSize.width,
autoRound: true,
resizeInterval: 999999999999
resizeInterval: 999999999999,
},
scene: [EntryScene,
scene: [
EntryScene,
LoginScene,
isMobile() ? SelectCharacterMobileScene : SelectCharacterScene,
SelectCompanionScene,
@ -102,37 +102,39 @@ const config: GameConfig = {
//resolution: window.devicePixelRatio / 2,
fps: fps,
dom: {
createContainer: true
createContainer: true,
},
render: {
pixelArt: true,
roundPixels: true,
antialias: false
antialias: false,
},
plugins: {
global: [{
key: 'rexWebFontLoader',
plugin: WebFontLoaderPlugin,
start: true
}]
global: [
{
key: "rexWebFontLoader",
plugin: WebFontLoaderPlugin,
start: true,
},
],
},
physics: {
default: "arcade",
arcade: {
debug: DEBUG_MODE,
}
},
},
// Instruct systems with 2 GPU to choose the low power one. We don't need that extra power and we want to save battery
powerPreference: "low-power",
callbacks: {
postBoot: game => {
postBoot: (game) => {
// Install rexOutlinePipeline only if the renderer is WebGL.
const renderer = game.renderer;
if (renderer instanceof WebGLRenderer) {
game.plugins.install('rexOutlinePipeline', OutlinePipelinePlugin, true);
game.plugins.install("rexOutlinePipeline", OutlinePipelinePlugin, true);
}
}
}
},
},
};
//const game = new Phaser.Game(config);
@ -140,7 +142,7 @@ const game = new Game(config);
waScaleManager.setGame(game);
window.addEventListener('resize', function (event) {
window.addEventListener("resize", function (event) {
coWebsiteManager.resetStyle();
waScaleManager.applyNewSize();
@ -153,10 +155,23 @@ coWebsiteManager.onResize.subscribe(() => {
iframeListener.init();
const app = new App({
target: HtmlUtils.getElementByIdOrFail('svelte-overlay'),
target: HtmlUtils.getElementByIdOrFail("svelte-overlay"),
props: {
game: game
game: game,
},
})
});
export default app
export default app;
if ("serviceWorker" in navigator) {
window.addEventListener("load", function () {
navigator.serviceWorker
.register("/resources/service-worker.js")
.then((serviceWorker) => {
console.log("Service Worker registered: ", serviceWorker);
})
.catch((error) => {
console.error("Error registering the Service Worker: ", error);
});
});
}