Merge branch 'develop' of github.com:thecodingmachine/workadventure into feature/animated-tiles
# Conflicts: # front/package.json # front/tsconfig.json # front/yarn.lock
|
@ -1,8 +1,8 @@
|
|||
import {HtmlUtils} from "../WebRtc/HtmlUtils";
|
||||
import {UserInputManager} from "../Phaser/UserInput/UserInputManager";
|
||||
import {RoomConnection} from "../Connexion/RoomConnection";
|
||||
import {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels";
|
||||
import {ADMIN_URL} from "../Enum/EnvironmentVariable";
|
||||
import type {UserInputManager} from "../Phaser/UserInput/UserInputManager";
|
||||
import type {RoomConnection} from "../Connexion/RoomConnection";
|
||||
import type {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels";
|
||||
import {AdminMessageEventTypes} from "../Connexion/AdminMessagesService";
|
||||
|
||||
export const CLASS_CONSOLE_MESSAGE = 'main-console';
|
||||
export const INPUT_CONSOLE_MESSAGE = 'input-send-text';
|
||||
|
@ -10,13 +10,16 @@ export const UPLOAD_CONSOLE_MESSAGE = 'input-upload-music';
|
|||
export const INPUT_TYPE_CONSOLE = 'input-type';
|
||||
export const VIDEO_QUALITY_SELECT = 'select-video-quality';
|
||||
|
||||
export const AUDIO_TYPE = 'audio';
|
||||
export const MESSAGE_TYPE = 'message';
|
||||
export const AUDIO_TYPE = AdminMessageEventTypes.audio;
|
||||
export const MESSAGE_TYPE = AdminMessageEventTypes.admin;
|
||||
|
||||
interface EventTargetFiles extends EventTarget {
|
||||
files: Array<File>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export class ConsoleGlobalMessageManager {
|
||||
|
||||
private readonly divMainConsole: HTMLDivElement;
|
||||
|
@ -48,7 +51,7 @@ export class ConsoleGlobalMessageManager {
|
|||
//this.buttonAdminMainConsole = document.createElement('img');
|
||||
this.userInputManager = userInputManager;
|
||||
this.initialise();
|
||||
|
||||
|
||||
}
|
||||
|
||||
initialise() {
|
||||
|
@ -140,7 +143,7 @@ export class ConsoleGlobalMessageManager {
|
|||
const div = document.createElement('div');
|
||||
div.id = INPUT_CONSOLE_MESSAGE
|
||||
const buttonSend = document.createElement('button');
|
||||
buttonSend.innerText = 'Envoyer';
|
||||
buttonSend.innerText = 'Send';
|
||||
buttonSend.classList.add('btn');
|
||||
buttonSend.addEventListener('click', (event: MouseEvent) => {
|
||||
this.sendMessage();
|
||||
|
@ -158,42 +161,46 @@ export class ConsoleGlobalMessageManager {
|
|||
this.divMessageConsole.appendChild(section);
|
||||
|
||||
(async () => {
|
||||
// Start loading CSS
|
||||
const cssPromise = ConsoleGlobalMessageManager.loadCss();
|
||||
// Import quill
|
||||
const Quill:any = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
// Wait for CSS to be loaded
|
||||
await cssPromise;
|
||||
try{
|
||||
// Start loading CSS
|
||||
const cssPromise = ConsoleGlobalMessageManager.loadCss();
|
||||
// Import quill
|
||||
const {default: Quill}:any = await import("quill"); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
// Wait for CSS to be loaded
|
||||
await cssPromise;
|
||||
|
||||
const toolbarOptions = [
|
||||
['bold', 'italic', 'underline', 'strike'], // toggled buttons
|
||||
['blockquote', 'code-block'],
|
||||
const toolbarOptions = [
|
||||
['bold', 'italic', 'underline', 'strike'], // toggled buttons
|
||||
['blockquote', 'code-block'],
|
||||
|
||||
[{'header': 1}, {'header': 2}], // custom button values
|
||||
[{'list': 'ordered'}, {'list': 'bullet'}],
|
||||
[{'script': 'sub'}, {'script': 'super'}], // superscript/subscript
|
||||
[{'indent': '-1'}, {'indent': '+1'}], // outdent/indent
|
||||
[{'direction': 'rtl'}], // text direction
|
||||
[{'header': 1}, {'header': 2}], // custom button values
|
||||
[{'list': 'ordered'}, {'list': 'bullet'}],
|
||||
[{'script': 'sub'}, {'script': 'super'}], // superscript/subscript
|
||||
[{'indent': '-1'}, {'indent': '+1'}], // outdent/indent
|
||||
[{'direction': 'rtl'}], // text direction
|
||||
|
||||
[{'size': ['small', false, 'large', 'huge']}], // custom dropdown
|
||||
[{'header': [1, 2, 3, 4, 5, 6, false]}],
|
||||
[{'size': ['small', false, 'large', 'huge']}], // custom dropdown
|
||||
[{'header': [1, 2, 3, 4, 5, 6, false]}],
|
||||
|
||||
[{'color': []}, {'background': []}], // dropdown with defaults from theme
|
||||
[{'font': []}],
|
||||
[{'align': []}],
|
||||
[{'color': []}, {'background': []}], // dropdown with defaults from theme
|
||||
[{'font': []}],
|
||||
[{'align': []}],
|
||||
|
||||
['clean'],
|
||||
['clean'],
|
||||
|
||||
['link', 'image', 'video']
|
||||
// remove formatting button
|
||||
];
|
||||
['link', 'image', 'video']
|
||||
// remove formatting button
|
||||
];
|
||||
|
||||
new Quill(`#${INPUT_CONSOLE_MESSAGE}`, {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: toolbarOptions
|
||||
},
|
||||
});
|
||||
new Quill(`#${INPUT_CONSOLE_MESSAGE}`, {
|
||||
theme: 'snow',
|
||||
modules: {
|
||||
toolbar: toolbarOptions
|
||||
},
|
||||
});
|
||||
}catch(err){
|
||||
console.error(err);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
|
@ -242,7 +249,7 @@ export class ConsoleGlobalMessageManager {
|
|||
div.appendChild(input);
|
||||
|
||||
const buttonSend = document.createElement('button');
|
||||
buttonSend.innerText = 'Envoyer';
|
||||
buttonSend.innerText = 'Send';
|
||||
buttonSend.classList.add('btn');
|
||||
buttonSend.addEventListener('click', (event: MouseEvent) => {
|
||||
this.sendMessage();
|
||||
|
@ -332,7 +339,7 @@ export class ConsoleGlobalMessageManager {
|
|||
}
|
||||
|
||||
active(){
|
||||
this.userInputManager.clearAllInputKeyboard();
|
||||
this.userInputManager.disableControls();
|
||||
this.divMainConsole.style.top = '0';
|
||||
this.activeConsole = true;
|
||||
}
|
||||
|
@ -372,23 +379,6 @@ export class ConsoleGlobalMessageManager {
|
|||
this.buttonSendMainConsole.classList.remove('active');
|
||||
}
|
||||
|
||||
/*activeSettingConsole(){
|
||||
this.activeSetting = true;
|
||||
if(this.activeMessage){
|
||||
this.disabledSettingConsole();
|
||||
}
|
||||
this.active();
|
||||
this.divSettingConsole.classList.add('active');
|
||||
//this.buttonSettingsMainConsole.classList.add('active');
|
||||
}
|
||||
|
||||
disabledSettingConsole(){
|
||||
this.activeSetting = false;
|
||||
this.disabled();
|
||||
this.divSettingConsole.classList.remove('active');
|
||||
//this.buttonSettingsMainConsole.classList.remove('active');
|
||||
}*/
|
||||
|
||||
private getSectionId(id: string) : string {
|
||||
return `section-${id}`;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import {HtmlUtils} from "./../WebRtc/HtmlUtils";
|
||||
import {AUDIO_TYPE, MESSAGE_TYPE} from "./ConsoleGlobalMessageManager";
|
||||
import {API_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable";
|
||||
import {RoomConnection} from "../Connexion/RoomConnection";
|
||||
import {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels";
|
||||
import {PUSHER_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable";
|
||||
import type {RoomConnection} from "../Connexion/RoomConnection";
|
||||
import type {PlayGlobalMessageInterface} from "../Connexion/ConnexionModels";
|
||||
import {soundPlayingStore} from "../Stores/SoundPlayingStore";
|
||||
import {soundManager} from "../Phaser/Game/SoundManager";
|
||||
|
||||
export class GlobalMessageManager {
|
||||
|
||||
|
@ -43,45 +45,8 @@ export class GlobalMessageManager {
|
|||
}
|
||||
}
|
||||
|
||||
private playAudioMessage(messageId : string, urlMessage: string){
|
||||
//delete previous elements
|
||||
const previousDivAudio = document.getElementsByClassName('audio-playing');
|
||||
for(let i = 0; i < previousDivAudio.length; i++){
|
||||
previousDivAudio[i].remove();
|
||||
}
|
||||
|
||||
//create new element
|
||||
const divAudio : HTMLDivElement = document.createElement('div');
|
||||
divAudio.id = `audio-playing-${messageId}`;
|
||||
divAudio.classList.add('audio-playing');
|
||||
const imgAudio : HTMLImageElement = document.createElement('img');
|
||||
imgAudio.src = '/resources/logos/megaphone.svg';
|
||||
const pAudio : HTMLParagraphElement = document.createElement('p');
|
||||
pAudio.textContent = 'Message audio'
|
||||
divAudio.appendChild(imgAudio);
|
||||
divAudio.appendChild(pAudio);
|
||||
|
||||
const mainSectionDiv = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('main-container');
|
||||
mainSectionDiv.appendChild(divAudio);
|
||||
|
||||
const messageAudio : HTMLAudioElement = document.createElement('audio');
|
||||
messageAudio.id = this.getHtmlMessageId(messageId);
|
||||
messageAudio.autoplay = true;
|
||||
messageAudio.style.display = 'none';
|
||||
messageAudio.onended = () => {
|
||||
divAudio.classList.remove('active');
|
||||
messageAudio.remove();
|
||||
setTimeout(() => {
|
||||
divAudio.remove();
|
||||
}, 1000);
|
||||
}
|
||||
messageAudio.onplay = () => {
|
||||
divAudio.classList.add('active');
|
||||
}
|
||||
const messageAudioSource : HTMLSourceElement = document.createElement('source');
|
||||
messageAudioSource.src = `${UPLOADER_URL}${urlMessage}`;
|
||||
messageAudio.appendChild(messageAudioSource);
|
||||
mainSectionDiv.appendChild(messageAudio);
|
||||
private playAudioMessage(messageId : string, urlMessage: string) {
|
||||
soundPlayingStore.playSound(UPLOADER_URL + urlMessage);
|
||||
}
|
||||
|
||||
private playTextMessage(messageId : string, htmlMessage: string){
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {TypeMessageInterface} from "./UserMessageManager";
|
||||
import type {TypeMessageInterface} from "./UserMessageManager";
|
||||
import {HtmlUtils} from "../WebRtc/HtmlUtils";
|
||||
|
||||
let modalTimeOut : NodeJS.Timeout;
|
||||
|
@ -44,7 +44,13 @@ export class TypeMessageExt implements TypeMessageInterface{
|
|||
mainSectionDiv.appendChild(div);
|
||||
|
||||
const reportMessageAudio = HtmlUtils.getElementByIdOrFail<HTMLAudioElement>('report-message');
|
||||
reportMessageAudio.play();
|
||||
// FIXME: this will fail on iOS
|
||||
// We should move the sound playing into the GameScene and listen to the event of a report using a store
|
||||
try {
|
||||
reportMessageAudio.play();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
this.nbSecond = this.maxNbSecond;
|
||||
setTimeout((c) => {
|
||||
|
@ -77,11 +83,13 @@ export class TypeMessageExt implements TypeMessageInterface{
|
|||
}
|
||||
}
|
||||
}
|
||||
export class Ban extends TypeMessageExt {
|
||||
}
|
||||
|
||||
export class Message extends TypeMessageExt {}
|
||||
|
||||
export class Ban extends TypeMessageExt {}
|
||||
|
||||
export class Banned extends TypeMessageExt {
|
||||
showMessage(message: string){
|
||||
super.showMessage(message, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,29 @@
|
|||
import {RoomConnection} from "../Connexion/RoomConnection";
|
||||
import * as TypeMessages from "./TypeMessage";
|
||||
import {Banned} from "./TypeMessage";
|
||||
import {adminMessagesService} from "../Connexion/AdminMessagesService";
|
||||
|
||||
export interface TypeMessageInterface {
|
||||
showMessage(message: string): void;
|
||||
}
|
||||
|
||||
export class UserMessageManager {
|
||||
class UserMessageManager {
|
||||
|
||||
typeMessages: Map<string, TypeMessageInterface> = new Map<string, TypeMessageInterface>();
|
||||
receiveBannedMessageListener!: Function;
|
||||
|
||||
constructor(private Connection: RoomConnection) {
|
||||
constructor() {
|
||||
const valueTypeMessageTab = Object.values(TypeMessages);
|
||||
Object.keys(TypeMessages).forEach((value: string, index: number) => {
|
||||
const typeMessageInstance: TypeMessageInterface = (new valueTypeMessageTab[index]() as TypeMessageInterface);
|
||||
this.typeMessages.set(value.toLowerCase(), typeMessageInstance);
|
||||
});
|
||||
this.initialise();
|
||||
}
|
||||
|
||||
initialise() {
|
||||
//receive signal to show message
|
||||
this.Connection.receiveUserMessage((type: string, message: string) => {
|
||||
this.showMessage(type, message);
|
||||
});
|
||||
adminMessagesService.messageStream.subscribe((event) => {
|
||||
const typeMessage = this.showMessage(event.type, event.text);
|
||||
if(typeMessage instanceof Banned) {
|
||||
this.receiveBannedMessageListener();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
showMessage(type: string, message: string) {
|
||||
|
@ -32,5 +33,11 @@ export class UserMessageManager {
|
|||
return;
|
||||
}
|
||||
classTypeMessage.showMessage(message);
|
||||
return classTypeMessage;
|
||||
}
|
||||
}
|
||||
|
||||
setReceiveBanListener(callback: Function){
|
||||
this.receiveBannedMessageListener = callback;
|
||||
}
|
||||
}
|
||||
export const userMessageManager = new UserMessageManager()
|
11
front/src/Api/Events/ButtonClickedEvent.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isButtonClickedEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
popupId: tg.isNumber,
|
||||
buttonId: tg.isNumber,
|
||||
}).get();
|
||||
/**
|
||||
* A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property.
|
||||
*/
|
||||
export type ButtonClickedEvent = tg.GuardedType<typeof isButtonClickedEvent>;
|
11
front/src/Api/Events/ChatEvent.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isChatEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
message: tg.isString,
|
||||
author: tg.isString,
|
||||
}).get();
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type ChatEvent = tg.GuardedType<typeof isChatEvent>;
|
11
front/src/Api/Events/ClosePopupEvent.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isClosePopupEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
popupId: tg.isNumber,
|
||||
}).get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type ClosePopupEvent = tg.GuardedType<typeof isClosePopupEvent>;
|
10
front/src/Api/Events/EnterLeaveEvent.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isEnterLeaveEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
name: tg.isString,
|
||||
}).get();
|
||||
/**
|
||||
* A message sent from the game to the iFrame when a user enters or leaves a zone marked with the "zone" property.
|
||||
*/
|
||||
export type EnterLeaveEvent = tg.GuardedType<typeof isEnterLeaveEvent>;
|
13
front/src/Api/Events/GoToPageEvent.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
|
||||
|
||||
export const isGoToPageEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
url: tg.isString,
|
||||
}).get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type GoToPageEvent = tg.GuardedType<typeof isGoToPageEvent>;
|
62
front/src/Api/Events/IframeEvent.ts
Normal file
|
@ -0,0 +1,62 @@
|
|||
|
||||
|
||||
import type { ButtonClickedEvent } from './ButtonClickedEvent';
|
||||
import type { ChatEvent } from './ChatEvent';
|
||||
import type { ClosePopupEvent } from './ClosePopupEvent';
|
||||
import type { EnterLeaveEvent } from './EnterLeaveEvent';
|
||||
import type { GoToPageEvent } from './GoToPageEvent';
|
||||
import type { LoadPageEvent } from './LoadPageEvent';
|
||||
import type { OpenCoWebSiteEvent } from './OpenCoWebSiteEvent';
|
||||
import type { OpenPopupEvent } from './OpenPopupEvent';
|
||||
import type { OpenTabEvent } from './OpenTabEvent';
|
||||
import type { UserInputChatEvent } from './UserInputChatEvent';
|
||||
import type { LoadSoundEvent} from "./LoadSoundEvent";
|
||||
import type {PlaySoundEvent} from "./PlaySoundEvent";
|
||||
|
||||
|
||||
export interface TypedMessageEvent<T> extends MessageEvent {
|
||||
data: T
|
||||
}
|
||||
|
||||
export type IframeEventMap = {
|
||||
//getState: GameStateEvent,
|
||||
// updateTile: UpdateTileEvent
|
||||
loadPage: LoadPageEvent
|
||||
chat: ChatEvent,
|
||||
openPopup: OpenPopupEvent
|
||||
closePopup: ClosePopupEvent
|
||||
openTab: OpenTabEvent
|
||||
goToPage: GoToPageEvent
|
||||
openCoWebSite: OpenCoWebSiteEvent
|
||||
closeCoWebSite: null
|
||||
disablePlayerControls: null
|
||||
restorePlayerControls: null
|
||||
displayBubble: null
|
||||
removeBubble: null
|
||||
loadSound: LoadSoundEvent
|
||||
playSound: PlaySoundEvent
|
||||
stopSound: null,
|
||||
}
|
||||
export interface IframeEvent<T extends keyof IframeEventMap> {
|
||||
type: T;
|
||||
data: IframeEventMap[T];
|
||||
}
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const isIframeEventWrapper = (event: any): event is IframeEvent<keyof IframeEventMap> => typeof event.type === 'string';
|
||||
|
||||
export interface IframeResponseEventMap {
|
||||
userInputChat: UserInputChatEvent
|
||||
enterEvent: EnterLeaveEvent
|
||||
leaveEvent: EnterLeaveEvent
|
||||
buttonClickedEvent: ButtonClickedEvent
|
||||
// gameState: GameStateEvent
|
||||
}
|
||||
export interface IframeResponseEvent<T extends keyof IframeResponseEventMap> {
|
||||
type: T;
|
||||
data: IframeResponseEventMap[T];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const isIframeResponseEventWrapper = (event: { type?: string }): event is IframeResponseEvent<keyof IframeResponseEventMap> => typeof event.type === 'string';
|
13
front/src/Api/Events/LoadPageEvent.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
|
||||
|
||||
export const isLoadPageEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
url: tg.isString,
|
||||
}).get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type LoadPageEvent = tg.GuardedType<typeof isLoadPageEvent>;
|
11
front/src/Api/Events/LoadSoundEvent.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isLoadSoundEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
url: tg.isString,
|
||||
}).get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type LoadSoundEvent = tg.GuardedType<typeof isLoadSoundEvent>;
|
13
front/src/Api/Events/OpenCoWebSiteEvent.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
|
||||
|
||||
export const isOpenCoWebsite =
|
||||
new tg.IsInterface().withProperties({
|
||||
url: tg.isString,
|
||||
}).get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type OpenCoWebSiteEvent = tg.GuardedType<typeof isOpenCoWebsite>;
|
20
front/src/Api/Events/OpenPopupEvent.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
const isButtonDescriptor =
|
||||
new tg.IsInterface().withProperties({
|
||||
label: tg.isString,
|
||||
className: tg.isOptional(tg.isString)
|
||||
}).get();
|
||||
|
||||
export const isOpenPopupEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
popupId: tg.isNumber,
|
||||
targetObject: tg.isString,
|
||||
message: tg.isString,
|
||||
buttons: tg.isArray(isButtonDescriptor)
|
||||
}).get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type OpenPopupEvent = tg.GuardedType<typeof isOpenPopupEvent>;
|
13
front/src/Api/Events/OpenTabEvent.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
|
||||
|
||||
export const isOpenTabEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
url: tg.isString,
|
||||
}).get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type OpenTabEvent = tg.GuardedType<typeof isOpenTabEvent>;
|
24
front/src/Api/Events/PlaySoundEvent.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
|
||||
const isSoundConfig =
|
||||
new tg.IsInterface().withProperties({
|
||||
volume: tg.isOptional(tg.isNumber),
|
||||
loop: tg.isOptional(tg.isBoolean),
|
||||
mute: tg.isOptional(tg.isBoolean),
|
||||
rate: tg.isOptional(tg.isNumber),
|
||||
detune: tg.isOptional(tg.isNumber),
|
||||
seek: tg.isOptional(tg.isNumber),
|
||||
delay: tg.isOptional(tg.isNumber)
|
||||
}).get();
|
||||
|
||||
export const isPlaySoundEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
url: tg.isString,
|
||||
config : tg.isOptional(isSoundConfig),
|
||||
}).get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type PlaySoundEvent = tg.GuardedType<typeof isPlaySoundEvent>;
|
11
front/src/Api/Events/StopSoundEvent.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isStopSoundEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
url: tg.isString,
|
||||
}).get();
|
||||
|
||||
/**
|
||||
* A message sent from the iFrame to the game to add a message in the chat.
|
||||
*/
|
||||
export type StopSoundEvent = tg.GuardedType<typeof isStopSoundEvent>;
|
10
front/src/Api/Events/UserInputChatEvent.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import * as tg from "generic-type-guard";
|
||||
|
||||
export const isUserInputChatEvent =
|
||||
new tg.IsInterface().withProperties({
|
||||
message: tg.isString,
|
||||
}).get();
|
||||
/**
|
||||
* A message sent from the game to the iFrame when a user types a message in the chat.
|
||||
*/
|
||||
export type UserInputChatEvent = tg.GuardedType<typeof isUserInputChatEvent>;
|
274
front/src/Api/IframeListener.ts
Normal file
|
@ -0,0 +1,274 @@
|
|||
|
||||
import { Subject } from "rxjs";
|
||||
import { ChatEvent, isChatEvent } from "./Events/ChatEvent";
|
||||
import { HtmlUtils } from "../WebRtc/HtmlUtils";
|
||||
import type { EnterLeaveEvent } from "./Events/EnterLeaveEvent";
|
||||
import { isOpenPopupEvent, OpenPopupEvent } from "./Events/OpenPopupEvent";
|
||||
import { isOpenTabEvent, OpenTabEvent } from "./Events/OpenTabEvent";
|
||||
import type { ButtonClickedEvent } from "./Events/ButtonClickedEvent";
|
||||
import { ClosePopupEvent, isClosePopupEvent } from "./Events/ClosePopupEvent";
|
||||
import { scriptUtils } from "./ScriptUtils";
|
||||
import { GoToPageEvent, isGoToPageEvent } from "./Events/GoToPageEvent";
|
||||
import { isOpenCoWebsite, OpenCoWebSiteEvent } from "./Events/OpenCoWebSiteEvent";
|
||||
import { IframeEventMap, IframeEvent, IframeResponseEvent, IframeResponseEventMap, isIframeEventWrapper, TypedMessageEvent } from "./Events/IframeEvent";
|
||||
import type { UserInputChatEvent } from "./Events/UserInputChatEvent";
|
||||
import { isLoadPageEvent } from './Events/LoadPageEvent';
|
||||
import {isPlaySoundEvent, PlaySoundEvent} from "./Events/PlaySoundEvent";
|
||||
import {isStopSoundEvent, StopSoundEvent} from "./Events/StopSoundEvent";
|
||||
import {isLoadSoundEvent, LoadSoundEvent} from "./Events/LoadSoundEvent";
|
||||
/**
|
||||
* Listens to messages from iframes and turn those messages into easy to use observables.
|
||||
* Also allows to send messages to those iframes.
|
||||
*/
|
||||
class IframeListener {
|
||||
private readonly _chatStream: Subject<ChatEvent> = new Subject();
|
||||
public readonly chatStream = this._chatStream.asObservable();
|
||||
|
||||
private readonly _openPopupStream: Subject<OpenPopupEvent> = new Subject();
|
||||
public readonly openPopupStream = this._openPopupStream.asObservable();
|
||||
|
||||
private readonly _openTabStream: Subject<OpenTabEvent> = new Subject();
|
||||
public readonly openTabStream = this._openTabStream.asObservable();
|
||||
|
||||
private readonly _goToPageStream: Subject<GoToPageEvent> = new Subject();
|
||||
public readonly goToPageStream = this._goToPageStream.asObservable();
|
||||
|
||||
|
||||
private readonly _loadPageStream: Subject<string> = new Subject();
|
||||
public readonly loadPageStream = this._loadPageStream.asObservable();
|
||||
|
||||
private readonly _openCoWebSiteStream: Subject<OpenCoWebSiteEvent> = new Subject();
|
||||
public readonly openCoWebSiteStream = this._openCoWebSiteStream.asObservable();
|
||||
|
||||
private readonly _closeCoWebSiteStream: Subject<void> = new Subject();
|
||||
public readonly closeCoWebSiteStream = this._closeCoWebSiteStream.asObservable();
|
||||
|
||||
private readonly _disablePlayerControlStream: Subject<void> = new Subject();
|
||||
public readonly disablePlayerControlStream = this._disablePlayerControlStream.asObservable();
|
||||
|
||||
private readonly _enablePlayerControlStream: Subject<void> = new Subject();
|
||||
public readonly enablePlayerControlStream = this._enablePlayerControlStream.asObservable();
|
||||
|
||||
private readonly _closePopupStream: Subject<ClosePopupEvent> = new Subject();
|
||||
public readonly closePopupStream = this._closePopupStream.asObservable();
|
||||
|
||||
private readonly _displayBubbleStream: Subject<void> = new Subject();
|
||||
public readonly displayBubbleStream = this._displayBubbleStream.asObservable();
|
||||
|
||||
private readonly _removeBubbleStream: Subject<void> = new Subject();
|
||||
public readonly removeBubbleStream = this._removeBubbleStream.asObservable();
|
||||
|
||||
private readonly _playSoundStream: Subject<PlaySoundEvent> = new Subject();
|
||||
public readonly playSoundStream = this._playSoundStream.asObservable();
|
||||
|
||||
private readonly _stopSoundStream: Subject<StopSoundEvent> = new Subject();
|
||||
public readonly stopSoundStream = this._stopSoundStream.asObservable();
|
||||
|
||||
private readonly _loadSoundStream: Subject<LoadSoundEvent> = new Subject();
|
||||
public readonly loadSoundStream = this._loadSoundStream.asObservable();
|
||||
|
||||
private readonly iframes = new Set<HTMLIFrameElement>();
|
||||
private readonly scripts = new Map<string, HTMLIFrameElement>();
|
||||
|
||||
init() {
|
||||
window.addEventListener("message", (message: TypedMessageEvent<IframeEvent<keyof IframeEventMap>>) => {
|
||||
// Do we trust the sender of this message?
|
||||
// Let's only accept messages from the iframe that are allowed.
|
||||
// Note: maybe we could restrict on the domain too for additional security (in case the iframe goes to another domain).
|
||||
let foundSrc: string | undefined;
|
||||
|
||||
foundSrc = [...this.scripts.keys()].find(key => {
|
||||
return this.scripts.get(key)?.contentWindow == message.source
|
||||
});
|
||||
|
||||
if (foundSrc === undefined) {
|
||||
for (const iframe of this.iframes) {
|
||||
if (iframe.contentWindow === message.source) {
|
||||
foundSrc = iframe.src;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundSrc === undefined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const payload = message.data;
|
||||
if (isIframeEventWrapper(payload)) {
|
||||
if (payload.type === 'chat' && isChatEvent(payload.data)) {
|
||||
this._chatStream.next(payload.data);
|
||||
} else if (payload.type === 'openPopup' && isOpenPopupEvent(payload.data)) {
|
||||
this._openPopupStream.next(payload.data);
|
||||
} else if (payload.type === 'closePopup' && isClosePopupEvent(payload.data)) {
|
||||
this._closePopupStream.next(payload.data);
|
||||
}
|
||||
else if (payload.type === 'openTab' && isOpenTabEvent(payload.data)) {
|
||||
scriptUtils.openTab(payload.data.url);
|
||||
}
|
||||
else if (payload.type === 'goToPage' && isGoToPageEvent(payload.data)) {
|
||||
scriptUtils.goToPage(payload.data.url);
|
||||
}
|
||||
else if (payload.type === 'playSound' && isPlaySoundEvent(payload.data)) {
|
||||
this._playSoundStream.next(payload.data);
|
||||
}
|
||||
else if (payload.type === 'stopSound' && isStopSoundEvent(payload.data)) {
|
||||
this._stopSoundStream.next(payload.data);
|
||||
}
|
||||
else if (payload.type === 'loadSound' && isLoadSoundEvent(payload.data)) {
|
||||
this._loadSoundStream.next(payload.data);
|
||||
}
|
||||
else if (payload.type === 'openCoWebSite' && isOpenCoWebsite(payload.data)) {
|
||||
scriptUtils.openCoWebsite(payload.data.url, foundSrc);
|
||||
}
|
||||
|
||||
else if (payload.type === 'closeCoWebSite') {
|
||||
scriptUtils.closeCoWebSite();
|
||||
}
|
||||
|
||||
else if (payload.type === 'disablePlayerControls') {
|
||||
this._disablePlayerControlStream.next();
|
||||
}
|
||||
else if (payload.type === 'restorePlayerControls') {
|
||||
this._enablePlayerControlStream.next();
|
||||
}
|
||||
else if (payload.type === 'displayBubble') {
|
||||
this._displayBubbleStream.next();
|
||||
}
|
||||
else if (payload.type === 'removeBubble') {
|
||||
this._removeBubbleStream.next();
|
||||
}else if (payload.type === 'loadPage' && isLoadPageEvent(payload.data)){
|
||||
this._loadPageStream.next(payload.data.url);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}, false);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows the passed iFrame to send/receive messages via the API.
|
||||
*/
|
||||
registerIframe(iframe: HTMLIFrameElement): void {
|
||||
this.iframes.add(iframe);
|
||||
}
|
||||
|
||||
unregisterIframe(iframe: HTMLIFrameElement): void {
|
||||
this.iframes.delete(iframe);
|
||||
}
|
||||
|
||||
registerScript(scriptUrl: string): void {
|
||||
console.log('Loading map related script at ', scriptUrl)
|
||||
|
||||
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'development') {
|
||||
// Using external iframe mode (
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.id = this.getIFrameId(scriptUrl);
|
||||
iframe.style.display = 'none';
|
||||
iframe.src = '/iframe.html?script=' + encodeURIComponent(scriptUrl);
|
||||
|
||||
// We are putting a sandbox on this script because it will run in the same domain as the main website.
|
||||
iframe.sandbox.add('allow-scripts');
|
||||
iframe.sandbox.add('allow-top-navigation-by-user-activation');
|
||||
|
||||
document.body.prepend(iframe);
|
||||
|
||||
this.scripts.set(scriptUrl, iframe);
|
||||
this.registerIframe(iframe);
|
||||
} else {
|
||||
// production code
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.id = this.getIFrameId(scriptUrl);
|
||||
iframe.style.display = 'none';
|
||||
|
||||
// We are putting a sandbox on this script because it will run in the same domain as the main website.
|
||||
iframe.sandbox.add('allow-scripts');
|
||||
iframe.sandbox.add('allow-top-navigation-by-user-activation');
|
||||
|
||||
const html = '<!doctype html>\n' +
|
||||
'\n' +
|
||||
'<html lang="en">\n' +
|
||||
'<head>\n' +
|
||||
'<script src="' + window.location.protocol + '//' + window.location.host + '/iframe_api.js" ></script>\n' +
|
||||
'<script src="' + scriptUrl + '" ></script>\n' +
|
||||
'</head>\n' +
|
||||
'</html>\n';
|
||||
|
||||
//iframe.src = "data:text/html;charset=utf-8," + escape(html);
|
||||
iframe.srcdoc = html;
|
||||
|
||||
document.body.prepend(iframe);
|
||||
|
||||
this.scripts.set(scriptUrl, iframe);
|
||||
this.registerIframe(iframe);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private getIFrameId(scriptUrl: string): string {
|
||||
return 'script' + btoa(scriptUrl);
|
||||
}
|
||||
|
||||
unregisterScript(scriptUrl: string): void {
|
||||
const iFrameId = this.getIFrameId(scriptUrl);
|
||||
const iframe = HtmlUtils.getElementByIdOrFail<HTMLIFrameElement>(iFrameId);
|
||||
if (!iframe) {
|
||||
throw new Error('Unknown iframe for script "' + scriptUrl + '"');
|
||||
}
|
||||
this.unregisterIframe(iframe);
|
||||
iframe.remove();
|
||||
|
||||
this.scripts.delete(scriptUrl);
|
||||
}
|
||||
|
||||
sendUserInputChat(message: string) {
|
||||
this.postMessage({
|
||||
'type': 'userInputChat',
|
||||
'data': {
|
||||
'message': message,
|
||||
} as UserInputChatEvent
|
||||
});
|
||||
}
|
||||
|
||||
sendEnterEvent(name: string) {
|
||||
this.postMessage({
|
||||
'type': 'enterEvent',
|
||||
'data': {
|
||||
"name": name
|
||||
} as EnterLeaveEvent
|
||||
});
|
||||
}
|
||||
|
||||
sendLeaveEvent(name: string) {
|
||||
this.postMessage({
|
||||
'type': 'leaveEvent',
|
||||
'data': {
|
||||
"name": name
|
||||
} as EnterLeaveEvent
|
||||
});
|
||||
}
|
||||
|
||||
sendButtonClickedEvent(popupId: number, buttonId: number): void {
|
||||
this.postMessage({
|
||||
'type': 'buttonClickedEvent',
|
||||
'data': {
|
||||
popupId,
|
||||
buttonId
|
||||
} as ButtonClickedEvent
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the message... to all allowed iframes.
|
||||
*/
|
||||
private postMessage(message: IframeResponseEvent<keyof IframeResponseEventMap>) {
|
||||
for (const iframe of this.iframes) {
|
||||
iframe.contentWindow?.postMessage(message, '*');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const iframeListener = new IframeListener();
|
23
front/src/Api/ScriptUtils.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import {coWebsiteManager} from "../WebRtc/CoWebsiteManager";
|
||||
|
||||
class ScriptUtils {
|
||||
|
||||
public openTab(url : string){
|
||||
window.open(url);
|
||||
}
|
||||
|
||||
public goToPage(url : string){
|
||||
window.location.href = url;
|
||||
|
||||
}
|
||||
|
||||
public openCoWebsite(url: string, base: string) {
|
||||
coWebsiteManager.loadCoWebsite(url, base);
|
||||
}
|
||||
|
||||
public closeCoWebSite(){
|
||||
coWebsiteManager.closeCoWebsite();
|
||||
}
|
||||
}
|
||||
|
||||
export const scriptUtils = new ScriptUtils();
|
31
front/src/Api/iframe/IframeApiContribution.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import type * as tg from "generic-type-guard";
|
||||
import type { IframeEvent, IframeEventMap, IframeResponseEventMap } from '../Events/IframeEvent';
|
||||
|
||||
export function sendToWorkadventure(content: IframeEvent<keyof IframeEventMap>) {
|
||||
window.parent.postMessage(content, "*")
|
||||
}
|
||||
type GuardedType<Guard extends tg.TypeGuard<unknown>> = Guard extends tg.TypeGuard<infer T> ? T : never
|
||||
|
||||
export interface IframeCallback<Key extends keyof IframeResponseEventMap, T = IframeResponseEventMap[Key], Guard = tg.TypeGuard<T>> {
|
||||
|
||||
typeChecker: Guard,
|
||||
callback: (payloadData: T) => void
|
||||
}
|
||||
|
||||
export interface IframeCallbackContribution<Key extends keyof IframeResponseEventMap> extends IframeCallback<Key> {
|
||||
|
||||
type: Key
|
||||
}
|
||||
|
||||
/**
|
||||
* !! be aware that the implemented attributes (addMethodsAtRoot and subObjectIdentifier) must be readonly
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
export abstract class IframeApiContribution<T extends {
|
||||
callbacks: Array<IframeCallbackContribution<keyof IframeResponseEventMap>>,
|
||||
}> {
|
||||
|
||||
abstract callbacks: T["callbacks"]
|
||||
}
|
39
front/src/Api/iframe/Sound/Sound.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import {sendToWorkadventure} from "../IframeApiContribution";
|
||||
import type {LoadSoundEvent} from "../../Events/LoadSoundEvent";
|
||||
import type {PlaySoundEvent} from "../../Events/PlaySoundEvent";
|
||||
import type {StopSoundEvent} from "../../Events/StopSoundEvent";
|
||||
import SoundConfig = Phaser.Types.Sound.SoundConfig;
|
||||
|
||||
export class Sound {
|
||||
constructor(private url: string) {
|
||||
sendToWorkadventure({
|
||||
"type": 'loadSound',
|
||||
"data": {
|
||||
url: this.url,
|
||||
} as LoadSoundEvent
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
public play(config: SoundConfig) {
|
||||
sendToWorkadventure({
|
||||
"type": 'playSound',
|
||||
"data": {
|
||||
url: this.url,
|
||||
config
|
||||
} as PlaySoundEvent
|
||||
|
||||
});
|
||||
return this.url;
|
||||
}
|
||||
public stop() {
|
||||
sendToWorkadventure({
|
||||
"type": 'stopSound',
|
||||
"data": {
|
||||
url: this.url,
|
||||
} as StopSoundEvent
|
||||
|
||||
});
|
||||
return this.url;
|
||||
}
|
||||
}
|
18
front/src/Api/iframe/Ui/ButtonDescriptor.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import type {Popup} from "./Popup";
|
||||
|
||||
export type ButtonClickedCallback = (popup: Popup) => void;
|
||||
|
||||
export interface ButtonDescriptor {
|
||||
/**
|
||||
* The label of the button
|
||||
*/
|
||||
label: string,
|
||||
/**
|
||||
* The type of the button. Can be one of "normal", "primary", "success", "warning", "error", "disabled"
|
||||
*/
|
||||
className?: "normal" | "primary" | "success" | "warning" | "error" | "disabled",
|
||||
/**
|
||||
* Callback called if the button is pressed
|
||||
*/
|
||||
callback: ButtonClickedCallback,
|
||||
}
|
19
front/src/Api/iframe/Ui/Popup.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import {sendToWorkadventure} from "../IframeApiContribution";
|
||||
import type {ClosePopupEvent} from "../../Events/ClosePopupEvent";
|
||||
|
||||
export class Popup {
|
||||
constructor(private id: number) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the popup
|
||||
*/
|
||||
public close(): void {
|
||||
sendToWorkadventure({
|
||||
'type': 'closePopup',
|
||||
'data': {
|
||||
'popupId': this.id,
|
||||
} as ClosePopupEvent
|
||||
});
|
||||
}
|
||||
}
|
38
front/src/Api/iframe/chat.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import type { ChatEvent } from '../Events/ChatEvent'
|
||||
import { isUserInputChatEvent, UserInputChatEvent } from '../Events/UserInputChatEvent'
|
||||
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution'
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
import {Subject} from "rxjs";
|
||||
|
||||
const chatStream = new Subject<string>();
|
||||
|
||||
class WorkadventureChatCommands extends IframeApiContribution<WorkadventureChatCommands> {
|
||||
|
||||
callbacks = [apiCallback({
|
||||
callback: (event: UserInputChatEvent) => {
|
||||
chatStream.next(event.message);
|
||||
},
|
||||
type: "userInputChat",
|
||||
typeChecker: isUserInputChatEvent
|
||||
})]
|
||||
|
||||
|
||||
sendChatMessage(message: string, author: string) {
|
||||
sendToWorkadventure({
|
||||
type: 'chat',
|
||||
data: {
|
||||
'message': message,
|
||||
'author': author
|
||||
} as ChatEvent
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen to messages sent by the local user, in the chat.
|
||||
*/
|
||||
onChatMessage(callback: (message: string) => void) {
|
||||
chatStream.subscribe(callback);
|
||||
}
|
||||
}
|
||||
|
||||
export default new WorkadventureChatCommands()
|
16
front/src/Api/iframe/controls.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
|
||||
|
||||
class WorkadventureControlsCommands extends IframeApiContribution<WorkadventureControlsCommands> {
|
||||
callbacks = []
|
||||
|
||||
disablePlayerControls(): void {
|
||||
sendToWorkadventure({ 'type': 'disablePlayerControls', data: null });
|
||||
}
|
||||
|
||||
restorePlayerControls(): void {
|
||||
sendToWorkadventure({ 'type': 'restorePlayerControls', data: null });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default new WorkadventureControlsCommands();
|
56
front/src/Api/iframe/nav.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import type { GoToPageEvent } from '../Events/GoToPageEvent';
|
||||
import type { OpenTabEvent } from '../Events/OpenTabEvent';
|
||||
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
|
||||
import type {OpenCoWebSiteEvent} from "../Events/OpenCoWebSiteEvent";
|
||||
|
||||
|
||||
class WorkadventureNavigationCommands extends IframeApiContribution<WorkadventureNavigationCommands> {
|
||||
callbacks = []
|
||||
|
||||
|
||||
openTab(url: string): void {
|
||||
sendToWorkadventure({
|
||||
"type": 'openTab',
|
||||
"data": {
|
||||
url
|
||||
} as OpenTabEvent
|
||||
});
|
||||
}
|
||||
|
||||
goToPage(url: string): void {
|
||||
sendToWorkadventure({
|
||||
"type": 'goToPage',
|
||||
"data": {
|
||||
url
|
||||
} as GoToPageEvent
|
||||
});
|
||||
}
|
||||
|
||||
goToRoom(url: string): void {
|
||||
sendToWorkadventure({
|
||||
"type": 'loadPage',
|
||||
"data": {
|
||||
url
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openCoWebSite(url: string): void {
|
||||
sendToWorkadventure({
|
||||
"type": 'openCoWebSite',
|
||||
"data": {
|
||||
url
|
||||
} as OpenCoWebSiteEvent
|
||||
});
|
||||
}
|
||||
|
||||
closeCoWebSite(): void {
|
||||
sendToWorkadventure({
|
||||
"type": 'closeCoWebSite',
|
||||
data: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default new WorkadventureNavigationCommands();
|
16
front/src/Api/iframe/registeredCallbacks.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import type {IframeResponseEventMap} from "../../Api/Events/IframeEvent";
|
||||
import type {IframeCallback} from "../../Api/iframe/IframeApiContribution";
|
||||
import type {IframeCallbackContribution} from "../../Api/iframe/IframeApiContribution";
|
||||
|
||||
export const registeredCallbacks: { [K in keyof IframeResponseEventMap]?: IframeCallback<K> } = {}
|
||||
|
||||
export function apiCallback<T extends keyof IframeResponseEventMap>(callbackData: IframeCallbackContribution<T>): IframeCallbackContribution<keyof IframeResponseEventMap> {
|
||||
const iframeCallback = {
|
||||
typeChecker: callbackData.typeChecker,
|
||||
callback: callbackData.callback
|
||||
} as IframeCallback<T>;
|
||||
|
||||
const newCallback = { [callbackData.type]: iframeCallback };
|
||||
Object.assign(registeredCallbacks, newCallback)
|
||||
return callbackData as unknown as IframeCallbackContribution<keyof IframeResponseEventMap>;
|
||||
}
|
50
front/src/Api/iframe/room.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { Subject } from "rxjs";
|
||||
import { EnterLeaveEvent, isEnterLeaveEvent } from '../Events/EnterLeaveEvent';
|
||||
import { IframeApiContribution } from './IframeApiContribution';
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
|
||||
const enterStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
||||
const leaveStreams: Map<string, Subject<EnterLeaveEvent>> = new Map<string, Subject<EnterLeaveEvent>>();
|
||||
|
||||
class WorkadventureRoomCommands extends IframeApiContribution<WorkadventureRoomCommands> {
|
||||
callbacks = [
|
||||
apiCallback({
|
||||
callback: (payloadData: EnterLeaveEvent) => {
|
||||
enterStreams.get(payloadData.name)?.next();
|
||||
},
|
||||
type: "enterEvent",
|
||||
typeChecker: isEnterLeaveEvent
|
||||
}),
|
||||
apiCallback({
|
||||
type: "leaveEvent",
|
||||
typeChecker: isEnterLeaveEvent,
|
||||
callback: (payloadData) => {
|
||||
leaveStreams.get(payloadData.name)?.next();
|
||||
}
|
||||
})
|
||||
|
||||
]
|
||||
|
||||
|
||||
onEnterZone(name: string, callback: () => void): void {
|
||||
let subject = enterStreams.get(name);
|
||||
if (subject === undefined) {
|
||||
subject = new Subject<EnterLeaveEvent>();
|
||||
enterStreams.set(name, subject);
|
||||
}
|
||||
subject.subscribe(callback);
|
||||
|
||||
}
|
||||
onLeaveZone(name: string, callback: () => void): void {
|
||||
let subject = leaveStreams.get(name);
|
||||
if (subject === undefined) {
|
||||
subject = new Subject<EnterLeaveEvent>();
|
||||
leaveStreams.set(name, subject);
|
||||
}
|
||||
subject.subscribe(callback);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default new WorkadventureRoomCommands();
|
17
front/src/Api/iframe/sound.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import type { LoadSoundEvent } from '../Events/LoadSoundEvent';
|
||||
import type { PlaySoundEvent } from '../Events/PlaySoundEvent';
|
||||
import type { StopSoundEvent } from '../Events/StopSoundEvent';
|
||||
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
|
||||
import {Sound} from "./Sound/Sound";
|
||||
|
||||
class WorkadventureSoundCommands extends IframeApiContribution<WorkadventureSoundCommands> {
|
||||
callbacks = []
|
||||
|
||||
loadSound(url: string): Sound {
|
||||
return new Sound(url);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default new WorkadventureSoundCommands();
|
83
front/src/Api/iframe/ui.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { isButtonClickedEvent } from '../Events/ButtonClickedEvent';
|
||||
import type { ClosePopupEvent } from '../Events/ClosePopupEvent';
|
||||
import { IframeApiContribution, sendToWorkadventure } from './IframeApiContribution';
|
||||
import { apiCallback } from "./registeredCallbacks";
|
||||
import {Popup} from "./Ui/Popup";
|
||||
import type {ButtonClickedCallback, ButtonDescriptor} from "./Ui/ButtonDescriptor";
|
||||
|
||||
let popupId = 0;
|
||||
const popups: Map<number, Popup> = new Map<number, Popup>();
|
||||
const popupCallbacks: Map<number, Map<number, ButtonClickedCallback>> = new Map<number, Map<number, ButtonClickedCallback>>();
|
||||
|
||||
interface ZonedPopupOptions {
|
||||
zone: string
|
||||
objectLayerName?: string,
|
||||
popupText: string,
|
||||
delay?: number
|
||||
popupOptions: Array<ButtonDescriptor>
|
||||
}
|
||||
|
||||
|
||||
class WorkAdventureUiCommands extends IframeApiContribution<WorkAdventureUiCommands> {
|
||||
|
||||
callbacks = [apiCallback({
|
||||
type: "buttonClickedEvent",
|
||||
typeChecker: isButtonClickedEvent,
|
||||
callback: (payloadData) => {
|
||||
const callback = popupCallbacks.get(payloadData.popupId)?.get(payloadData.buttonId);
|
||||
const popup = popups.get(payloadData.popupId);
|
||||
if (popup === undefined) {
|
||||
throw new Error('Could not find popup with ID "' + payloadData.popupId + '"');
|
||||
}
|
||||
if (callback) {
|
||||
callback(popup);
|
||||
}
|
||||
}
|
||||
})];
|
||||
|
||||
|
||||
openPopup(targetObject: string, message: string, buttons: ButtonDescriptor[]): Popup {
|
||||
popupId++;
|
||||
const popup = new Popup(popupId);
|
||||
const btnMap = new Map<number, () => void>();
|
||||
popupCallbacks.set(popupId, btnMap);
|
||||
let id = 0;
|
||||
for (const button of buttons) {
|
||||
const callback = button.callback;
|
||||
if (callback) {
|
||||
btnMap.set(id, () => {
|
||||
callback(popup);
|
||||
});
|
||||
}
|
||||
id++;
|
||||
}
|
||||
|
||||
sendToWorkadventure({
|
||||
'type': 'openPopup',
|
||||
'data': {
|
||||
popupId,
|
||||
targetObject,
|
||||
message,
|
||||
buttons: buttons.map((button) => {
|
||||
return {
|
||||
label: button.label,
|
||||
className: button.className
|
||||
};
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
popups.set(popupId, popup)
|
||||
return popup;
|
||||
}
|
||||
|
||||
displayBubble(): void {
|
||||
sendToWorkadventure({ 'type': 'displayBubble', data: null });
|
||||
}
|
||||
|
||||
removeBubble(): void {
|
||||
sendToWorkadventure({ 'type': 'removeBubble', data: null });
|
||||
}
|
||||
}
|
||||
|
||||
export default new WorkAdventureUiCommands();
|
86
front/src/Components/App.svelte
Normal file
|
@ -0,0 +1,86 @@
|
|||
<script lang="typescript">
|
||||
import {enableCameraSceneVisibilityStore, gameOverlayVisibilityStore} from "../Stores/MediaStore";
|
||||
import CameraControls from "./CameraControls.svelte";
|
||||
import MyCamera from "./MyCamera.svelte";
|
||||
import SelectCompanionScene from "./SelectCompanion/SelectCompanionScene.svelte";
|
||||
import {selectCompanionSceneVisibleStore} from "../Stores/SelectCompanionStore";
|
||||
import {selectCharacterSceneVisibleStore} from "../Stores/SelectCharacterStore";
|
||||
import SelectCharacterScene from "./selectCharacter/SelectCharacterScene.svelte";
|
||||
import {customCharacterSceneVisibleStore} from "../Stores/CustomCharacterStore";
|
||||
import {errorStore} from "../Stores/ErrorStore";
|
||||
import CustomCharacterScene from "./CustomCharacterScene/CustomCharacterScene.svelte";
|
||||
import LoginScene from "./Login/LoginScene.svelte";
|
||||
import {loginSceneVisibleStore} from "../Stores/LoginSceneStore";
|
||||
import EnableCameraScene from "./EnableCamera/EnableCameraScene.svelte";
|
||||
import VisitCard from "./VisitCard/VisitCard.svelte";
|
||||
import {requestVisitCardsStore} from "../Stores/GameStore";
|
||||
|
||||
import type {Game} from "../Phaser/Game/Game";
|
||||
import {helpCameraSettingsVisibleStore} from "../Stores/HelpCameraSettingsStore";
|
||||
import HelpCameraSettingsPopup from "./HelpCameraSettings/HelpCameraSettingsPopup.svelte";
|
||||
import AudioPlaying from "./UI/AudioPlaying.svelte";
|
||||
import {soundPlayingStore} from "../Stores/SoundPlayingStore";
|
||||
import ErrorDialog from "./UI/ErrorDialog.svelte";
|
||||
|
||||
export let game: Game;
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if $loginSceneVisibleStore}
|
||||
<div class="scrollable">
|
||||
<LoginScene game={game}></LoginScene>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $selectCharacterSceneVisibleStore}
|
||||
<div>
|
||||
<SelectCharacterScene game={ game }></SelectCharacterScene>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $customCharacterSceneVisibleStore}
|
||||
<div>
|
||||
<CustomCharacterScene game={ game }></CustomCharacterScene>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $selectCompanionSceneVisibleStore}
|
||||
<div>
|
||||
<SelectCompanionScene game={ game }></SelectCompanionScene>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $enableCameraSceneVisibilityStore}
|
||||
<div class="scrollable">
|
||||
<EnableCameraScene game={game}></EnableCameraScene>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $soundPlayingStore}
|
||||
<div>
|
||||
<AudioPlaying url={$soundPlayingStore} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!--
|
||||
{#if $menuIconVisible}
|
||||
<div>
|
||||
<MenuIcon />
|
||||
</div>
|
||||
{/if}
|
||||
-->
|
||||
{#if $gameOverlayVisibilityStore}
|
||||
<div>
|
||||
<MyCamera></MyCamera>
|
||||
<CameraControls></CameraControls>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $helpCameraSettingsVisibleStore}
|
||||
<div>
|
||||
<HelpCameraSettingsPopup></HelpCameraSettingsPopup>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $requestVisitCardsStore}
|
||||
<VisitCard visitCardUrl={$requestVisitCardsStore}></VisitCard>
|
||||
{/if}
|
||||
{#if $errorStore.length > 0}
|
||||
<div>
|
||||
<ErrorDialog></ErrorDialog>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
61
front/src/Components/CameraControls.svelte
Normal file
|
@ -0,0 +1,61 @@
|
|||
<script lang="typescript">
|
||||
import {requestedScreenSharingState, screenSharingAvailableStore} from "../Stores/ScreenSharingStore";
|
||||
import {requestedCameraState, requestedMicrophoneState} from "../Stores/MediaStore";
|
||||
import monitorImg from "./images/monitor.svg";
|
||||
import monitorCloseImg from "./images/monitor-close.svg";
|
||||
import cinemaImg from "./images/cinema.svg";
|
||||
import cinemaCloseImg from "./images/cinema-close.svg";
|
||||
import microphoneImg from "./images/microphone.svg";
|
||||
import microphoneCloseImg from "./images/microphone-close.svg";
|
||||
|
||||
function screenSharingClick(): void {
|
||||
if ($requestedScreenSharingState === true) {
|
||||
requestedScreenSharingState.disableScreenSharing();
|
||||
} else {
|
||||
requestedScreenSharingState.enableScreenSharing();
|
||||
}
|
||||
}
|
||||
|
||||
function cameraClick(): void {
|
||||
if ($requestedCameraState === true) {
|
||||
requestedCameraState.disableWebcam();
|
||||
} else {
|
||||
requestedCameraState.enableWebcam();
|
||||
}
|
||||
}
|
||||
|
||||
function microphoneClick(): void {
|
||||
if ($requestedMicrophoneState === true) {
|
||||
requestedMicrophoneState.disableMicrophone();
|
||||
} else {
|
||||
requestedMicrophoneState.enableMicrophone();
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div class="btn-cam-action">
|
||||
<div class="btn-monitor" on:click={screenSharingClick} class:hide={!$screenSharingAvailableStore} class:enabled={$requestedScreenSharingState}>
|
||||
{#if $requestedScreenSharingState}
|
||||
<img src={monitorImg} alt="Start screen sharing">
|
||||
{:else}
|
||||
<img src={monitorCloseImg} alt="Stop screen sharing">
|
||||
{/if}
|
||||
</div>
|
||||
<div class="btn-video" on:click={cameraClick} class:disabled={!$requestedCameraState}>
|
||||
{#if $requestedCameraState}
|
||||
<img src={cinemaImg} alt="Turn on webcam">
|
||||
{:else}
|
||||
<img src={cinemaCloseImg} alt="Turn off webcam">
|
||||
{/if}
|
||||
</div>
|
||||
<div class="btn-micro" on:click={microphoneClick} class:disabled={!$requestedMicrophoneState}>
|
||||
{#if $requestedMicrophoneState}
|
||||
<img src={microphoneImg} alt="Turn on microphone">
|
||||
{:else}
|
||||
<img src={microphoneCloseImg} alt="Turn off microphone">
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,119 @@
|
|||
<script lang="typescript">
|
||||
import type { Game } from "../../Phaser/Game/Game";
|
||||
import {CustomizeScene, CustomizeSceneName} from "../../Phaser/Login/CustomizeScene";
|
||||
|
||||
export let game: Game;
|
||||
|
||||
const customCharacterScene = game.scene.getScene(CustomizeSceneName) as CustomizeScene;
|
||||
let activeRow = customCharacterScene.activeRow;
|
||||
|
||||
function selectLeft() {
|
||||
customCharacterScene.moveCursorHorizontally(-1);
|
||||
}
|
||||
|
||||
function selectRight() {
|
||||
customCharacterScene.moveCursorHorizontally(1);
|
||||
}
|
||||
|
||||
function selectUp() {
|
||||
customCharacterScene.moveCursorVertically(-1);
|
||||
activeRow = customCharacterScene.activeRow;
|
||||
}
|
||||
|
||||
function selectDown() {
|
||||
customCharacterScene.moveCursorVertically(1);
|
||||
activeRow = customCharacterScene.activeRow;
|
||||
}
|
||||
|
||||
function previousScene() {
|
||||
customCharacterScene.backToPreviousScene();
|
||||
}
|
||||
|
||||
function finish() {
|
||||
customCharacterScene.nextSceneToCamera();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<form class="customCharacterScene">
|
||||
<section class="text-center">
|
||||
<h2>Customize your WOKA</h2>
|
||||
</section>
|
||||
<section class="action action-move">
|
||||
<button class="customCharacterSceneButton customCharacterSceneButtonLeft nes-btn" on:click|preventDefault={ selectLeft }> < </button>
|
||||
<button class="customCharacterSceneButton customCharacterSceneButtonRight nes-btn" on:click|preventDefault={ selectRight }> > </button>
|
||||
</section>
|
||||
<section class="action">
|
||||
{#if activeRow === 0}
|
||||
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={ previousScene }>Return</button>
|
||||
{/if}
|
||||
{#if activeRow !== 0}
|
||||
<button type="submit" class="customCharacterSceneFormBack nes-btn" on:click|preventDefault={ selectUp }>Back <img src="resources/objects/arrow_up_black.png" alt=""/></button>
|
||||
{/if}
|
||||
{#if activeRow === 5}
|
||||
<button type="submit" class="customCharacterSceneFormSubmit nes-btn is-primary" on:click|preventDefault={ finish }>Finish</button>
|
||||
{/if}
|
||||
{#if activeRow !== 5}
|
||||
<button type="submit" class="customCharacterSceneFormSubmit nes-btn is-primary" on:click|preventDefault={ selectDown }>Next <img src="resources/objects/arrow_down.png" alt=""/></button>
|
||||
{/if}
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<style lang="scss">
|
||||
form.customCharacterScene {
|
||||
font-family: "Press Start 2P";
|
||||
pointer-events: auto;
|
||||
color: #ebeeee;
|
||||
|
||||
section {
|
||||
margin: 10px;
|
||||
|
||||
&.action {
|
||||
text-align: center;
|
||||
margin-top: 55vh;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: "Press Start 2P";
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
&.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button.customCharacterSceneButton {
|
||||
position: absolute;
|
||||
top: 33vh;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
button.customCharacterSceneFormBack {
|
||||
color: #292929;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: "Press Start 2P";
|
||||
|
||||
&.customCharacterSceneButtonLeft {
|
||||
left: 33vw;
|
||||
}
|
||||
|
||||
&.customCharacterSceneButtonRight {
|
||||
right: 33vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
form.customCharacterScene button.customCharacterSceneButtonLeft{
|
||||
left: 5vw;
|
||||
}
|
||||
form.customCharacterScene button.customCharacterSceneButtonRight{
|
||||
right: 5vw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
229
front/src/Components/EnableCamera/EnableCameraScene.svelte
Normal file
|
@ -0,0 +1,229 @@
|
|||
<script lang="typescript">
|
||||
import type {Game} from "../../Phaser/Game/Game";
|
||||
import {EnableCameraScene, EnableCameraSceneName} from "../../Phaser/Login/EnableCameraScene";
|
||||
import {
|
||||
audioConstraintStore,
|
||||
cameraListStore,
|
||||
localStreamStore,
|
||||
microphoneListStore,
|
||||
videoConstraintStore
|
||||
} from "../../Stores/MediaStore";
|
||||
import {onDestroy} from "svelte";
|
||||
import HorizontalSoundMeterWidget from "./HorizontalSoundMeterWidget.svelte";
|
||||
import cinemaCloseImg from "../images/cinema-close.svg";
|
||||
import cinemaImg from "../images/cinema.svg";
|
||||
import microphoneImg from "../images/microphone.svg";
|
||||
|
||||
export let game: Game;
|
||||
let selectedCamera : string|undefined = undefined;
|
||||
let selectedMicrophone : string|undefined = undefined;
|
||||
|
||||
const enableCameraScene = game.scene.getScene(EnableCameraSceneName) as EnableCameraScene;
|
||||
|
||||
function submit() {
|
||||
enableCameraScene.login();
|
||||
}
|
||||
|
||||
function srcObject(node: HTMLVideoElement, stream: MediaStream) {
|
||||
node.srcObject = stream;
|
||||
return {
|
||||
update(newStream: MediaStream) {
|
||||
if (node.srcObject != newStream) {
|
||||
node.srcObject = newStream
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let stream: MediaStream | null;
|
||||
|
||||
const unsubscribe = localStreamStore.subscribe(value => {
|
||||
if (value.type === 'success') {
|
||||
stream = value.stream;
|
||||
|
||||
if (stream !== null) {
|
||||
const videoTracks = stream.getVideoTracks();
|
||||
if (videoTracks.length > 0) {
|
||||
selectedCamera = videoTracks[0].getSettings().deviceId;
|
||||
}
|
||||
const audioTracks = stream.getAudioTracks();
|
||||
if (audioTracks.length > 0) {
|
||||
selectedMicrophone = audioTracks[0].getSettings().deviceId;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stream = null;
|
||||
selectedCamera = undefined;
|
||||
selectedMicrophone = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(unsubscribe);
|
||||
|
||||
function normalizeDeviceName(label: string): string {
|
||||
// remove IDs (that can appear in Chrome, like: "HD Pro Webcam (4df7:4eda)"
|
||||
return label.replace(/(\([[0-9a-f]{4}:[0-9a-f]{4}\))/g, '').trim();
|
||||
}
|
||||
|
||||
function selectCamera() {
|
||||
videoConstraintStore.setDeviceId(selectedCamera);
|
||||
}
|
||||
|
||||
function selectMicrophone() {
|
||||
audioConstraintStore.setDeviceId(selectedMicrophone);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<form class="enableCameraScene" on:submit|preventDefault={submit}>
|
||||
<section class="text-center">
|
||||
<h2>Turn on your camera and microphone</h2>
|
||||
</section>
|
||||
{#if $localStreamStore.type === 'success' && $localStreamStore.stream}
|
||||
<video class="myCamVideoSetup" use:srcObject={$localStreamStore.stream} autoplay muted playsinline></video>
|
||||
{:else }
|
||||
<div class="webrtcsetup">
|
||||
<img class="background-img" src={cinemaCloseImg} alt="">
|
||||
</div>
|
||||
{/if}
|
||||
<HorizontalSoundMeterWidget stream={stream}></HorizontalSoundMeterWidget>
|
||||
|
||||
<section class="selectWebcamForm">
|
||||
|
||||
{#if $cameraListStore.length > 1 }
|
||||
<div class="control-group">
|
||||
<img src={cinemaImg} alt="Camera" />
|
||||
<div class="nes-select is-dark">
|
||||
<select bind:value={selectedCamera} on:change={selectCamera}>
|
||||
{#each $cameraListStore as camera}
|
||||
<option value={camera.deviceId}>
|
||||
{normalizeDeviceName(camera.label)}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $microphoneListStore.length > 1 }
|
||||
<div class="control-group">
|
||||
<img src={microphoneImg} alt="Microphone" />
|
||||
<div class="nes-select is-dark">
|
||||
<select bind:value={selectedMicrophone} on:change={selectMicrophone}>
|
||||
{#each $microphoneListStore as microphone}
|
||||
<option value={microphone.deviceId}>
|
||||
{normalizeDeviceName(microphone.label)}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</section>
|
||||
<section class="action">
|
||||
<button type="submit" class="nes-btn is-primary letsgo" >Let's go!</button>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<style lang="scss">
|
||||
.enableCameraScene {
|
||||
pointer-events: auto;
|
||||
margin: 20px auto 0;
|
||||
color: #ebeeee;
|
||||
|
||||
section.selectWebcamForm {
|
||||
margin-top: 3vh;
|
||||
margin-bottom: 3vh;
|
||||
min-height: 10vh;
|
||||
width: 50vw;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
select {
|
||||
font-family: "Press Start 2P";
|
||||
margin-top: 1vh;
|
||||
margin-bottom: 1vh;
|
||||
}
|
||||
|
||||
option {
|
||||
font-family: "Press Start 2P";
|
||||
}
|
||||
}
|
||||
|
||||
section.action{
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h2{
|
||||
font-family: "Press Start 2P";
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
section.text-center{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button.letsgo {
|
||||
font-size: 200%;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
max-height: 60px;
|
||||
margin-top: 10px;
|
||||
|
||||
img {
|
||||
width: 30px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.webrtcsetup{
|
||||
margin-top: 2vh;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
height: 28.125vw;
|
||||
width: 50vw;
|
||||
border: white 6px solid;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
img.background-img {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
.myCamVideoSetup {
|
||||
margin-top: 2vh;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-height: 50vh;
|
||||
width: 50vw;
|
||||
border: white 6px solid;
|
||||
-webkit-transform: scaleX(-1);
|
||||
transform: scaleX(-1);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
.enableCameraScene h2 {
|
||||
font-size: 80%;
|
||||
}
|
||||
.enableCameraScene .control-group .nes-select {
|
||||
font-size: 80%;
|
||||
}
|
||||
.enableCameraScene button.letsgo {
|
||||
font-size: 160%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
<script lang="typescript">
|
||||
import { AudioContext } from 'standardized-audio-context';
|
||||
import {SoundMeter} from "../../Phaser/Components/SoundMeter";
|
||||
import {onDestroy} from "svelte";
|
||||
|
||||
export let stream: MediaStream | null;
|
||||
let volume = 0;
|
||||
|
||||
const NB_BARS = 20;
|
||||
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
const soundMeter = new SoundMeter();
|
||||
let display = false;
|
||||
|
||||
$: {
|
||||
if (stream && stream.getAudioTracks().length > 0) {
|
||||
display = true;
|
||||
soundMeter.connectToSource(stream, new AudioContext());
|
||||
|
||||
if (timeout) {
|
||||
clearInterval(timeout);
|
||||
}
|
||||
|
||||
timeout = setInterval(() => {
|
||||
try{
|
||||
volume = parseInt((soundMeter.getVolume() / 100 * NB_BARS).toFixed(0));
|
||||
//console.log(volume);
|
||||
}catch(err){
|
||||
|
||||
}
|
||||
}, 100);
|
||||
|
||||
} else {
|
||||
display = false;
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
soundMeter.stop();
|
||||
if (timeout) {
|
||||
clearInterval(timeout);
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
function color(i: number, volume: number) {
|
||||
const red = 255 * i / NB_BARS;
|
||||
const green = 255 * (1 - i / NB_BARS);
|
||||
|
||||
let alpha = 1;
|
||||
if (i >= volume) {
|
||||
alpha = 0.5;
|
||||
}
|
||||
|
||||
return 'background-color:rgba('+red+', '+green+', 0, '+alpha+')';
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div class="horizontal-sound-meter" class:active={display}>
|
||||
{#each [...Array(NB_BARS).keys()] as i}
|
||||
<div style={color(i, volume)}></div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.horizontal-sound-meter {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 50%;
|
||||
height: 30px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 1vh;
|
||||
}
|
||||
|
||||
.horizontal-sound-meter div {
|
||||
margin-left: 5px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -0,0 +1,73 @@
|
|||
<script lang="typescript">
|
||||
import { fly } from 'svelte/transition';
|
||||
import {helpCameraSettingsVisibleStore} from "../../Stores/HelpCameraSettingsStore";
|
||||
import firefoxImg from "./images/help-setting-camera-permission-firefox.png";
|
||||
import chromeImg from "./images/help-setting-camera-permission-chrome.png";
|
||||
|
||||
let isAndroid = window.navigator.userAgent.includes('Android');
|
||||
let isFirefox = window.navigator.userAgent.includes('Firefox');
|
||||
let isChrome = window.navigator.userAgent.includes('Chrome');
|
||||
|
||||
function refresh() {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function close() {
|
||||
helpCameraSettingsVisibleStore.set(false);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<form class="helpCameraSettings nes-container" on:submit|preventDefault={close} transition:fly="{{ y: -900, duration: 500 }}">
|
||||
<section>
|
||||
<h2>Camera / Microphone access needed</h2>
|
||||
<p class="err">Permission denied</p>
|
||||
<p>You must allow camera and microphone access in your browser.</p>
|
||||
<p>
|
||||
{#if isFirefox }
|
||||
<p class="err">Please click the "Remember this decision" checkbox, if you don't want Firefox to keep asking you the authorization.</p>
|
||||
<img src={firefoxImg} alt="" />
|
||||
{:else if isChrome && !isAndroid }
|
||||
<img src={chromeImg} alt="" />
|
||||
{/if}
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<button class="helpCameraSettingsFormRefresh nes-btn" on:click|preventDefault={refresh}>Refresh</button>
|
||||
<button type="submit" class="helpCameraSettingsFormContinue nes-btn is-primary" on:click|preventDefault={close}>Continue without webcam</button>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
.helpCameraSettings {
|
||||
pointer-events: auto;
|
||||
background: #eceeee;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 10vh;
|
||||
max-height: 80vh;
|
||||
max-width: 80vw;
|
||||
overflow: auto;
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
font-family: 'Press Start 2P';
|
||||
}
|
||||
|
||||
section {
|
||||
p {
|
||||
margin: 15px;
|
||||
font-family: 'Press Start 2P';
|
||||
|
||||
& .err {
|
||||
color: #ff0000;
|
||||
}
|
||||
}
|
||||
img {
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
After Width: | Height: | Size: 65 KiB |
After Width: | Height: | Size: 32 KiB |
122
front/src/Components/Login/LoginScene.svelte
Normal file
|
@ -0,0 +1,122 @@
|
|||
<script lang="typescript">
|
||||
import type {Game} from "../../Phaser/Game/Game";
|
||||
import {LoginScene, LoginSceneName} from "../../Phaser/Login/LoginScene";
|
||||
import {DISPLAY_TERMS_OF_USE, MAX_USERNAME_LENGTH} from "../../Enum/EnvironmentVariable";
|
||||
import logoImg from "../images/logo.png";
|
||||
import {gameManager} from "../../Phaser/Game/GameManager";
|
||||
|
||||
export let game: Game;
|
||||
|
||||
const loginScene = game.scene.getScene(LoginSceneName) as LoginScene;
|
||||
|
||||
let name = gameManager.getPlayerName() || '';
|
||||
let startValidating = false;
|
||||
|
||||
function submit() {
|
||||
startValidating = true;
|
||||
|
||||
let finalName = name.trim();
|
||||
if (finalName !== '') {
|
||||
loginScene.login(finalName);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="loginScene" on:submit|preventDefault={submit}>
|
||||
<section class="text-center">
|
||||
<img src={logoImg} alt="WorkAdventure logo" />
|
||||
</section>
|
||||
<section class="text-center">
|
||||
<h2>Enter your name</h2>
|
||||
</section>
|
||||
<input type="text" name="loginSceneName" class="nes-input is-dark" autofocus maxlength={MAX_USERNAME_LENGTH} bind:value={name} on:keypress={() => {startValidating = true}} class:is-error={name.trim() === '' && startValidating} />
|
||||
<section class="error-section">
|
||||
{#if name.trim() === '' && startValidating }
|
||||
<p class="err">The name is empty</p>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#if DISPLAY_TERMS_OF_USE}
|
||||
<section class="terms-and-conditions">
|
||||
<p>By continuing, you are agreeing our <a href="https://workadventu.re/terms-of-use" target="_blank">terms of use</a>, <a href="https://workadventu.re/privacy-policy" target="_blank">privacy policy</a> and <a href="https://workadventu.re/cookie-policy" target="_blank">cookie policy</a>.</p>
|
||||
</section>
|
||||
{/if}
|
||||
<section class="action">
|
||||
<button type="submit" class="nes-btn is-primary loginSceneFormSubmit">Continue</button>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<style lang="scss">
|
||||
.loginScene {
|
||||
pointer-events: auto;
|
||||
margin: 20px auto 0;
|
||||
width: 90%;
|
||||
color: #ebeeee;
|
||||
|
||||
display: flex;
|
||||
flex-flow: column wrap;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
text-align: center;
|
||||
font-family: "Press Start 2P";
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.terms-and-conditions {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
p.err {
|
||||
color: #ce372b;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
section {
|
||||
margin: 10px;
|
||||
|
||||
&.error-section {
|
||||
min-height: 2rem;
|
||||
margin: 0;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.action {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: "Press Start 2P";
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
&.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
color: #ebeeee;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: left;
|
||||
margin: 10px 10px;
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
33
front/src/Components/Menu/MenuIcon.svelte
Normal file
|
@ -0,0 +1,33 @@
|
|||
<script lang="typescript">
|
||||
|
||||
</script>
|
||||
|
||||
<main class="menuIcon">
|
||||
<section>
|
||||
<button>
|
||||
<img src="/static/images/menu.svg" alt="Open menu">
|
||||
</button>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style lang="scss">
|
||||
.menuIcon button {
|
||||
background-color: black;
|
||||
color: white;
|
||||
border-radius: 7px;
|
||||
padding: 2px 8px;
|
||||
img {
|
||||
width: 14px;
|
||||
padding-top: 0;
|
||||
/*cursor: url('/resources/logos/cursor_pointer.png'), pointer;*/
|
||||
}
|
||||
}
|
||||
.menuIcon section {
|
||||
margin: 10px;
|
||||
}
|
||||
@media only screen and (max-height: 700px) {
|
||||
.menuIcon section {
|
||||
margin: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
46
front/src/Components/MyCamera.svelte
Normal file
|
@ -0,0 +1,46 @@
|
|||
<script lang="typescript">
|
||||
import {localStreamStore} from "../Stores/MediaStore";
|
||||
import SoundMeterWidget from "./SoundMeterWidget.svelte";
|
||||
import {onDestroy} from "svelte";
|
||||
|
||||
function srcObject(node: HTMLVideoElement, stream: MediaStream) {
|
||||
node.srcObject = stream;
|
||||
return {
|
||||
update(newStream: MediaStream) {
|
||||
if (node.srcObject != newStream) {
|
||||
node.srcObject = newStream
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let stream : MediaStream|null;
|
||||
/*$: {
|
||||
if ($localStreamStore.type === 'success') {
|
||||
stream = $localStreamStore.stream;
|
||||
} else {
|
||||
stream = null;
|
||||
}
|
||||
}*/
|
||||
|
||||
const unsubscribe = localStreamStore.subscribe(value => {
|
||||
if (value.type === 'success') {
|
||||
stream = value.stream;
|
||||
} else {
|
||||
stream = null;
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(unsubscribe);
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div>
|
||||
<div class="video-container div-myCamVideo" class:hide={!$localStreamStore.constraints.video}>
|
||||
{#if $localStreamStore.type === "success" && $localStreamStore.stream }
|
||||
<video class="myCamVideo" use:srcObject={$localStreamStore.stream} autoplay muted playsinline></video>
|
||||
<SoundMeterWidget stream={stream}></SoundMeterWidget>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,87 @@
|
|||
<script lang="typescript">
|
||||
import type {Game} from "../../Phaser/Game/Game";
|
||||
import {SelectCompanionScene, SelectCompanionSceneName} from "../../Phaser/Login/SelectCompanionScene";
|
||||
|
||||
export let game: Game;
|
||||
|
||||
const selectCompanionScene = game.scene.getScene(SelectCompanionSceneName) as SelectCompanionScene;
|
||||
|
||||
function selectLeft() {
|
||||
selectCompanionScene.moveToLeft();
|
||||
}
|
||||
|
||||
function selectRight() {
|
||||
selectCompanionScene.moveToRight();
|
||||
}
|
||||
|
||||
function noCompanion() {
|
||||
selectCompanionScene.closeScene();
|
||||
}
|
||||
|
||||
function selectCompanion() {
|
||||
selectCompanionScene.selectCompanion();
|
||||
}
|
||||
</script>
|
||||
|
||||
<form class="selectCompanionScene">
|
||||
<section class="text-center">
|
||||
<h2>Select your companion</h2>
|
||||
<button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={selectLeft}> < </button>
|
||||
<button class="selectCharacterButton selectCharacterButtonRight nes-btn" on:click|preventDefault={selectRight}> > </button>
|
||||
</section>
|
||||
<section class="action">
|
||||
<button href="/" class="selectCompanionSceneFormBack nes-btn" on:click|preventDefault={noCompanion}>No companion</button>
|
||||
<button type="submit" class="selectCompanionSceneFormSubmit nes-btn is-primary" on:click|preventDefault={selectCompanion}>Continue</button>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<style lang="scss">
|
||||
form.selectCompanionScene {
|
||||
font-family: "Press Start 2P";
|
||||
pointer-events: auto;
|
||||
color: #ebeeee;
|
||||
|
||||
section {
|
||||
margin: 10px;
|
||||
|
||||
&.action {
|
||||
text-align: center;
|
||||
margin-top: 55vh;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: "Press Start 2P";
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
&.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button.selectCharacterButton {
|
||||
position: absolute;
|
||||
top: 33vh;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
button.selectCharacterButtonLeft {
|
||||
left: 33vw;
|
||||
}
|
||||
|
||||
button.selectCharacterButtonRight {
|
||||
right: 33vw;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
form.selectCompanionScene button.selectCharacterButtonLeft{
|
||||
left: 5vw;
|
||||
}
|
||||
form.selectCompanionScene button.selectCharacterButtonRight{
|
||||
right: 5vw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
53
front/src/Components/SoundMeterWidget.svelte
Normal file
|
@ -0,0 +1,53 @@
|
|||
<script lang="typescript">
|
||||
import { AudioContext } from 'standardized-audio-context';
|
||||
import {SoundMeter} from "../Phaser/Components/SoundMeter";
|
||||
import {onDestroy} from "svelte";
|
||||
|
||||
export let stream: MediaStream|null;
|
||||
let volume = 0;
|
||||
|
||||
const NB_BARS = 5;
|
||||
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
const soundMeter = new SoundMeter();
|
||||
let display = false;
|
||||
|
||||
$: {
|
||||
if (stream && stream.getAudioTracks().length > 0) {
|
||||
display = true;
|
||||
soundMeter.connectToSource(stream, new AudioContext());
|
||||
|
||||
if (timeout) {
|
||||
clearInterval(timeout);
|
||||
}
|
||||
|
||||
timeout = setInterval(() => {
|
||||
try{
|
||||
volume = parseInt((soundMeter.getVolume() / 100 * NB_BARS).toFixed(0));
|
||||
//console.log(volume);
|
||||
}catch(err){
|
||||
|
||||
}
|
||||
}, 100);
|
||||
|
||||
} else {
|
||||
display = false;
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
soundMeter.stop();
|
||||
if (timeout) {
|
||||
clearInterval(timeout);
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<div class="sound-progress" class:active={display}>
|
||||
<span class:active={volume > 1}></span>
|
||||
<span class:active={volume > 2}></span>
|
||||
<span class:active={volume > 3}></span>
|
||||
<span class:active={volume > 4}></span>
|
||||
<span class:active={volume > 5}></span>
|
||||
</div>
|
52
front/src/Components/UI/AudioPlaying.svelte
Normal file
|
@ -0,0 +1,52 @@
|
|||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition';
|
||||
import megaphoneImg from "./images/megaphone.svg";
|
||||
import {soundPlayingStore} from "../../Stores/SoundPlayingStore";
|
||||
import {afterUpdate} from "svelte";
|
||||
|
||||
export let url: string;
|
||||
let audio: HTMLAudioElement;
|
||||
|
||||
function soundEnded() {
|
||||
soundPlayingStore.soundEnded();
|
||||
}
|
||||
|
||||
afterUpdate(() => {
|
||||
audio.play();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="audio-playing" transition:fly="{{ x: 210, duration: 500 }}">
|
||||
<img src={megaphoneImg} alt="Audio playing" />
|
||||
<p>Audio message</p>
|
||||
<audio bind:this={audio} src={url} on:ended={soundEnded} >
|
||||
<track kind="captions">
|
||||
</audio>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
/*audio html when audio message playing*/
|
||||
.audio-playing {
|
||||
position: absolute;
|
||||
width: 200px;
|
||||
height: 54px;
|
||||
right: 0;
|
||||
top: 40px;
|
||||
transition: all 0.1s ease-out;
|
||||
background-color: black;
|
||||
border-radius: 30px 0 0 30px;
|
||||
display: inline-flex;
|
||||
|
||||
img {
|
||||
border-radius: 50%;
|
||||
background-color: #ffda01;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: white;
|
||||
margin-left: 10px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
48
front/src/Components/UI/ErrorDialog.svelte
Normal file
|
@ -0,0 +1,48 @@
|
|||
<script lang="ts">
|
||||
import {errorStore} from "../../Stores/ErrorStore";
|
||||
|
||||
function close(): boolean {
|
||||
errorStore.clearMessages();
|
||||
return false;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<div class="error-div nes-container is-dark is-rounded" open>
|
||||
<p class="nes-text is-error title">Error</p>
|
||||
<div class="body">
|
||||
{#each $errorStore as error}
|
||||
<p>{error}</p>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
<button class="nes-btn is-error" on:click={close}>Close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
div.error-div {
|
||||
pointer-events: auto;
|
||||
margin-top: 10vh;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
width: max-content;
|
||||
max-width: 80vw;
|
||||
|
||||
.button-bar {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.body {
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: "Press Start 2P";
|
||||
|
||||
&.title {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
18
front/src/Components/UI/images/megaphone.svg
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 451.7 512" style="enable-background:new 0 0 451.7 512;" xml:space="preserve">
|
||||
<path d="M436.9,212.6L237.2,12.9c-11.7-11.7-30.7-11.7-42.4,0s-11.7,30.7,0,42.4L394.5,255c11.5,11.9,30.5,12.2,42.4,0.7
|
||||
c11.9-11.5,12.2-30.5,0.7-42.4C437.4,213.1,437.2,212.8,436.9,212.6z"/>
|
||||
<path d="M179.5,83.1l-1.5,7.5c-10.4,53-36,103.4-70.6,144.3l109,108.3c40.7-34.9,90.2-61.5,143.1-72.3l7.5-1.5L179.5,83.1z"/>
|
||||
<path d="M87.4,257l-74.2,74.2c-17.6,17.6-17.6,46.1,0,63.6c0,0,0,0,0,0l42.4,42.4c17.6,17.6,46.1,17.6,63.6,0c0,0,0,0,0,0l74.2-74.2
|
||||
L87.4,257z M98,373.7c-6.1,5.6-15.6,5.3-21.2-0.8c-5.4-5.8-5.4-14.7,0-20.5l21.2-21.2c6-5.8,15.5-5.6,21.2,0.4
|
||||
c5.6,5.8,5.6,15,0,20.8L98,373.7z"/>
|
||||
<path d="M256.1,445.3l20.4-20.4c17.6-17.6,17.6-46.1,0-63.6l-15.1-15.2c-8.4,5.7-16.4,11.7-24.2,18.3l18.1,18.1
|
||||
c5.8,5.9,5.8,15.3,0,21.2l-20.7,20.8l-30.5-29.5l-42.4,42.4l68.1,65.9c11.7,11.7,30.7,11.7,42.4,0c11.7-11.7,11.7-30.7,0-42.4l0,0
|
||||
L256.1,445.3z"/>
|
||||
<path d="M316.7,0c-8.3,0-15,6.7-15,15v30c0,8.3,6.7,15,15,15c8.3,0,15-6.7,15-15V15C331.7,6.7,325,0,316.7,0z"/>
|
||||
<path d="M436.7,120h-30c-8.3,0-15,6.7-15,15s6.7,15,15,15h30c8.3,0,15-6.7,15-15S445,120,436.7,120z"/>
|
||||
<path d="M417.3,34.4c-5.9-5.9-15.4-5.9-21.2,0l-30,30c-6,5.8-6.1,15.3-0.4,21.2c5.8,6,15.3,6.1,21.2,0.4c0.1-0.1,0.2-0.2,0.4-0.4
|
||||
l30-30C423.2,49.7,423.2,40.2,417.3,34.4z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
84
front/src/Components/VisitCard/VisitCard.svelte
Normal file
|
@ -0,0 +1,84 @@
|
|||
<script lang="typescript">
|
||||
import { fly } from 'svelte/transition';
|
||||
import {requestVisitCardsStore} from "../../Stores/GameStore";
|
||||
import {onMount} from "svelte";
|
||||
|
||||
export let visitCardUrl: string;
|
||||
let w = '500px';
|
||||
let h = '250px';
|
||||
let hidden = true;
|
||||
let cvIframe: HTMLIFrameElement;
|
||||
|
||||
function closeCard() {
|
||||
requestVisitCardsStore.set(null);
|
||||
}
|
||||
|
||||
function handleIframeMessage(message:any) {
|
||||
if (message.data.type === 'cvIframeSize') {
|
||||
w = (message.data.data.w) + 'px';
|
||||
h = (message.data.data.h) + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
cvIframe.onload = () => hidden = false
|
||||
cvIframe.onerror = () => hidden = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.loader {
|
||||
border: 16px solid #f3f3f3; /* Light grey */
|
||||
border-top: 16px solid #3498db; /* Blue */
|
||||
border-radius: 50%;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin:auto;
|
||||
animation: spin 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.visitCard {
|
||||
pointer-events: all;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-top: 200px;
|
||||
max-width: 80vw;
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
max-width: 80vw;
|
||||
overflow: hidden;
|
||||
|
||||
&.hidden {
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<section class="visitCard" transition:fly="{{ y: -200, duration: 1000 }}" style="width: {w}">
|
||||
{#if hidden}
|
||||
<div class="loader"></div>
|
||||
{/if}
|
||||
<iframe title="visitCard" src={visitCardUrl} allow="clipboard-read; clipboard-write self {visitCardUrl}" style="width: {w}; height: {h}" class:hidden={hidden} bind:this={cvIframe}></iframe>
|
||||
{#if !hidden}
|
||||
<div class="buttonContainer">
|
||||
<button class="nes-btn is-popUpElement" on:click={closeCard}>Close</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</section>
|
||||
|
||||
<svelte:window on:message={handleIframeMessage}/>
|
41
front/src/Components/images/cinema-close.svg
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 332.8 332.8" style="enable-background:new 0 0 332.8 332.8;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M330.8,171c-3.6-6.4-12-8.8-18.8-4.8l-45.6,26.4l-11.6,6.8v63.2l10.8,6.4c0.4,0,0.4,0.4,0.8,0.4l44.8,26
|
||||
c2,1.6,4.8,2.4,7.6,2.4c7.6,0,13.6-6,13.6-13.6v-53.6l0.4-52.8C332.8,175.4,332.4,173,330.8,171z"/>
|
||||
<path class="st0" d="M193.2,150.6c35.6,0,64.4-28.8,64.4-64.4s-28.8-64.4-64.4-64.4s-64.4,28.8-64.4,64.4
|
||||
C128.8,121.8,157.6,150.6,193.2,150.6z M193.2,59.8c14.8,0,26.4,12,26.4,26.4c0,14.8-12,26.4-26.4,26.4s-26.4-12-26.4-26.4
|
||||
C166.8,71.4,178.4,59.8,193.2,59.8z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
</g>
|
||||
</g>
|
||||
<rect x="134.8" y="-45.3" transform="matrix(-0.7402 0.6723 -0.6723 -0.7402 376.0669 224.8258)" class="st0" width="19.6" height="460.7"/>
|
||||
<path class="st0" d="M90.6,83.3c-0.2-2.2-1.3-8.9-6.7-14.9c-5.4-5.9-11.9-7.6-14.1-8.1C59.7,49.2,49.5,38,39.4,26.8
|
||||
c24.3-9.8,52-4.4,70.2,13.6c19.9,19.7,24.7,50.8,11.5,76.4C110.9,105.6,100.8,94.5,90.6,83.3z"/>
|
||||
<path class="st0" d="M10.1,51.6c9.4,10.2,18.8,20.4,28.2,30.6c-0.2,1.8-1.4,11.7,5.5,20.5c8.2,10.3,20.7,10.2,22.1,10.1
|
||||
c9.2,10.3,18.5,20.6,27.7,30.8c-4.8,2.3-24.6,11.2-48.3,4.1c-6-1.8-20.7-7.3-32.1-22C-0.3,108.1-0.2,89.1,0.1,83.4
|
||||
C0.8,68,6.8,56.8,10.1,51.6z"/>
|
||||
<g>
|
||||
<path class="st0" d="M243.4,178.2c0.1,24.5,0.2,49,0.2,73.5c-30.7-33.8-61.3-67.7-92-101.5c5.9,3.9,20.9,12.4,41.6,12.4
|
||||
c16,0,28.2-5.2,34.4-8.4c2.5,1.5,7,4.6,10.7,10.3C242,170,243,175.4,243.4,178.2z"/>
|
||||
<g>
|
||||
<path class="st0" d="M211.2,311C150.8,258.7,90.4,206.5,30,154.2c6.1,3.1,18.2,8.4,34.4,8.4c18.1,0,31.5-6.5,37.5-9.9
|
||||
c44.5,49,89.1,98.1,133.6,147.1c-1.8,2.1-5.3,5.5-10.6,8.1C219.2,310.6,214,311,211.2,311z"/>
|
||||
<path class="st0" d="M46.8,311C36,267.7,25.2,224.3,14.4,181c0.1-3.2,0.7-11.3,6.5-18.8c3.1-4.1,6.7-6.6,9.1-8
|
||||
C90.4,206.5,150.8,258.7,211.2,311C156.4,311,101.6,311,46.8,311z"/>
|
||||
<path class="st0" d="M14.4,278.6L14.4,278.6c0-32.5,0-65.1,0-97.6c10.8,43.3,21.6,86.7,32.4,130c-2.6,0-12.7-0.4-21.5-8.1
|
||||
C14.7,293.5,14.4,280.7,14.4,278.6z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
24
front/src/Components/images/cinema.svg
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 332.8 332.8" style="enable-background:new 0 0 332.8 332.8;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M330.8,171c-3.6-6.4-12-8.8-18.8-4.8l-45.6,26.4l-11.6,6.8v63.2l10.8,6.4c0.4,0,0.4,0.4,0.8,0.4l44.8,26
|
||||
c2,1.6,4.8,2.4,7.6,2.4c7.6,0,13.6-6,13.6-13.6v-53.6l0.4-52.8C332.8,175.4,332.4,173,330.8,171z"/>
|
||||
<path class="st0" d="M64.4,150.6c35.6,0,64.4-28.8,64.4-64.4S100,21.8,64.4,21.8S0,50.6,0,86.2C-0.4,121.8,28.8,150.6,64.4,150.6
|
||||
z M64.4,59.8c14.8,0,26.4,12,26.4,26.4c0,14.8-12,26.4-26.4,26.4S38,100.6,38,86.2C37.6,71.4,49.6,59.8,64.4,59.8z"/>
|
||||
<path class="st0" d="M227.6,154.2c-10.4,5.2-22,8.4-34.4,8.4c-15.2,0-29.6-4.4-41.6-12.4H106c-12,8-26.4,12.4-41.6,12.4
|
||||
c-12.4,0-24-2.8-34.4-8.4c-9.2,5.2-15.6,15.6-15.6,26.8v97.6c0,18,14.8,32.4,32.4,32.4h164.4c18,0,32.4-14.8,32.4-32.4V181
|
||||
C243.2,169.8,236.8,159.4,227.6,154.2z"/>
|
||||
<path class="st0" d="M193.2,150.6c35.6,0,64.4-28.8,64.4-64.4s-28.8-64.4-64.4-64.4s-64.4,28.8-64.4,64.4
|
||||
C128.8,121.8,157.6,150.6,193.2,150.6z M193.2,59.8c14.8,0,26.4,12,26.4,26.4c0,14.8-12,26.4-26.4,26.4s-26.4-12-26.4-26.4
|
||||
C166.8,71.4,178.4,59.8,193.2,59.8z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
BIN
front/src/Components/images/logo.png
Normal file
After Width: | Height: | Size: 16 KiB |
27
front/src/Components/images/microphone-close.svg
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<rect x="257" y="-47.9" transform="matrix(-0.7402 0.6723 -0.6723 -0.7402 643.9641 283.6469)" class="st0" width="20.4" height="628.3"/>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M333.6,250.3c-52.6-43.9-105.1-87.9-157.7-131.8c0-17.9,0-35.8,0-53.6c6.5-38.6,40.3-67,79.3-66.8
|
||||
c38.6,0.2,71.9,28.5,78.4,66.8C333.6,126.7,333.6,188.5,333.6,250.3z"/>
|
||||
<path class="st0" d="M322.6,279.9c-48.9-53.8-97.8-107.6-146.6-161.4l0,0c52.6,43.9,105.1,87.9,157.7,131.8
|
||||
c-0.2,1.6-0.5,3.3-0.9,5C330.5,265.2,326.6,273.5,322.6,279.9z"/>
|
||||
</g>
|
||||
<path class="st0" d="M292.5,308.1c-2.3,1.2-39.5,20.3-76.7-1c-36.4-20.8-39.4-61.2-39.6-64.1c-0.1-21-0.1-42.1-0.2-63.1
|
||||
C214.8,222.6,253.6,265.3,292.5,308.1z"/>
|
||||
</g>
|
||||
<path class="st0" d="M431.6,238.5c-0.9-8.4-8.5-14.4-16.6-13.5c-7.9,0.9-13.9,8.1-13.2,16.3c-0.1,13.3-2.2,34.6-12.6,57.9
|
||||
c-6.3,14.2-14,25.2-20.6,33.1c6.8,7.5,13.6,14.9,20.3,22.4c9.5-10.9,23.4-29.7,32.8-56.3C430.3,273.9,431.8,252.5,431.6,238.5z"/>
|
||||
<line class="st0" x1="354.5" y1="347.2" x2="374.6" y2="369.4"/>
|
||||
<path class="st0" d="M338.5,359.9c6.8,7.4,13.5,14.9,20.3,22.3c-52.6,37.6-121.5,43.7-179.2,15.8c-60.3-29.1-98.9-90.7-99.3-158.2
|
||||
c0-8.2,6.8-15,15-15s15,6.8,15,15c0.1,13.5,2.4,54.4,32.4,91.6c4.2,5.2,45.1,54.1,113.3,54.1C297,385.6,326.7,367.9,338.5,359.9z"/>
|
||||
<rect x="241" y="409.6" class="st0" width="29.9" height="102.3"/>
|
||||
<path class="st0" d="M304.2,511.9h-97.1c-8-0.4-14.3-7.1-14.3-15c0-8.1,6.7-14.9,15-15c31.7,0,63.4,0.1,95.1,0.1
|
||||
c8.9-0.6,16.3,6.5,16.3,14.9C319.2,504.8,312.6,511.7,304.2,511.9z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
21
front/src/Components/images/microphone.svg
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M431.7,239.9c0-8.3-6.7-15-15-15c-8.3,0-15,6.7-15,15c0,80.3-65.3,145.7-145.7,145.7s-145.7-65.3-145.7-145.7
|
||||
c0-8.3-6.7-15-15-15c-8.3,0-15,6.7-15,15c0,91.8,70.8,167.4,160.7,175v67h-33.2c-8.3,0-15,6.7-15,15s6.7,15,15,15h96.4
|
||||
c8.3,0,15-6.7,15-15s-6.7-15-15-15H271v-67C360.9,407.3,431.7,331.7,431.7,239.9z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M256,0c-43.7,0-79.3,35.6-79.3,79.3v160.7c0,43.7,35.6,79.3,79.3,79.3c43.7,0,79.3-35.6,79.3-79.3V79.3
|
||||
C335.3,35.6,299.7,0,256,0z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 937 B |
1
front/src/Components/images/monitor-close.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg id="Capa_1" data-name="Capa 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 469.33 426.67"><defs><style>.cls-1{fill:#fff;}</style></defs><path class="cls-1" d="M426.67,21.33h-384A42.66,42.66,0,0,0,0,64V320a42.66,42.66,0,0,0,42.67,42.67H192v42.66H149.33V448H320V405.33H277.33V362.67H426.67A42.66,42.66,0,0,0,469.33,320V64A42.66,42.66,0,0,0,426.67,21.33Zm0,298.67h-384V64h384Z" transform="translate(0 -21.33)"/><path class="cls-1" d="M267.2,127.15V86.26a8.14,8.14,0,0,1,14.18-5.44l73.2,81.34a8.12,8.12,0,0,1,.25,10.57l-73.2,89.47a8.14,8.14,0,0,1-14.43-5.13V216.54c-64.25,2.09-104.35,29.55-122.42,83.77a8.13,8.13,0,0,1-15.84-2.58C128.94,202,186.59,131.59,267.2,127.15Zm8.14,73a8.13,8.13,0,0,1,8.13,8.14v26l54.36-66.44-54.36-60.39v27.6a8.13,8.13,0,0,1-8.13,8.14c-63.93,0-111.77,44.24-125.87,111.73C175.65,218.53,217.8,200.13,275.34,200.13Z" transform="translate(0 -21.33)"/></svg>
|
After Width: | Height: | Size: 884 B |
24
front/src/Components/images/monitor.svg
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 469.3 469.3" style="enable-background:new 0 0 469.3 469.3;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FFFFFF;}
|
||||
.st1{opacity:0.9;fill:#FFFFFF;stroke:#FFFFFF;stroke-width:15;stroke-miterlimit:10;enable-background:new ;}
|
||||
</style>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M426.7,21.3h-384C19.1,21.3,0,40.4,0,64v256c0,23.6,19.1,42.7,42.7,42.7H192v42.7h-42.7V448H320v-42.7h-42.7
|
||||
v-42.7h149.3c23.6,0,42.7-19.1,42.7-42.7V64C469.3,40.4,450.2,21.3,426.7,21.3z M426.7,320h-384V64h384V320z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<g>
|
||||
<path class="st0" d="M267.2,127.2V86.3c0-4.5,3.6-8.1,8.1-8.1c2.3,0,4.5,1,6,2.7l73.2,81.3c2.7,3,2.8,7.5,0.3,10.6l-73.2,89.5
|
||||
c-2.8,3.5-8,4-11.4,1.1c-1.9-1.5-3-3.8-3-6.3v-40.5c-64.3,2.1-104.4,29.6-122.4,83.8c-1.1,3.3-4.2,5.6-7.7,5.6
|
||||
c-0.4,0-0.9,0-1.3-0.1c-3.9-0.6-6.8-4-6.8-8C128.9,202,186.6,131.6,267.2,127.2z M275.3,200.1c4.5,0,8.1,3.6,8.1,8.1v26l54.4-66.4
|
||||
l-54.4-60.4V135c0,4.5-3.6,8.1-8.1,8.1c-63.9,0-111.8,44.2-125.9,111.7C175.6,218.5,217.8,200.1,275.3,200.1z"/>
|
||||
</g>
|
||||
</g>
|
||||
<path class="st1" d="M13.4,42.7C153.6,142.3,293.8,241.9,434,341.5"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -0,0 +1,92 @@
|
|||
<script lang="typescript">
|
||||
import type { Game } from "../../Phaser/Game/Game";
|
||||
import {SelectCharacterScene, SelectCharacterSceneName} from "../../Phaser/Login/SelectCharacterScene";
|
||||
|
||||
export let game: Game;
|
||||
|
||||
const selectCharacterScene = game.scene.getScene(SelectCharacterSceneName) as SelectCharacterScene;
|
||||
|
||||
function selectLeft() {
|
||||
selectCharacterScene.moveToLeft();
|
||||
}
|
||||
|
||||
function selectRight() {
|
||||
selectCharacterScene.moveToRight();
|
||||
}
|
||||
|
||||
function cameraScene() {
|
||||
selectCharacterScene.nextSceneToCameraScene();
|
||||
}
|
||||
|
||||
function customizeScene() {
|
||||
selectCharacterScene.nextSceneToCustomizeScene();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<form class="selectCharacterScene">
|
||||
<section class="text-center">
|
||||
<h2>Select your WOKA</h2>
|
||||
<button class="selectCharacterButton selectCharacterButtonLeft nes-btn" on:click|preventDefault={ selectLeft }> < </button>
|
||||
<button class="selectCharacterButton selectCharacterButtonRight nes-btn" on:click|preventDefault={ selectRight }> > </button>
|
||||
</section>
|
||||
<section class="action">
|
||||
<button type="submit" class="selectCharacterSceneFormSubmit nes-btn is-primary" on:click|preventDefault={ cameraScene }>Continue</button>
|
||||
<button type="submit" class="selectCharacterSceneFormCustomYourOwnSubmit nes-btn" on:click|preventDefault={ customizeScene }>Customize your WOKA</button>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<style lang="scss">
|
||||
form.selectCharacterScene {
|
||||
font-family: "Press Start 2P";
|
||||
pointer-events: auto;
|
||||
color: #ebeeee;
|
||||
|
||||
section {
|
||||
margin: 10px;
|
||||
|
||||
&.action {
|
||||
text-align: center;
|
||||
margin-top: 55vh;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-family: "Press Start 2P";
|
||||
margin: 1px;
|
||||
}
|
||||
|
||||
&.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button.selectCharacterButton {
|
||||
position: absolute;
|
||||
top: 33vh;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: "Press Start 2P";
|
||||
|
||||
&.selectCharacterButtonLeft {
|
||||
left: 33vw;
|
||||
}
|
||||
|
||||
&.selectCharacterButtonRight {
|
||||
right: 33vw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 800px) {
|
||||
form.selectCharacterScene button.selectCharacterButtonLeft{
|
||||
left: 5vw;
|
||||
}
|
||||
form.selectCharacterScene button.selectCharacterButtonRight{
|
||||
right: 5vw;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
35
front/src/Connexion/AdminMessagesService.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import {Subject} from "rxjs";
|
||||
import type {BanUserMessage, SendUserMessage} from "../Messages/generated/messages_pb";
|
||||
|
||||
export enum AdminMessageEventTypes {
|
||||
admin = 'message',
|
||||
audio = 'audio',
|
||||
ban = 'ban',
|
||||
banned = 'banned',
|
||||
}
|
||||
|
||||
interface AdminMessageEvent {
|
||||
type: AdminMessageEventTypes,
|
||||
text: string;
|
||||
//todo add optional properties for other event types
|
||||
}
|
||||
|
||||
//this class is designed to easily allow communication between the RoomConnection objects (that receive the message)
|
||||
//and the various objects that may render the message on screen
|
||||
class AdminMessagesService {
|
||||
private _messageStream: Subject<AdminMessageEvent> = new Subject();
|
||||
public messageStream = this._messageStream.asObservable();
|
||||
|
||||
constructor() {
|
||||
this.messageStream.subscribe((event) => console.log('message', event))
|
||||
}
|
||||
|
||||
onSendusermessage(message: SendUserMessage|BanUserMessage) {
|
||||
this._messageStream.next({
|
||||
type: message.getType() as unknown as AdminMessageEventTypes,
|
||||
text: message.getMessage(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const adminMessagesService = new AdminMessagesService();
|
|
@ -1,18 +1,30 @@
|
|||
import Axios from "axios";
|
||||
import {API_URL} from "../Enum/EnvironmentVariable";
|
||||
import {PUSHER_URL, START_ROOM_URL} from "../Enum/EnvironmentVariable";
|
||||
import {RoomConnection} from "./RoomConnection";
|
||||
import {OnConnectInterface, PositionInterface, ViewportInterface} from "./ConnexionModels";
|
||||
import type {OnConnectInterface, PositionInterface, ViewportInterface} from "./ConnexionModels";
|
||||
import {GameConnexionTypes, urlManager} from "../Url/UrlManager";
|
||||
import {localUserStore} from "./LocalUserStore";
|
||||
import {LocalUser} from "./LocalUser";
|
||||
import {CharacterTexture, LocalUser} from "./LocalUser";
|
||||
import {Room} from "./Room";
|
||||
|
||||
const URL_ROOM_STARTED = '/Floor0/floor0.json';
|
||||
|
||||
class ConnectionManager {
|
||||
private localUser!:LocalUser;
|
||||
|
||||
private connexionType?: GameConnexionTypes
|
||||
private reconnectingTimeout: NodeJS.Timeout|null = null;
|
||||
private _unloading:boolean = false;
|
||||
|
||||
get unloading () {
|
||||
return this._unloading;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
window.addEventListener('beforeunload', () => {
|
||||
this._unloading = true;
|
||||
if (this.reconnectingTimeout) clearTimeout(this.reconnectingTimeout)
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Tries to login to the node server and return the starting map url to be loaded
|
||||
*/
|
||||
|
@ -22,20 +34,20 @@ class ConnectionManager {
|
|||
this.connexionType = connexionType;
|
||||
if(connexionType === GameConnexionTypes.register) {
|
||||
const organizationMemberToken = urlManager.getOrganizationToken();
|
||||
const data = await Axios.post(`${API_URL}/register`, {organizationMemberToken}).then(res => res.data);
|
||||
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;
|
||||
urlManager.editUrlForRoom(roomSlug, organizationSlug, worldSlug);
|
||||
|
||||
const room = new Room(window.location.pathname + window.location.hash);
|
||||
const room = new Room('/@/'+organizationSlug+'/'+worldSlug+'/'+roomSlug + window.location.search + window.location.hash);
|
||||
urlManager.pushRoomIdToUrl(room);
|
||||
return Promise.resolve(room);
|
||||
} else if (connexionType === GameConnexionTypes.organization || connexionType === GameConnexionTypes.anonymous || connexionType === GameConnexionTypes.empty) {
|
||||
const localUser = localUserStore.getLocalUser();
|
||||
|
||||
let localUser = localUserStore.getLocalUser();
|
||||
if (localUser && localUser.jwtToken && localUser.uuid && localUser.textures) {
|
||||
this.localUser = localUser;
|
||||
try {
|
||||
|
@ -45,28 +57,53 @@ class ConnectionManager {
|
|||
console.error('JWT token invalid. Did it expire? Login anonymously instead.');
|
||||
await this.anonymousLogin();
|
||||
}
|
||||
} else {
|
||||
}else{
|
||||
await this.anonymousLogin();
|
||||
}
|
||||
let roomId: string
|
||||
if (connexionType === GameConnexionTypes.empty) {
|
||||
const defaultMapUrl = window.location.host.replace('play.', 'maps.') + URL_ROOM_STARTED;
|
||||
roomId = urlManager.editUrlForRoom(defaultMapUrl, null, null);
|
||||
} else {
|
||||
roomId = window.location.pathname + window.location.hash;
|
||||
|
||||
localUser = localUserStore.getLocalUser();
|
||||
if(!localUser){
|
||||
throw "Error to store local user data";
|
||||
}
|
||||
return Promise.resolve(new Room(roomId));
|
||||
|
||||
let roomId: string;
|
||||
if (connexionType === GameConnexionTypes.empty) {
|
||||
roomId = START_ROOM_URL;
|
||||
} else {
|
||||
roomId = 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) {
|
||||
//check if texture was changed
|
||||
if(localUser.textures.length === 0){
|
||||
localUser.textures = mapDetail.textures;
|
||||
}else{
|
||||
mapDetail.textures.forEach((newTexture) => {
|
||||
const alreadyExistTexture = localUser?.textures.find((c) => newTexture.id === c.id);
|
||||
if(localUser?.textures.findIndex((c) => newTexture.id === c.id) !== -1){
|
||||
return;
|
||||
}
|
||||
localUser?.textures.push(newTexture)
|
||||
});
|
||||
}
|
||||
this.localUser = localUser;
|
||||
localUserStore.saveUser(localUser);
|
||||
}
|
||||
return Promise.resolve(room);
|
||||
}
|
||||
|
||||
return Promise.reject('Invalid URL');
|
||||
return Promise.reject(new Error('Invalid URL'));
|
||||
}
|
||||
|
||||
private async verifyToken(token: string): Promise<void> {
|
||||
await Axios.get(`${API_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(`${API_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.
|
||||
localUserStore.saveUser(this.localUser);
|
||||
|
@ -77,9 +114,9 @@ class ConnectionManager {
|
|||
this.localUser = new LocalUser('', 'test', []);
|
||||
}
|
||||
|
||||
public connectToRoomSocket(roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface): Promise<OnConnectInterface> {
|
||||
public connectToRoomSocket(roomId: 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);
|
||||
const connection = new RoomConnection(this.localUser.jwtToken, roomId, name, characterLayers, position, viewport, companion);
|
||||
connection.onConnectError((error: object) => {
|
||||
console.log('An error occurred while connecting to socket server. Retrying');
|
||||
reject(error);
|
||||
|
@ -97,10 +134,10 @@ class ConnectionManager {
|
|||
}).catch((err) => {
|
||||
// Let's retry in 4-6 seconds
|
||||
return new Promise<OnConnectInterface>((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
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).then((connection) => resolve(connection));
|
||||
this.connectToRoomSocket(roomId, name, characterLayers, position, viewport, companion).then((connection) => resolve(connection));
|
||||
}, 4000 + Math.floor(Math.random() * 2000) );
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import {PlayerAnimationNames} from "../Phaser/Player/Animation";
|
||||
import {UserSimplePeerInterface} from "../WebRtc/SimplePeer";
|
||||
import {SignalData} from "simple-peer";
|
||||
import {BodyResourceDescriptionInterface} from "../Phaser/Entity/body_character";
|
||||
import {RoomConnection} from "./RoomConnection";
|
||||
import type {SignalData} from "simple-peer";
|
||||
import type {RoomConnection} from "./RoomConnection";
|
||||
import type {BodyResourceDescriptionInterface} from "../Phaser/Entity/PlayerTextures";
|
||||
|
||||
export enum EventMessage{
|
||||
CONNECT = "connect",
|
||||
|
@ -42,19 +40,13 @@ export interface PointInterface {
|
|||
moving: boolean;
|
||||
}
|
||||
|
||||
export class Point implements PointInterface{
|
||||
constructor(public x : number, public y : number, public direction : string = PlayerAnimationNames.WalkDown, public moving : boolean = false) {
|
||||
if(x === null || y === null){
|
||||
throw Error("position x and y cannot be null");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface MessageUserPositionInterface {
|
||||
userId: number;
|
||||
name: string;
|
||||
characterLayers: BodyResourceDescriptionInterface[];
|
||||
position: PointInterface;
|
||||
visitCardUrl: string|null;
|
||||
companion: string|null;
|
||||
}
|
||||
|
||||
export interface MessageUserMovedInterface {
|
||||
|
@ -66,7 +58,9 @@ export interface MessageUserJoined {
|
|||
userId: number;
|
||||
name: string;
|
||||
characterLayers: BodyResourceDescriptionInterface[];
|
||||
position: PointInterface
|
||||
position: PointInterface;
|
||||
visitCardUrl: string | null;
|
||||
companion: string|null;
|
||||
}
|
||||
|
||||
export interface PositionInterface {
|
||||
|
@ -80,28 +74,15 @@ export interface GroupCreatedUpdatedMessageInterface {
|
|||
groupSize: number
|
||||
}
|
||||
|
||||
export interface WebRtcStartMessageInterface {
|
||||
roomId: string,
|
||||
clients: UserSimplePeerInterface[]
|
||||
}
|
||||
|
||||
export interface WebRtcDisconnectMessageInterface {
|
||||
userId: number
|
||||
}
|
||||
|
||||
export interface WebRtcSignalSentMessageInterface {
|
||||
receiverId: number,
|
||||
signal: SignalData
|
||||
}
|
||||
|
||||
export interface WebRtcSignalReceivedMessageInterface {
|
||||
userId: number,
|
||||
signal: SignalData
|
||||
}
|
||||
|
||||
export interface StartMapInterface {
|
||||
mapUrlStart: string,
|
||||
startInstance: string
|
||||
signal: SignalData,
|
||||
webRtcUser: string | undefined,
|
||||
webRtcPassword: string | undefined
|
||||
}
|
||||
|
||||
export interface ViewportInterface {
|
||||
|
@ -111,11 +92,6 @@ export interface ViewportInterface {
|
|||
bottom: number,
|
||||
}
|
||||
|
||||
export interface BatchedMessageInterface {
|
||||
event: string,
|
||||
payload: unknown
|
||||
}
|
||||
|
||||
export interface ItemEventMessageInterface {
|
||||
itemId: number,
|
||||
event: string,
|
||||
|
|
19
front/src/Connexion/EmoteEventStream.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import {Subject} from "rxjs";
|
||||
|
||||
interface EmoteEvent {
|
||||
userId: number,
|
||||
emoteName: string,
|
||||
}
|
||||
|
||||
class EmoteEventStream {
|
||||
|
||||
private _stream:Subject<EmoteEvent> = new Subject();
|
||||
public stream = this._stream.asObservable();
|
||||
|
||||
|
||||
fire(userId: number, emoteName:string) {
|
||||
this._stream.next({userId, emoteName});
|
||||
}
|
||||
}
|
||||
|
||||
export const emoteEventStream = new EmoteEventStream();
|
|
@ -1,3 +1,5 @@
|
|||
import {MAX_USERNAME_LENGTH} from "../Enum/EnvironmentVariable";
|
||||
|
||||
export interface CharacterTexture {
|
||||
id: number,
|
||||
level: number,
|
||||
|
@ -5,7 +7,23 @@ export interface CharacterTexture {
|
|||
rights: string
|
||||
}
|
||||
|
||||
export const maxUserNameLength: number = MAX_USERNAME_LENGTH;
|
||||
|
||||
export function isUserNameValid(value: unknown): boolean {
|
||||
return typeof value === "string" && value.length > 0 && value.length <= maxUserNameLength && value.indexOf(' ') === -1;
|
||||
}
|
||||
|
||||
export function areCharacterLayersValid(value: string[] | null): boolean {
|
||||
if (!value || !value.length) return false;
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
if (/^\w+$/.exec(value[i]) === null) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export class LocalUser {
|
||||
constructor(public readonly uuid:string, public readonly jwtToken: string, public readonly textures: CharacterTexture[]) {
|
||||
constructor(public readonly uuid:string, public readonly jwtToken: string, public textures: CharacterTexture[]) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
import {LocalUser} from "./LocalUser";
|
||||
import {areCharacterLayersValid, isUserNameValid, LocalUser} from "./LocalUser";
|
||||
|
||||
const characterLayersKey = 'characterLayers';
|
||||
const gameQualityKey = 'gameQuality';
|
||||
const videoQualityKey = 'videoQuality';
|
||||
const playerNameKey = 'playerName';
|
||||
const selectedPlayerKey = 'selectedPlayer';
|
||||
const customCursorPositionKey = 'customCursorPosition';
|
||||
const characterLayersKey = 'characterLayers';
|
||||
const companionKey = 'companion';
|
||||
const gameQualityKey = 'gameQuality';
|
||||
const videoQualityKey = 'videoQuality';
|
||||
const audioPlayerVolumeKey = 'audioVolume';
|
||||
const audioPlayerMuteKey = 'audioMute';
|
||||
const helpCameraSettingsShown = 'helpCameraSettingsShown';
|
||||
const fullscreenKey = 'fullscreen';
|
||||
|
||||
//todo: add localstorage fallback
|
||||
class LocalUserStore {
|
||||
|
||||
saveUser(localUser: LocalUser) {
|
||||
localStorage.setItem('localUser', JSON.stringify(localUser));
|
||||
}
|
||||
|
@ -14,48 +20,94 @@ class LocalUserStore {
|
|||
const data = localStorage.getItem('localUser');
|
||||
return data ? JSON.parse(data) : null;
|
||||
}
|
||||
|
||||
|
||||
setName(name:string): void {
|
||||
window.localStorage.setItem('playerName', name);
|
||||
localStorage.setItem(playerNameKey, name);
|
||||
}
|
||||
getName(): string {
|
||||
return window.localStorage.getItem('playerName') ?? '';
|
||||
getName(): string|null {
|
||||
const value = localStorage.getItem(playerNameKey) || '';
|
||||
return isUserNameValid(value) ? value : null;
|
||||
}
|
||||
|
||||
setPlayerCharacterIndex(playerCharacterIndex: number): void {
|
||||
window.localStorage.setItem('selectedPlayer', ''+playerCharacterIndex);
|
||||
localStorage.setItem(selectedPlayerKey, ''+playerCharacterIndex);
|
||||
}
|
||||
getPlayerCharacterIndex(): number {
|
||||
return parseInt(window.localStorage.getItem('selectedPlayer') || '');
|
||||
return parseInt(localStorage.getItem(selectedPlayerKey) || '');
|
||||
}
|
||||
|
||||
setCustomCursorPosition(activeRow:number, selectedLayers: number[]): void {
|
||||
window.localStorage.setItem('customCursorPosition', JSON.stringify({activeRow, selectedLayers}));
|
||||
localStorage.setItem(customCursorPositionKey, JSON.stringify({activeRow, selectedLayers}));
|
||||
}
|
||||
getCustomCursorPosition(): {activeRow:number, selectedLayers:number[]}|null {
|
||||
return JSON.parse(window.localStorage.getItem('customCursorPosition') || "null");
|
||||
return JSON.parse(localStorage.getItem(customCursorPositionKey) || "null");
|
||||
}
|
||||
|
||||
setCharacterLayers(layers: string[]): void {
|
||||
window.localStorage.setItem(characterLayersKey, JSON.stringify(layers));
|
||||
localStorage.setItem(characterLayersKey, JSON.stringify(layers));
|
||||
}
|
||||
getCharacterLayers(): string[]|null {
|
||||
return JSON.parse(window.localStorage.getItem(characterLayersKey) || "null");
|
||||
const value = JSON.parse(localStorage.getItem(characterLayersKey) || "null");
|
||||
return areCharacterLayersValid(value) ? value : null;
|
||||
}
|
||||
|
||||
getGameQualityValue(): number {
|
||||
return parseInt(window.localStorage.getItem(gameQualityKey) || '') || 60;
|
||||
|
||||
setCompanion(companion: string|null): void {
|
||||
return localStorage.setItem(companionKey, JSON.stringify(companion));
|
||||
}
|
||||
getCompanion(): string|null {
|
||||
const companion = JSON.parse(localStorage.getItem(companionKey) || "null");
|
||||
|
||||
if (typeof companion !== "string" || companion === "") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return companion;
|
||||
}
|
||||
wasCompanionSet(): boolean {
|
||||
return localStorage.getItem(companionKey) ? true : false;
|
||||
}
|
||||
|
||||
setGameQualityValue(value: number): void {
|
||||
localStorage.setItem(gameQualityKey, '' + value);
|
||||
}
|
||||
|
||||
getVideoQualityValue(): number {
|
||||
return parseInt(window.localStorage.getItem(videoQualityKey) || '') || 20;
|
||||
getGameQualityValue(): number {
|
||||
return parseInt(localStorage.getItem(gameQualityKey) || '60');
|
||||
}
|
||||
|
||||
setVideoQualityValue(value: number): void {
|
||||
localStorage.setItem(videoQualityKey, '' + value);
|
||||
}
|
||||
getVideoQualityValue(): number {
|
||||
return parseInt(localStorage.getItem(videoQualityKey) || '20');
|
||||
}
|
||||
|
||||
setAudioPlayerVolume(value: number): void {
|
||||
localStorage.setItem(audioPlayerVolumeKey, '' + value);
|
||||
}
|
||||
getAudioPlayerVolume(): number {
|
||||
return parseFloat(localStorage.getItem(audioPlayerVolumeKey) || '1');
|
||||
}
|
||||
|
||||
setAudioPlayerMuted(value: boolean): void {
|
||||
localStorage.setItem(audioPlayerMuteKey, value.toString());
|
||||
}
|
||||
getAudioPlayerMuted(): boolean {
|
||||
return localStorage.getItem(audioPlayerMuteKey) === 'true';
|
||||
}
|
||||
|
||||
setHelpCameraSettingsShown(): void {
|
||||
localStorage.setItem(helpCameraSettingsShown, '1');
|
||||
}
|
||||
getHelpCameraSettingsShown(): boolean {
|
||||
return localStorage.getItem(helpCameraSettingsShown) === '1';
|
||||
}
|
||||
|
||||
setFullscreen(value: boolean): void {
|
||||
localStorage.setItem(fullscreenKey, value.toString());
|
||||
}
|
||||
getFullscreen(): boolean {
|
||||
return localStorage.getItem(fullscreenKey) === 'true';
|
||||
}
|
||||
}
|
||||
|
||||
export const localUserStore = new LocalUserStore();
|
||||
export const localUserStore = new LocalUserStore();
|
||||
|
|
|
@ -1,31 +1,39 @@
|
|||
import Axios from "axios";
|
||||
import {API_URL} from "../Enum/EnvironmentVariable";
|
||||
import {PUSHER_URL} from "../Enum/EnvironmentVariable";
|
||||
import type {CharacterTexture} from "./LocalUser";
|
||||
|
||||
export class MapDetail{
|
||||
constructor(public readonly mapUrl: string, public readonly textures : CharacterTexture[]|undefined) {
|
||||
}
|
||||
}
|
||||
|
||||
export class Room {
|
||||
public readonly id: string;
|
||||
public readonly isPublic: boolean;
|
||||
private mapUrl: string|undefined;
|
||||
private textures: CharacterTexture[]|undefined;
|
||||
private instance: string|undefined;
|
||||
private _search: URLSearchParams;
|
||||
|
||||
constructor(id: string) {
|
||||
if (id.startsWith('/')) {
|
||||
id = id.substr(1);
|
||||
const url = new URL(id, 'https://example.com');
|
||||
|
||||
this.id = url.pathname;
|
||||
|
||||
if (this.id.startsWith('/')) {
|
||||
this.id = this.id.substr(1);
|
||||
}
|
||||
this.id = id;
|
||||
if (id.startsWith('_/')) {
|
||||
if (this.id.startsWith('_/')) {
|
||||
this.isPublic = true;
|
||||
} else if (id.startsWith('@/')) {
|
||||
} else if (this.id.startsWith('@/')) {
|
||||
this.isPublic = false;
|
||||
} else {
|
||||
throw new Error('Invalid room ID');
|
||||
}
|
||||
|
||||
const indexOfHash = this.id.indexOf('#');
|
||||
if (indexOfHash !== -1) {
|
||||
this.id = this.id.substr(0, indexOfHash);
|
||||
}
|
||||
this._search = new URLSearchParams(url.search);
|
||||
}
|
||||
|
||||
|
||||
public static getIdFromIdentifier(identifier: string, baseUrl: string, currentInstance: string): {roomId: string, hash: string} {
|
||||
let roomId = '';
|
||||
let hash = '';
|
||||
|
@ -49,10 +57,10 @@ export class Room {
|
|||
return {roomId, hash}
|
||||
}
|
||||
|
||||
public async getMapUrl(): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
if (this.mapUrl !== undefined) {
|
||||
resolve(this.mapUrl);
|
||||
public async getMapDetail(): Promise<MapDetail> {
|
||||
return new Promise<MapDetail>((resolve, reject) => {
|
||||
if (this.mapUrl !== undefined && this.textures != undefined) {
|
||||
resolve(new MapDetail(this.mapUrl, this.textures));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -60,20 +68,21 @@ export class Room {
|
|||
const match = /_\/[^/]+\/(.+)/.exec(this.id);
|
||||
if (!match) throw new Error('Could not extract url from "'+this.id+'"');
|
||||
this.mapUrl = window.location.protocol+'//'+match[1];
|
||||
resolve(this.mapUrl);
|
||||
resolve(new MapDetail(this.mapUrl, this.textures));
|
||||
return;
|
||||
} else {
|
||||
// We have a private ID, we need to query the map URL from the server.
|
||||
const urlParts = this.parsePrivateUrl(this.id);
|
||||
|
||||
Axios.get(`${API_URL}/map`, {
|
||||
Axios.get(`${PUSHER_URL}/map`, {
|
||||
params: urlParts
|
||||
}).then(({data}) => {
|
||||
console.log('Map ', this.id, ' resolves to URL ', data.mapUrl);
|
||||
resolve(data.mapUrl);
|
||||
resolve(data);
|
||||
return;
|
||||
}).catch((reason) => {
|
||||
reject(reason);
|
||||
});
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -116,4 +125,17 @@ export class Room {
|
|||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
public isDisconnected(): boolean
|
||||
{
|
||||
const alone = this._search.get('alone');
|
||||
if (alone && alone !== '0' && alone.toLowerCase() !== 'false') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public get search(): URLSearchParams {
|
||||
return this._search;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {API_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable";
|
||||
import {PUSHER_URL, UPLOADER_URL} from "../Enum/EnvironmentVariable";
|
||||
import Axios from "axios";
|
||||
import {
|
||||
BatchMessage,
|
||||
|
@ -27,10 +27,13 @@ import {
|
|||
SendJitsiJwtMessage,
|
||||
CharacterLayerMessage,
|
||||
PingMessage,
|
||||
SendUserMessage
|
||||
EmoteEventMessage,
|
||||
EmotePromptMessage,
|
||||
SendUserMessage,
|
||||
BanUserMessage,
|
||||
} from "../Messages/generated/messages_pb"
|
||||
|
||||
import {UserSimplePeerInterface} from "../WebRtc/SimplePeer";
|
||||
import type {UserSimplePeerInterface} from "../WebRtc/SimplePeer";
|
||||
import Direction = PositionMessage.Direction;
|
||||
import {ProtobufClientUtils} from "../Network/ProtobufClientUtils";
|
||||
import {
|
||||
|
@ -41,7 +44,12 @@ import {
|
|||
ViewportInterface, WebRtcDisconnectMessageInterface,
|
||||
WebRtcSignalReceivedMessageInterface,
|
||||
} from "./ConnexionModels";
|
||||
import {BodyResourceDescriptionInterface} from "../Phaser/Entity/body_character";
|
||||
import type {BodyResourceDescriptionInterface} from "../Phaser/Entity/PlayerTextures";
|
||||
import {adminMessagesService} from "./AdminMessagesService";
|
||||
import {worldFullMessageStream} from "./WorldFullMessageStream";
|
||||
import {worldFullWarningStream} from "./WorldFullWarningStream";
|
||||
import {connectionManager} from "./ConnectionManager";
|
||||
import {emoteEventStream} from "./EmoteEventStream";
|
||||
|
||||
const manualPingDelay = 20000;
|
||||
|
||||
|
@ -62,9 +70,13 @@ 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]"
|
||||
*/
|
||||
public constructor(token: string|null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface) {
|
||||
let url = API_URL.replace('http://', 'ws://').replace('https://', 'wss://');
|
||||
url += '/room';
|
||||
public constructor(token: string|null, roomId: string, name: string, characterLayers: string[], position: PositionInterface, viewport: ViewportInterface, companion: string|null) {
|
||||
let url = new URL(PUSHER_URL, window.location.toString()).toString();
|
||||
url = url.replace('http://', 'ws://').replace('https://', 'wss://');
|
||||
if (!url.endsWith('/')) {
|
||||
url += '/';
|
||||
}
|
||||
url += 'room';
|
||||
url += '?roomId='+(roomId ?encodeURIComponent(roomId):'');
|
||||
url += '&token='+(token ?encodeURIComponent(token):'');
|
||||
url += '&name='+encodeURIComponent(name);
|
||||
|
@ -78,6 +90,10 @@ export class RoomConnection implements RoomConnection {
|
|||
url += '&left='+Math.floor(viewport.left);
|
||||
url += '&right='+Math.floor(viewport.right);
|
||||
|
||||
if (typeof companion === 'string') {
|
||||
url += '&companion='+encodeURIComponent(companion);
|
||||
}
|
||||
|
||||
if (RoomConnection.websocketFactory) {
|
||||
this.socket = RoomConnection.websocketFactory(url);
|
||||
} else {
|
||||
|
@ -100,7 +116,7 @@ export class RoomConnection implements RoomConnection {
|
|||
}
|
||||
|
||||
// If we are not connected yet (if a JoinRoomMessage was not sent), we need to retry.
|
||||
if (this.userId === null) {
|
||||
if (this.userId === null && !this.closed) {
|
||||
this.dispatch(EventMessage.CONNECTING_ERROR, event);
|
||||
}
|
||||
});
|
||||
|
@ -111,7 +127,7 @@ export class RoomConnection implements RoomConnection {
|
|||
|
||||
if (message.hasBatchmessage()) {
|
||||
for (const subMessage of (message.getBatchmessage() as BatchMessage).getPayloadList()) {
|
||||
let event: string;
|
||||
let event: string|null = null;
|
||||
let payload;
|
||||
if (subMessage.hasUsermovedmessage()) {
|
||||
event = EventMessage.USER_MOVED;
|
||||
|
@ -131,17 +147,20 @@ export class RoomConnection implements RoomConnection {
|
|||
} else if (subMessage.hasItemeventmessage()) {
|
||||
event = EventMessage.ITEM_EVENT;
|
||||
payload = subMessage.getItemeventmessage();
|
||||
} else if (subMessage.hasEmoteeventmessage()) {
|
||||
const emoteMessage = subMessage.getEmoteeventmessage() as EmoteEventMessage;
|
||||
emoteEventStream.fire(emoteMessage.getActoruserid(), emoteMessage.getEmote());
|
||||
} else {
|
||||
throw new Error('Unexpected batch message type');
|
||||
}
|
||||
|
||||
this.dispatch(event, payload);
|
||||
if (event) {
|
||||
this.dispatch(event, payload);
|
||||
}
|
||||
}
|
||||
} else if (message.hasRoomjoinedmessage()) {
|
||||
const roomJoinedMessage = message.getRoomjoinedmessage() as RoomJoinedMessage;
|
||||
|
||||
//const users: Array<MessageUserJoined> = roomJoinedMessage.getUserList().map(this.toMessageUserJoined.bind(this));
|
||||
//const groups: Array<GroupCreatedUpdatedMessageInterface> = roomJoinedMessage.getGroupList().map(this.toGroupCreatedUpdatedMessage.bind(this));
|
||||
const items: { [itemId: number] : unknown } = {};
|
||||
for (const item of roomJoinedMessage.getItemList()) {
|
||||
items[item.getItemid()] = JSON.parse(item.getStatejson());
|
||||
|
@ -150,25 +169,19 @@ export class RoomConnection implements RoomConnection {
|
|||
this.userId = roomJoinedMessage.getCurrentuserid();
|
||||
this.tags = roomJoinedMessage.getTagList();
|
||||
|
||||
//console.log('Dispatching CONNECT')
|
||||
this.dispatch(EventMessage.CONNECT, {
|
||||
connection: this,
|
||||
room: {
|
||||
//users,
|
||||
//groups,
|
||||
items
|
||||
} as RoomJoinedMessageInterface
|
||||
});
|
||||
|
||||
/*console.log('Dispatching START_ROOM')
|
||||
this.dispatch(EventMessage.START_ROOM, {
|
||||
//users,
|
||||
//groups,
|
||||
items
|
||||
});*/
|
||||
} else if (message.hasErrormessage()) {
|
||||
console.error(EventMessage.MESSAGE_ERROR, message.getErrormessage()?.getMessage());
|
||||
} else if (message.hasWebrtcsignaltoclientmessage()) {
|
||||
} else if (message.hasWorldfullmessage()) {
|
||||
worldFullMessageStream.onMessage();
|
||||
this.closed = true;
|
||||
} else if (message.hasWorldconnexionmessage()) {
|
||||
worldFullMessageStream.onMessage(message.getWorldconnexionmessage()?.getMessage());
|
||||
this.closed = true;
|
||||
}else if (message.hasWebrtcsignaltoclientmessage()) {
|
||||
this.dispatch(EventMessage.WEBRTC_SIGNAL, message.getWebrtcsignaltoclientmessage());
|
||||
} else if (message.hasWebrtcscreensharingsignaltoclientmessage()) {
|
||||
this.dispatch(EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL, message.getWebrtcscreensharingsignaltoclientmessage());
|
||||
|
@ -185,7 +198,13 @@ export class RoomConnection implements RoomConnection {
|
|||
} else if (message.hasSendjitsijwtmessage()) {
|
||||
this.dispatch(EventMessage.START_JITSI_ROOM, message.getSendjitsijwtmessage());
|
||||
} else if (message.hasSendusermessage()) {
|
||||
this.dispatch(EventMessage.USER_MESSAGE, message.getSendusermessage());
|
||||
adminMessagesService.onSendusermessage(message.getSendusermessage() as SendUserMessage);
|
||||
} else if (message.hasBanusermessage()) {
|
||||
adminMessagesService.onSendusermessage(message.getBanusermessage() as BanUserMessage);
|
||||
} else if (message.hasWorldfullwarningmessage()) {
|
||||
worldFullWarningStream.onMessage();
|
||||
} else if (message.hasRefreshroommessage()) {
|
||||
//todo: implement a way to notify the user the room was refreshed.
|
||||
} else {
|
||||
throw new Error('Unknown message received');
|
||||
}
|
||||
|
@ -319,11 +338,15 @@ export class RoomConnection implements RoomConnection {
|
|||
}
|
||||
})
|
||||
|
||||
const companion = message.getCompanion();
|
||||
|
||||
return {
|
||||
userId: message.getUserid(),
|
||||
name: message.getName(),
|
||||
characterLayers,
|
||||
position: ProtobufClientUtils.toPointInterface(position)
|
||||
visitCardUrl: message.getVisitcardurl(),
|
||||
position: ProtobufClientUtils.toPointInterface(position),
|
||||
companion: companion ? companion.getName() : null
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -385,9 +408,6 @@ export class RoomConnection implements RoomConnection {
|
|||
this.socket.addEventListener('error', callback)
|
||||
}
|
||||
|
||||
/*public onConnect(callback: (e: Event) => void): void {
|
||||
this.socket.addEventListener('open', callback)
|
||||
}*/
|
||||
public onConnect(callback: (roomConnection: OnConnectInterface) => void): void {
|
||||
//this.socket.addEventListener('open', callback)
|
||||
this.onMessage(EventMessage.CONNECT, callback);
|
||||
|
@ -427,7 +447,9 @@ export class RoomConnection implements RoomConnection {
|
|||
callback({
|
||||
userId: message.getUserid(),
|
||||
name: message.getName(),
|
||||
initiator: message.getInitiator()
|
||||
initiator: message.getInitiator(),
|
||||
webRtcUser: message.getWebrtcusername() ?? undefined,
|
||||
webRtcPassword: message.getWebrtcpassword() ?? undefined,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -436,7 +458,9 @@ export class RoomConnection implements RoomConnection {
|
|||
this.onMessage(EventMessage.WEBRTC_SIGNAL, (message: WebRtcSignalToClientMessage) => {
|
||||
callback({
|
||||
userId: message.getUserid(),
|
||||
signal: JSON.parse(message.getSignal())
|
||||
signal: JSON.parse(message.getSignal()),
|
||||
webRtcUser: message.getWebrtcusername() ?? undefined,
|
||||
webRtcPassword: message.getWebrtcpassword() ?? undefined,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -445,14 +469,16 @@ export class RoomConnection implements RoomConnection {
|
|||
this.onMessage(EventMessage.WEBRTC_SCREEN_SHARING_SIGNAL, (message: WebRtcSignalToClientMessage) => {
|
||||
callback({
|
||||
userId: message.getUserid(),
|
||||
signal: JSON.parse(message.getSignal())
|
||||
signal: JSON.parse(message.getSignal()),
|
||||
webRtcUser: message.getWebrtcusername() ?? undefined,
|
||||
webRtcPassword: message.getWebrtcpassword() ?? undefined,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public onServerDisconnected(callback: (event: CloseEvent) => void): void {
|
||||
public onServerDisconnected(callback: () => void): void {
|
||||
this.socket.addEventListener('close', (event) => {
|
||||
if (this.closed === true) {
|
||||
if (this.closed === true || connectionManager.unloading) {
|
||||
return;
|
||||
}
|
||||
console.log('Socket closed with code '+event.code+". Reason: "+event.reason);
|
||||
|
@ -460,11 +486,12 @@ export class RoomConnection implements RoomConnection {
|
|||
// Normal closure case
|
||||
return;
|
||||
}
|
||||
callback(event);
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
public getUserId(): number|null {
|
||||
public getUserId(): number {
|
||||
if (this.userId === null) throw 'UserId cannot be null!'
|
||||
return this.userId;
|
||||
}
|
||||
|
||||
|
@ -532,12 +559,6 @@ export class RoomConnection implements RoomConnection {
|
|||
});
|
||||
}
|
||||
|
||||
public receiveUserMessage(callback: (type: string, message: string) => void) {
|
||||
return this.onMessage(EventMessage.USER_MESSAGE, (message: SendUserMessage) => {
|
||||
callback(message.getType(), message.getMessage());
|
||||
});
|
||||
}
|
||||
|
||||
public emitGlobalMessage(message: PlayGlobalMessageInterface){
|
||||
const playGlobalMessage = new PlayGlobalMessage();
|
||||
playGlobalMessage.setId(message.id);
|
||||
|
@ -583,8 +604,18 @@ export class RoomConnection implements RoomConnection {
|
|||
public hasTag(tag: string): boolean {
|
||||
return this.tags.includes(tag);
|
||||
}
|
||||
|
||||
|
||||
public isAdmin(): boolean {
|
||||
return this.hasTag('admin');
|
||||
}
|
||||
|
||||
public emitEmoteEvent(emoteName: string): void {
|
||||
const emoteMessage = new EmotePromptMessage();
|
||||
emoteMessage.setEmote(emoteName)
|
||||
|
||||
const clientToServerMessage = new ClientToServerMessage();
|
||||
clientToServerMessage.setEmotepromptmessage(emoteMessage);
|
||||
|
||||
this.socket.send(clientToServerMessage.serializeBinary().buffer);
|
||||
}
|
||||
}
|
||||
|
|
14
front/src/Connexion/WorldFullMessageStream.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import {Subject} from "rxjs";
|
||||
|
||||
class WorldFullMessageStream {
|
||||
|
||||
private _stream:Subject<string|null> = new Subject<string|null>();
|
||||
public stream = this._stream.asObservable();
|
||||
|
||||
|
||||
onMessage(message? :string) {
|
||||
this._stream.next(message);
|
||||
}
|
||||
}
|
||||
|
||||
export const worldFullMessageStream = new WorldFullMessageStream();
|
14
front/src/Connexion/WorldFullWarningStream.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import {Subject} from "rxjs";
|
||||
|
||||
class WorldFullWarningStream {
|
||||
|
||||
private _stream:Subject<void> = new Subject();
|
||||
public stream = this._stream.asObservable();
|
||||
|
||||
|
||||
onMessage() {
|
||||
this._stream.next();
|
||||
}
|
||||
}
|
||||
|
||||
export const worldFullWarningStream = new WorldFullWarningStream();
|
|
@ -1,36 +0,0 @@
|
|||
declare let window:WindowWithCypressAsserter;
|
||||
|
||||
interface WindowWithCypressAsserter extends Window {
|
||||
cypressAsserter: CypressAsserter;
|
||||
}
|
||||
|
||||
//this class is used to communicate with cypress, our e2e testing client
|
||||
//Since cypress cannot manipulate canvas, we notified it with console logs
|
||||
class CypressAsserter {
|
||||
|
||||
constructor() {
|
||||
window.cypressAsserter = this
|
||||
}
|
||||
|
||||
gameStarted() {
|
||||
console.log('Started the game')
|
||||
}
|
||||
|
||||
preloadStarted() {
|
||||
console.log('Preloading')
|
||||
}
|
||||
|
||||
preloadFinished() {
|
||||
console.log('Preloading done')
|
||||
}
|
||||
|
||||
initStarted() {
|
||||
console.log('startInit')
|
||||
}
|
||||
|
||||
initFinished() {
|
||||
console.log('startInit done')
|
||||
}
|
||||
}
|
||||
|
||||
export const cypressAsserter = new CypressAsserter()
|
|
@ -1,26 +1,33 @@
|
|||
const DEBUG_MODE: boolean = process.env.DEBUG_MODE == "true";
|
||||
const API_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.API_URL || "pusher.workadventure.localhost");
|
||||
const UPLOADER_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.UPLOADER_URL || 'uploader.workadventure.localhost');
|
||||
const ADMIN_URL = (process.env.API_PROTOCOL || (typeof(window) !== 'undefined' ? window.location.protocol : 'http:')) + '//' + (process.env.ADMIN_URL || "admin.workadventure.localhost");
|
||||
const TURN_SERVER: string = process.env.TURN_SERVER || "turn:numb.viagenie.ca";
|
||||
const TURN_USER: string = process.env.TURN_USER || 'g.parant@thecodingmachine.com';
|
||||
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || 'itcugcOHxle9Acqi$';
|
||||
const START_ROOM_URL : string = process.env.START_ROOM_URL || '/_/global/maps.workadventure.localhost/Floor0/floor0.json';
|
||||
const PUSHER_URL = process.env.PUSHER_URL || '//pusher.workadventure.localhost';
|
||||
const UPLOADER_URL = process.env.UPLOADER_URL || '//uploader.workadventure.localhost';
|
||||
const STUN_SERVER: string = process.env.STUN_SERVER || "stun:stun.l.google.com:19302";
|
||||
const TURN_SERVER: string = process.env.TURN_SERVER || "";
|
||||
const SKIP_RENDER_OPTIMIZATIONS: boolean = process.env.SKIP_RENDER_OPTIMIZATIONS == "true";
|
||||
const DISABLE_NOTIFICATIONS: boolean = process.env.DISABLE_NOTIFICATIONS == "true";
|
||||
const TURN_USER: string = process.env.TURN_USER || '';
|
||||
const TURN_PASSWORD: string = process.env.TURN_PASSWORD || '';
|
||||
const JITSI_URL : string|undefined = (process.env.JITSI_URL === '') ? undefined : process.env.JITSI_URL;
|
||||
const JITSI_PRIVATE_MODE : boolean = process.env.JITSI_PRIVATE_MODE == "true";
|
||||
const RESOLUTION = 2;
|
||||
const ZOOM_LEVEL = 1/*3/4*/;
|
||||
const POSITION_DELAY = 200; // Wait 200ms between sending position events
|
||||
const MAX_EXTRAPOLATION_TIME = 100; // Extrapolate a maximum of 250ms if no new movement is sent by the player
|
||||
export const MAX_USERNAME_LENGTH = parseInt(process.env.MAX_USERNAME_LENGTH || '') || 8;
|
||||
export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || '4');
|
||||
export const DISPLAY_TERMS_OF_USE = process.env.DISPLAY_TERMS_OF_USE == 'true';
|
||||
|
||||
export const isMobile = ():boolean => ( ( window.innerWidth <= 800 ) || ( window.innerHeight <= 600 ) );
|
||||
|
||||
export {
|
||||
DEBUG_MODE,
|
||||
API_URL,
|
||||
START_ROOM_URL,
|
||||
SKIP_RENDER_OPTIMIZATIONS,
|
||||
DISABLE_NOTIFICATIONS,
|
||||
PUSHER_URL,
|
||||
UPLOADER_URL,
|
||||
ADMIN_URL,
|
||||
RESOLUTION,
|
||||
ZOOM_LEVEL,
|
||||
POSITION_DELAY,
|
||||
MAX_EXTRAPOLATION_TIME,
|
||||
STUN_SERVER,
|
||||
TURN_SERVER,
|
||||
TURN_USER,
|
||||
TURN_PASSWORD,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {PositionMessage} from "../Messages/generated/messages_pb";
|
||||
import Direction = PositionMessage.Direction;
|
||||
import {PointInterface} from "../Connexion/ConnexionModels";
|
||||
import type {PointInterface} from "../Connexion/ConnexionModels";
|
||||
|
||||
export class ProtobufClientUtils {
|
||||
|
||||
|
|
220
front/src/Phaser/Companion/Companion.ts
Normal file
|
@ -0,0 +1,220 @@
|
|||
import Sprite = Phaser.GameObjects.Sprite;
|
||||
import Container = Phaser.GameObjects.Container;
|
||||
import { PlayerAnimationDirections, PlayerAnimationTypes } from "../Player/Animation";
|
||||
|
||||
export interface CompanionStatus {
|
||||
x: number;
|
||||
y: number;
|
||||
name: string;
|
||||
moving: boolean;
|
||||
direction: PlayerAnimationDirections;
|
||||
}
|
||||
|
||||
export class Companion extends Container {
|
||||
public sprites: Map<string, Sprite>;
|
||||
|
||||
private delta: number;
|
||||
private invisible: boolean;
|
||||
private updateListener: Function;
|
||||
private target: { x: number, y: number, direction: PlayerAnimationDirections };
|
||||
|
||||
private companionName: string;
|
||||
private direction: PlayerAnimationDirections;
|
||||
private animationType: PlayerAnimationTypes;
|
||||
|
||||
constructor(scene: Phaser.Scene, x: number, y: number, name: string, texturePromise: Promise<string>) {
|
||||
super(scene, x + 14, y + 4);
|
||||
|
||||
this.sprites = new Map<string, Sprite>();
|
||||
|
||||
this.delta = 0;
|
||||
this.invisible = true;
|
||||
this.target = { x, y, direction: PlayerAnimationDirections.Down };
|
||||
|
||||
this.direction = PlayerAnimationDirections.Down;
|
||||
this.animationType = PlayerAnimationTypes.Idle;
|
||||
|
||||
this.companionName = name;
|
||||
|
||||
texturePromise.then(resource => {
|
||||
this.addResource(resource);
|
||||
this.invisible = false;
|
||||
})
|
||||
|
||||
this.scene.physics.world.enableBody(this);
|
||||
|
||||
this.getBody().setImmovable(true);
|
||||
this.getBody().setCollideWorldBounds(false);
|
||||
this.setSize(16, 16);
|
||||
this.getBody().setSize(16, 16);
|
||||
this.getBody().setOffset(0, 8);
|
||||
|
||||
this.setDepth(-1);
|
||||
|
||||
this.updateListener = this.step.bind(this);
|
||||
this.scene.events.addListener('update', this.updateListener);
|
||||
|
||||
this.scene.add.existing(this);
|
||||
}
|
||||
|
||||
public setTarget(x: number, y: number, direction: PlayerAnimationDirections) {
|
||||
this.target = { x, y: y + 4, direction };
|
||||
}
|
||||
|
||||
public step(time: number, delta: number) {
|
||||
if (typeof this.target === 'undefined') return;
|
||||
|
||||
this.delta += delta;
|
||||
if (this.delta < 128) {
|
||||
return;
|
||||
}
|
||||
this.delta = 0;
|
||||
|
||||
const xDist = this.target.x - this.x;
|
||||
const yDist = this.target.y - this.y;
|
||||
|
||||
const distance = Math.pow(xDist, 2) + Math.pow(yDist, 2);
|
||||
|
||||
if (distance < 650) {
|
||||
this.animationType = PlayerAnimationTypes.Idle;
|
||||
this.direction = this.target.direction;
|
||||
|
||||
this.getBody().stop();
|
||||
} else {
|
||||
this.animationType = PlayerAnimationTypes.Walk;
|
||||
|
||||
const xDir = xDist / Math.max(Math.abs(xDist), 1);
|
||||
const yDir = yDist / Math.max(Math.abs(yDist), 1);
|
||||
|
||||
const speed = 256;
|
||||
this.getBody().setVelocity(Math.min(Math.abs(xDist * 2.5), speed) * xDir, Math.min(Math.abs(yDist * 2.5), speed) * yDir);
|
||||
|
||||
if (Math.abs(xDist) > Math.abs(yDist)) {
|
||||
if (xDist < 0) {
|
||||
this.direction = PlayerAnimationDirections.Left;
|
||||
} else {
|
||||
this.direction = PlayerAnimationDirections.Right;
|
||||
}
|
||||
} else {
|
||||
if (yDist < 0) {
|
||||
this.direction = PlayerAnimationDirections.Up;
|
||||
} else {
|
||||
this.direction = PlayerAnimationDirections.Down;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.setDepth(this.y);
|
||||
this.playAnimation(this.direction, this.animationType);
|
||||
}
|
||||
|
||||
public getStatus(): CompanionStatus {
|
||||
const { x, y, direction, animationType, companionName } = this;
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
direction,
|
||||
moving: animationType === PlayerAnimationTypes.Walk,
|
||||
name: companionName
|
||||
}
|
||||
}
|
||||
|
||||
private playAnimation(direction: PlayerAnimationDirections, type: PlayerAnimationTypes): void {
|
||||
if (this.invisible) return;
|
||||
|
||||
for (const [resource, sprite] of this.sprites.entries()) {
|
||||
sprite.play(`${resource}-${direction}-${type}`, true);
|
||||
}
|
||||
}
|
||||
|
||||
private addResource(resource: string, frame?: string | number): void {
|
||||
const sprite = new Sprite(this.scene, 0, 0, resource, frame);
|
||||
|
||||
this.add(sprite);
|
||||
|
||||
this.getAnimations(resource).forEach(animation => {
|
||||
this.scene.anims.create(animation);
|
||||
});
|
||||
|
||||
this.scene.sys.updateList.add(sprite);
|
||||
this.sprites.set(resource, sprite);
|
||||
}
|
||||
|
||||
private getAnimations(resource: string): Phaser.Types.Animations.Animation[] {
|
||||
return [
|
||||
{
|
||||
key: `${resource}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Idle}`,
|
||||
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [1]}),
|
||||
frameRate: 10,
|
||||
repeat: 1
|
||||
},
|
||||
{
|
||||
key: `${resource}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Idle}`,
|
||||
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [4]}),
|
||||
frameRate: 10,
|
||||
repeat: 1
|
||||
},
|
||||
{
|
||||
key: `${resource}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Idle}`,
|
||||
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [7]}),
|
||||
frameRate: 10,
|
||||
repeat: 1
|
||||
},
|
||||
{
|
||||
key: `${resource}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Idle}`,
|
||||
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [10]}),
|
||||
frameRate: 10,
|
||||
repeat: 1
|
||||
},
|
||||
{
|
||||
key: `${resource}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Walk}`,
|
||||
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [0, 1, 2]}),
|
||||
frameRate: 15,
|
||||
repeat: -1
|
||||
},
|
||||
{
|
||||
key: `${resource}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Walk}`,
|
||||
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [3, 4, 5]}),
|
||||
frameRate: 15,
|
||||
repeat: -1
|
||||
},
|
||||
{
|
||||
key: `${resource}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Walk}`,
|
||||
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [6, 7, 8]}),
|
||||
frameRate: 15,
|
||||
repeat: -1
|
||||
},
|
||||
{
|
||||
key: `${resource}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Walk}`,
|
||||
frames: this.scene.anims.generateFrameNumbers(resource, {frames: [9, 10, 11]}),
|
||||
frameRate: 15,
|
||||
repeat: -1
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
private getBody(): Phaser.Physics.Arcade.Body {
|
||||
const body = this.body;
|
||||
|
||||
if (!(body instanceof Phaser.Physics.Arcade.Body)) {
|
||||
throw new Error('Container does not have arcade body');
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
for (const sprite of this.sprites.values()) {
|
||||
if (this.scene) {
|
||||
this.scene.sys.updateList.remove(sprite);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.scene) {
|
||||
this.scene.events.removeListener('update', this.updateListener);
|
||||
}
|
||||
|
||||
super.destroy();
|
||||
}
|
||||
}
|
14
front/src/Phaser/Companion/CompanionTextures.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
export interface CompanionResourceDescriptionInterface {
|
||||
name: string,
|
||||
img: string,
|
||||
behaviour: "dog" | "cat"
|
||||
}
|
||||
|
||||
export const COMPANION_RESOURCES: CompanionResourceDescriptionInterface[] = [
|
||||
{ name: "dog1", img: "resources/characters/pipoya/Dog 01-1.png", behaviour: "dog" },
|
||||
{ name: "dog2", img: "resources/characters/pipoya/Dog 01-2.png", behaviour: "dog" },
|
||||
{ name: "dog3", img: "resources/characters/pipoya/Dog 01-3.png", behaviour: "dog" },
|
||||
{ name: "cat1", img: "resources/characters/pipoya/Cat 01-1.png", behaviour: "cat" },
|
||||
{ name: "cat2", img: "resources/characters/pipoya/Cat 01-2.png", behaviour: "cat" },
|
||||
{ name: "cat3", img: "resources/characters/pipoya/Cat 01-3.png", behaviour: "cat" },
|
||||
]
|
|
@ -0,0 +1,29 @@
|
|||
import LoaderPlugin = Phaser.Loader.LoaderPlugin;
|
||||
import { COMPANION_RESOURCES, CompanionResourceDescriptionInterface } from "./CompanionTextures";
|
||||
|
||||
export const getAllCompanionResources = (loader: LoaderPlugin): CompanionResourceDescriptionInterface[] => {
|
||||
COMPANION_RESOURCES.forEach((resource: CompanionResourceDescriptionInterface) => {
|
||||
lazyLoadCompanionResource(loader, resource.name);
|
||||
});
|
||||
|
||||
return COMPANION_RESOURCES;
|
||||
}
|
||||
|
||||
export const lazyLoadCompanionResource = (loader: LoaderPlugin, name: string): Promise<string> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const resource = COMPANION_RESOURCES.find(item => item.name === name);
|
||||
|
||||
if (typeof resource === 'undefined') {
|
||||
return reject(`Texture '${name}' not found!`);
|
||||
}
|
||||
|
||||
if (loader.textureManager.exists(resource.name)) {
|
||||
return resolve(resource.name);
|
||||
}
|
||||
|
||||
loader.spritesheet(resource.name, resource.img, { frameWidth: 32, frameHeight: 32, endFrame: 12 });
|
||||
loader.once(`filecomplete-spritesheet-${resource.name}`, () => resolve(resource.name));
|
||||
|
||||
loader.start(); // It's only automatically started during the Scene preload.
|
||||
});
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import { DEPTH_INGAME_TEXT_INDEX } from "../Game/DepthIndexes";
|
||||
|
||||
export class ChatModeIcon extends Phaser.GameObjects.Sprite {
|
||||
constructor(scene: Phaser.Scene, x: number, y: number) {
|
||||
super(scene, x, y, 'layout_modes', 3);
|
||||
|
@ -6,6 +8,6 @@ export class ChatModeIcon extends Phaser.GameObjects.Sprite {
|
|||
this.setOrigin(0, 1);
|
||||
this.setInteractive();
|
||||
this.setVisible(false);
|
||||
this.setDepth(99999);
|
||||
this.setDepth(DEPTH_INGAME_TEXT_INDEX);
|
||||
}
|
||||
}
|
54
front/src/Phaser/Components/Loader.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import ImageFrameConfig = Phaser.Types.Loader.FileTypes.ImageFrameConfig;
|
||||
|
||||
const LogoNameIndex: string = 'logoLoading';
|
||||
const TextName: string = 'Loading...';
|
||||
const LogoResource: string = 'resources/logos/logo.png';
|
||||
const LogoFrame: ImageFrameConfig = {frameWidth: 307, frameHeight: 59};
|
||||
|
||||
export const addLoader = (scene: Phaser.Scene): void => {
|
||||
// If there is nothing to load, do not display the loader.
|
||||
if (scene.load.list.entries.length === 0) {
|
||||
return;
|
||||
}
|
||||
let loadingText: Phaser.GameObjects.Text|null = null;
|
||||
const loadingBarWidth: number = Math.floor(scene.game.renderer.width / 3);
|
||||
const loadingBarHeight: number = 16;
|
||||
const padding: number = 5;
|
||||
|
||||
const promiseLoadLogoTexture = new Promise<Phaser.GameObjects.Image>((res) => {
|
||||
if(scene.load.textureManager.exists(LogoNameIndex)){
|
||||
return res(scene.add.image(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 150, LogoNameIndex));
|
||||
}else{
|
||||
//add loading if logo image is not ready
|
||||
loadingText = scene.add.text(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 50, TextName);
|
||||
}
|
||||
scene.load.spritesheet(LogoNameIndex, LogoResource, LogoFrame);
|
||||
scene.load.once(`filecomplete-spritesheet-${LogoNameIndex}`, () => {
|
||||
if(loadingText){
|
||||
loadingText.destroy();
|
||||
}
|
||||
return res(scene.add.image(scene.game.renderer.width / 2, scene.game.renderer.height / 2 - 150, LogoNameIndex));
|
||||
});
|
||||
});
|
||||
|
||||
const progressContainer = scene.add.graphics();
|
||||
const progress = scene.add.graphics();
|
||||
progressContainer.fillStyle(0x444444, 0.8);
|
||||
progressContainer.fillRect((scene.game.renderer.width - loadingBarWidth) / 2 - padding, scene.game.renderer.height / 2 + 50 - padding, loadingBarWidth + padding * 2, loadingBarHeight + padding * 2);
|
||||
|
||||
scene.load.on('progress', (value: number) => {
|
||||
progress.clear();
|
||||
progress.fillStyle(0xBBBBBB, 1);
|
||||
progress.fillRect((scene.game.renderer.width - loadingBarWidth) / 2, scene.game.renderer.height / 2 + 50, loadingBarWidth * value, loadingBarHeight);
|
||||
});
|
||||
scene.load.on('complete', () => {
|
||||
if(loadingText){
|
||||
loadingText.destroy();
|
||||
}
|
||||
promiseLoadLogoTexture.then((resLoadingImage: Phaser.GameObjects.Image) => {
|
||||
resLoadingImage.destroy();
|
||||
});
|
||||
progress.destroy();
|
||||
progressContainer.destroy();
|
||||
});
|
||||
}
|
65
front/src/Phaser/Components/MobileJoystick.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import VirtualJoystick from 'phaser3-rex-plugins/plugins/virtualjoystick.js';
|
||||
import {waScaleManager} from "../Services/WaScaleManager";
|
||||
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes";
|
||||
|
||||
//the assets were found here: https://hannemann.itch.io/virtual-joystick-pack-free
|
||||
export const joystickBaseKey = 'joystickBase';
|
||||
export const joystickBaseImg = 'resources/objects/joystickSplitted.png';
|
||||
export const joystickThumbKey = 'joystickThumb';
|
||||
export const joystickThumbImg = 'resources/objects/smallHandleFilledGrey.png';
|
||||
|
||||
const baseSize = 50;
|
||||
const thumbSize = 25;
|
||||
const radius = 17.5;
|
||||
|
||||
export class MobileJoystick extends VirtualJoystick {
|
||||
private resizeCallback: () => void;
|
||||
|
||||
constructor(scene: Phaser.Scene) {
|
||||
super(scene, {
|
||||
x: -1000,
|
||||
y: -1000,
|
||||
radius: radius * window.devicePixelRatio,
|
||||
base: scene.add.image(0, 0, joystickBaseKey).setDisplaySize(baseSize * window.devicePixelRatio, baseSize * window.devicePixelRatio).setDepth(DEPTH_INGAME_TEXT_INDEX),
|
||||
thumb: scene.add.image(0, 0, joystickThumbKey).setDisplaySize(thumbSize * window.devicePixelRatio, thumbSize * window.devicePixelRatio).setDepth(DEPTH_INGAME_TEXT_INDEX),
|
||||
enable: true,
|
||||
dir: "8dir",
|
||||
});
|
||||
this.visible = false;
|
||||
this.enable = false;
|
||||
|
||||
this.scene.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
||||
if (!pointer.wasTouch) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Let's only display the joystick if there is one finger on the screen
|
||||
if ((pointer.event as TouchEvent).touches.length === 1) {
|
||||
this.x = pointer.x;
|
||||
this.y = pointer.y;
|
||||
this.visible = true;
|
||||
this.enable = true;
|
||||
} else {
|
||||
this.visible = false;
|
||||
this.enable = false;
|
||||
}
|
||||
});
|
||||
this.scene.input.on('pointerup', () => {
|
||||
this.visible = false;
|
||||
this.enable = false;
|
||||
});
|
||||
this.resizeCallback = this.resize.bind(this);
|
||||
this.scene.scale.on(Phaser.Scale.Events.RESIZE, this.resizeCallback);
|
||||
}
|
||||
|
||||
private resize() {
|
||||
this.base.setDisplaySize(baseSize / waScaleManager.zoomModifier * window.devicePixelRatio, baseSize / waScaleManager.zoomModifier * window.devicePixelRatio);
|
||||
this.thumb.setDisplaySize(thumbSize / waScaleManager.zoomModifier * window.devicePixelRatio, thumbSize / waScaleManager.zoomModifier * window.devicePixelRatio);
|
||||
this.setRadius(radius / waScaleManager.zoomModifier * window.devicePixelRatio);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
super.destroy();
|
||||
this.scene.scale.removeListener(Phaser.Scale.Events.RESIZE, this.resizeCallback);
|
||||
}
|
||||
}
|
|
@ -1,17 +1,16 @@
|
|||
import {discussionManager} from "../../WebRtc/DiscussionManager";
|
||||
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes";
|
||||
|
||||
export const openChatIconName = 'openChatIcon';
|
||||
export class OpenChatIcon extends Phaser.GameObjects.Image {
|
||||
constructor(scene: Phaser.Scene, x: number, y: number) {
|
||||
super(scene, x, y, openChatIconName);
|
||||
super(scene, x, y, openChatIconName, 3);
|
||||
scene.add.existing(this);
|
||||
this.setScrollFactor(0, 0);
|
||||
this.setOrigin(0, 1);
|
||||
this.displayWidth = 30;
|
||||
this.displayHeight = 30;
|
||||
this.setInteractive();
|
||||
this.setVisible(false)
|
||||
this.setDepth(99999);
|
||||
this.setVisible(false);
|
||||
this.setDepth(DEPTH_INGAME_TEXT_INDEX);
|
||||
|
||||
this.on("pointerup", () => discussionManager.showDiscussionPart());
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes";
|
||||
|
||||
export class PresentationModeIcon extends Phaser.GameObjects.Sprite {
|
||||
constructor(scene: Phaser.Scene, x: number, y: number) {
|
||||
super(scene, x, y, 'layout_modes', 0);
|
||||
|
@ -6,6 +8,6 @@ export class PresentationModeIcon extends Phaser.GameObjects.Sprite {
|
|||
this.setOrigin(0, 1);
|
||||
this.setInteractive();
|
||||
this.setVisible(false);
|
||||
this.setDepth(99999);
|
||||
this.setDepth(DEPTH_INGAME_TEXT_INDEX);
|
||||
}
|
||||
}
|
74
front/src/Phaser/Components/RadialMenu.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
import Sprite = Phaser.GameObjects.Sprite;
|
||||
import {DEPTH_UI_INDEX} from "../Game/DepthIndexes";
|
||||
import {waScaleManager} from "../Services/WaScaleManager";
|
||||
|
||||
export interface RadialMenuItem {
|
||||
image: string,
|
||||
name: string,
|
||||
}
|
||||
|
||||
export const RadialMenuClickEvent = 'radialClick';
|
||||
|
||||
export class RadialMenu extends Phaser.GameObjects.Container {
|
||||
private resizeCallback: OmitThisParameter<() => void>;
|
||||
|
||||
constructor(scene: Phaser.Scene, x: number, y: number, private items: RadialMenuItem[]) {
|
||||
super(scene, x, y);
|
||||
this.setDepth(DEPTH_UI_INDEX)
|
||||
this.scene.add.existing(this);
|
||||
this.initItems();
|
||||
|
||||
this.resize();
|
||||
this.resizeCallback = this.resize.bind(this);
|
||||
this.scene.scale.on(Phaser.Scale.Events.RESIZE, this.resizeCallback);
|
||||
}
|
||||
|
||||
private initItems() {
|
||||
const itemsNumber = this.items.length;
|
||||
const menuRadius = 70 + (waScaleManager.uiScalingFactor - 1) * 20;
|
||||
this.items.forEach((item, index) => this.createRadialElement(item, index, itemsNumber, menuRadius))
|
||||
}
|
||||
|
||||
private createRadialElement(item: RadialMenuItem, index: number, itemsNumber: number, menuRadius: number) {
|
||||
const image = new Sprite(this.scene, 0, menuRadius, item.image);
|
||||
this.add(image);
|
||||
this.scene.sys.updateList.add(image);
|
||||
const scalingFactor = waScaleManager.uiScalingFactor * 0.075;
|
||||
image.setScale(scalingFactor)
|
||||
image.setInteractive({
|
||||
useHandCursor: true,
|
||||
});
|
||||
image.on('pointerdown', () => this.emit(RadialMenuClickEvent, item));
|
||||
image.on('pointerover', () => {
|
||||
this.scene.tweens.add({
|
||||
targets: image,
|
||||
props: {
|
||||
scale: 2 * scalingFactor,
|
||||
},
|
||||
duration: 500,
|
||||
ease: 'Power3',
|
||||
})
|
||||
});
|
||||
image.on('pointerout', () => {
|
||||
this.scene.tweens.add({
|
||||
targets: image,
|
||||
props: {
|
||||
scale: scalingFactor,
|
||||
},
|
||||
duration: 500,
|
||||
ease: 'Power3',
|
||||
})
|
||||
});
|
||||
const angle = 2 * Math.PI * index / itemsNumber;
|
||||
Phaser.Actions.RotateAroundDistance([image], {x: 0, y: 0}, angle, menuRadius);
|
||||
}
|
||||
|
||||
private resize() {
|
||||
this.setScale(waScaleManager.uiScalingFactor);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.scene.scale.removeListener(Phaser.Scale.Events.RESIZE, this.resizeCallback);
|
||||
super.destroy();
|
||||
}
|
||||
}
|
|
@ -1,3 +1,5 @@
|
|||
import type {IAnalyserNode, IAudioContext, IMediaStreamAudioSourceNode} from 'standardized-audio-context';
|
||||
|
||||
/**
|
||||
* Class to measure the sound volume of a media stream
|
||||
*/
|
||||
|
@ -5,10 +7,10 @@ export class SoundMeter {
|
|||
private instant: number;
|
||||
private clip: number;
|
||||
//private script: ScriptProcessorNode;
|
||||
private analyser: AnalyserNode|undefined;
|
||||
private analyser: IAnalyserNode<IAudioContext>|undefined;
|
||||
private dataArray: Uint8Array|undefined;
|
||||
private context: AudioContext|undefined;
|
||||
private source: MediaStreamAudioSourceNode|undefined;
|
||||
private context: IAudioContext|undefined;
|
||||
private source: IMediaStreamAudioSourceNode<IAudioContext>|undefined;
|
||||
|
||||
constructor() {
|
||||
this.instant = 0.0;
|
||||
|
@ -16,19 +18,21 @@ export class SoundMeter {
|
|||
//this.script = context.createScriptProcessor(2048, 1, 1);
|
||||
}
|
||||
|
||||
private init(context: AudioContext) {
|
||||
if (this.context === undefined) {
|
||||
this.context = context;
|
||||
this.analyser = this.context.createAnalyser();
|
||||
private init(context: IAudioContext) {
|
||||
this.context = context;
|
||||
this.analyser = this.context.createAnalyser();
|
||||
|
||||
this.analyser.fftSize = 2048;
|
||||
const bufferLength = this.analyser.fftSize;
|
||||
this.dataArray = new Uint8Array(bufferLength);
|
||||
}
|
||||
this.analyser.fftSize = 2048;
|
||||
const bufferLength = this.analyser.fftSize;
|
||||
this.dataArray = new Uint8Array(bufferLength);
|
||||
}
|
||||
|
||||
public connectToSource(stream: MediaStream, context: AudioContext): void
|
||||
public connectToSource(stream: MediaStream, context: IAudioContext): void
|
||||
{
|
||||
if (this.source !== undefined) {
|
||||
this.stop();
|
||||
}
|
||||
|
||||
this.init(context);
|
||||
|
||||
this.source = this.context?.createMediaStreamSource(stream);
|
||||
|
@ -83,56 +87,3 @@ export class SoundMeter {
|
|||
|
||||
}
|
||||
|
||||
|
||||
// Meter class that generates a number correlated to audio volume.
|
||||
// The meter class itself displays nothing, but it makes the
|
||||
// instantaneous and time-decaying volumes available for inspection.
|
||||
// It also reports on the fraction of samples that were at or near
|
||||
// the top of the measurement range.
|
||||
/*function SoundMeter(context) {
|
||||
this.context = context;
|
||||
this.instant = 0.0;
|
||||
this.slow = 0.0;
|
||||
this.clip = 0.0;
|
||||
this.script = context.createScriptProcessor(2048, 1, 1);
|
||||
const that = this;
|
||||
this.script.onaudioprocess = function(event) {
|
||||
const input = event.inputBuffer.getChannelData(0);
|
||||
let i;
|
||||
let sum = 0.0;
|
||||
let clipcount = 0;
|
||||
for (i = 0; i < input.length; ++i) {
|
||||
sum += input[i] * input[i];
|
||||
if (Math.abs(input[i]) > 0.99) {
|
||||
clipcount += 1;
|
||||
}
|
||||
}
|
||||
that.instant = Math.sqrt(sum / input.length);
|
||||
that.slow = 0.95 * that.slow + 0.05 * that.instant;
|
||||
that.clip = clipcount / input.length;
|
||||
};
|
||||
}
|
||||
|
||||
SoundMeter.prototype.connectToSource = function(stream, callback) {
|
||||
console.log('SoundMeter connecting');
|
||||
try {
|
||||
this.mic = this.context.createMediaStreamSource(stream);
|
||||
this.mic.connect(this.script);
|
||||
// necessary to make sample run, but should not be.
|
||||
this.script.connect(this.context.destination);
|
||||
if (typeof callback !== 'undefined') {
|
||||
callback(null);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (typeof callback !== 'undefined') {
|
||||
callback(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
SoundMeter.prototype.stop = function() {
|
||||
this.mic.disconnect();
|
||||
this.script.disconnect();
|
||||
};
|
||||
*/
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
import Container = Phaser.GameObjects.Container;
|
||||
import {Scene} from "phaser";
|
||||
import GameObject = Phaser.GameObjects.GameObject;
|
||||
import Rectangle = Phaser.GameObjects.Rectangle;
|
||||
|
||||
|
||||
export class SoundMeterSprite extends Container {
|
||||
private rectangles: Rectangle[] = new Array<Rectangle>();
|
||||
private static readonly NB_BARS = 20;
|
||||
|
||||
constructor(scene: Scene, x?: number, y?: number, children?: GameObject[]) {
|
||||
super(scene, x, y, children);
|
||||
|
||||
for (let i = 0; i < SoundMeterSprite.NB_BARS; i++) {
|
||||
const rectangle = new Rectangle(scene, i * 13, 0, 10, 20, (Math.round(255 - i * 255 / SoundMeterSprite.NB_BARS) << 8) + (Math.round(i * 255 / SoundMeterSprite.NB_BARS) << 16));
|
||||
this.add(rectangle);
|
||||
this.rectangles.push(rectangle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A number between 0 and 100
|
||||
*
|
||||
* @param volume
|
||||
*/
|
||||
public setVolume(volume: number): void {
|
||||
|
||||
const normalizedVolume = volume / 100 * SoundMeterSprite.NB_BARS;
|
||||
for (let i = 0; i < SoundMeterSprite.NB_BARS; i++) {
|
||||
if (normalizedVolume < i) {
|
||||
this.rectangles[i].alpha = 0.5;
|
||||
} else {
|
||||
this.rectangles[i].alpha = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public getWidth(): number {
|
||||
return SoundMeterSprite.NB_BARS * 13;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -1,46 +1,68 @@
|
|||
|
||||
const IGNORED_KEYS = new Set([
|
||||
'Esc',
|
||||
'Escape',
|
||||
'Alt',
|
||||
'Meta',
|
||||
'Control',
|
||||
'Ctrl',
|
||||
'Space',
|
||||
'Backspace'
|
||||
])
|
||||
|
||||
export class TextInput extends Phaser.GameObjects.BitmapText {
|
||||
private minUnderLineLength = 4;
|
||||
private underLine: Phaser.GameObjects.Text;
|
||||
private domInput = document.createElement('input');
|
||||
|
||||
constructor(scene: Phaser.Scene, x: number, y: number, maxLength: number, text: string, onChange: (text: string) => void) {
|
||||
constructor(scene: Phaser.Scene, x: number, y: number, maxLength: number, text: string,
|
||||
onChange: (text: string) => void) {
|
||||
super(scene, x, y, 'main_font', text, 32);
|
||||
this.setOrigin(0.5).setCenterAlign()
|
||||
this.setOrigin(0.5).setCenterAlign();
|
||||
this.scene.add.existing(this);
|
||||
|
||||
this.underLine = this.scene.add.text(x, y+1, this.getUnderLineBody(text.length), { fontFamily: 'Arial', fontSize: "32px", color: '#ffffff'})
|
||||
this.underLine.setOrigin(0.5)
|
||||
const style = {fontFamily: 'Arial', fontSize: "32px", color: '#ffffff'};
|
||||
this.underLine = this.scene.add.text(x, y+1, this.getUnderLineBody(text.length), style);
|
||||
this.underLine.setOrigin(0.5);
|
||||
|
||||
this.domInput.maxLength = maxLength;
|
||||
this.domInput.style.opacity = "0";
|
||||
if (text) {
|
||||
this.domInput.value = text;
|
||||
}
|
||||
|
||||
this.scene.input.keyboard.on('keydown', (event: KeyboardEvent) => {
|
||||
if (event.keyCode === 8 && this.text.length > 0) {
|
||||
this.deleteLetter();
|
||||
} else if ((event.keyCode === 32 || (event.keyCode >= 48 && event.keyCode <= 90)) && this.text.length < maxLength) {
|
||||
this.addLetter(event.key);
|
||||
this.domInput.addEventListener('keydown', event => {
|
||||
if (IGNORED_KEYS.has(event.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/[a-zA-Z0-9:.!&?()+-]/.exec(event.key)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
this.domInput.addEventListener('input', (event) => {
|
||||
if (event.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
this.text = this.domInput.value;
|
||||
this.underLine.text = this.getUnderLineBody(this.text.length);
|
||||
onChange(this.text);
|
||||
});
|
||||
|
||||
document.body.append(this.domInput);
|
||||
this.focus();
|
||||
}
|
||||
|
||||
|
||||
private getUnderLineBody(textLength:number): string {
|
||||
if (textLength < this.minUnderLineLength) textLength = this.minUnderLineLength;
|
||||
let text = '_______';
|
||||
for (let i = this.minUnderLineLength; i < textLength; i++) {
|
||||
text += '__'
|
||||
text += '__';
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
private deleteLetter() {
|
||||
this.text = this.text.substr(0, this.text.length - 1);
|
||||
}
|
||||
|
||||
|
||||
private addLetter(letter: string) {
|
||||
this.text += letter;
|
||||
}
|
||||
|
||||
getText(): string {
|
||||
return this.text;
|
||||
}
|
||||
|
@ -56,4 +78,13 @@ export class TextInput extends Phaser.GameObjects.BitmapText {
|
|||
this.underLine.y = y+1;
|
||||
return this;
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.domInput.focus();
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
super.destroy();
|
||||
this.domInput.remove();
|
||||
}
|
||||
}
|
||||
|
|
51
front/src/Phaser/Components/TextUtils.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import type {ITiledMapObject} from "../Map/ITiledMap";
|
||||
import type {GameScene} from "../Game/GameScene";
|
||||
|
||||
export class TextUtils {
|
||||
public static createTextFromITiledMapObject(scene: GameScene, object: ITiledMapObject): void {
|
||||
if (object.text === undefined) {
|
||||
throw new Error('This object has not textual representation.');
|
||||
}
|
||||
const options: {
|
||||
fontStyle?: string,
|
||||
fontSize?: string,
|
||||
fontFamily?: string,
|
||||
color?: string,
|
||||
align?: string,
|
||||
wordWrap?: {
|
||||
width: number,
|
||||
useAdvancedWrap?: boolean
|
||||
}
|
||||
} = {};
|
||||
if (object.text.italic) {
|
||||
options.fontStyle = 'italic';
|
||||
}
|
||||
// Note: there is no support for "strikeout" and "underline"
|
||||
let fontSize: number = 16;
|
||||
if (object.text.pixelsize) {
|
||||
fontSize = object.text.pixelsize;
|
||||
}
|
||||
options.fontSize = fontSize + 'px';
|
||||
if (object.text.fontfamily) {
|
||||
options.fontFamily = '"'+object.text.fontfamily+'"';
|
||||
}
|
||||
let color = '#000000';
|
||||
if (object.text.color !== undefined) {
|
||||
color = object.text.color;
|
||||
}
|
||||
options.color = color;
|
||||
if (object.text.wrap === true) {
|
||||
options.wordWrap = {
|
||||
width: object.width,
|
||||
//useAdvancedWrap: true
|
||||
}
|
||||
}
|
||||
if (object.text.halign !== undefined) {
|
||||
options.align = object.text.halign;
|
||||
}
|
||||
|
||||
console.warn(options);
|
||||
const textElem = scene.add.text(object.x, object.y, object.text.text, options);
|
||||
textElem.setAngle(object.rotation);
|
||||
}
|
||||
}
|
14
front/src/Phaser/Components/WarningContainer.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
|
||||
export const warningContainerKey = 'warningContainer';
|
||||
export const warningContainerHtml = 'resources/html/warningContainer.html';
|
||||
|
||||
export class WarningContainer extends Phaser.GameObjects.DOMElement {
|
||||
|
||||
constructor(scene: Phaser.Scene) {
|
||||
super(scene, 100, 0);
|
||||
this.setOrigin(0, 0);
|
||||
this.createFromCache(warningContainerKey);
|
||||
this.scene.add.existing(this);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,83 +1,74 @@
|
|||
import {PlayerAnimationNames} from "../Player/Animation";
|
||||
import {PlayerAnimationDirections, PlayerAnimationTypes} from "../Player/Animation";
|
||||
import {SpeechBubble} from "./SpeechBubble";
|
||||
import BitmapText = Phaser.GameObjects.BitmapText;
|
||||
import Text = Phaser.GameObjects.Text;
|
||||
import Container = Phaser.GameObjects.Container;
|
||||
import Sprite = Phaser.GameObjects.Sprite;
|
||||
import {TextureError} from "../../Exception/TextureError";
|
||||
import {Companion} from "../Companion/Companion";
|
||||
import type {GameScene} from "../Game/GameScene";
|
||||
import {DEPTH_INGAME_TEXT_INDEX} from "../Game/DepthIndexes";
|
||||
import {waScaleManager} from "../Services/WaScaleManager";
|
||||
|
||||
export interface PlayerResourceDescriptionInterface {
|
||||
name: string,
|
||||
img: string
|
||||
}
|
||||
|
||||
export const PLAYER_RESOURCES: Array<PlayerResourceDescriptionInterface> = [
|
||||
{name: "male1", img: "resources/characters/pipoya/Male 01-1.png" /*, x: 32, y: 32*/},
|
||||
{name: "male2", img: "resources/characters/pipoya/Male 02-2.png"/*, x: 64, y: 32*/},
|
||||
{name: "male3", img: "resources/characters/pipoya/Male 03-4.png"/*, x: 96, y: 32*/},
|
||||
{name: "male4", img: "resources/characters/pipoya/Male 09-1.png"/*, x: 128, y: 32*/},
|
||||
|
||||
{name: "male5", img: "resources/characters/pipoya/Male 10-3.png"/*, x: 32, y: 64*/},
|
||||
{name: "male6", img: "resources/characters/pipoya/Male 17-2.png"/*, x: 64, y: 64*/},
|
||||
{name: "male7", img: "resources/characters/pipoya/Male 18-1.png"/*, x: 96, y: 64*/},
|
||||
{name: "male8", img: "resources/characters/pipoya/Male 16-4.png"/*, x: 128, y: 64*/},
|
||||
|
||||
{name: "Female1", img: "resources/characters/pipoya/Female 01-1.png"/*, x: 32, y: 96*/},
|
||||
{name: "Female2", img: "resources/characters/pipoya/Female 02-2.png"/*, x: 64, y: 96*/},
|
||||
{name: "Female3", img: "resources/characters/pipoya/Female 03-4.png"/*, x: 96, y: 96*/},
|
||||
{name: "Female4", img: "resources/characters/pipoya/Female 09-1.png"/*, x: 128, y: 96*/},
|
||||
|
||||
{name: "Female5", img: "resources/characters/pipoya/Female 10-3.png"/*, x: 32, y: 128*/},
|
||||
{name: "Female6", img: "resources/characters/pipoya/Female 17-2.png"/*, x: 64, y: 128*/},
|
||||
{name: "Female7", img: "resources/characters/pipoya/Female 18-1.png"/*, x: 96, y: 128*/},
|
||||
{name: "Female8", img: "resources/characters/pipoya/Female 16-4.png"/*, x: 128, y: 128*/}
|
||||
];
|
||||
const playerNameY = - 25;
|
||||
|
||||
interface AnimationData {
|
||||
key: string;
|
||||
frameRate: number;
|
||||
repeat: number;
|
||||
frameModel: string; //todo use an enum
|
||||
frameStart: number;
|
||||
frameEnd: number;
|
||||
frames : number[]
|
||||
}
|
||||
|
||||
const interactiveRadius = 35;
|
||||
|
||||
export abstract class Character extends Container {
|
||||
private bubble: SpeechBubble|null = null;
|
||||
private readonly playerName: BitmapText;
|
||||
private readonly playerName: Text;
|
||||
public PlayerValue: string;
|
||||
public sprites: Map<string, Sprite>;
|
||||
private lastDirection: string = PlayerAnimationNames.WalkDown;
|
||||
private lastDirection: PlayerAnimationDirections = PlayerAnimationDirections.Down;
|
||||
//private teleportation: Sprite;
|
||||
private invisible: boolean;
|
||||
public companion?: Companion;
|
||||
private emote: Phaser.GameObjects.Sprite | null = null;
|
||||
private emoteTween: Phaser.Tweens.Tween|null = null;
|
||||
|
||||
constructor(scene: Phaser.Scene,
|
||||
constructor(scene: GameScene,
|
||||
x: number,
|
||||
y: number,
|
||||
textures: string[],
|
||||
texturesPromise: Promise<string[]>,
|
||||
name: string,
|
||||
direction: string,
|
||||
direction: PlayerAnimationDirections,
|
||||
moving: boolean,
|
||||
frame?: string | number
|
||||
frame: string | number,
|
||||
isClickable: boolean,
|
||||
companion: string|null,
|
||||
companionTexturePromise?: Promise<string>
|
||||
) {
|
||||
super(scene, x, y/*, texture, frame*/);
|
||||
this.PlayerValue = name;
|
||||
this.invisible = true
|
||||
|
||||
this.sprites = new Map<string, Sprite>();
|
||||
|
||||
this.addTextures(textures, frame);
|
||||
//textures are inside a Promise in case they need to be lazyloaded before use.
|
||||
texturesPromise.then((textures) => {
|
||||
this.addTextures(textures, frame);
|
||||
this.invisible = false
|
||||
})
|
||||
|
||||
/*this.teleportation = new Sprite(scene, -20, -10, 'teleportation', 3);
|
||||
this.teleportation.setInteractive();
|
||||
this.teleportation.visible = false;
|
||||
this.teleportation.on('pointerup', () => {
|
||||
this.report.visible = false;
|
||||
this.teleportation.visible = false;
|
||||
});
|
||||
this.add(this.teleportation);*/
|
||||
|
||||
this.playerName = new BitmapText(scene, 0, - 25, 'main_font', name, 7);
|
||||
this.playerName.setOrigin(0.5).setCenterAlign().setDepth(99999);
|
||||
this.playerName = new Text(scene, 0, playerNameY, name, {fontFamily: '"Press Start 2P"', fontSize: '8px', strokeThickness: 2, stroke: "gray"});
|
||||
this.playerName.setOrigin(0.5).setDepth(DEPTH_INGAME_TEXT_INDEX);
|
||||
this.add(this.playerName);
|
||||
|
||||
if (isClickable) {
|
||||
this.setInteractive({
|
||||
hitArea: new Phaser.Geom.Circle(0, 0, interactiveRadius),
|
||||
hitAreaCallback: Phaser.Geom.Circle.Contains, //eslint-disable-line @typescript-eslint/unbound-method
|
||||
useHandCursor: true,
|
||||
});
|
||||
}
|
||||
|
||||
scene.add.existing(this);
|
||||
|
||||
this.scene.physics.world.enableBody(this);
|
||||
|
@ -89,20 +80,29 @@ export abstract class Character extends Container {
|
|||
this.setDepth(-1);
|
||||
|
||||
this.playAnimation(direction, moving);
|
||||
|
||||
if (typeof companion === 'string') {
|
||||
this.addCompanion(companion, companionTexturePromise);
|
||||
}
|
||||
}
|
||||
|
||||
public addCompanion(name: string, texturePromise?: Promise<string>): void {
|
||||
if (typeof texturePromise !== 'undefined') {
|
||||
this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise);
|
||||
}
|
||||
}
|
||||
|
||||
public addTextures(textures: string[], frame?: string | number): void {
|
||||
for (const texture of textures) {
|
||||
if(!this.scene.textures.exists(texture)){
|
||||
if(this.scene && !this.scene.textures.exists(texture)){
|
||||
throw new TextureError('texture not found');
|
||||
}
|
||||
const sprite = new Sprite(this.scene, 0, 0, texture, frame);
|
||||
sprite.setInteractive({useHandCursor: true});
|
||||
this.add(sprite);
|
||||
this.getPlayerAnimations(texture).forEach(d => {
|
||||
this.scene.anims.create({
|
||||
key: d.key,
|
||||
frames: this.scene.anims.generateFrameNumbers(d.frameModel, {start: d.frameStart, end: d.frameEnd}),
|
||||
frames: this.scene.anims.generateFrameNumbers(d.frameModel, {frames: d.frames}),
|
||||
frameRate: d.frameRate,
|
||||
repeat: d.repeat
|
||||
});
|
||||
|
@ -117,47 +117,67 @@ export abstract class Character extends Container {
|
|||
|
||||
private getPlayerAnimations(name: string): AnimationData[] {
|
||||
return [{
|
||||
key: `${name}-${PlayerAnimationNames.WalkDown}`,
|
||||
key: `${name}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Walk}`,
|
||||
frameModel: name,
|
||||
frameStart: 0,
|
||||
frameEnd: 2,
|
||||
frames: [0, 1, 2, 1],
|
||||
frameRate: 10,
|
||||
repeat: -1
|
||||
}, {
|
||||
key: `${name}-${PlayerAnimationNames.WalkLeft}`,
|
||||
key: `${name}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Walk}`,
|
||||
frameModel: name,
|
||||
frameStart: 3,
|
||||
frameEnd: 5,
|
||||
frames: [3, 4, 5, 4],
|
||||
frameRate: 10,
|
||||
repeat: -1
|
||||
}, {
|
||||
key: `${name}-${PlayerAnimationNames.WalkRight}`,
|
||||
key: `${name}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Walk}`,
|
||||
frameModel: name,
|
||||
frameStart: 6,
|
||||
frameEnd: 8,
|
||||
frames: [6, 7, 8, 7],
|
||||
frameRate: 10,
|
||||
repeat: -1
|
||||
}, {
|
||||
key: `${name}-${PlayerAnimationNames.WalkUp}`,
|
||||
key: `${name}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Walk}`,
|
||||
frameModel: name,
|
||||
frameStart: 9,
|
||||
frameEnd: 11,
|
||||
frames: [9, 10, 11, 10],
|
||||
frameRate: 10,
|
||||
repeat: -1
|
||||
},{
|
||||
key: `${name}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Idle}`,
|
||||
frameModel: name,
|
||||
frames: [1],
|
||||
frameRate: 10,
|
||||
repeat: 1
|
||||
}, {
|
||||
key: `${name}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Idle}`,
|
||||
frameModel: name,
|
||||
frames: [4],
|
||||
frameRate: 10,
|
||||
repeat: 1
|
||||
}, {
|
||||
key: `${name}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Idle}`,
|
||||
frameModel: name,
|
||||
frames: [7],
|
||||
frameRate: 10,
|
||||
repeat: 1
|
||||
}, {
|
||||
key: `${name}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Idle}`,
|
||||
frameModel: name,
|
||||
frames: [10],
|
||||
frameRate: 10,
|
||||
repeat: 1
|
||||
}];
|
||||
}
|
||||
|
||||
protected playAnimation(direction : string, moving: boolean): void {
|
||||
protected playAnimation(direction : PlayerAnimationDirections, moving: boolean): void {
|
||||
if (this.invisible) return;
|
||||
for (const [texture, sprite] of this.sprites.entries()) {
|
||||
if (!sprite.anims) {
|
||||
console.error('ANIMS IS NOT DEFINED!!!');
|
||||
return;
|
||||
}
|
||||
if (moving && (!sprite.anims.currentAnim || sprite.anims.currentAnim.key !== direction)) {
|
||||
sprite.play(texture+'-'+direction, true);
|
||||
sprite.play(texture+'-'+direction+'-'+PlayerAnimationTypes.Walk, true);
|
||||
} else if (!moving) {
|
||||
sprite.anims.play(texture + '-' + direction, true);
|
||||
sprite.anims.stop();
|
||||
sprite.anims.play(texture + '-' + direction + '-'+PlayerAnimationTypes.Idle, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -177,21 +197,24 @@ export abstract class Character extends Container {
|
|||
|
||||
// up or down animations are prioritized over left and right
|
||||
if (body.velocity.y < 0) { //moving up
|
||||
this.lastDirection = PlayerAnimationNames.WalkUp;
|
||||
this.playAnimation(PlayerAnimationNames.WalkUp, true);
|
||||
this.lastDirection = PlayerAnimationDirections.Up;
|
||||
this.playAnimation(PlayerAnimationDirections.Up, true);
|
||||
} else if (body.velocity.y > 0) { //moving down
|
||||
this.lastDirection = PlayerAnimationNames.WalkDown;
|
||||
this.playAnimation(PlayerAnimationNames.WalkDown, true);
|
||||
this.lastDirection = PlayerAnimationDirections.Down;
|
||||
this.playAnimation(PlayerAnimationDirections.Down, true);
|
||||
} else if (body.velocity.x > 0) { //moving right
|
||||
this.lastDirection = PlayerAnimationNames.WalkRight;
|
||||
this.playAnimation(PlayerAnimationNames.WalkRight, true);
|
||||
this.lastDirection = PlayerAnimationDirections.Right;
|
||||
this.playAnimation(PlayerAnimationDirections.Right, true);
|
||||
} else if (body.velocity.x < 0) { //moving left
|
||||
this.lastDirection = PlayerAnimationNames.WalkLeft;
|
||||
this.playAnimation(PlayerAnimationNames.WalkLeft, true);
|
||||
this.lastDirection = PlayerAnimationDirections.Left;
|
||||
this.playAnimation(PlayerAnimationDirections.Left, true);
|
||||
}
|
||||
|
||||
//update depth user
|
||||
this.setDepth(this.y);
|
||||
|
||||
if (this.companion) {
|
||||
this.companion.setTarget(this.x, this.y, this.lastDirection);
|
||||
}
|
||||
}
|
||||
|
||||
stop(){
|
||||
|
@ -216,7 +239,84 @@ export abstract class Character extends Container {
|
|||
this.scene.sys.updateList.remove(sprite);
|
||||
}
|
||||
}
|
||||
this.list.forEach(objectContaining => objectContaining.destroy())
|
||||
super.destroy();
|
||||
this.playerName.destroy();
|
||||
}
|
||||
|
||||
playEmote(emoteKey: string) {
|
||||
this.cancelPreviousEmote();
|
||||
|
||||
const scalingFactor = waScaleManager.uiScalingFactor * 0.05;
|
||||
const emoteY = -30 - scalingFactor * 10;
|
||||
|
||||
this.playerName.setVisible(false);
|
||||
this.emote = new Sprite(this.scene, 0, 0, emoteKey);
|
||||
this.emote.setAlpha(0);
|
||||
this.emote.setScale(0.1 * scalingFactor);
|
||||
this.add(this.emote);
|
||||
this.scene.sys.updateList.add(this.emote);
|
||||
|
||||
this.createStartTransition(scalingFactor, emoteY);
|
||||
}
|
||||
|
||||
private createStartTransition(scalingFactor: number, emoteY: number) {
|
||||
this.emoteTween = this.scene?.tweens.add({
|
||||
targets: this.emote,
|
||||
props: {
|
||||
scale: scalingFactor,
|
||||
alpha: 1,
|
||||
y: emoteY,
|
||||
},
|
||||
ease: 'Power2',
|
||||
duration: 500,
|
||||
onComplete: () => {
|
||||
this.startPulseTransition(emoteY, scalingFactor);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private startPulseTransition(emoteY: number, scalingFactor: number) {
|
||||
this.emoteTween = this.scene?.tweens.add({
|
||||
targets: this.emote,
|
||||
props: {
|
||||
y: emoteY * 1.3,
|
||||
scale: scalingFactor * 1.1
|
||||
},
|
||||
duration: 250,
|
||||
yoyo: true,
|
||||
repeat: 1,
|
||||
completeDelay: 200,
|
||||
onComplete: () => {
|
||||
this.startExitTransition(emoteY);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private startExitTransition(emoteY: number) {
|
||||
this.emoteTween = this.scene?.tweens.add({
|
||||
targets: this.emote,
|
||||
props: {
|
||||
alpha: 0,
|
||||
y: 2 * emoteY,
|
||||
},
|
||||
ease: 'Power2',
|
||||
duration: 500,
|
||||
onComplete: () => {
|
||||
this.destroyEmote();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
cancelPreviousEmote() {
|
||||
if (!this.emote) return;
|
||||
|
||||
this.emoteTween?.remove();
|
||||
this.destroyEmote()
|
||||
}
|
||||
|
||||
private destroyEmote() {
|
||||
this.emote?.destroy();
|
||||
this.emote = null;
|
||||
this.playerName.setVisible(true);
|
||||
}
|
||||
}
|
||||
|
|
20
front/src/Phaser/Entity/CustomizedCharacter.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import Container = Phaser.GameObjects.Container;
|
||||
import type {Scene} from "phaser";
|
||||
import Sprite = Phaser.GameObjects.Sprite;
|
||||
|
||||
/**
|
||||
* A sprite of a customized character (used in the Customize Scene only)
|
||||
*/
|
||||
export class CustomizedCharacter extends Container {
|
||||
public constructor(scene: Scene, x: number, y: number, layers: string[]) {
|
||||
super(scene, x, y);
|
||||
this.updateSprites(layers);
|
||||
}
|
||||
|
||||
public updateSprites(layers: string[]): void {
|
||||
this.removeAll(true);
|
||||
for (const layer of layers) {
|
||||
this.add(new Sprite(this.scene, 0, 0, layer));
|
||||
}
|
||||
}
|
||||
}
|
344
front/src/Phaser/Entity/PlayerTextures.ts
Normal file
|
@ -0,0 +1,344 @@
|
|||
//The list of all the player textures, both the default models and the partial textures used for customization
|
||||
|
||||
export interface BodyResourceDescriptionListInterface {
|
||||
[key: string]: BodyResourceDescriptionInterface
|
||||
}
|
||||
|
||||
export interface BodyResourceDescriptionInterface {
|
||||
name: string,
|
||||
img: string,
|
||||
level?: number
|
||||
}
|
||||
|
||||
export const PLAYER_RESOURCES: BodyResourceDescriptionListInterface = {
|
||||
"male1": {name: "male1", img: "resources/characters/pipoya/Male 01-1.png"},
|
||||
"male2": {name: "male2", img: "resources/characters/pipoya/Male 02-2.png"},
|
||||
"male3": {name: "male3", img: "resources/characters/pipoya/Male 03-4.png"},
|
||||
"male4": {name: "male4", img: "resources/characters/pipoya/Male 09-1.png"},
|
||||
"male5": {name: "male5", img: "resources/characters/pipoya/Male 10-3.png"},
|
||||
"male6": {name: "male6", img: "resources/characters/pipoya/Male 17-2.png"},
|
||||
"male7": {name: "male7", img: "resources/characters/pipoya/Male 18-1.png"},
|
||||
"male8": {name: "male8", img: "resources/characters/pipoya/Male 16-4.png"},
|
||||
"male9": {name: "male9", img: "resources/characters/pipoya/Male 07-2.png"},
|
||||
"male10": {name: "male10", img: "resources/characters/pipoya/Male 05-3.png"},
|
||||
"male11": {name: "male11", img: "resources/characters/pipoya/Teacher male 02.png"},
|
||||
"male12": {name: "male12", img: "resources/characters/pipoya/su4 Student male 12.png"},
|
||||
|
||||
"Female1": {name: "Female1", img: "resources/characters/pipoya/Female 01-1.png"},
|
||||
"Female2": {name: "Female2", img: "resources/characters/pipoya/Female 02-2.png"},
|
||||
"Female3": {name: "Female3", img: "resources/characters/pipoya/Female 03-4.png"},
|
||||
"Female4": {name: "Female4", img: "resources/characters/pipoya/Female 09-1.png"},
|
||||
"Female5": {name: "Female5", img: "resources/characters/pipoya/Female 10-3.png"},
|
||||
"Female6": {name: "Female6", img: "resources/characters/pipoya/Female 17-2.png"},
|
||||
"Female7": {name: "Female7", img: "resources/characters/pipoya/Female 18-1.png"},
|
||||
"Female8": {name: "Female8", img: "resources/characters/pipoya/Female 16-4.png"},
|
||||
"Female9": {name: "Female9", img: "resources/characters/pipoya/Female 07-2.png"},
|
||||
"Female10": {name: "Female10", img: "resources/characters/pipoya/Female 05-3.png"},
|
||||
"Female11": {name: "Female11", img: "resources/characters/pipoya/Teacher fmale 02.png"},
|
||||
"Female12": {name: "Female12", img: "resources/characters/pipoya/su4 Student fmale 12.png"},
|
||||
};
|
||||
|
||||
export const COLOR_RESOURCES: BodyResourceDescriptionListInterface = {
|
||||
"color_1": {name: "color_1", img: "resources/customisation/character_color/character_color0.png"},
|
||||
"color_2": {name: "color_2", img: "resources/customisation/character_color/character_color1.png"},
|
||||
"color_3": {name: "color_3", img: "resources/customisation/character_color/character_color2.png"},
|
||||
"color_4": {name: "color_4", img: "resources/customisation/character_color/character_color3.png"},
|
||||
"color_5": {name: "color_5", img: "resources/customisation/character_color/character_color4.png"},
|
||||
"color_6": {name: "color_6", img: "resources/customisation/character_color/character_color5.png"},
|
||||
"color_7": {name: "color_7", img: "resources/customisation/character_color/character_color6.png"},
|
||||
"color_8": {name: "color_8", img: "resources/customisation/character_color/character_color7.png"},
|
||||
"color_9": {name: "color_9", img: "resources/customisation/character_color/character_color8.png"},
|
||||
"color_10": {name: "color_10", img: "resources/customisation/character_color/character_color9.png"},
|
||||
"color_11": {name: "color_11", img: "resources/customisation/character_color/character_color10.png"},
|
||||
"color_12": {name: "color_12", img: "resources/customisation/character_color/character_color11.png"},
|
||||
"color_13": {name: "color_13", img: "resources/customisation/character_color/character_color12.png"},
|
||||
"color_14": {name: "color_14", img: "resources/customisation/character_color/character_color13.png"},
|
||||
"color_15": {name: "color_15", img: "resources/customisation/character_color/character_color14.png"},
|
||||
"color_16": {name: "color_16", img: "resources/customisation/character_color/character_color15.png"},
|
||||
"color_17": {name: "color_17", img: "resources/customisation/character_color/character_color16.png"},
|
||||
"color_18": {name: "color_18", img: "resources/customisation/character_color/character_color17.png"},
|
||||
"color_19": {name: "color_19", img: "resources/customisation/character_color/character_color18.png"},
|
||||
"color_20": {name: "color_20", img: "resources/customisation/character_color/character_color19.png"},
|
||||
"color_21": {name: "color_21", img: "resources/customisation/character_color/character_color20.png"},
|
||||
"color_22": {name: "color_22", img: "resources/customisation/character_color/character_color21.png"},
|
||||
"color_23": {name: "color_23", img: "resources/customisation/character_color/character_color22.png"},
|
||||
"color_24": {name: "color_24", img: "resources/customisation/character_color/character_color23.png"},
|
||||
"color_25": {name: "color_25", img: "resources/customisation/character_color/character_color24.png"},
|
||||
"color_26": {name: "color_26", img: "resources/customisation/character_color/character_color25.png"},
|
||||
"color_27": {name: "color_27", img: "resources/customisation/character_color/character_color26.png"},
|
||||
"color_28": {name: "color_28", img: "resources/customisation/character_color/character_color27.png"},
|
||||
"color_29": {name: "color_29", img: "resources/customisation/character_color/character_color28.png"},
|
||||
"color_30": {name: "color_30", img: "resources/customisation/character_color/character_color29.png"},
|
||||
"color_31": {name: "color_31", img: "resources/customisation/character_color/character_color30.png"},
|
||||
"color_32": {name: "color_32", img: "resources/customisation/character_color/character_color31.png"},
|
||||
"color_33": {name: "color_33", img: "resources/customisation/character_color/character_color32.png"}
|
||||
};
|
||||
|
||||
export const EYES_RESOURCES: BodyResourceDescriptionListInterface = {
|
||||
"eyes_1": {name: "eyes_1", img: "resources/customisation/character_eyes/character_eyes1.png"},
|
||||
"eyes_2": {name: "eyes_2", img: "resources/customisation/character_eyes/character_eyes2.png"},
|
||||
"eyes_3": {name: "eyes_3", img: "resources/customisation/character_eyes/character_eyes3.png"},
|
||||
"eyes_4": {name: "eyes_4", img: "resources/customisation/character_eyes/character_eyes4.png"},
|
||||
"eyes_5": {name: "eyes_5", img: "resources/customisation/character_eyes/character_eyes5.png"},
|
||||
"eyes_6": {name: "eyes_6", img: "resources/customisation/character_eyes/character_eyes6.png"},
|
||||
"eyes_7": {name: "eyes_7", img: "resources/customisation/character_eyes/character_eyes7.png"},
|
||||
"eyes_8": {name: "eyes_8", img: "resources/customisation/character_eyes/character_eyes8.png"},
|
||||
"eyes_9": {name: "eyes_9", img: "resources/customisation/character_eyes/character_eyes9.png"},
|
||||
"eyes_10": {name: "eyes_10", img: "resources/customisation/character_eyes/character_eyes10.png"},
|
||||
"eyes_11": {name: "eyes_11", img: "resources/customisation/character_eyes/character_eyes11.png"},
|
||||
"eyes_12": {name: "eyes_12", img: "resources/customisation/character_eyes/character_eyes12.png"},
|
||||
"eyes_13": {name: "eyes_13", img: "resources/customisation/character_eyes/character_eyes13.png"},
|
||||
"eyes_14": {name: "eyes_14", img: "resources/customisation/character_eyes/character_eyes14.png"},
|
||||
"eyes_15": {name: "eyes_15", img: "resources/customisation/character_eyes/character_eyes15.png"},
|
||||
"eyes_16": {name: "eyes_16", img: "resources/customisation/character_eyes/character_eyes16.png"},
|
||||
"eyes_17": {name: "eyes_17", img: "resources/customisation/character_eyes/character_eyes17.png"},
|
||||
"eyes_18": {name: "eyes_18", img: "resources/customisation/character_eyes/character_eyes18.png"},
|
||||
"eyes_19": {name: "eyes_19", img: "resources/customisation/character_eyes/character_eyes19.png"},
|
||||
"eyes_20": {name: "eyes_20", img: "resources/customisation/character_eyes/character_eyes20.png"},
|
||||
"eyes_21": {name: "eyes_21", img: "resources/customisation/character_eyes/character_eyes21.png"},
|
||||
"eyes_22": {name: "eyes_22", img: "resources/customisation/character_eyes/character_eyes22.png"},
|
||||
"eyes_23": {name: "eyes_23", img: "resources/customisation/character_eyes/character_eyes23.png"},
|
||||
"eyes_24": {name: "eyes_24", img: "resources/customisation/character_eyes/character_eyes24.png"},
|
||||
"eyes_25": {name: "eyes_25", img: "resources/customisation/character_eyes/character_eyes25.png"},
|
||||
"eyes_26": {name: "eyes_26", img: "resources/customisation/character_eyes/character_eyes26.png"},
|
||||
"eyes_27": {name: "eyes_27", img: "resources/customisation/character_eyes/character_eyes27.png"},
|
||||
"eyes_28": {name: "eyes_28", img: "resources/customisation/character_eyes/character_eyes28.png"},
|
||||
"eyes_29": {name: "eyes_29", img: "resources/customisation/character_eyes/character_eyes29.png"},
|
||||
"eyes_30": {name: "eyes_30", img: "resources/customisation/character_eyes/character_eyes30.png"}
|
||||
|
||||
};
|
||||
|
||||
export const HAIR_RESOURCES: BodyResourceDescriptionListInterface = {
|
||||
"hair_1": {name:"hair_1", img: "resources/customisation/character_hairs/character_hairs0.png"},
|
||||
"hair_2": {name:"hair_2", img: "resources/customisation/character_hairs/character_hairs1.png"},
|
||||
"hair_3": {name:"hair_3", img: "resources/customisation/character_hairs/character_hairs2.png"},
|
||||
"hair_4": {name:"hair_4", img: "resources/customisation/character_hairs/character_hairs3.png"},
|
||||
"hair_5": {name:"hair_5", img: "resources/customisation/character_hairs/character_hairs4.png"},
|
||||
"hair_6": {name:"hair_6", img: "resources/customisation/character_hairs/character_hairs5.png"},
|
||||
"hair_7": {name:"hair_7", img: "resources/customisation/character_hairs/character_hairs6.png"},
|
||||
"hair_8": {name:"hair_8", img: "resources/customisation/character_hairs/character_hairs7.png"},
|
||||
"hair_9": {name:"hair_9", img: "resources/customisation/character_hairs/character_hairs8.png"},
|
||||
"hair_10": {name:"hair_10",img: "resources/customisation/character_hairs/character_hairs9.png"},
|
||||
"hair_11": {name:"hair_11",img: "resources/customisation/character_hairs/character_hairs10.png"},
|
||||
"hair_12": {name:"hair_12",img: "resources/customisation/character_hairs/character_hairs11.png"},
|
||||
"hair_13": {name:"hair_13",img: "resources/customisation/character_hairs/character_hairs12.png"},
|
||||
"hair_14": {name:"hair_14",img: "resources/customisation/character_hairs/character_hairs13.png"},
|
||||
"hair_15": {name:"hair_15",img: "resources/customisation/character_hairs/character_hairs14.png"},
|
||||
"hair_16": {name:"hair_16",img: "resources/customisation/character_hairs/character_hairs15.png"},
|
||||
"hair_17": {name:"hair_17",img: "resources/customisation/character_hairs/character_hairs16.png"},
|
||||
"hair_18": {name:"hair_18",img: "resources/customisation/character_hairs/character_hairs17.png"},
|
||||
"hair_19": {name:"hair_19",img: "resources/customisation/character_hairs/character_hairs18.png"},
|
||||
"hair_20": {name:"hair_20",img: "resources/customisation/character_hairs/character_hairs19.png"},
|
||||
"hair_21": {name:"hair_21",img: "resources/customisation/character_hairs/character_hairs20.png"},
|
||||
"hair_22": {name:"hair_22",img: "resources/customisation/character_hairs/character_hairs21.png"},
|
||||
"hair_23": {name:"hair_23",img: "resources/customisation/character_hairs/character_hairs22.png"},
|
||||
"hair_24": {name:"hair_24",img: "resources/customisation/character_hairs/character_hairs23.png"},
|
||||
"hair_25": {name:"hair_25",img: "resources/customisation/character_hairs/character_hairs24.png"},
|
||||
"hair_26": {name:"hair_26",img: "resources/customisation/character_hairs/character_hairs25.png"},
|
||||
"hair_27": {name:"hair_27",img: "resources/customisation/character_hairs/character_hairs26.png"},
|
||||
"hair_28": {name:"hair_28",img: "resources/customisation/character_hairs/character_hairs27.png"},
|
||||
"hair_29": {name:"hair_29",img: "resources/customisation/character_hairs/character_hairs28.png"},
|
||||
"hair_30": {name:"hair_30",img: "resources/customisation/character_hairs/character_hairs29.png"},
|
||||
"hair_31": {name:"hair_31",img: "resources/customisation/character_hairs/character_hairs30.png"},
|
||||
"hair_32": {name:"hair_32",img: "resources/customisation/character_hairs/character_hairs31.png"},
|
||||
"hair_33": {name:"hair_33",img: "resources/customisation/character_hairs/character_hairs32.png"},
|
||||
"hair_34": {name:"hair_34",img: "resources/customisation/character_hairs/character_hairs33.png"},
|
||||
"hair_35": {name:"hair_35",img: "resources/customisation/character_hairs/character_hairs34.png"},
|
||||
"hair_36": {name:"hair_36",img: "resources/customisation/character_hairs/character_hairs35.png"},
|
||||
"hair_37": {name:"hair_37",img: "resources/customisation/character_hairs/character_hairs36.png"},
|
||||
"hair_38": {name:"hair_38",img: "resources/customisation/character_hairs/character_hairs37.png"},
|
||||
"hair_39": {name:"hair_39",img: "resources/customisation/character_hairs/character_hairs38.png"},
|
||||
"hair_40": {name:"hair_40",img: "resources/customisation/character_hairs/character_hairs39.png"},
|
||||
"hair_41": {name:"hair_41",img: "resources/customisation/character_hairs/character_hairs40.png"},
|
||||
"hair_42": {name:"hair_42",img: "resources/customisation/character_hairs/character_hairs41.png"},
|
||||
"hair_43": {name:"hair_43",img: "resources/customisation/character_hairs/character_hairs42.png"},
|
||||
"hair_44": {name:"hair_44",img: "resources/customisation/character_hairs/character_hairs43.png"},
|
||||
"hair_45": {name:"hair_45",img: "resources/customisation/character_hairs/character_hairs44.png"},
|
||||
"hair_46": {name:"hair_46",img: "resources/customisation/character_hairs/character_hairs45.png"},
|
||||
"hair_47": {name:"hair_47",img: "resources/customisation/character_hairs/character_hairs46.png"},
|
||||
"hair_48": {name:"hair_48",img: "resources/customisation/character_hairs/character_hairs47.png"},
|
||||
"hair_49": {name:"hair_49",img: "resources/customisation/character_hairs/character_hairs48.png"},
|
||||
"hair_50": {name:"hair_50",img: "resources/customisation/character_hairs/character_hairs49.png"},
|
||||
"hair_51": {name:"hair_51",img: "resources/customisation/character_hairs/character_hairs50.png"},
|
||||
"hair_52": {name:"hair_52",img: "resources/customisation/character_hairs/character_hairs51.png"},
|
||||
"hair_53": {name:"hair_53",img: "resources/customisation/character_hairs/character_hairs52.png"},
|
||||
"hair_54": {name:"hair_54",img: "resources/customisation/character_hairs/character_hairs53.png"},
|
||||
"hair_55": {name:"hair_55",img: "resources/customisation/character_hairs/character_hairs54.png"},
|
||||
"hair_56": {name:"hair_56",img: "resources/customisation/character_hairs/character_hairs55.png"},
|
||||
"hair_57": {name:"hair_57",img: "resources/customisation/character_hairs/character_hairs56.png"},
|
||||
"hair_58": {name:"hair_58",img: "resources/customisation/character_hairs/character_hairs57.png"},
|
||||
"hair_59": {name:"hair_59",img: "resources/customisation/character_hairs/character_hairs58.png"},
|
||||
"hair_60": {name:"hair_60",img: "resources/customisation/character_hairs/character_hairs59.png"},
|
||||
"hair_61": {name:"hair_61",img: "resources/customisation/character_hairs/character_hairs60.png"},
|
||||
"hair_62": {name:"hair_62",img: "resources/customisation/character_hairs/character_hairs61.png"},
|
||||
"hair_63": {name:"hair_63",img: "resources/customisation/character_hairs/character_hairs62.png"},
|
||||
"hair_64": {name:"hair_64",img: "resources/customisation/character_hairs/character_hairs63.png"},
|
||||
"hair_65": {name:"hair_65",img: "resources/customisation/character_hairs/character_hairs64.png"},
|
||||
"hair_66": {name:"hair_66",img: "resources/customisation/character_hairs/character_hairs65.png"},
|
||||
"hair_67": {name:"hair_67",img: "resources/customisation/character_hairs/character_hairs66.png"},
|
||||
"hair_68": {name:"hair_68",img: "resources/customisation/character_hairs/character_hairs67.png"},
|
||||
"hair_69": {name:"hair_69",img: "resources/customisation/character_hairs/character_hairs68.png"},
|
||||
"hair_70": {name:"hair_70",img: "resources/customisation/character_hairs/character_hairs69.png"},
|
||||
"hair_71": {name:"hair_71",img: "resources/customisation/character_hairs/character_hairs70.png"},
|
||||
"hair_72": {name:"hair_72",img: "resources/customisation/character_hairs/character_hairs71.png"},
|
||||
"hair_73": {name:"hair_73",img: "resources/customisation/character_hairs/character_hairs72.png"},
|
||||
"hair_74": {name:"hair_74",img: "resources/customisation/character_hairs/character_hairs73.png"}
|
||||
};
|
||||
|
||||
|
||||
export const CLOTHES_RESOURCES: BodyResourceDescriptionListInterface = {
|
||||
"clothes_1": {name:"clothes_1", img: "resources/customisation/character_clothes/character_clothes0.png"},
|
||||
"clothes_2": {name:"clothes_2", img: "resources/customisation/character_clothes/character_clothes1.png"},
|
||||
"clothes_3": {name:"clothes_3", img: "resources/customisation/character_clothes/character_clothes2.png"},
|
||||
"clothes_4": {name:"clothes_4", img: "resources/customisation/character_clothes/character_clothes3.png"},
|
||||
"clothes_5": {name:"clothes_5", img: "resources/customisation/character_clothes/character_clothes4.png"},
|
||||
"clothes_6": {name:"clothes_6", img: "resources/customisation/character_clothes/character_clothes5.png"},
|
||||
"clothes_7": {name:"clothes_7", img: "resources/customisation/character_clothes/character_clothes6.png"},
|
||||
"clothes_8": {name:"clothes_8", img: "resources/customisation/character_clothes/character_clothes7.png"},
|
||||
"clothes_9": {name:"clothes_9", img: "resources/customisation/character_clothes/character_clothes8.png"},
|
||||
"clothes_10": {name:"clothes_10",img: "resources/customisation/character_clothes/character_clothes9.png"},
|
||||
"clothes_11": {name:"clothes_11",img: "resources/customisation/character_clothes/character_clothes10.png"},
|
||||
"clothes_12": {name:"clothes_12",img: "resources/customisation/character_clothes/character_clothes11.png"},
|
||||
"clothes_13": {name:"clothes_13",img: "resources/customisation/character_clothes/character_clothes12.png"},
|
||||
"clothes_14": {name:"clothes_14",img: "resources/customisation/character_clothes/character_clothes13.png"},
|
||||
"clothes_15": {name:"clothes_15",img: "resources/customisation/character_clothes/character_clothes14.png"},
|
||||
"clothes_16": {name:"clothes_16",img: "resources/customisation/character_clothes/character_clothes15.png"},
|
||||
"clothes_17": {name:"clothes_17",img: "resources/customisation/character_clothes/character_clothes16.png"},
|
||||
"clothes_18": {name:"clothes_18",img: "resources/customisation/character_clothes/character_clothes17.png"},
|
||||
"clothes_19": {name:"clothes_19",img: "resources/customisation/character_clothes/character_clothes18.png"},
|
||||
"clothes_20": {name:"clothes_20",img: "resources/customisation/character_clothes/character_clothes19.png"},
|
||||
"clothes_21": {name:"clothes_21",img: "resources/customisation/character_clothes/character_clothes20.png"},
|
||||
"clothes_22": {name:"clothes_22",img: "resources/customisation/character_clothes/character_clothes21.png"},
|
||||
"clothes_23": {name:"clothes_23",img: "resources/customisation/character_clothes/character_clothes22.png"},
|
||||
"clothes_24": {name:"clothes_24",img: "resources/customisation/character_clothes/character_clothes23.png"},
|
||||
"clothes_25": {name:"clothes_25",img: "resources/customisation/character_clothes/character_clothes24.png"},
|
||||
"clothes_26": {name:"clothes_26",img: "resources/customisation/character_clothes/character_clothes25.png"},
|
||||
"clothes_27": {name:"clothes_27",img: "resources/customisation/character_clothes/character_clothes26.png"},
|
||||
"clothes_28": {name:"clothes_28",img: "resources/customisation/character_clothes/character_clothes27.png"},
|
||||
"clothes_29": {name:"clothes_29",img: "resources/customisation/character_clothes/character_clothes28.png"},
|
||||
"clothes_30": {name:"clothes_30",img: "resources/customisation/character_clothes/character_clothes29.png"},
|
||||
"clothes_31": {name:"clothes_31",img: "resources/customisation/character_clothes/character_clothes30.png"},
|
||||
"clothes_32": {name:"clothes_32",img: "resources/customisation/character_clothes/character_clothes31.png"},
|
||||
"clothes_33": {name:"clothes_33",img: "resources/customisation/character_clothes/character_clothes32.png"},
|
||||
"clothes_34": {name:"clothes_34",img: "resources/customisation/character_clothes/character_clothes33.png"},
|
||||
"clothes_35": {name:"clothes_35",img: "resources/customisation/character_clothes/character_clothes34.png"},
|
||||
"clothes_36": {name:"clothes_36",img: "resources/customisation/character_clothes/character_clothes35.png"},
|
||||
"clothes_37": {name:"clothes_37",img: "resources/customisation/character_clothes/character_clothes36.png"},
|
||||
"clothes_38": {name:"clothes_38",img: "resources/customisation/character_clothes/character_clothes37.png"},
|
||||
"clothes_39": {name:"clothes_39",img: "resources/customisation/character_clothes/character_clothes38.png"},
|
||||
"clothes_40": {name:"clothes_40",img: "resources/customisation/character_clothes/character_clothes39.png"},
|
||||
"clothes_41": {name:"clothes_41",img: "resources/customisation/character_clothes/character_clothes40.png"},
|
||||
"clothes_42": {name:"clothes_42",img: "resources/customisation/character_clothes/character_clothes41.png"},
|
||||
"clothes_43": {name:"clothes_43",img: "resources/customisation/character_clothes/character_clothes42.png"},
|
||||
"clothes_44": {name:"clothes_44",img: "resources/customisation/character_clothes/character_clothes43.png"},
|
||||
"clothes_45": {name:"clothes_45",img: "resources/customisation/character_clothes/character_clothes44.png"},
|
||||
"clothes_46": {name:"clothes_46",img: "resources/customisation/character_clothes/character_clothes45.png"},
|
||||
"clothes_47": {name:"clothes_47",img: "resources/customisation/character_clothes/character_clothes46.png"},
|
||||
"clothes_48": {name:"clothes_48",img: "resources/customisation/character_clothes/character_clothes47.png"},
|
||||
"clothes_49": {name:"clothes_49",img: "resources/customisation/character_clothes/character_clothes48.png"},
|
||||
"clothes_50": {name:"clothes_50",img: "resources/customisation/character_clothes/character_clothes49.png"},
|
||||
"clothes_51": {name:"clothes_51",img: "resources/customisation/character_clothes/character_clothes50.png"},
|
||||
"clothes_52": {name:"clothes_52",img: "resources/customisation/character_clothes/character_clothes51.png"},
|
||||
"clothes_53": {name:"clothes_53",img: "resources/customisation/character_clothes/character_clothes52.png"},
|
||||
"clothes_54": {name:"clothes_54",img: "resources/customisation/character_clothes/character_clothes53.png"},
|
||||
"clothes_55": {name:"clothes_55",img: "resources/customisation/character_clothes/character_clothes54.png"},
|
||||
"clothes_56": {name:"clothes_56",img: "resources/customisation/character_clothes/character_clothes55.png"},
|
||||
"clothes_57": {name:"clothes_57",img: "resources/customisation/character_clothes/character_clothes56.png"},
|
||||
"clothes_58": {name:"clothes_58",img: "resources/customisation/character_clothes/character_clothes57.png"},
|
||||
"clothes_59": {name:"clothes_59",img: "resources/customisation/character_clothes/character_clothes58.png"},
|
||||
"clothes_60": {name:"clothes_60",img: "resources/customisation/character_clothes/character_clothes59.png"},
|
||||
"clothes_61": {name:"clothes_61",img: "resources/customisation/character_clothes/character_clothes60.png"},
|
||||
"clothes_62": {name:"clothes_62",img: "resources/customisation/character_clothes/character_clothes61.png"},
|
||||
"clothes_63": {name:"clothes_63",img: "resources/customisation/character_clothes/character_clothes62.png"},
|
||||
"clothes_64": {name:"clothes_64",img: "resources/customisation/character_clothes/character_clothes63.png"},
|
||||
"clothes_65": {name:"clothes_65",img: "resources/customisation/character_clothes/character_clothes64.png"},
|
||||
"clothes_66": {name:"clothes_66",img: "resources/customisation/character_clothes/character_clothes65.png"},
|
||||
"clothes_67": {name:"clothes_67",img: "resources/customisation/character_clothes/character_clothes66.png"},
|
||||
"clothes_68": {name:"clothes_68",img: "resources/customisation/character_clothes/character_clothes67.png"},
|
||||
"clothes_69": {name:"clothes_69",img: "resources/customisation/character_clothes/character_clothes68.png"},
|
||||
"clothes_70": {name:"clothes_70",img: "resources/customisation/character_clothes/character_clothes69.png"},
|
||||
"clothes_pride_shirt": {name:"clothes_pride_shirt",img: "resources/customisation/character_clothes/pride_shirt.png"},
|
||||
"clothes_black_hoodie": {name:"clothes_black_hoodie",img: "resources/customisation/character_clothes/black_hoodie.png"},
|
||||
"clothes_white_hoodie": {name:"clothes_white_hoodie",img: "resources/customisation/character_clothes/white_hoodie.png"},
|
||||
"clothes_engelbert": {name:"clothes_engelbert",img: "resources/customisation/character_clothes/engelbert.png"}
|
||||
};
|
||||
|
||||
export const HATS_RESOURCES: BodyResourceDescriptionListInterface = {
|
||||
"hats_1": {name: "hats_1", img: "resources/customisation/character_hats/character_hats1.png"},
|
||||
"hats_2": {name: "hats_2", img: "resources/customisation/character_hats/character_hats2.png"},
|
||||
"hats_3": {name: "hats_3", img: "resources/customisation/character_hats/character_hats3.png"},
|
||||
"hats_4": {name: "hats_4", img: "resources/customisation/character_hats/character_hats4.png"},
|
||||
"hats_5": {name: "hats_5", img: "resources/customisation/character_hats/character_hats5.png"},
|
||||
"hats_6": {name: "hats_6", img: "resources/customisation/character_hats/character_hats6.png"},
|
||||
"hats_7": {name: "hats_7", img: "resources/customisation/character_hats/character_hats7.png"},
|
||||
"hats_8": {name: "hats_8", img: "resources/customisation/character_hats/character_hats8.png"},
|
||||
"hats_9": {name: "hats_9", img: "resources/customisation/character_hats/character_hats9.png"},
|
||||
"hats_10": {name: "hats_10", img: "resources/customisation/character_hats/character_hats10.png"},
|
||||
"hats_11": {name: "hats_11", img: "resources/customisation/character_hats/character_hats11.png"},
|
||||
"hats_12": {name: "hats_12", img: "resources/customisation/character_hats/character_hats12.png"},
|
||||
"hats_13": {name: "hats_13", img: "resources/customisation/character_hats/character_hats13.png"},
|
||||
"hats_14": {name: "hats_14", img: "resources/customisation/character_hats/character_hats14.png"},
|
||||
"hats_15": {name: "hats_15", img: "resources/customisation/character_hats/character_hats15.png"},
|
||||
"hats_16": {name: "hats_16", img: "resources/customisation/character_hats/character_hats16.png"},
|
||||
"hats_17": {name: "hats_17", img: "resources/customisation/character_hats/character_hats17.png"},
|
||||
"hats_18": {name: "hats_18", img: "resources/customisation/character_hats/character_hats18.png"},
|
||||
"hats_19": {name: "hats_19", img: "resources/customisation/character_hats/character_hats19.png"},
|
||||
"hats_20": {name: "hats_20", img: "resources/customisation/character_hats/character_hats20.png"},
|
||||
"hats_21": {name: "hats_21", img: "resources/customisation/character_hats/character_hats21.png"},
|
||||
"hats_22": {name: "hats_22", img: "resources/customisation/character_hats/character_hats22.png"},
|
||||
"hats_23": {name: "hats_23", img: "resources/customisation/character_hats/character_hats23.png"},
|
||||
"hats_24": {name: "hats_24", img: "resources/customisation/character_hats/character_hats24.png"},
|
||||
"hats_25": {name: "hats_25", img: "resources/customisation/character_hats/character_hats25.png"},
|
||||
"hats_26": {name: "hats_26", img: "resources/customisation/character_hats/character_hats26.png"},
|
||||
"tinfoil_hat1": {name: "tinfoil_hat1", img: "resources/customisation/character_hats/tinfoil_hat1.png"}
|
||||
};
|
||||
|
||||
export const ACCESSORIES_RESOURCES: BodyResourceDescriptionListInterface = {
|
||||
"accessory_1": {name: "accessory_1", img: "resources/customisation/character_accessories/character_accessories1.png"},
|
||||
"accessory_2": {name: "accessory_2", img: "resources/customisation/character_accessories/character_accessories2.png"},
|
||||
"accessory_3": {name: "accessory_3", img: "resources/customisation/character_accessories/character_accessories3.png"},
|
||||
"accessory_4": {name: "accessory_4", img: "resources/customisation/character_accessories/character_accessories4.png"},
|
||||
"accessory_5": {name: "accessory_5", img: "resources/customisation/character_accessories/character_accessories5.png"},
|
||||
"accessory_6": {name: "accessory_6", img: "resources/customisation/character_accessories/character_accessories6.png"},
|
||||
"accessory_7": {name: "accessory_7", img: "resources/customisation/character_accessories/character_accessories7.png"},
|
||||
"accessory_8": {name: "accessory_8", img: "resources/customisation/character_accessories/character_accessories8.png"},
|
||||
"accessory_9": {name: "accessory_9", img: "resources/customisation/character_accessories/character_accessories9.png"},
|
||||
"accessory_10": {name: "accessory_10", img: "resources/customisation/character_accessories/character_accessories10.png"},
|
||||
"accessory_11": {name: "accessory_11", img: "resources/customisation/character_accessories/character_accessories11.png"},
|
||||
"accessory_12": {name: "accessory_12", img: "resources/customisation/character_accessories/character_accessories12.png"},
|
||||
"accessory_13": {name: "accessory_13", img: "resources/customisation/character_accessories/character_accessories13.png"},
|
||||
"accessory_14": {name: "accessory_14", img: "resources/customisation/character_accessories/character_accessories14.png"},
|
||||
"accessory_15": {name: "accessory_15", img: "resources/customisation/character_accessories/character_accessories15.png"},
|
||||
"accessory_16": {name: "accessory_16", img: "resources/customisation/character_accessories/character_accessories16.png"},
|
||||
"accessory_17": {name: "accessory_17", img: "resources/customisation/character_accessories/character_accessories17.png"},
|
||||
"accessory_18": {name: "accessory_18", img: "resources/customisation/character_accessories/character_accessories18.png"},
|
||||
"accessory_19": {name: "accessory_19", img: "resources/customisation/character_accessories/character_accessories19.png"},
|
||||
"accessory_20": {name: "accessory_20", img: "resources/customisation/character_accessories/character_accessories20.png"},
|
||||
"accessory_21": {name: "accessory_21", img: "resources/customisation/character_accessories/character_accessories21.png"},
|
||||
"accessory_22": {name: "accessory_22", img: "resources/customisation/character_accessories/character_accessories22.png"},
|
||||
"accessory_23": {name: "accessory_23", img: "resources/customisation/character_accessories/character_accessories23.png"},
|
||||
"accessory_24": {name: "accessory_24", img: "resources/customisation/character_accessories/character_accessories24.png"},
|
||||
"accessory_25": {name: "accessory_25", img: "resources/customisation/character_accessories/character_accessories25.png"},
|
||||
"accessory_26": {name: "accessory_26", img: "resources/customisation/character_accessories/character_accessories26.png"},
|
||||
"accessory_27": {name: "accessory_27", img: "resources/customisation/character_accessories/character_accessories27.png"},
|
||||
"accessory_28": {name: "accessory_28", img: "resources/customisation/character_accessories/character_accessories28.png"},
|
||||
"accessory_29": {name: "accessory_29", img: "resources/customisation/character_accessories/character_accessories29.png"},
|
||||
"accessory_30": {name: "accessory_30", img: "resources/customisation/character_accessories/character_accessories30.png"},
|
||||
"accessory_31": {name: "accessory_31", img: "resources/customisation/character_accessories/character_accessories31.png"},
|
||||
"accessory_32": {name: "accessory_32", img: "resources/customisation/character_accessories/character_accessories32.png"},
|
||||
"accessory_mate_bottle": {name: "accessory_mate_bottle", img: "resources/customisation/character_accessories/mate_bottle1.png"},
|
||||
"accessory_mask": {name: "accessory_mask", img: "resources/customisation/character_accessories/mask.png"}
|
||||
};
|
||||
|
||||
export const LAYERS: BodyResourceDescriptionListInterface[] = [
|
||||
COLOR_RESOURCES,
|
||||
EYES_RESOURCES,
|
||||
HAIR_RESOURCES,
|
||||
CLOTHES_RESOURCES,
|
||||
HATS_RESOURCES,
|
||||
ACCESSORIES_RESOURCES
|
||||
];
|
||||
|
||||
export const OBJECTS: BodyResourceDescriptionInterface[] = [
|
||||
{name:'teleportation', img:'resources/objects/teleportation.png'},
|
||||
];
|
90
front/src/Phaser/Entity/PlayerTexturesLoadingManager.ts
Normal file
|
@ -0,0 +1,90 @@
|
|||
import LoaderPlugin = Phaser.Loader.LoaderPlugin;
|
||||
import type {CharacterTexture} from "../../Connexion/LocalUser";
|
||||
import {BodyResourceDescriptionInterface, LAYERS, PLAYER_RESOURCES} from "./PlayerTextures";
|
||||
|
||||
export interface FrameConfig {
|
||||
frameWidth: number,
|
||||
frameHeight: number,
|
||||
}
|
||||
|
||||
export const loadAllLayers = (load: LoaderPlugin): BodyResourceDescriptionInterface[][] => {
|
||||
const returnArray:BodyResourceDescriptionInterface[][] = [];
|
||||
LAYERS.forEach(layer => {
|
||||
const layerArray:BodyResourceDescriptionInterface[] = [];
|
||||
Object.values(layer).forEach((textureDescriptor) => {
|
||||
layerArray.push(textureDescriptor);
|
||||
load.spritesheet(textureDescriptor.name,textureDescriptor.img,{frameWidth: 32, frameHeight: 32});
|
||||
})
|
||||
returnArray.push(layerArray)
|
||||
});
|
||||
return returnArray;
|
||||
}
|
||||
export const loadAllDefaultModels = (load: LoaderPlugin): BodyResourceDescriptionInterface[] => {
|
||||
const returnArray = Object.values(PLAYER_RESOURCES);
|
||||
returnArray.forEach((playerResource: BodyResourceDescriptionInterface) => {
|
||||
load.spritesheet(playerResource.name, playerResource.img, {frameWidth: 32, frameHeight: 32});
|
||||
});
|
||||
return returnArray;
|
||||
}
|
||||
|
||||
export const loadCustomTexture = (loaderPlugin: LoaderPlugin, texture: CharacterTexture) : Promise<BodyResourceDescriptionInterface> => {
|
||||
const name = 'customCharacterTexture'+texture.id;
|
||||
const playerResourceDescriptor: BodyResourceDescriptionInterface = {name, img: texture.url, level: texture.level}
|
||||
return createLoadingPromise(loaderPlugin, playerResourceDescriptor, {
|
||||
frameWidth: 32,
|
||||
frameHeight: 32
|
||||
});
|
||||
}
|
||||
|
||||
export const lazyLoadPlayerCharacterTextures = (loadPlugin: LoaderPlugin, texturekeys:Array<string|BodyResourceDescriptionInterface>): Promise<string[]> => {
|
||||
const promisesList:Promise<unknown>[] = [];
|
||||
texturekeys.forEach((textureKey: string|BodyResourceDescriptionInterface) => {
|
||||
try {
|
||||
//TODO refactor
|
||||
const playerResourceDescriptor = getRessourceDescriptor(textureKey);
|
||||
if (playerResourceDescriptor && !loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
|
||||
promisesList.push(createLoadingPromise(loadPlugin, playerResourceDescriptor, {
|
||||
frameWidth: 32,
|
||||
frameHeight: 32
|
||||
}));
|
||||
}
|
||||
}catch (err){
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
let returnPromise:Promise<Array<string|BodyResourceDescriptionInterface>>;
|
||||
if (promisesList.length > 0) {
|
||||
loadPlugin.start();
|
||||
returnPromise = Promise.all(promisesList).then(() => texturekeys);
|
||||
} else {
|
||||
returnPromise = Promise.resolve(texturekeys);
|
||||
}
|
||||
return returnPromise.then((keys) => keys.map((key) => {
|
||||
return typeof key !== 'string' ? key.name : key;
|
||||
}))
|
||||
}
|
||||
|
||||
export const getRessourceDescriptor = (textureKey: string|BodyResourceDescriptionInterface): BodyResourceDescriptionInterface => {
|
||||
if (typeof textureKey !== 'string' && textureKey.img) {
|
||||
return textureKey;
|
||||
}
|
||||
const textureName:string = typeof textureKey === 'string' ? textureKey : textureKey.name;
|
||||
const playerResource = PLAYER_RESOURCES[textureName];
|
||||
if (playerResource !== undefined) return playerResource;
|
||||
|
||||
for (let i=0; i<LAYERS.length;i++) {
|
||||
const playerResource = LAYERS[i][textureName];
|
||||
if (playerResource !== undefined) return playerResource;
|
||||
}
|
||||
throw 'Could not find a data for texture '+textureName;
|
||||
}
|
||||
|
||||
export const createLoadingPromise = (loadPlugin: LoaderPlugin, playerResourceDescriptor: BodyResourceDescriptionInterface, frameConfig: FrameConfig) => {
|
||||
return new Promise<BodyResourceDescriptionInterface>((res) => {
|
||||
if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
|
||||
return res(playerResourceDescriptor);
|
||||
}
|
||||
loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, frameConfig);
|
||||
loadPlugin.once('filecomplete-spritesheet-' + playerResourceDescriptor.name, () => res(playerResourceDescriptor));
|
||||
});
|
||||
}
|
|
@ -1,13 +1,16 @@
|
|||
import {GameScene} from "../Game/GameScene";
|
||||
import {PointInterface} from "../../Connexion/ConnexionModels";
|
||||
import type {GameScene} from "../Game/GameScene";
|
||||
import type {PointInterface} from "../../Connexion/ConnexionModels";
|
||||
import {Character} from "../Entity/Character";
|
||||
import {Sprite} from "./Sprite";
|
||||
import type {PlayerAnimationDirections} from "../Player/Animation";
|
||||
import {requestVisitCardsStore} from "../../Stores/GameStore";
|
||||
|
||||
|
||||
/**
|
||||
* Class representing the sprite of a remote player (a player that plays on another computer)
|
||||
*/
|
||||
export class RemotePlayer extends Character {
|
||||
userId: number;
|
||||
private visitCardUrl: string|null;
|
||||
|
||||
constructor(
|
||||
userId: number,
|
||||
|
@ -15,21 +18,33 @@ export class RemotePlayer extends Character {
|
|||
x: number,
|
||||
y: number,
|
||||
name: string,
|
||||
PlayerTextures: string[],
|
||||
direction: string,
|
||||
moving: boolean
|
||||
texturesPromise: Promise<string[]>,
|
||||
direction: PlayerAnimationDirections,
|
||||
moving: boolean,
|
||||
visitCardUrl: string|null,
|
||||
companion: string|null,
|
||||
companionTexturePromise?: Promise<string>
|
||||
) {
|
||||
super(Scene, x, y, PlayerTextures, name, direction, moving, 1);
|
||||
|
||||
super(Scene, x, y, texturesPromise, name, direction, moving, 1, !!visitCardUrl, companion, companionTexturePromise);
|
||||
|
||||
//set data
|
||||
this.userId = userId;
|
||||
this.visitCardUrl = visitCardUrl;
|
||||
|
||||
this.on('pointerdown', () => {
|
||||
requestVisitCardsStore.set(this.visitCardUrl);
|
||||
})
|
||||
}
|
||||
|
||||
updatePosition(position: PointInterface): void {
|
||||
this.playAnimation(position.direction, position.moving);
|
||||
this.playAnimation(position.direction as PlayerAnimationDirections, position.moving);
|
||||
this.setX(position.x);
|
||||
this.setY(position.y);
|
||||
|
||||
|
||||
this.setDepth(position.y); //this is to make sure the perspective (player models closer the bottom of the screen will appear in front of models nearer the top of the screen).
|
||||
|
||||
if (this.companion) {
|
||||
this.companion.setTarget(position.x, position.y, position.direction as PlayerAnimationDirections);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import Scene = Phaser.Scene;
|
||||
import {Character} from "./Character";
|
||||
import type {Character} from "./Character";
|
||||
|
||||
//todo: improve this WIP
|
||||
export class SpeechBubble {
|
||||
private bubble: Phaser.GameObjects.Graphics;
|
||||
private content: Phaser.GameObjects.Text;
|
||||
|
||||
|
||||
|
||||
constructor(scene: Scene, player: Character, text: string = "") {
|
||||
|
||||
const bubbleHeight = 50;
|
||||
|
@ -14,7 +14,7 @@ export class SpeechBubble {
|
|||
const bubbleWidth = bubblePadding * 2 + text.length * 10;
|
||||
const arrowHeight = bubbleHeight / 4;
|
||||
|
||||
this.bubble = scene.add.graphics({ x: player.x + 16, y: player.y - 80 });
|
||||
this.bubble = scene.add.graphics({ x: 16, y: -80 });
|
||||
player.add(this.bubble);
|
||||
|
||||
// Bubble shadow
|
||||
|
|
|
@ -1,355 +0,0 @@
|
|||
import LoaderPlugin = Phaser.Loader.LoaderPlugin;
|
||||
import {PLAYER_RESOURCES, PlayerResourceDescriptionInterface} from "./Character";
|
||||
import {CharacterTexture} from "../../Connexion/LocalUser";
|
||||
|
||||
export interface BodyResourceDescriptionInterface {
|
||||
name: string,
|
||||
img: string
|
||||
}
|
||||
|
||||
export const COLOR_RESOURCES: Array<BodyResourceDescriptionInterface> = [
|
||||
{name:"color_1", img: "resources/customisation/character_color/character_color0.png"},
|
||||
{name:"color_2", img: "resources/customisation/character_color/character_color1.png"},
|
||||
{name:"color_3", img: "resources/customisation/character_color/character_color2.png"},
|
||||
{name:"color_4", img: "resources/customisation/character_color/character_color3.png"},
|
||||
{name:"color_5", img: "resources/customisation/character_color/character_color4.png"},
|
||||
{name:"color_6", img: "resources/customisation/character_color/character_color5.png"},
|
||||
{name:"color_7", img: "resources/customisation/character_color/character_color6.png"},
|
||||
{name:"color_8", img: "resources/customisation/character_color/character_color7.png"},
|
||||
{name:"color_9", img: "resources/customisation/character_color/character_color8.png"},
|
||||
{name:"color_10",img: "resources/customisation/character_color/character_color9.png"},
|
||||
{name:"color_11",img: "resources/customisation/character_color/character_color10.png"},
|
||||
{name:"color_12",img: "resources/customisation/character_color/character_color11.png"},
|
||||
{name:"color_13",img: "resources/customisation/character_color/character_color12.png"},
|
||||
{name:"color_14",img: "resources/customisation/character_color/character_color13.png"},
|
||||
{name:"color_15",img: "resources/customisation/character_color/character_color14.png"},
|
||||
{name:"color_16",img: "resources/customisation/character_color/character_color15.png"},
|
||||
{name:"color_17",img: "resources/customisation/character_color/character_color16.png"},
|
||||
{name:"color_18",img: "resources/customisation/character_color/character_color17.png"},
|
||||
{name:"color_19",img: "resources/customisation/character_color/character_color18.png"},
|
||||
{name:"color_20",img: "resources/customisation/character_color/character_color19.png"},
|
||||
{name:"color_21",img: "resources/customisation/character_color/character_color20.png"},
|
||||
{name:"color_22",img: "resources/customisation/character_color/character_color21.png"},
|
||||
{name:"color_23",img: "resources/customisation/character_color/character_color22.png"},
|
||||
{name:"color_24",img: "resources/customisation/character_color/character_color23.png"},
|
||||
{name:"color_25",img: "resources/customisation/character_color/character_color24.png"},
|
||||
{name:"color_26",img: "resources/customisation/character_color/character_color25.png"},
|
||||
{name:"color_27",img: "resources/customisation/character_color/character_color26.png"},
|
||||
{name:"color_28",img: "resources/customisation/character_color/character_color27.png"},
|
||||
{name:"color_29",img: "resources/customisation/character_color/character_color28.png"},
|
||||
{name:"color_30",img: "resources/customisation/character_color/character_color29.png"},
|
||||
{name:"color_31",img: "resources/customisation/character_color/character_color30.png"},
|
||||
{name:"color_32",img: "resources/customisation/character_color/character_color31.png"},
|
||||
{name:"color_33",img: "resources/customisation/character_color/character_color32.png"}
|
||||
];
|
||||
|
||||
export const EYES_RESOURCES: Array<BodyResourceDescriptionInterface> = [
|
||||
{name: "eyes_1", img: "resources/customisation/character_eyes/character_eyes1.png"},
|
||||
{name: "eyes_2", img: "resources/customisation/character_eyes/character_eyes2.png"},
|
||||
{name: "eyes_3", img: "resources/customisation/character_eyes/character_eyes3.png"},
|
||||
{name: "eyes_4", img: "resources/customisation/character_eyes/character_eyes4.png"},
|
||||
{name: "eyes_5", img: "resources/customisation/character_eyes/character_eyes5.png"},
|
||||
{name: "eyes_6", img: "resources/customisation/character_eyes/character_eyes6.png"},
|
||||
{name: "eyes_7", img: "resources/customisation/character_eyes/character_eyes7.png"},
|
||||
{name: "eyes_8", img: "resources/customisation/character_eyes/character_eyes8.png"},
|
||||
{name: "eyes_9", img: "resources/customisation/character_eyes/character_eyes9.png"},
|
||||
{name: "eyes_10", img: "resources/customisation/character_eyes/character_eyes10.png"},
|
||||
{name: "eyes_11", img: "resources/customisation/character_eyes/character_eyes11.png"},
|
||||
{name: "eyes_12", img: "resources/customisation/character_eyes/character_eyes12.png"},
|
||||
{name: "eyes_13", img: "resources/customisation/character_eyes/character_eyes13.png"},
|
||||
{name: "eyes_14", img: "resources/customisation/character_eyes/character_eyes14.png"},
|
||||
{name: "eyes_15", img: "resources/customisation/character_eyes/character_eyes15.png"},
|
||||
{name: "eyes_16", img: "resources/customisation/character_eyes/character_eyes16.png"},
|
||||
{name: "eyes_17", img: "resources/customisation/character_eyes/character_eyes17.png"},
|
||||
{name: "eyes_18", img: "resources/customisation/character_eyes/character_eyes18.png"},
|
||||
{name: "eyes_19", img: "resources/customisation/character_eyes/character_eyes19.png"},
|
||||
{name: "eyes_20", img: "resources/customisation/character_eyes/character_eyes20.png"},
|
||||
{name: "eyes_21", img: "resources/customisation/character_eyes/character_eyes21.png"},
|
||||
{name: "eyes_22", img: "resources/customisation/character_eyes/character_eyes22.png"},
|
||||
{name: "eyes_23", img: "resources/customisation/character_eyes/character_eyes23.png"},
|
||||
{name: "eyes_24", img: "resources/customisation/character_eyes/character_eyes24.png"},
|
||||
{name: "eyes_25", img: "resources/customisation/character_eyes/character_eyes25.png"},
|
||||
{name: "eyes_26", img: "resources/customisation/character_eyes/character_eyes26.png"},
|
||||
{name: "eyes_27", img: "resources/customisation/character_eyes/character_eyes27.png"},
|
||||
{name: "eyes_28", img: "resources/customisation/character_eyes/character_eyes28.png"},
|
||||
{name: "eyes_29", img: "resources/customisation/character_eyes/character_eyes29.png"},
|
||||
{name: "eyes_30", img: "resources/customisation/character_eyes/character_eyes30.png"}
|
||||
|
||||
]
|
||||
|
||||
export const HAIR_RESOURCES: Array<BodyResourceDescriptionInterface> = [
|
||||
{name:"hair_1", img: "resources/customisation/character_hairs/character_hairs0.png"},
|
||||
{name:"hair_2", img: "resources/customisation/character_hairs/character_hairs1.png"},
|
||||
{name:"hair_3", img: "resources/customisation/character_hairs/character_hairs2.png"},
|
||||
{name:"hair_4", img: "resources/customisation/character_hairs/character_hairs3.png"},
|
||||
{name:"hair_5", img: "resources/customisation/character_hairs/character_hairs4.png"},
|
||||
{name:"hair_6", img: "resources/customisation/character_hairs/character_hairs5.png"},
|
||||
{name:"hair_7", img: "resources/customisation/character_hairs/character_hairs6.png"},
|
||||
{name:"hair_8", img: "resources/customisation/character_hairs/character_hairs7.png"},
|
||||
{name:"hair_9", img: "resources/customisation/character_hairs/character_hairs8.png"},
|
||||
{name:"hair_10",img: "resources/customisation/character_hairs/character_hairs9.png"},
|
||||
{name:"hair_11",img: "resources/customisation/character_hairs/character_hairs10.png"},
|
||||
{name:"hair_12",img: "resources/customisation/character_hairs/character_hairs11.png"},
|
||||
{name:"hair_13",img: "resources/customisation/character_hairs/character_hairs12.png"},
|
||||
{name:"hair_14",img: "resources/customisation/character_hairs/character_hairs13.png"},
|
||||
{name:"hair_15",img: "resources/customisation/character_hairs/character_hairs14.png"},
|
||||
{name:"hair_16",img: "resources/customisation/character_hairs/character_hairs15.png"},
|
||||
{name:"hair_17",img: "resources/customisation/character_hairs/character_hairs16.png"},
|
||||
{name:"hair_18",img: "resources/customisation/character_hairs/character_hairs17.png"},
|
||||
{name:"hair_19",img: "resources/customisation/character_hairs/character_hairs18.png"},
|
||||
{name:"hair_20",img: "resources/customisation/character_hairs/character_hairs19.png"},
|
||||
{name:"hair_21",img: "resources/customisation/character_hairs/character_hairs20.png"},
|
||||
{name:"hair_22",img: "resources/customisation/character_hairs/character_hairs21.png"},
|
||||
{name:"hair_23",img: "resources/customisation/character_hairs/character_hairs22.png"},
|
||||
{name:"hair_24",img: "resources/customisation/character_hairs/character_hairs23.png"},
|
||||
{name:"hair_25",img: "resources/customisation/character_hairs/character_hairs24.png"},
|
||||
{name:"hair_26",img: "resources/customisation/character_hairs/character_hairs25.png"},
|
||||
{name:"hair_27",img: "resources/customisation/character_hairs/character_hairs26.png"},
|
||||
{name:"hair_28",img: "resources/customisation/character_hairs/character_hairs27.png"},
|
||||
{name:"hair_29",img: "resources/customisation/character_hairs/character_hairs28.png"},
|
||||
{name:"hair_30",img: "resources/customisation/character_hairs/character_hairs29.png"},
|
||||
{name:"hair_31",img: "resources/customisation/character_hairs/character_hairs30.png"},
|
||||
{name:"hair_32",img: "resources/customisation/character_hairs/character_hairs31.png"},
|
||||
{name:"hair_33",img: "resources/customisation/character_hairs/character_hairs32.png"},
|
||||
{name:"hair_34",img: "resources/customisation/character_hairs/character_hairs33.png"},
|
||||
{name:"hair_35",img: "resources/customisation/character_hairs/character_hairs34.png"},
|
||||
{name:"hair_36",img: "resources/customisation/character_hairs/character_hairs35.png"},
|
||||
{name:"hair_37",img: "resources/customisation/character_hairs/character_hairs36.png"},
|
||||
{name:"hair_38",img: "resources/customisation/character_hairs/character_hairs37.png"},
|
||||
{name:"hair_39",img: "resources/customisation/character_hairs/character_hairs38.png"},
|
||||
{name:"hair_40",img: "resources/customisation/character_hairs/character_hairs39.png"},
|
||||
{name:"hair_41",img: "resources/customisation/character_hairs/character_hairs40.png"},
|
||||
{name:"hair_42",img: "resources/customisation/character_hairs/character_hairs41.png"},
|
||||
{name:"hair_43",img: "resources/customisation/character_hairs/character_hairs42.png"},
|
||||
{name:"hair_44",img: "resources/customisation/character_hairs/character_hairs43.png"},
|
||||
{name:"hair_45",img: "resources/customisation/character_hairs/character_hairs44.png"},
|
||||
{name:"hair_46",img: "resources/customisation/character_hairs/character_hairs45.png"},
|
||||
{name:"hair_47",img: "resources/customisation/character_hairs/character_hairs46.png"},
|
||||
{name:"hair_48",img: "resources/customisation/character_hairs/character_hairs47.png"},
|
||||
{name:"hair_49",img: "resources/customisation/character_hairs/character_hairs48.png"},
|
||||
{name:"hair_50",img: "resources/customisation/character_hairs/character_hairs49.png"},
|
||||
{name:"hair_51",img: "resources/customisation/character_hairs/character_hairs50.png"},
|
||||
{name:"hair_52",img: "resources/customisation/character_hairs/character_hairs51.png"},
|
||||
{name:"hair_53",img: "resources/customisation/character_hairs/character_hairs52.png"},
|
||||
{name:"hair_54",img: "resources/customisation/character_hairs/character_hairs53.png"},
|
||||
{name:"hair_55",img: "resources/customisation/character_hairs/character_hairs54.png"},
|
||||
{name:"hair_56",img: "resources/customisation/character_hairs/character_hairs55.png"},
|
||||
{name:"hair_57",img: "resources/customisation/character_hairs/character_hairs56.png"},
|
||||
{name:"hair_58",img: "resources/customisation/character_hairs/character_hairs57.png"},
|
||||
{name:"hair_59",img: "resources/customisation/character_hairs/character_hairs58.png"},
|
||||
{name:"hair_60",img: "resources/customisation/character_hairs/character_hairs59.png"},
|
||||
{name:"hair_61",img: "resources/customisation/character_hairs/character_hairs60.png"},
|
||||
{name:"hair_62",img: "resources/customisation/character_hairs/character_hairs61.png"},
|
||||
{name:"hair_63",img: "resources/customisation/character_hairs/character_hairs62.png"},
|
||||
{name:"hair_64",img: "resources/customisation/character_hairs/character_hairs63.png"},
|
||||
{name:"hair_65",img: "resources/customisation/character_hairs/character_hairs64.png"},
|
||||
{name:"hair_66",img: "resources/customisation/character_hairs/character_hairs65.png"},
|
||||
{name:"hair_67",img: "resources/customisation/character_hairs/character_hairs66.png"},
|
||||
{name:"hair_68",img: "resources/customisation/character_hairs/character_hairs67.png"},
|
||||
{name:"hair_69",img: "resources/customisation/character_hairs/character_hairs68.png"},
|
||||
{name:"hair_70",img: "resources/customisation/character_hairs/character_hairs69.png"},
|
||||
{name:"hair_71",img: "resources/customisation/character_hairs/character_hairs70.png"},
|
||||
{name:"hair_72",img: "resources/customisation/character_hairs/character_hairs71.png"},
|
||||
{name:"hair_73",img: "resources/customisation/character_hairs/character_hairs72.png"},
|
||||
{name:"hair_74",img: "resources/customisation/character_hairs/character_hairs73.png"}
|
||||
];
|
||||
|
||||
|
||||
export const CLOTHES_RESOURCES: Array<BodyResourceDescriptionInterface> = [
|
||||
{name:"clothes_1", img: "resources/customisation/character_clothes/character_clothes0.png"},
|
||||
{name:"clothes_2", img: "resources/customisation/character_clothes/character_clothes1.png"},
|
||||
{name:"clothes_3", img: "resources/customisation/character_clothes/character_clothes2.png"},
|
||||
{name:"clothes_4", img: "resources/customisation/character_clothes/character_clothes3.png"},
|
||||
{name:"clothes_5", img: "resources/customisation/character_clothes/character_clothes4.png"},
|
||||
{name:"clothes_6", img: "resources/customisation/character_clothes/character_clothes5.png"},
|
||||
{name:"clothes_7", img: "resources/customisation/character_clothes/character_clothes6.png"},
|
||||
{name:"clothes_8", img: "resources/customisation/character_clothes/character_clothes7.png"},
|
||||
{name:"clothes_9", img: "resources/customisation/character_clothes/character_clothes8.png"},
|
||||
{name:"clothes_10",img: "resources/customisation/character_clothes/character_clothes9.png"},
|
||||
{name:"clothes_11",img: "resources/customisation/character_clothes/character_clothes10.png"},
|
||||
{name:"clothes_12",img: "resources/customisation/character_clothes/character_clothes11.png"},
|
||||
{name:"clothes_13",img: "resources/customisation/character_clothes/character_clothes12.png"},
|
||||
{name:"clothes_14",img: "resources/customisation/character_clothes/character_clothes13.png"},
|
||||
{name:"clothes_15",img: "resources/customisation/character_clothes/character_clothes14.png"},
|
||||
{name:"clothes_16",img: "resources/customisation/character_clothes/character_clothes15.png"},
|
||||
{name:"clothes_17",img: "resources/customisation/character_clothes/character_clothes16.png"},
|
||||
{name:"clothes_18",img: "resources/customisation/character_clothes/character_clothes17.png"},
|
||||
{name:"clothes_19",img: "resources/customisation/character_clothes/character_clothes18.png"},
|
||||
{name:"clothes_20",img: "resources/customisation/character_clothes/character_clothes19.png"},
|
||||
{name:"clothes_21",img: "resources/customisation/character_clothes/character_clothes20.png"},
|
||||
{name:"clothes_22",img: "resources/customisation/character_clothes/character_clothes21.png"},
|
||||
{name:"clothes_23",img: "resources/customisation/character_clothes/character_clothes22.png"},
|
||||
{name:"clothes_24",img: "resources/customisation/character_clothes/character_clothes23.png"},
|
||||
{name:"clothes_25",img: "resources/customisation/character_clothes/character_clothes24.png"},
|
||||
{name:"clothes_26",img: "resources/customisation/character_clothes/character_clothes25.png"},
|
||||
{name:"clothes_27",img: "resources/customisation/character_clothes/character_clothes26.png"},
|
||||
{name:"clothes_28",img: "resources/customisation/character_clothes/character_clothes27.png"},
|
||||
{name:"clothes_29",img: "resources/customisation/character_clothes/character_clothes28.png"},
|
||||
{name:"clothes_30",img: "resources/customisation/character_clothes/character_clothes29.png"},
|
||||
{name:"clothes_31",img: "resources/customisation/character_clothes/character_clothes30.png"},
|
||||
{name:"clothes_32",img: "resources/customisation/character_clothes/character_clothes31.png"},
|
||||
{name:"clothes_33",img: "resources/customisation/character_clothes/character_clothes32.png"},
|
||||
{name:"clothes_34",img: "resources/customisation/character_clothes/character_clothes33.png"},
|
||||
{name:"clothes_35",img: "resources/customisation/character_clothes/character_clothes34.png"},
|
||||
{name:"clothes_36",img: "resources/customisation/character_clothes/character_clothes35.png"},
|
||||
{name:"clothes_37",img: "resources/customisation/character_clothes/character_clothes36.png"},
|
||||
{name:"clothes_38",img: "resources/customisation/character_clothes/character_clothes37.png"},
|
||||
{name:"clothes_39",img: "resources/customisation/character_clothes/character_clothes38.png"},
|
||||
{name:"clothes_40",img: "resources/customisation/character_clothes/character_clothes39.png"},
|
||||
{name:"clothes_41",img: "resources/customisation/character_clothes/character_clothes40.png"},
|
||||
{name:"clothes_42",img: "resources/customisation/character_clothes/character_clothes41.png"},
|
||||
{name:"clothes_43",img: "resources/customisation/character_clothes/character_clothes42.png"},
|
||||
{name:"clothes_44",img: "resources/customisation/character_clothes/character_clothes43.png"},
|
||||
{name:"clothes_45",img: "resources/customisation/character_clothes/character_clothes44.png"},
|
||||
{name:"clothes_46",img: "resources/customisation/character_clothes/character_clothes45.png"},
|
||||
{name:"clothes_47",img: "resources/customisation/character_clothes/character_clothes46.png"},
|
||||
{name:"clothes_48",img: "resources/customisation/character_clothes/character_clothes47.png"},
|
||||
{name:"clothes_49",img: "resources/customisation/character_clothes/character_clothes48.png"},
|
||||
{name:"clothes_50",img: "resources/customisation/character_clothes/character_clothes49.png"},
|
||||
{name:"clothes_51",img: "resources/customisation/character_clothes/character_clothes50.png"},
|
||||
{name:"clothes_52",img: "resources/customisation/character_clothes/character_clothes51.png"},
|
||||
{name:"clothes_53",img: "resources/customisation/character_clothes/character_clothes52.png"},
|
||||
{name:"clothes_54",img: "resources/customisation/character_clothes/character_clothes53.png"},
|
||||
{name:"clothes_55",img: "resources/customisation/character_clothes/character_clothes54.png"},
|
||||
{name:"clothes_56",img: "resources/customisation/character_clothes/character_clothes55.png"},
|
||||
{name:"clothes_57",img: "resources/customisation/character_clothes/character_clothes56.png"},
|
||||
{name:"clothes_58",img: "resources/customisation/character_clothes/character_clothes57.png"},
|
||||
{name:"clothes_59",img: "resources/customisation/character_clothes/character_clothes58.png"},
|
||||
{name:"clothes_60",img: "resources/customisation/character_clothes/character_clothes59.png"},
|
||||
{name:"clothes_61",img: "resources/customisation/character_clothes/character_clothes60.png"},
|
||||
{name:"clothes_62",img: "resources/customisation/character_clothes/character_clothes61.png"},
|
||||
{name:"clothes_63",img: "resources/customisation/character_clothes/character_clothes62.png"},
|
||||
{name:"clothes_64",img: "resources/customisation/character_clothes/character_clothes63.png"},
|
||||
{name:"clothes_65",img: "resources/customisation/character_clothes/character_clothes64.png"},
|
||||
{name:"clothes_66",img: "resources/customisation/character_clothes/character_clothes65.png"},
|
||||
{name:"clothes_67",img: "resources/customisation/character_clothes/character_clothes66.png"},
|
||||
{name:"clothes_68",img: "resources/customisation/character_clothes/character_clothes67.png"},
|
||||
{name:"clothes_69",img: "resources/customisation/character_clothes/character_clothes68.png"},
|
||||
{name:"clothes_70",img: "resources/customisation/character_clothes/character_clothes69.png"},
|
||||
{name:"clothes_pride_shirt",img: "resources/customisation/character_clothes/pride_shirt.png"},
|
||||
{name:"clothes_black_hoodie",img: "resources/customisation/character_clothes/black_hoodie.png"},
|
||||
{name:"clothes_white_hoodie",img: "resources/customisation/character_clothes/white_hoodie.png"},
|
||||
{name:"clothes_engelbert",img: "resources/customisation/character_clothes/engelbert.png"}
|
||||
];
|
||||
|
||||
export const HATS_RESOURCES: Array<BodyResourceDescriptionInterface> = [
|
||||
{name: "hats_1", img: "resources/customisation/character_hats/character_hats1.png"},
|
||||
{name: "hats_2", img: "resources/customisation/character_hats/character_hats2.png"},
|
||||
{name: "hats_3", img: "resources/customisation/character_hats/character_hats3.png"},
|
||||
{name: "hats_4", img: "resources/customisation/character_hats/character_hats4.png"},
|
||||
{name: "hats_5", img: "resources/customisation/character_hats/character_hats5.png"},
|
||||
{name: "hats_6", img: "resources/customisation/character_hats/character_hats6.png"},
|
||||
{name: "hats_7", img: "resources/customisation/character_hats/character_hats7.png"},
|
||||
{name: "hats_8", img: "resources/customisation/character_hats/character_hats8.png"},
|
||||
{name: "hats_9", img: "resources/customisation/character_hats/character_hats9.png"},
|
||||
{name: "hats_10", img: "resources/customisation/character_hats/character_hats10.png"},
|
||||
{name: "hats_11", img: "resources/customisation/character_hats/character_hats11.png"},
|
||||
{name: "hats_12", img: "resources/customisation/character_hats/character_hats12.png"},
|
||||
{name: "hats_13", img: "resources/customisation/character_hats/character_hats13.png"},
|
||||
{name: "hats_14", img: "resources/customisation/character_hats/character_hats14.png"},
|
||||
{name: "hats_15", img: "resources/customisation/character_hats/character_hats15.png"},
|
||||
{name: "hats_16", img: "resources/customisation/character_hats/character_hats16.png"},
|
||||
{name: "hats_17", img: "resources/customisation/character_hats/character_hats17.png"},
|
||||
{name: "hats_18", img: "resources/customisation/character_hats/character_hats18.png"},
|
||||
{name: "hats_19", img: "resources/customisation/character_hats/character_hats19.png"},
|
||||
{name: "hats_20", img: "resources/customisation/character_hats/character_hats20.png"},
|
||||
{name: "hats_21", img: "resources/customisation/character_hats/character_hats21.png"},
|
||||
{name: "hats_22", img: "resources/customisation/character_hats/character_hats22.png"},
|
||||
{name: "hats_23", img: "resources/customisation/character_hats/character_hats23.png"},
|
||||
{name: "hats_24", img: "resources/customisation/character_hats/character_hats24.png"},
|
||||
{name: "hats_25", img: "resources/customisation/character_hats/character_hats25.png"},
|
||||
{name: "hats_26", img: "resources/customisation/character_hats/character_hats26.png"},
|
||||
{name: "tinfoil_hat1", img: "resources/customisation/character_hats/tinfoil_hat1.png"}
|
||||
];
|
||||
|
||||
export const ACCESSORIES_RESOURCES: Array<BodyResourceDescriptionInterface> = [
|
||||
{name: "accessory_1", img: "resources/customisation/character_accessories/character_accessories1.png"},
|
||||
{name: "accessory_2", img: "resources/customisation/character_accessories/character_accessories2.png"},
|
||||
{name: "accessory_3", img: "resources/customisation/character_accessories/character_accessories3.png"},
|
||||
{name: "accessory_4", img: "resources/customisation/character_accessories/character_accessories4.png"},
|
||||
{name: "accessory_5", img: "resources/customisation/character_accessories/character_accessories5.png"},
|
||||
{name: "accessory_6", img: "resources/customisation/character_accessories/character_accessories6.png"},
|
||||
{name: "accessory_7", img: "resources/customisation/character_accessories/character_accessories7.png"},
|
||||
{name: "accessory_8", img: "resources/customisation/character_accessories/character_accessories8.png"},
|
||||
{name: "accessory_9", img: "resources/customisation/character_accessories/character_accessories9.png"},
|
||||
{name: "accessory_10", img: "resources/customisation/character_accessories/character_accessories10.png"},
|
||||
{name: "accessory_11", img: "resources/customisation/character_accessories/character_accessories11.png"},
|
||||
{name: "accessory_12", img: "resources/customisation/character_accessories/character_accessories12.png"},
|
||||
{name: "accessory_13", img: "resources/customisation/character_accessories/character_accessories13.png"},
|
||||
{name: "accessory_14", img: "resources/customisation/character_accessories/character_accessories14.png"},
|
||||
{name: "accessory_15", img: "resources/customisation/character_accessories/character_accessories15.png"},
|
||||
{name: "accessory_16", img: "resources/customisation/character_accessories/character_accessories16.png"},
|
||||
{name: "accessory_17", img: "resources/customisation/character_accessories/character_accessories17.png"},
|
||||
{name: "accessory_18", img: "resources/customisation/character_accessories/character_accessories18.png"},
|
||||
{name: "accessory_19", img: "resources/customisation/character_accessories/character_accessories19.png"},
|
||||
{name: "accessory_20", img: "resources/customisation/character_accessories/character_accessories20.png"},
|
||||
{name: "accessory_21", img: "resources/customisation/character_accessories/character_accessories21.png"},
|
||||
{name: "accessory_22", img: "resources/customisation/character_accessories/character_accessories22.png"},
|
||||
{name: "accessory_23", img: "resources/customisation/character_accessories/character_accessories23.png"},
|
||||
{name: "accessory_24", img: "resources/customisation/character_accessories/character_accessories24.png"},
|
||||
{name: "accessory_25", img: "resources/customisation/character_accessories/character_accessories25.png"},
|
||||
{name: "accessory_26", img: "resources/customisation/character_accessories/character_accessories26.png"},
|
||||
{name: "accessory_27", img: "resources/customisation/character_accessories/character_accessories27.png"},
|
||||
{name: "accessory_28", img: "resources/customisation/character_accessories/character_accessories28.png"},
|
||||
{name: "accessory_29", img: "resources/customisation/character_accessories/character_accessories29.png"},
|
||||
{name: "accessory_30", img: "resources/customisation/character_accessories/character_accessories30.png"},
|
||||
{name: "accessory_31", img: "resources/customisation/character_accessories/character_accessories31.png"},
|
||||
{name: "accessory_32", img: "resources/customisation/character_accessories/character_accessories32.png"},
|
||||
{name: "accessory_mate_bottle", img: "resources/customisation/character_accessories/mate_bottle1.png"},
|
||||
{name: "accessory_mask", img: "resources/customisation/character_accessories/mask.png"}
|
||||
];
|
||||
|
||||
export const LAYERS: Array<Array<BodyResourceDescriptionInterface>> = [
|
||||
COLOR_RESOURCES,
|
||||
EYES_RESOURCES,
|
||||
HAIR_RESOURCES,
|
||||
CLOTHES_RESOURCES,
|
||||
HATS_RESOURCES,
|
||||
ACCESSORIES_RESOURCES
|
||||
];
|
||||
|
||||
export const loadAllLayers = (load: LoaderPlugin) => {
|
||||
for (let j = 0; j < LAYERS.length; j++) {
|
||||
for (let i = 0; i < LAYERS[j].length; i++) {
|
||||
load.spritesheet(
|
||||
LAYERS[j][i].name,
|
||||
LAYERS[j][i].img,
|
||||
{frameWidth: 32, frameHeight: 32}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const loadCustomTexture = (load: LoaderPlugin, texture: CharacterTexture) => {
|
||||
const name = 'customCharacterTexture'+texture.id;
|
||||
load.spritesheet(
|
||||
name,
|
||||
texture.url,
|
||||
{frameWidth: 32, frameHeight: 32}
|
||||
);
|
||||
}
|
||||
|
||||
export const OBJECTS: Array<PlayerResourceDescriptionInterface> = [
|
||||
{name:'layout_modes', img:'resources/objects/layout_modes.png'},
|
||||
{name:'teleportation', img:'resources/objects/teleportation.png'},
|
||||
];
|
||||
|
||||
export const loadObject = (load: LoaderPlugin) => {
|
||||
for (let j = 0; j < OBJECTS.length; j++) {
|
||||
load.spritesheet(
|
||||
OBJECTS[j].name,
|
||||
OBJECTS[j].img,
|
||||
{frameWidth: 32, frameHeight: 32}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const loadPlayerCharacters = (load: LoaderPlugin) => {
|
||||
PLAYER_RESOURCES.forEach((playerResource: PlayerResourceDescriptionInterface) => {
|
||||
load.spritesheet(
|
||||
playerResource.name,
|
||||
playerResource.img,
|
||||
{frameWidth: 32, frameHeight: 32}
|
||||
);
|
||||
});
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
import {PointInterface} from "../../Connexion/ConnexionModels";
|
||||
import {BodyResourceDescriptionInterface} from "../Entity/body_character";
|
||||
import type {PointInterface} from "../../Connexion/ConnexionModels";
|
||||
import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
|
||||
|
||||
export interface AddPlayerInterface {
|
||||
userId: number;
|
||||
name: string;
|
||||
characterLayers: BodyResourceDescriptionInterface[];
|
||||
position: PointInterface;
|
||||
visitCardUrl: string|null;
|
||||
companion: string|null;
|
||||
}
|
||||
|
|
8
front/src/Phaser/Game/DepthIndexes.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
//this file contains all the depth indexes which will be used in our game
|
||||
|
||||
export const DEPTH_TILE_INDEX = 0;
|
||||
//Note: Player characters use their y coordinate as their depth to simulate a perspective.
|
||||
//See the Character class.
|
||||
export const DEPTH_OVERLAY_INDEX = 10000;
|
||||
export const DEPTH_INGAME_TEXT_INDEX = 100000;
|
||||
export const DEPTH_UI_INDEX = 1000000;
|
75
front/src/Phaser/Game/DirtyScene.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import {ResizableScene} from "../Login/ResizableScene";
|
||||
import GameObject = Phaser.GameObjects.GameObject;
|
||||
import Events = Phaser.Scenes.Events;
|
||||
import AnimationEvents = Phaser.Animations.Events;
|
||||
import StructEvents = Phaser.Structs.Events;
|
||||
import {SKIP_RENDER_OPTIMIZATIONS} from "../../Enum/EnvironmentVariable";
|
||||
|
||||
/**
|
||||
* A scene that can track its dirty/pristine state.
|
||||
*/
|
||||
export abstract class DirtyScene extends ResizableScene {
|
||||
private isAlreadyTracking: boolean = false;
|
||||
protected dirty:boolean = true;
|
||||
private objectListChanged:boolean = true;
|
||||
private physicsEnabled: boolean = false;
|
||||
|
||||
/**
|
||||
* Track all objects added to the scene and adds a callback each time an animation is added.
|
||||
* Whenever an object is added, removed, or when an animation is played, the dirty state is set to true.
|
||||
*
|
||||
* Note: this does not work with animations from sprites inside containers.
|
||||
*/
|
||||
protected trackDirtyAnims(): void {
|
||||
if (this.isAlreadyTracking || SKIP_RENDER_OPTIMIZATIONS) {
|
||||
return;
|
||||
}
|
||||
this.isAlreadyTracking = true;
|
||||
const trackAnimationFunction = this.trackAnimation.bind(this);
|
||||
this.sys.updateList.on(StructEvents.PROCESS_QUEUE_ADD, (gameObject: GameObject) => {
|
||||
this.objectListChanged = true;
|
||||
gameObject.on(AnimationEvents.ANIMATION_UPDATE, trackAnimationFunction);
|
||||
});
|
||||
this.sys.updateList.on(StructEvents.PROCESS_QUEUE_REMOVE, (gameObject: GameObject) => {
|
||||
this.objectListChanged = true;
|
||||
gameObject.removeListener(AnimationEvents.ANIMATION_UPDATE, trackAnimationFunction);
|
||||
});
|
||||
|
||||
this.events.on(Events.RENDER, () => {
|
||||
this.objectListChanged = false;
|
||||
});
|
||||
|
||||
this.physics.disableUpdate();
|
||||
this.events.on(Events.POST_UPDATE, () => {
|
||||
let objectMoving = false;
|
||||
for (const body of this.physics.world.bodies.entries) {
|
||||
if (body.velocity.x !== 0 || body.velocity.y !== 0) {
|
||||
this.objectListChanged = true;
|
||||
objectMoving = true;
|
||||
if (!this.physicsEnabled) {
|
||||
this.physics.enableUpdate();
|
||||
this.physicsEnabled = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!objectMoving && this.physicsEnabled) {
|
||||
this.physics.disableUpdate();
|
||||
this.physicsEnabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private trackAnimation(): void {
|
||||
this.objectListChanged = true;
|
||||
}
|
||||
|
||||
public isDirty(): boolean {
|
||||
return this.dirty || this.objectListChanged;
|
||||
}
|
||||
|
||||
public onResize(): void {
|
||||
this.objectListChanged = true;
|
||||
}
|
||||
}
|
73
front/src/Phaser/Game/EmoteManager.ts
Normal file
|
@ -0,0 +1,73 @@
|
|||
import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
|
||||
import {emoteEventStream} from "../../Connexion/EmoteEventStream";
|
||||
import type {GameScene} from "./GameScene";
|
||||
import type {RadialMenuItem} from "../Components/RadialMenu";
|
||||
import LoaderPlugin = Phaser.Loader.LoaderPlugin;
|
||||
import type {Subscription} from "rxjs";
|
||||
|
||||
|
||||
interface RegisteredEmote extends BodyResourceDescriptionInterface {
|
||||
name: string;
|
||||
img: string;
|
||||
}
|
||||
|
||||
export const emotes: {[key: string]: RegisteredEmote} = {
|
||||
'emote-heart': {name: 'emote-heart', img: 'resources/emotes/heart-emote.png'},
|
||||
'emote-clap': {name: 'emote-clap', img: 'resources/emotes/clap-emote.png'},
|
||||
'emote-hand': {name: 'emote-hand', img: 'resources/emotes/hand-emote.png'},
|
||||
'emote-thanks': {name: 'emote-thanks', img: 'resources/emotes/thanks-emote.png'},
|
||||
'emote-thumb-up': {name: 'emote-thumb-up', img: 'resources/emotes/thumb-up-emote.png'},
|
||||
'emote-thumb-down': {name: 'emote-thumb-down', img: 'resources/emotes/thumb-down-emote.png'},
|
||||
};
|
||||
|
||||
export class EmoteManager {
|
||||
private subscription: Subscription;
|
||||
|
||||
constructor(private scene: GameScene) {
|
||||
this.subscription = emoteEventStream.stream.subscribe((event) => {
|
||||
const actor = this.scene.MapPlayersByKey.get(event.userId);
|
||||
if (actor) {
|
||||
this.lazyLoadEmoteTexture(event.emoteName).then(emoteKey => {
|
||||
actor.playEmote(emoteKey);
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
createLoadingPromise(loadPlugin: LoaderPlugin, playerResourceDescriptor: BodyResourceDescriptionInterface) {
|
||||
return new Promise<string>((res) => {
|
||||
if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
|
||||
return res(playerResourceDescriptor.name);
|
||||
}
|
||||
loadPlugin.image(playerResourceDescriptor.name, playerResourceDescriptor.img);
|
||||
loadPlugin.once('filecomplete-image-' + playerResourceDescriptor.name, () => res(playerResourceDescriptor.name));
|
||||
});
|
||||
}
|
||||
|
||||
lazyLoadEmoteTexture(textureKey: string): Promise<string> {
|
||||
const emoteDescriptor = emotes[textureKey];
|
||||
if (emoteDescriptor === undefined) {
|
||||
throw 'Emote not found!';
|
||||
}
|
||||
const loadPromise = this.createLoadingPromise(this.scene.load, emoteDescriptor);
|
||||
this.scene.load.start();
|
||||
return loadPromise
|
||||
}
|
||||
|
||||
getMenuImages(): Promise<RadialMenuItem[]> {
|
||||
const promises = [];
|
||||
for (const key in emotes) {
|
||||
const promise = this.lazyLoadEmoteTexture(key).then((textureKey) => {
|
||||
return {
|
||||
image: textureKey,
|
||||
name: textureKey,
|
||||
}
|
||||
});
|
||||
promises.push(promise);
|
||||
}
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
}
|
132
front/src/Phaser/Game/Game.ts
Normal file
|
@ -0,0 +1,132 @@
|
|||
import {SKIP_RENDER_OPTIMIZATIONS} from "../../Enum/EnvironmentVariable";
|
||||
import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager";
|
||||
import {waScaleManager} from "../Services/WaScaleManager";
|
||||
import {ResizableScene} from "../Login/ResizableScene";
|
||||
|
||||
const Events = Phaser.Core.Events;
|
||||
|
||||
/**
|
||||
* A specialization of the main Phaser Game scene.
|
||||
* It comes with an optimization to skip rendering.
|
||||
*
|
||||
* Beware, the "step" function might vary in future versions of Phaser.
|
||||
*
|
||||
* It also automatically calls "onResize" on any scenes extending ResizableScene.
|
||||
*/
|
||||
export class Game extends Phaser.Game {
|
||||
|
||||
private _isDirty = false;
|
||||
|
||||
|
||||
constructor(GameConfig: Phaser.Types.Core.GameConfig) {
|
||||
super(GameConfig);
|
||||
|
||||
this.scale.on(Phaser.Scale.Events.RESIZE, () => {
|
||||
for (const scene of this.scene.getScenes(true)) {
|
||||
if (scene instanceof ResizableScene) {
|
||||
scene.onResize();
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/*window.addEventListener('resize', (event) => {
|
||||
// Let's trigger the onResize method of any active scene that is a ResizableScene
|
||||
for (const scene of this.scene.getScenes(true)) {
|
||||
if (scene instanceof ResizableScene) {
|
||||
scene.onResize(event);
|
||||
}
|
||||
}
|
||||
});*/
|
||||
}
|
||||
|
||||
public step(time: number, delta: number)
|
||||
{
|
||||
// @ts-ignore
|
||||
if (this.pendingDestroy)
|
||||
{
|
||||
// @ts-ignore
|
||||
return this.runDestroy();
|
||||
}
|
||||
|
||||
const eventEmitter = this.events;
|
||||
|
||||
// Global Managers like Input and Sound update in the prestep
|
||||
|
||||
eventEmitter.emit(Events.PRE_STEP, time, delta);
|
||||
|
||||
// This is mostly meant for user-land code and plugins
|
||||
|
||||
eventEmitter.emit(Events.STEP, time, delta);
|
||||
|
||||
// Update the Scene Manager and all active Scenes
|
||||
|
||||
this.scene.update(time, delta);
|
||||
|
||||
// Our final event before rendering starts
|
||||
|
||||
eventEmitter.emit(Events.POST_STEP, time, delta);
|
||||
|
||||
// This "if" is the changed introduced by the new "Game" class to avoid rendering unnecessarily.
|
||||
if (SKIP_RENDER_OPTIMIZATIONS || this.isDirty()) {
|
||||
const renderer = this.renderer;
|
||||
|
||||
// Run the Pre-render (clearing the canvas, setting background colors, etc)
|
||||
|
||||
renderer.preRender();
|
||||
|
||||
eventEmitter.emit(Events.PRE_RENDER, renderer, time, delta);
|
||||
|
||||
// The main render loop. Iterates all Scenes and all Cameras in those scenes, rendering to the renderer instance.
|
||||
|
||||
this.scene.render(renderer);
|
||||
|
||||
// The Post-Render call. Tidies up loose end, takes snapshots, etc.
|
||||
|
||||
renderer.postRender();
|
||||
|
||||
// The final event before the step repeats. Your last chance to do anything to the canvas before it all starts again.
|
||||
|
||||
eventEmitter.emit(Events.POST_RENDER, renderer, time, delta);
|
||||
} else {
|
||||
// @ts-ignore
|
||||
this.scene.isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
private isDirty(): boolean {
|
||||
if (this._isDirty) {
|
||||
this._isDirty = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Loop through the scenes in forward order
|
||||
for (let i = 0; i < this.scene.scenes.length; i++)
|
||||
{
|
||||
const scene = this.scene.scenes[i];
|
||||
const sys = scene.sys;
|
||||
|
||||
if (sys.settings.visible && sys.settings.status >= Phaser.Scenes.LOADING && sys.settings.status < Phaser.Scenes.SLEEPING)
|
||||
{
|
||||
// @ts-ignore
|
||||
if(typeof scene.isDirty === 'function') {
|
||||
// @ts-ignore
|
||||
const isDirty = scene.isDirty() || scene.tweens.getAllTweens().length > 0;
|
||||
if (isDirty) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the game as needing to be redrawn.
|
||||
*/
|
||||
public markDirty(): void {
|
||||
this._isDirty = true;
|
||||
}
|
||||
}
|
|
@ -1,11 +1,14 @@
|
|||
import {GameScene} from "./GameScene";
|
||||
import {connectionManager} from "../../Connexion/ConnectionManager";
|
||||
import {Room} from "../../Connexion/Room";
|
||||
import type {Room} from "../../Connexion/Room";
|
||||
import {MenuScene, MenuSceneName} from "../Menu/MenuScene";
|
||||
import {LoginSceneName} from "../Login/LoginScene";
|
||||
import {SelectCharacterSceneName} from "../Login/SelectCharacterScene";
|
||||
import {EnableCameraSceneName} from "../Login/EnableCameraScene";
|
||||
import {localUserStore} from "../../Connexion/LocalUserStore";
|
||||
import {get} from "svelte/store";
|
||||
import {requestedCameraState, requestedMicrophoneState} from "../../Stores/MediaStore";
|
||||
import {helpCameraSettingsVisibleStore} from "../../Stores/HelpCameraSettingsStore";
|
||||
|
||||
export interface HasMovedEvent {
|
||||
direction: string;
|
||||
|
@ -20,21 +23,23 @@ export interface HasMovedEvent {
|
|||
export class GameManager {
|
||||
private playerName: string|null;
|
||||
private characterLayers: string[]|null;
|
||||
private companion: string|null;
|
||||
private startRoom!:Room;
|
||||
currentGameSceneName: string|null = null;
|
||||
|
||||
|
||||
constructor() {
|
||||
this.playerName = localUserStore.getName();
|
||||
this.characterLayers = localUserStore.getCharacterLayers();
|
||||
this.companion = localUserStore.getCompanion();
|
||||
}
|
||||
|
||||
public async init(scenePlugin: Phaser.Scenes.ScenePlugin): Promise<string> {
|
||||
this.startRoom = await connectionManager.initGameConnexion();
|
||||
await this.loadMap(this.startRoom, scenePlugin);
|
||||
|
||||
|
||||
if (!this.playerName) {
|
||||
return LoginSceneName;
|
||||
} else if (!this.characterLayers) {
|
||||
} else if (!this.characterLayers || !this.characterLayers.length) {
|
||||
return SelectCharacterSceneName;
|
||||
} else {
|
||||
return EnableCameraSceneName;
|
||||
|
@ -55,18 +60,29 @@ export class GameManager {
|
|||
return this.playerName;
|
||||
}
|
||||
|
||||
getCharacterLayers(): string[]|null {
|
||||
getCharacterLayers(): string[] {
|
||||
if (!this.characterLayers) {
|
||||
throw 'characterLayers are not set';
|
||||
}
|
||||
return this.characterLayers;
|
||||
}
|
||||
|
||||
|
||||
setCompanion(companion: string|null): void {
|
||||
this.companion = companion;
|
||||
}
|
||||
|
||||
getCompanion(): string|null {
|
||||
return this.companion;
|
||||
}
|
||||
|
||||
public async loadMap(room: Room, scenePlugin: Phaser.Scenes.ScenePlugin): Promise<void> {
|
||||
const roomID = room.id;
|
||||
const mapUrl = await room.getMapUrl();
|
||||
const mapDetail = await room.getMapDetail();
|
||||
|
||||
const gameIndex = scenePlugin.getIndex(roomID);
|
||||
if(gameIndex === -1){
|
||||
const game : Phaser.Scene = new GameScene(room, mapUrl);
|
||||
const game : Phaser.Scene = new GameScene(room, mapDetail.mapUrl);
|
||||
scenePlugin.add(roomID, game, false);
|
||||
}
|
||||
}
|
||||
|
@ -75,8 +91,13 @@ export class GameManager {
|
|||
console.log('starting '+ (this.currentGameSceneName || this.startRoom.id))
|
||||
scenePlugin.start(this.currentGameSceneName || this.startRoom.id);
|
||||
scenePlugin.launch(MenuSceneName);
|
||||
|
||||
if(!localUserStore.getHelpCameraSettingsShown() && (!get(requestedMicrophoneState) || !get(requestedCameraState))){
|
||||
helpCameraSettingsVisibleStore.set(true);
|
||||
localUserStore.setHelpCameraSettingsShown();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public gameSceneIsCreated(scene: GameScene) {
|
||||
this.currentGameSceneName = scene.scene.key;
|
||||
const menuScene: MenuScene = scene.scene.get(MenuSceneName) as MenuScene;
|
||||
|
@ -110,7 +131,7 @@ export class GameManager {
|
|||
scene.scene.run(fallbackSceneName)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public getCurrentGameScene(scene: Phaser.Scene): GameScene {
|
||||
if (this.currentGameSceneName === null) throw 'No current scene id set!';
|
||||
return scene.scene.get(this.currentGameSceneName) as GameScene
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {ITiledMap} from "../Map/ITiledMap";
|
||||
import type {ITiledMap, ITiledMapLayer} from "../Map/ITiledMap";
|
||||
import {LayersIterator} from "../Map/LayersIterator";
|
||||
|
||||
export type PropertyChangeCallback = (newValue: string | number | boolean | undefined, oldValue: string | number | boolean | undefined, allProps: Map<string, string | boolean | number>) => void;
|
||||
|
||||
|
@ -10,8 +11,10 @@ export class GameMap {
|
|||
private key: number|undefined;
|
||||
private lastProperties = new Map<string, string|boolean|number>();
|
||||
private callbacks = new Map<string, Array<PropertyChangeCallback>>();
|
||||
public readonly layersIterator: LayersIterator;
|
||||
|
||||
public constructor(private map: ITiledMap) {
|
||||
this.layersIterator = new LayersIterator(map);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -29,6 +32,7 @@ export class GameMap {
|
|||
|
||||
const newProps = this.getProperties(key);
|
||||
const oldProps = this.lastProperties;
|
||||
this.lastProperties = newProps;
|
||||
|
||||
// Let's compare the 2 maps:
|
||||
// First new properties vs oldProperties
|
||||
|
@ -45,14 +49,16 @@ export class GameMap {
|
|||
this.trigger(oldPropName, oldPropValue, undefined, newProps);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.lastProperties = newProps;
|
||||
public getCurrentProperties(): Map<string, string|boolean|number> {
|
||||
return this.lastProperties;
|
||||
}
|
||||
|
||||
private getProperties(key: number): Map<string, string|boolean|number> {
|
||||
const properties = new Map<string, string|boolean|number>();
|
||||
|
||||
for (const layer of this.map.layers) {
|
||||
for (const layer of this.layersIterator) {
|
||||
if (layer.type !== 'tilelayer') {
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {HasMovedEvent} from "./GameManager";
|
||||
import type {HasMovedEvent} from "./GameManager";
|
||||
import {MAX_EXTRAPOLATION_TIME} from "../../Enum/EnvironmentVariable";
|
||||
import {PositionInterface} from "../../Connexion/ConnexionModels";
|
||||
import type {PositionInterface} from "../../Connexion/ConnexionModels";
|
||||
|
||||
export class PlayerMovement {
|
||||
public constructor(private startPosition: PositionInterface, private startTick: number, private endPosition: HasMovedEvent, private endTick: number) {
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
* This class is in charge of computing the position of all players.
|
||||
* Player movement is delayed by 200ms so position depends on ticks.
|
||||
*/
|
||||
import {PlayerMovement} from "./PlayerMovement";
|
||||
import {HasMovedEvent} from "./GameManager";
|
||||
import type {PlayerMovement} from "./PlayerMovement";
|
||||
import type {HasMovedEvent} from "./GameManager";
|
||||
|
||||
export class PlayersPositionInterpolator {
|
||||
playerMovements: Map<number, PlayerMovement> = new Map<number, PlayerMovement>();
|
||||
|
|