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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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));
}
}
}

View file

@ -1,8 +1,11 @@
import LoaderPlugin = Phaser.Loader.LoaderPlugin;
import TextureManager = Phaser.Textures.TextureManager;
import {CharacterTexture} from "../../Connexion/LocalUser";
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[][] = [];
@ -27,7 +30,10 @@ export const loadAllDefaultModels = (load: LoaderPlugin): BodyResourceDescriptio
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);
return createLoadingPromise(loaderPlugin, playerResourceDescriptor, {
frameWidth: 32,
frameHeight: 32
});
}
export const lazyLoadPlayerCharacterTextures = (loadPlugin: LoaderPlugin, texturekeys:Array<string|BodyResourceDescriptionInterface>): Promise<string[]> => {
@ -37,7 +43,10 @@ export const lazyLoadPlayerCharacterTextures = (loadPlugin: LoaderPlugin, textur
//TODO refactor
const playerResourceDescriptor = getRessourceDescriptor(textureKey);
if (playerResourceDescriptor && !loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
promisesList.push(createLoadingPromise(loadPlugin, playerResourceDescriptor));
promisesList.push(createLoadingPromise(loadPlugin, playerResourceDescriptor, {
frameWidth: 32,
frameHeight: 32
}));
}
}catch (err){
console.error(err);
@ -70,15 +79,12 @@ export const getRessourceDescriptor = (textureKey: string|BodyResourceDescriptio
throw 'Could not find a data for texture '+textureName;
}
const createLoadingPromise = (loadPlugin: LoaderPlugin, playerResourceDescriptor: BodyResourceDescriptionInterface) => {
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, {
frameWidth: 32,
frameHeight: 32
});
loadPlugin.spritesheet(playerResourceDescriptor.name, playerResourceDescriptor.img, frameConfig);
loadPlugin.once('filecomplete-spritesheet-' + playerResourceDescriptor.name, () => res(playerResourceDescriptor));
});
}

View file

@ -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 {PlayerAnimationDirections} from "../Player/Animation";
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,
@ -18,26 +21,28 @@ export class RemotePlayer extends Character {
texturesPromise: Promise<string[]>,
direction: PlayerAnimationDirections,
moving: boolean,
visitCardUrl: string|null,
companion: string|null,
companionTexturePromise?: Promise<string>
) {
super(Scene, x, y, texturesPromise, name, direction, moving, 1);
super(Scene, x, y, texturesPromise, name, direction, moving, 1, !!visitCardUrl, companion, companionTexturePromise);
//set data
this.userId = userId;
if (typeof companion === 'string') {
this.addCompanion(companion, companionTexturePromise);
}
this.visitCardUrl = visitCardUrl;
this.on('pointerdown', () => {
requestVisitCardsStore.set(this.visitCardUrl);
})
}
updatePosition(position: PointInterface): void {
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);
}

View file

@ -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;

View file

@ -1,10 +1,11 @@
import {PointInterface} from "../../Connexion/ConnexionModels";
import {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
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;
}

View 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;

View file

@ -2,6 +2,8 @@ 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.
@ -10,6 +12,7 @@ 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.
@ -18,17 +21,16 @@ export abstract class DirtyScene extends ResizableScene {
* Note: this does not work with animations from sprites inside containers.
*/
protected trackDirtyAnims(): void {
if (this.isAlreadyTracking) {
if (this.isAlreadyTracking || SKIP_RENDER_OPTIMIZATIONS) {
return;
}
this.isAlreadyTracking = true;
const trackAnimationFunction = this.trackAnimation.bind(this);
this.events.on(Events.ADDED_TO_SCENE, (gameObject: GameObject) => {
this.sys.updateList.on(StructEvents.PROCESS_QUEUE_ADD, (gameObject: GameObject) => {
this.objectListChanged = true;
gameObject.on(AnimationEvents.ANIMATION_UPDATE, trackAnimationFunction);
});
this.events.on(Events.REMOVED_FROM_SCENE, (gameObject: GameObject) => {
this.sys.updateList.on(StructEvents.PROCESS_QUEUE_REMOVE, (gameObject: GameObject) => {
this.objectListChanged = true;
gameObject.removeListener(AnimationEvents.ANIMATION_UPDATE, trackAnimationFunction);
});
@ -36,6 +38,27 @@ export abstract class DirtyScene extends ResizableScene {
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 {
@ -46,7 +69,7 @@ export abstract class DirtyScene extends ResizableScene {
return this.dirty || this.objectListChanged;
}
public onResize(ev: UIEvent): void {
public onResize(): void {
this.objectListChanged = true;
}
}

View 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();
}
}

View file

@ -1,3 +1,8 @@
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;
/**
@ -5,8 +10,35 @@ const Events = Phaser.Core.Events;
* 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
@ -35,7 +67,7 @@ export class Game extends Phaser.Game {
eventEmitter.emit(Events.POST_STEP, time, delta);
// This "if" is the changed introduced by the new "Game" class to avoid rendering unnecessarily.
if (this.isDirty()) {
if (SKIP_RENDER_OPTIMIZATIONS || this.isDirty()) {
const renderer = this.renderer;
// Run the Pre-render (clearing the canvas, setting background colors, etc)
@ -62,6 +94,11 @@ export class Game extends Phaser.Game {
}
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++)
{
@ -85,4 +122,11 @@ export class Game extends Phaser.Game {
return false;
}
/**
* Marks the game as needing to be redrawn.
*/
public markDirty(): void {
this._isDirty = true;
}
}

View file

@ -1,12 +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 {HelpCameraSettingsScene, HelpCameraSettingsSceneName} from "../Menu/HelpCameraSettingsScene";
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;
@ -24,7 +26,7 @@ export class GameManager {
private companion: string|null;
private startRoom!:Room;
currentGameSceneName: string|null = null;
constructor() {
this.playerName = localUserStore.getName();
this.characterLayers = localUserStore.getCharacterLayers();
@ -65,7 +67,7 @@ export class GameManager {
return this.characterLayers;
}
setCompanion(companion: string|null): void {
this.companion = companion;
}
@ -76,11 +78,11 @@ export class GameManager {
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);
}
}
@ -89,9 +91,13 @@ export class GameManager {
console.log('starting '+ (this.currentGameSceneName || this.startRoom.id))
scenePlugin.start(this.currentGameSceneName || this.startRoom.id);
scenePlugin.launch(MenuSceneName);
scenePlugin.launch(HelpCameraSettingsSceneName);//700
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;
@ -125,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

View file

@ -1,4 +1,4 @@
import {ITiledMap, ITiledMapLayer} 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;

View file

@ -1,5 +1,5 @@
import {gameManager, HasMovedEvent} from "./GameManager";
import {
import type {
GroupCreatedUpdatedMessageInterface,
MessageUserJoined,
MessageUserMovedInterface,
@ -9,23 +9,22 @@ import {
PositionInterface,
RoomJoinedMessageInterface
} from "../../Connexion/ConnexionModels";
import {CurrentGamerInterface, hasMovedEventName, Player} from "../Player/Player";
import {hasMovedEventName, Player, requestEmoteEventName} from "../Player/Player";
import {
DEBUG_MODE,
JITSI_PRIVATE_MODE,
MAX_PER_GROUP,
POSITION_DELAY,
} from "../../Enum/EnvironmentVariable";
import {
import type {
ITiledMap,
ITiledMapLayer,
ITiledMapLayerProperty,
ITiledMapObject,
ITiledText,
ITiledMapTileLayer,
ITiledTileSet
} from "../Map/ITiledMap";
import {AddPlayerInterface} from "./AddPlayerInterface";
import type {AddPlayerInterface} from "./AddPlayerInterface";
import {PlayerAnimationDirections} from "../Player/Animation";
import {PlayerMovement} from "./PlayerMovement";
import {PlayersPositionInterpolator} from "./PlayersPositionInterpolator";
@ -49,13 +48,14 @@ import {
import {GameMap} from "./GameMap";
import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager";
import {mediaManager} from "../../WebRtc/MediaManager";
import {ItemFactoryInterface} from "../Items/ItemFactoryInterface";
import {ActionableItem} from "../Items/ActionableItem";
import type {ItemFactoryInterface} from "../Items/ItemFactoryInterface";
import type {ActionableItem} from "../Items/ActionableItem";
import {UserInputManager} from "../UserInput/UserInputManager";
import {UserMovedMessage} from "../../Messages/generated/messages_pb";
import {soundManager} from "./SoundManager";
import type {UserMovedMessage} from "../../Messages/generated/messages_pb";
import {ProtobufClientUtils} from "../../Network/ProtobufClientUtils";
import {connectionManager} from "../../Connexion/ConnectionManager";
import {RoomConnection} from "../../Connexion/RoomConnection";
import type {RoomConnection} from "../../Connexion/RoomConnection";
import {GlobalMessageManager} from "../../Administration/GlobalMessageManager";
import {userMessageManager} from "../../Administration/UserMessageManager";
import {ConsoleGlobalMessageManager} from "../../Administration/ConsoleGlobalMessageManager";
@ -81,7 +81,7 @@ import GameObject = Phaser.GameObjects.GameObject;
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
import DOMElement = Phaser.GameObjects.DOMElement;
import EVENT_TYPE =Phaser.Scenes.Events
import {Subscription} from "rxjs";
import type {Subscription} from "rxjs";
import {worldFullMessageStream} from "../../Connexion/WorldFullMessageStream";
import { lazyLoadCompanionResource } from "../Companion/CompanionTexturesLoadingManager";
import RenderTexture = Phaser.GameObjects.RenderTexture;
@ -91,7 +91,10 @@ import {TextUtils} from "../Components/TextUtils";
import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager";
import {joystickBaseImg, joystickBaseKey, joystickThumbImg, joystickThumbKey} from "../Components/MobileJoystick";
import {DEPTH_OVERLAY_INDEX} from "./DepthIndexes";
import {waScaleManager} from "../Services/WaScaleManager";
import {peerStore} from "../../Stores/PeerStore";
import {EmoteManager} from "./EmoteManager";
export interface GameSceneInitInterface {
initPosition: PointInterface|null,
@ -132,7 +135,7 @@ const defaultStartLayerName = 'start';
export class GameScene extends DirtyScene implements CenterListener {
Terrains : Array<Phaser.Tilemaps.Tileset>;
CurrentPlayer!: CurrentGamerInterface;
CurrentPlayer!: Player;
MapPlayers!: Phaser.Physics.Arcade.Group;
MapPlayersByKey : Map<number, RemotePlayer> = new Map<number, RemotePlayer>();
Map!: Phaser.Tilemaps.Tilemap;
@ -152,11 +155,12 @@ export class GameScene extends DirtyScene implements CenterListener {
private GlobalMessageManager!: GlobalMessageManager;
public ConsoleGlobalMessageManager!: ConsoleGlobalMessageManager;
private connectionAnswerPromise: Promise<RoomJoinedMessageInterface>;
private connectionAnswerPromiseResolve!: (value?: RoomJoinedMessageInterface | PromiseLike<RoomJoinedMessageInterface>) => void;
private connectionAnswerPromiseResolve!: (value: RoomJoinedMessageInterface | PromiseLike<RoomJoinedMessageInterface>) => void;
// A promise that will resolve when the "create" method is called (signaling loading is ended)
private createPromise: Promise<void>;
private createPromiseResolve!: (value?: void | PromiseLike<void>) => void;
private iframeSubscriptionList! : Array<Subscription>;
private peerStoreUnsubscribe!: () => void;
MapUrlFile: string;
RoomId: string;
instance: string;
@ -187,6 +191,8 @@ export class GameScene extends DirtyScene implements CenterListener {
private popUpElements : Map<number, DOMElement> = new Map<number, Phaser.GameObjects.DOMElement>();
private originalMapUrl: string|undefined;
private pinchManager: PinchManager|undefined;
private mapTransitioning: boolean = false; //used to prevent transitions happenning at the same time.
private emoteManager!: EmoteManager;
constructor(private room: Room, MapUrlFile: string, customKey?: string|undefined) {
super({
@ -202,7 +208,7 @@ export class GameScene extends DirtyScene implements CenterListener {
this.createPromise = new Promise<void>((resolve, reject): void => {
this.createPromiseResolve = resolve;
})
});
this.connectionAnswerPromise = new Promise<RoomJoinedMessageInterface>((resolve, reject): void => {
this.connectionAnswerPromiseResolve = resolve;
});
@ -223,6 +229,11 @@ export class GameScene extends DirtyScene implements CenterListener {
this.load.image(joystickBaseKey, joystickBaseImg);
this.load.image(joystickThumbKey, joystickThumbImg);
}
this.load.audio('audio-webrtc-in', '/resources/objects/webrtc-in.mp3');
this.load.audio('audio-webrtc-out', '/resources/objects/webrtc-out.mp3');
//this.load.audio('audio-report-message', '/resources/objects/report-message.mp3');
this.sound.pauseOnBlur = false;
this.load.on(FILE_LOAD_ERROR, (file: {src: string}) => {
// If we happen to be in HTTP and we are trying to load a URL in HTTPS only... (this happens only in dev environments)
if (window.location.protocol === 'http:' && file.src === this.MapUrlFile && file.src.startsWith('http:') && this.originalMapUrl === undefined) {
@ -269,6 +280,14 @@ export class GameScene extends DirtyScene implements CenterListener {
this.load.spritesheet('layout_modes', 'resources/objects/layout_modes.png', {frameWidth: 32, frameHeight: 32});
this.load.bitmapFont('main_font', 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
//eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.load as any).rexWebFont({
custom: {
families: ['Press Start 2P'],
urls: ['/resources/fonts/fonts.css'],
testString: 'abcdefg'
},
});
//this function must stay at the end of preload function
addLoader(this);
@ -418,7 +437,7 @@ export class GameScene extends DirtyScene implements CenterListener {
}
}
if (layer.type === 'objectgroup' && layer.name === 'floorLayer') {
depth = 10000;
depth = DEPTH_OVERLAY_INDEX;
}
if (layer.type === 'objectgroup') {
for (const object of layer.objects) {
@ -504,6 +523,23 @@ export class GameScene extends DirtyScene implements CenterListener {
if (!this.room.isDisconnected()) {
this.connect();
}
this.emoteManager = new EmoteManager(this);
let oldPeerNumber = 0;
this.peerStoreUnsubscribe = peerStore.subscribe((peers) => {
const newPeerNumber = peers.size;
if (newPeerNumber > oldPeerNumber) {
this.sound.play('audio-webrtc-in', {
volume: 0.2
});
} else if (newPeerNumber < oldPeerNumber) {
this.sound.play('audio-webrtc-out', {
volume: 0.2
});
}
oldPeerNumber = newPeerNumber;
});
}
/**
@ -536,6 +572,7 @@ export class GameScene extends DirtyScene implements CenterListener {
characterLayers: message.characterLayers,
name: message.name,
position: message.position,
visitCardUrl: message.visitCardUrl,
companion: message.companion
}
this.addPlayer(userMessage);
@ -608,6 +645,7 @@ export class GameScene extends DirtyScene implements CenterListener {
// When connection is performed, let's connect SimplePeer
this.simplePeer = new SimplePeer(this.connection, !this.room.isPublic, this.playerName);
peerStore.connectToSimplePeer(this.simplePeer);
this.GlobalMessageManager = new GlobalMessageManager(this.connection);
userMessageManager.setReceiveBanListener(this.bannedUser.bind(this));
@ -862,6 +900,24 @@ ${escapedMessage}
this.userInputManager.disableControls();
}));
this.iframeSubscriptionList.push(iframeListener.playSoundStream.subscribe((playSoundEvent)=>
{
const url = new URL(playSoundEvent.url, this.MapUrlFile);
soundManager.playSound(this.load,this.sound,url.toString(),playSoundEvent.config);
}))
this.iframeSubscriptionList.push(iframeListener.stopSoundStream.subscribe((stopSoundEvent)=>
{
const url = new URL(stopSoundEvent.url, this.MapUrlFile);
soundManager.stopSound(this.sound,url.toString());
}))
this.iframeSubscriptionList.push(iframeListener.loadSoundStream.subscribe((loadSoundEvent)=>
{
const url = new URL(loadSoundEvent.url, this.MapUrlFile);
soundManager.loadSound(this.load,this.sound,url.toString());
}))
this.iframeSubscriptionList.push(iframeListener.enablePlayerControlStream.subscribe(()=>{
this.userInputManager.restoreControls();
}));
@ -890,6 +946,8 @@ ${escapedMessage}
}
private onMapExit(exitKey: string) {
if (this.mapTransitioning) return;
this.mapTransitioning = true;
const {roomId, hash} = Room.getIdFromIdentifier(exitKey, this.MapUrlFile, this.instance);
if (!roomId) throw new Error('Could not find the room from its exit key: '+exitKey);
urlManager.pushStartLayerNameToUrl(hash);
@ -907,6 +965,7 @@ ${escapedMessage}
this.initPositionFromLayerName(hash || defaultStartLayerName);
this.CurrentPlayer.x = this.startX;
this.CurrentPlayer.y = this.startY;
setTimeout(() => this.mapTransitioning = false, 500);
}
}
@ -928,6 +987,10 @@ ${escapedMessage}
this.messageSubscription?.unsubscribe();
this.userInputManager.destroy();
this.pinchManager?.destroy();
this.emoteManager.destroy();
this.peerStoreUnsubscribe();
mediaManager.hideGameOverlay();
for(const iframeEvents of this.iframeSubscriptionList){
iframeEvents.unsubscribe();
@ -1039,10 +1102,10 @@ ${escapedMessage}
}
//todo: push that into the gameManager
private async loadNextGame(exitSceneIdentifier: string){
private loadNextGame(exitSceneIdentifier: string): void {
const {roomId, hash} = Room.getIdFromIdentifier(exitSceneIdentifier, this.MapUrlFile, this.instance);
const room = new Room(roomId);
await gameManager.loadMap(room, this.scene);
gameManager.loadMap(room, this.scene).catch(() => {});
}
private startUser(layer: ITiledMapTileLayer): PositionInterface {
@ -1084,7 +1147,6 @@ ${escapedMessage}
}
createCollisionWithPlayer() {
this.physics.disableUpdate();
//add collision layer
this.Layers.forEach((Layer: Phaser.Tilemaps.TilemapLayer) => {
this.physics.add.collider(this.CurrentPlayer, Layer, (object1: GameObject, object2: GameObject) => {
@ -1118,6 +1180,15 @@ ${escapedMessage}
this.companion,
this.companion !== null ? lazyLoadCompanionResource(this.load, this.companion) : undefined
);
this.CurrentPlayer.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
if (pointer.wasTouch && (pointer.event as TouchEvent).touches.length > 1) {
return; //we don't want the menu to open when pinching on a touch screen.
}
this.emoteManager.getMenuImages().then((emoteMenuElements) => this.CurrentPlayer.openOrCloseEmoteMenu(emoteMenuElements))
})
this.CurrentPlayer.on(requestEmoteEventName, (emoteKey: string) => {
this.connection?.emitEmoteEvent(emoteKey);
})
}catch (err){
if(err instanceof TextureError) {
gameManager.leaveGame(this, SelectCharacterSceneName, new SelectCharacterScene());
@ -1218,17 +1289,7 @@ ${escapedMessage}
this.dirty = false;
mediaManager.updateScene();
this.currentTick = time;
if (this.CurrentPlayer.isMoving()) {
this.dirty = true;
}
this.CurrentPlayer.moveUser(delta);
if (this.CurrentPlayer.isMoving()) {
this.dirty = true;
this.physics.enableUpdate();
} else {
this.physics.disableUpdate();
}
// Let's handle all events
while (this.pendingEvents.length !== 0) {
@ -1322,6 +1383,7 @@ ${escapedMessage}
texturesPromise,
addPlayerData.position.direction as PlayerAnimationDirections,
addPlayerData.position.moving,
addPlayerData.visitCardUrl,
addPlayerData.companion,
addPlayerData.companion !== null ? lazyLoadCompanionResource(this.load, addPlayerData.companion) : undefined
);
@ -1428,8 +1490,8 @@ ${escapedMessage}
this.connection?.emitActionableEvent(itemId, eventName, state, parameters);
}
public onResize(ev: UIEvent): void {
super.onResize(ev);
public onResize(): void {
super.onResize();
this.reposition();
// Send new viewport to server

View file

@ -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) {

View file

@ -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>();

View file

@ -0,0 +1,39 @@
import LoaderPlugin = Phaser.Loader.LoaderPlugin;
import BaseSoundManager = Phaser.Sound.BaseSoundManager;
import BaseSound = Phaser.Sound.BaseSound;
import SoundConfig = Phaser.Types.Sound.SoundConfig;
class SoundManager {
private soundPromises : Map<string,Promise<BaseSound>> = new Map<string, Promise<Phaser.Sound.BaseSound>>();
public loadSound (loadPlugin: LoaderPlugin, soundManager : BaseSoundManager, soundUrl: string) : Promise<BaseSound> {
let soundPromise = this.soundPromises.get(soundUrl);
if (soundPromise !== undefined) {
return soundPromise;
}
soundPromise = new Promise<BaseSound>((res) => {
const sound = soundManager.get(soundUrl);
if (sound !== null) {
return res(sound);
}
loadPlugin.audio(soundUrl, soundUrl);
loadPlugin.once('filecomplete-audio-' + soundUrl, () => {
res(soundManager.add(soundUrl));
});
loadPlugin.start();
});
this.soundPromises.set(soundUrl,soundPromise);
return soundPromise;
}
public async playSound(loadPlugin: LoaderPlugin, soundManager : BaseSoundManager, soundUrl: string, config: SoundConfig|undefined) : Promise<void> {
const sound = await this.loadSound(loadPlugin,soundManager,soundUrl);
if (config === undefined) sound.play();
else sound.play(config);
}
public stopSound(soundManager : BaseSoundManager,soundUrl : string){
soundManager.get(soundUrl).stop();
}
}
export const soundManager = new SoundManager();

View file

@ -4,7 +4,7 @@
*/
import Sprite = Phaser.GameObjects.Sprite;
import {OutlinePipeline} from "../Shaders/OutlinePipeline";
import {GameScene} from "../Game/GameScene";
import type {GameScene} from "../Game/GameScene";
type EventCallback = (state: unknown, parameters: unknown) => void;

View file

@ -1,9 +1,9 @@
import * as Phaser from 'phaser';
import {Scene} from "phaser";
import Sprite = Phaser.GameObjects.Sprite;
import {ITiledMapObject} from "../../Map/ITiledMap";
import {ItemFactoryInterface} from "../ItemFactoryInterface";
import {GameScene} from "../../Game/GameScene";
import type {ITiledMapObject} from "../../Map/ITiledMap";
import type {ItemFactoryInterface} from "../ItemFactoryInterface";
import type {GameScene} from "../../Game/GameScene";
import {ActionableItem} from "../ActionableItem";
import * as tg from "generic-type-guard";

View file

@ -1,7 +1,7 @@
import type {GameScene} from "../Game/GameScene";
import type {ITiledMapObject} from "../Map/ITiledMap";
import type {ActionableItem} from "./ActionableItem";
import LoaderPlugin = Phaser.Loader.LoaderPlugin;
import {GameScene} from "../Game/GameScene";
import {ITiledMapObject} from "../Map/ITiledMap";
import {ActionableItem} from "./ActionableItem";
export interface ItemFactoryInterface {
preload: (loader: LoaderPlugin) => void;

View file

@ -1,8 +1,8 @@
import {ResizableScene} from "./ResizableScene";
import {localUserStore} from "../../Connexion/LocalUserStore";
import {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
import {loadCustomTexture} from "../Entity/PlayerTexturesLoadingManager";
import {CharacterTexture} from "../../Connexion/LocalUser";
import type {CharacterTexture} from "../../Connexion/LocalUser";
export abstract class AbstractCharacterScene extends ResizableScene {
@ -38,4 +38,4 @@ export abstract class AbstractCharacterScene extends ResizableScene {
const localUser = localUserStore.getLocalUser();
return localUser?.textures;
}
}
}

View file

@ -6,11 +6,16 @@ import Container = Phaser.GameObjects.Container;
import {gameManager} from "../Game/GameManager";
import {localUserStore} from "../../Connexion/LocalUserStore";
import {addLoader} from "../Components/Loader";
import {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
import {AbstractCharacterScene} from "./AbstractCharacterScene";
import {areCharacterLayersValid} from "../../Connexion/LocalUser";
import { MenuScene } from "../Menu/MenuScene";
import { SelectCharacterSceneName } from "./SelectCharacterScene";
import {customCharacterSceneVisibleStore} from "../../Stores/CustomCharacterStore";
import {selectCharacterSceneVisibleStore} from "../../Stores/SelectCharacterStore";
import {waScaleManager} from "../Services/WaScaleManager";
import {isMobile} from "../../Enum/EnvironmentVariable";
import {CustomizedCharacter} from "../Entity/CustomizedCharacter";
export const CustomizeSceneName = "CustomizeScene";
@ -21,11 +26,14 @@ export class CustomizeScene extends AbstractCharacterScene {
private Rectangle!: Rectangle;
private selectedLayers: number[] = [0];
private containersRow: Container[][] = [];
private activeRow:number = 0;
private containersRow: CustomizedCharacter[][] = [];
public activeRow:number = 0;
private layers: BodyResourceDescriptionInterface[][] = [];
private customizeSceneElement!: Phaser.GameObjects.DOMElement;
protected lazyloadingAttempt = true; //permit to update texture loaded after renderer
private moveHorizontally: number = 0;
private moveVertically: number = 0;
constructor() {
super({
@ -36,7 +44,6 @@ export class CustomizeScene extends AbstractCharacterScene {
preload() {
this.load.html(customizeSceneKey, 'resources/html/CustomCharacterScene.html');
this.layers = loadAllLayers(this.load);
this.loadCustomSceneSelectCharacters().then((bodyResourceDescriptions) => {
bodyResourceDescriptions.forEach((bodyResourceDescription) => {
if(bodyResourceDescription.level == undefined || bodyResourceDescription.level < 0 || bodyResourceDescription.level > 5 ){
@ -44,43 +51,28 @@ export class CustomizeScene extends AbstractCharacterScene {
}
this.layers[bodyResourceDescription.level].unshift(bodyResourceDescription);
});
this.lazyloadingAttempt = true;
});
this.layers = loadAllLayers(this.load);
this.lazyloadingAttempt = false;
//this function must stay at the end of preload function
addLoader(this);
}
create() {
this.customizeSceneElement = this.add.dom(-1000, 0).createFromCache(customizeSceneKey);
this.centerXDomElement(this.customizeSceneElement, 150);
MenuScene.revealMenusAfterInit(this.customizeSceneElement, customizeSceneKey);
this.customizeSceneElement.addListener('click');
this.customizeSceneElement.on('click', (event:MouseEvent) => {
event.preventDefault();
if((event?.target as HTMLInputElement).id === 'customizeSceneButtonLeft') {
this.moveCursorHorizontally(-1);
}else if((event?.target as HTMLInputElement).id === 'customizeSceneButtonRight') {
this.moveCursorHorizontally(1);
}else if((event?.target as HTMLInputElement).id === 'customizeSceneButtonDown') {
this.moveCursorVertically(1);
}else if((event?.target as HTMLInputElement).id === 'customizeSceneButtonUp') {
this.moveCursorVertically(-1);
}else if((event?.target as HTMLInputElement).id === 'customizeSceneFormBack') {
if(this.activeRow > 0){
this.moveCursorVertically(-1);
}else{
this.backToPreviousScene();
}
}else if((event?.target as HTMLButtonElement).id === 'customizeSceneFormSubmit') {
if(this.activeRow < 5){
this.moveCursorVertically(1);
}else{
this.nextSceneToCamera();
}
}
customCharacterSceneVisibleStore.set(true);
this.events.addListener('wake', () => {
waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 3 : 1;
customCharacterSceneVisibleStore.set(true);
});
waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 3 : 1;
this.Rectangle = this.add.rectangle(this.cameras.main.worldView.x + this.cameras.main.width / 2, this.cameras.main.worldView.y + this.cameras.main.height / 3, 32, 33)
this.Rectangle.setStrokeStyle(2, 0xFFFFFF);
this.add.existing(this.Rectangle);
@ -100,10 +92,13 @@ export class CustomizeScene extends AbstractCharacterScene {
this.backToPreviousScene();
});
this.input.keyboard.on('keyup-RIGHT', () => this.moveCursorHorizontally(1));
this.input.keyboard.on('keyup-LEFT', () => this.moveCursorHorizontally(-1));
this.input.keyboard.on('keyup-DOWN', () => this.moveCursorVertically(1));
this.input.keyboard.on('keyup-UP', () => this.moveCursorVertically(-1));
// Note: the key bindings are not directly put on the moveCursorVertically or moveCursorHorizontally methods
// because if 2 such events are fired close to one another, it makes the whole application crawl to a halt (for a reason I cannot
// explain, the list of sprites managed by the update list become immense
this.input.keyboard.on('keyup-RIGHT', () => this.moveHorizontally = 1);
this.input.keyboard.on('keyup-LEFT', () => this.moveHorizontally = -1);
this.input.keyboard.on('keyup-DOWN', () => this.moveVertically = 1);
this.input.keyboard.on('keyup-UP', () => this.moveVertically = -1);
const customCursorPosition = localUserStore.getCustomCursorPosition();
if (customCursorPosition) {
@ -116,7 +111,15 @@ export class CustomizeScene extends AbstractCharacterScene {
this.onResize();
}
private moveCursorHorizontally(index: number): void {
public moveCursorHorizontally(index: number): void {
this.moveHorizontally = index;
}
public moveCursorVertically(index: number): void {
this.moveVertically = index;
}
private doMoveCursorHorizontally(index: number): void {
this.selectedLayers[this.activeRow] += index;
if (this.selectedLayers[this.activeRow] < 0) {
this.selectedLayers[this.activeRow] = 0
@ -128,27 +131,7 @@ export class CustomizeScene extends AbstractCharacterScene {
this.saveInLocalStorage();
}
private moveCursorVertically(index:number): void {
if(index === -1 && this.activeRow === 5){
const button = this.customizeSceneElement.getChildByID('customizeSceneFormSubmit') as HTMLButtonElement;
button.innerHTML = `Next <img src="resources/objects/arrow_up.png"/>`;
}
if(index === 1 && this.activeRow === 4){
const button = this.customizeSceneElement.getChildByID('customizeSceneFormSubmit') as HTMLButtonElement;
button.innerText = 'Finish';
}
if(index === -1 && this.activeRow === 1){
const button = this.customizeSceneElement.getChildByID('customizeSceneFormBack') as HTMLButtonElement;
button.innerText = `Return`;
}
if(index === 1 && this.activeRow === 0){
const button = this.customizeSceneElement.getChildByID('customizeSceneFormBack') as HTMLButtonElement;
button.innerHTML = `Back <img src="resources/objects/arrow_up.png"/>`;
}
private doMoveCursorVertically(index:number): void {
this.activeRow += index;
if (this.activeRow < 0) {
@ -197,20 +180,20 @@ export class CustomizeScene extends AbstractCharacterScene {
* @param selectedItem, The number of the item select (0 for black body...)
*/
private generateCharacter(x: number, y: number, layerNumber: number, selectedItem: number) {
return new Container(this, x, y,this.getContainerChildren(layerNumber,selectedItem));
return new CustomizedCharacter(this, x, y, this.getContainerChildren(layerNumber,selectedItem));
}
private getContainerChildren(layerNumber: number, selectedItem: number): Array<Sprite> {
const children: Array<Sprite> = new Array<Sprite>();
private getContainerChildren(layerNumber: number, selectedItem: number): Array<string> {
const children: Array<string> = new Array<string>();
for (let j = 0; j <= layerNumber; j++) {
if (j === layerNumber) {
children.push(this.generateLayers(0, 0, this.layers[j][selectedItem].name));
children.push(this.layers[j][selectedItem].name);
} else {
const layer = this.selectedLayers[j];
if (layer === undefined) {
continue;
}
children.push(this.generateLayers(0, 0, this.layers[j][layer].name));
children.push(this.layers[j][layer].name);
}
}
return children;
@ -247,30 +230,44 @@ export class CustomizeScene extends AbstractCharacterScene {
* @return a new sprite
*/
private generateLayers(x: number, y: number, name: string): Sprite {
return new Sprite(this, x, y, name);
//return new Sprite(this, x, y, name);
return this.add.sprite(0, 0, name);
}
private updateSelectedLayer() {
for(let i = 0; i < this.containersRow.length; i++){
for(let j = 0; j < this.containersRow[i].length; j++){
const children = this.getContainerChildren(i, j);
this.containersRow[i][j].removeAll(true);
this.containersRow[i][j].add(children);
const children = this.getContainerChildren(i, j);
this.containersRow[i][j].updateSprites(children);
}
}
}
update(time: number, delta: number): void {
if(this.lazyloadingAttempt){
this.moveLayers();
this.lazyloadingAttempt = false;
}
if (this.moveHorizontally !== 0) {
this.doMoveCursorHorizontally(this.moveHorizontally);
this.moveHorizontally = 0;
}
if (this.moveVertically !== 0) {
this.doMoveCursorVertically(this.moveVertically);
this.moveVertically = 0;
}
}
public onResize(): void {
this.moveLayers();
this.Rectangle.x = this.cameras.main.worldView.x + this.cameras.main.width / 2;
this.Rectangle.y = this.cameras.main.worldView.y + this.cameras.main.height / 3;
this.centerXDomElement(this.customizeSceneElement, 150);
}
private nextSceneToCamera(){
@ -288,12 +285,16 @@ export class CustomizeScene extends AbstractCharacterScene {
gameManager.setCharacterLayers(layers);
this.scene.sleep(CustomizeSceneName);
this.scene.remove(SelectCharacterSceneName);
waScaleManager.restoreZoom();
this.events.removeListener('wake');
gameManager.tryResumingGame(this, EnableCameraSceneName);
customCharacterSceneVisibleStore.set(false);
}
private backToPreviousScene(){
this.scene.sleep(CustomizeSceneName);
waScaleManager.restoreZoom();
this.scene.run(SelectCharacterSceneName);
customCharacterSceneVisibleStore.set(false);
}
}

View file

@ -3,280 +3,48 @@ import {TextField} from "../Components/TextField";
import Image = Phaser.GameObjects.Image;
import {mediaManager} from "../../WebRtc/MediaManager";
import {SoundMeter} from "../Components/SoundMeter";
import {SoundMeterSprite} from "../Components/SoundMeterSprite";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager";
import Zone = Phaser.GameObjects.Zone;
import { MenuScene } from "../Menu/MenuScene";
import {ResizableScene} from "./ResizableScene";
import {
enableCameraSceneVisibilityStore,
} from "../../Stores/MediaStore";
export const EnableCameraSceneName = "EnableCameraScene";
enum LoginTextures {
playButton = "play_button",
icon = "icon",
mainFont = "main_font",
arrowRight = "arrow_right",
arrowUp = "arrow_up"
}
const enableCameraSceneKey = 'enableCameraScene';
export class EnableCameraScene extends ResizableScene {
private textField!: TextField;
private cameraNameField!: TextField;
private arrowLeft!: Image;
private arrowRight!: Image;
private arrowDown!: Image;
private arrowUp!: Image;
private microphonesList: MediaDeviceInfo[] = new Array<MediaDeviceInfo>();
private camerasList: MediaDeviceInfo[] = new Array<MediaDeviceInfo>();
private cameraSelected: number = 0;
private microphoneSelected: number = 0;
private soundMeter: SoundMeter;
private soundMeterSprite!: SoundMeterSprite;
private microphoneNameField!: TextField;
private enableCameraSceneElement!: Phaser.GameObjects.DOMElement;
private mobileTapZone!: Zone;
constructor() {
super({
key: EnableCameraSceneName
});
this.soundMeter = new SoundMeter();
}
preload() {
this.load.html(enableCameraSceneKey, 'resources/html/EnableCameraScene.html');
this.load.image(LoginTextures.playButton, "resources/objects/play_button.png");
this.load.image(LoginTextures.arrowRight, "resources/objects/arrow_right.png");
this.load.image(LoginTextures.arrowUp, "resources/objects/arrow_up.png");
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
this.load.bitmapFont(LoginTextures.mainFont, 'resources/fonts/arcade.png', 'resources/fonts/arcade.xml');
}
create() {
this.enableCameraSceneElement = this.add.dom(-1000, 0).createFromCache(enableCameraSceneKey);
this.centerXDomElement(this.enableCameraSceneElement, 300);
MenuScene.revealMenusAfterInit(this.enableCameraSceneElement, enableCameraSceneKey);
const continuingButton = this.enableCameraSceneElement.getChildByID('enableCameraSceneFormSubmit') as HTMLButtonElement;
continuingButton.addEventListener('click', (e) => {
e.preventDefault();
this.login();
});
if (touchScreenManager.supportTouchScreen) {
new PinchManager(this);
}
//this.scale.setZoom(ZOOM_LEVEL);
//Phaser.Display.Align.In.BottomCenter(this.pressReturnField, zone);
/* FIX ME */
this.textField = new TextField(this, this.scale.width / 2, 20, '');
// For mobile purposes - we need a big enough touchable area.
this.mobileTapZone = this.add.zone(this.scale.width / 2,this.scale.height - 30,200,50)
.setInteractive().on("pointerdown", () => {
this.login();
});
this.cameraNameField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height - 60, '');
this.microphoneNameField = new TextField(this, this.game.renderer.width / 2, this.game.renderer.height - 40, '');
this.arrowRight = new Image(this, 0, 0, LoginTextures.arrowRight);
this.arrowRight.setVisible(false);
this.arrowRight.setInteractive().on('pointerdown', this.nextCam.bind(this));
this.add.existing(this.arrowRight);
this.arrowLeft = new Image(this, 0, 0, LoginTextures.arrowRight);
this.arrowLeft.setVisible(false);
this.arrowLeft.flipX = true;
this.arrowLeft.setInteractive().on('pointerdown', this.previousCam.bind(this));
this.add.existing(this.arrowLeft);
this.arrowUp = new Image(this, 0, 0, LoginTextures.arrowUp);
this.arrowUp.setVisible(false);
this.arrowUp.setInteractive().on('pointerdown', this.previousMic.bind(this));
this.add.existing(this.arrowUp);
this.arrowDown = new Image(this, 0, 0, LoginTextures.arrowUp);
this.arrowDown.setVisible(false);
this.arrowDown.flipY = true;
this.arrowDown.setInteractive().on('pointerdown', this.nextMic.bind(this));
this.add.existing(this.arrowDown);
this.input.keyboard.on('keyup-ENTER', () => {
this.login();
});
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').classList.add('active');
const mediaPromise = mediaManager.getCamera();
mediaPromise.then(this.getDevices.bind(this));
mediaPromise.then(this.setupStream.bind(this));
this.input.keyboard.on('keydown-RIGHT', this.nextCam.bind(this));
this.input.keyboard.on('keydown-LEFT', this.previousCam.bind(this));
this.input.keyboard.on('keydown-DOWN', this.nextMic.bind(this));
this.input.keyboard.on('keydown-UP', this.previousMic.bind(this));
this.soundMeterSprite = new SoundMeterSprite(this, 50, 50);
this.soundMeterSprite.setVisible(false);
this.add.existing(this.soundMeterSprite);
this.onResize();
}
private previousCam(): void {
if (this.cameraSelected === 0 || this.camerasList.length === 0) {
return;
}
this.cameraSelected--;
mediaManager.setCamera(this.camerasList[this.cameraSelected].deviceId).then(this.setupStream.bind(this));
}
private nextCam(): void {
if (this.cameraSelected === this.camerasList.length - 1 || this.camerasList.length === 0) {
return;
}
this.cameraSelected++;
// TODO: the change of camera should be OBSERVED (reactive)
mediaManager.setCamera(this.camerasList[this.cameraSelected].deviceId).then(this.setupStream.bind(this));
}
private previousMic(): void {
if (this.microphoneSelected === 0 || this.microphonesList.length === 0) {
return;
}
this.microphoneSelected--;
mediaManager.setMicrophone(this.microphonesList[this.microphoneSelected].deviceId).then(this.setupStream.bind(this));
}
private nextMic(): void {
if (this.microphoneSelected === this.microphonesList.length - 1 || this.microphonesList.length === 0) {
return;
}
this.microphoneSelected++;
// TODO: the change of camera should be OBSERVED (reactive)
mediaManager.setMicrophone(this.microphonesList[this.microphoneSelected].deviceId).then(this.setupStream.bind(this));
}
/**
* Function called each time a camera is changed
*/
private setupStream(stream: MediaStream): void {
const img = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('webRtcSetupNoVideo');
img.style.display = 'none';
const div = HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('myCamVideoSetup');
div.srcObject = stream;
this.soundMeter.connectToSource(stream, new window.AudioContext());
this.soundMeterSprite.setVisible(true);
this.updateWebCamName();
}
private updateWebCamName(): void {
if (this.camerasList.length > 1) {
let label = this.camerasList[this.cameraSelected].label;
// remove text in parenthesis
label = label.replace(/\([^()]*\)/g, '').trim();
// remove accents
label = label.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
this.cameraNameField.text = label;
this.arrowRight.setVisible(this.cameraSelected < this.camerasList.length - 1);
this.arrowLeft.setVisible(this.cameraSelected > 0);
}
if (this.microphonesList.length > 1) {
let label = this.microphonesList[this.microphoneSelected].label;
// remove text in parenthesis
label = label.replace(/\([^()]*\)/g, '').trim();
// remove accents
label = label.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
this.microphoneNameField.text = label;
this.arrowDown.setVisible(this.microphoneSelected < this.microphonesList.length - 1);
this.arrowUp.setVisible(this.microphoneSelected > 0);
}
enableCameraSceneVisibilityStore.showEnableCameraScene();
}
public onResize(): void {
let div = HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('myCamVideoSetup');
let bounds = div.getBoundingClientRect();
if (!div.srcObject) {
div = HtmlUtils.getElementByIdOrFail<HTMLVideoElement>('webRtcSetup');
bounds = div.getBoundingClientRect();
}
this.textField.x = this.game.renderer.width / 2;
this.mobileTapZone.x = this.game.renderer.width / 2;
this.cameraNameField.x = this.game.renderer.width / 2;
this.microphoneNameField.x = this.game.renderer.width / 2;
this.cameraNameField.y = bounds.top / this.scale.zoom - 8;
this.soundMeterSprite.x = this.game.renderer.width / 2 - this.soundMeterSprite.getWidth() / 2;
this.soundMeterSprite.y = bounds.bottom / this.scale.zoom + 16;
this.microphoneNameField.y = this.soundMeterSprite.y + 22;
this.arrowRight.x = bounds.right / this.scale.zoom + 16;
this.arrowRight.y = (bounds.top + bounds.height / 2) / this.scale.zoom;
this.arrowLeft.x = bounds.left / this.scale.zoom - 16;
this.arrowLeft.y = (bounds.top + bounds.height / 2) / this.scale.zoom;
this.arrowDown.x = this.microphoneNameField.x + this.microphoneNameField.width / 2 + 16;
this.arrowDown.y = this.microphoneNameField.y;
this.arrowUp.x = this.microphoneNameField.x - this.microphoneNameField.width / 2 - 16;
this.arrowUp.y = this.microphoneNameField.y;
const actionBtn = document.querySelector<HTMLDivElement>('#enableCameraScene .action');
if (actionBtn !== null) {
actionBtn.style.top = (this.scale.height - 65) + 'px';
}
}
update(time: number, delta: number): void {
this.soundMeterSprite.setVolume(this.soundMeter.getVolume());
mediaManager.updateScene();
this.centerXDomElement(this.enableCameraSceneElement, 300);
}
private login(): void {
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').style.display = 'none';
this.soundMeter.stop();
public login(): void {
enableCameraSceneVisibilityStore.hideEnableCameraScene();
mediaManager.stopCamera();
mediaManager.stopMicrophone();
this.scene.sleep(EnableCameraSceneName)
this.scene.sleep(EnableCameraSceneName);
gameManager.goToStartingMap(this.scene);
}
private async getDevices() {
const mediaDeviceInfos = await navigator.mediaDevices.enumerateDevices();
for (const mediaDeviceInfo of mediaDeviceInfos) {
if (mediaDeviceInfo.kind === 'audioinput') {
this.microphonesList.push(mediaDeviceInfo);
} else if (mediaDeviceInfo.kind === 'videoinput') {
this.camerasList.push(mediaDeviceInfo);
}
}
this.updateWebCamName();
}
}

View file

@ -1,17 +1,12 @@
import {gameManager} from "../Game/GameManager";
import {SelectCharacterSceneName} from "./SelectCharacterScene";
import {ResizableScene} from "./ResizableScene";
import { localUserStore } from "../../Connexion/LocalUserStore";
import {MenuScene} from "../Menu/MenuScene";
import { isUserNameValid } from "../../Connexion/LocalUser";
import {loginSceneVisibleStore} from "../../Stores/LoginSceneStore";
export const LoginSceneName = "LoginScene";
const loginSceneKey = 'loginScene';
export class LoginScene extends ResizableScene {
private loginSceneElement!: Phaser.GameObjects.DOMElement;
private name: string = '';
constructor() {
@ -22,65 +17,25 @@ export class LoginScene extends ResizableScene {
}
preload() {
this.load.html(loginSceneKey, 'resources/html/loginScene.html');
}
create() {
this.loginSceneElement = this.add.dom(-1000, 0).createFromCache(loginSceneKey);
this.centerXDomElement(this.loginSceneElement, 200);
MenuScene.revealMenusAfterInit(this.loginSceneElement, loginSceneKey);
const pErrorElement = this.loginSceneElement.getChildByID('errorLoginScene') as HTMLInputElement;
const inputElement = this.loginSceneElement.getChildByID('loginSceneName') as HTMLInputElement;
inputElement.value = localUserStore.getName() ?? '';
inputElement.focus();
inputElement.addEventListener('keypress', (event: KeyboardEvent) => {
if(inputElement.value.length > 7){
event.preventDefault();
return;
}
pErrorElement.innerHTML = '';
if(inputElement.value && !isUserNameValid(inputElement.value)){
pErrorElement.innerHTML = 'Invalid user name: only letters and numbers are allowed. No spaces.';
}
if (event.key === 'Enter') {
event.preventDefault();
this.login(inputElement);
return;
}
});
const continuingButton = this.loginSceneElement.getChildByID('loginSceneFormSubmit') as HTMLButtonElement;
continuingButton.addEventListener('click', (e) => {
e.preventDefault();
this.login(inputElement);
});
loginSceneVisibleStore.set(true);
}
private login(inputElement: HTMLInputElement): void {
const pErrorElement = this.loginSceneElement.getChildByID('errorLoginScene') as HTMLInputElement;
this.name = inputElement.value;
if (this.name === '') {
pErrorElement.innerHTML = 'The name is empty';
return
}
if(!isUserNameValid(this.name)){
pErrorElement.innerHTML = 'Invalid user name: only letters and numbers are allowed. No spaces.';
return
}
if (this.name === '') return
gameManager.setPlayerName(this.name);
public login(name: string): void {
name = name.trim();
gameManager.setPlayerName(name);
this.scene.stop(LoginSceneName)
gameManager.tryResumingGame(this, SelectCharacterSceneName);
this.scene.remove(LoginSceneName)
this.scene.remove(LoginSceneName);
loginSceneVisibleStore.set(false);
}
update(time: number, delta: number): void {
}
public onResize(ev: UIEvent): void {
this.centerXDomElement(this.loginSceneElement, 200);
public onResize(): void {
}
}

View file

@ -2,7 +2,7 @@ import {Scene} from "phaser";
import DOMElement = Phaser.GameObjects.DOMElement;
export abstract class ResizableScene extends Scene {
public abstract onResize(ev: UIEvent): void;
public abstract onResize(): void;
/**
* Centers the DOM element on the X axis.
@ -17,7 +17,7 @@ export abstract class ResizableScene extends Scene {
&& object.node
&& object.node.getBoundingClientRect().width > 0
? (object.node.getBoundingClientRect().width / 2 / this.scale.zoom)
: (300 / this.scale.zoom)
: (defaultWidth / this.scale.zoom)
);
}
}

View file

@ -1,66 +1,53 @@
import {gameManager} from "../Game/GameManager";
import {TextField} from "../Components/TextField";
import Image = Phaser.GameObjects.Image;
import Rectangle = Phaser.GameObjects.Rectangle;
import {EnableCameraSceneName} from "./EnableCameraScene";
import {CustomizeSceneName} from "./CustomizeScene";
import {localUserStore} from "../../Connexion/LocalUserStore";
import {loadAllDefaultModels} from "../Entity/PlayerTexturesLoadingManager";
import {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
import {AbstractCharacterScene} from "./AbstractCharacterScene";
import {areCharacterLayersValid} from "../../Connexion/LocalUser";
import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager";
import {MenuScene} from "../Menu/MenuScene";
import { SelectCharacterScene, SelectCharacterSceneName } from "./SelectCharacterScene";
import { SelectCharacterScene } from "./SelectCharacterScene";
export class SelectCharacterMobileScene extends SelectCharacterScene {
create(){
super.create();
this.onResize();
this.selectedRectangle.destroy();
}
protected defineSetupPlayer(numero: number){
protected defineSetupPlayer(num: number){
const deltaX = 30;
const deltaY = 2;
let [playerX, playerY] = this.getCharacterPosition();
let playerVisible = true;
let playerScale = 1.5;
let playserOpactity = 1;
let playerOpacity = 1;
if( this.currentSelectUser !== numero ){
if( this.currentSelectUser !== num ){
playerVisible = false;
}
if( numero === (this.currentSelectUser + 1) ){
if( num === (this.currentSelectUser + 1) ){
playerY -= deltaY;
playerX += deltaX;
playerScale = 0.8;
playserOpactity = 0.6;
playerOpacity = 0.6;
playerVisible = true;
}
if( numero === (this.currentSelectUser + 2) ){
if( num === (this.currentSelectUser + 2) ){
playerY -= deltaY;
playerX += (deltaX * 2);
playerScale = 0.8;
playserOpactity = 0.6;
playerOpacity = 0.6;
playerVisible = true;
}
if( numero === (this.currentSelectUser - 1) ){
if( num === (this.currentSelectUser - 1) ){
playerY -= deltaY;
playerX -= deltaX;
playerScale = 0.8;
playserOpactity = 0.6;
playerOpacity = 0.6;
playerVisible = true;
}
if( numero === (this.currentSelectUser - 2) ){
if( num === (this.currentSelectUser - 2) ){
playerY -= deltaY;
playerX -= (deltaX * 2);
playerScale = 0.8;
playserOpactity = 0.6;
playerOpacity = 0.6;
playerVisible = true;
}
return {playerX, playerY, playerScale, playserOpactity, playerVisible}
return {playerX, playerY, playerScale, playerOpacity, playerVisible}
}
/**

View file

@ -1,18 +1,20 @@
import {gameManager} from "../Game/GameManager";
import Image = Phaser.GameObjects.Image;
import Rectangle = Phaser.GameObjects.Rectangle;
import {EnableCameraSceneName} from "./EnableCameraScene";
import {CustomizeSceneName} from "./CustomizeScene";
import {localUserStore} from "../../Connexion/LocalUserStore";
import {loadAllDefaultModels} from "../Entity/PlayerTexturesLoadingManager";
import {addLoader} from "../Components/Loader";
import {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
import {AbstractCharacterScene} from "./AbstractCharacterScene";
import {areCharacterLayersValid} from "../../Connexion/LocalUser";
import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager";
import {MenuScene} from "../Menu/MenuScene";
import { SelectCharacterMobileScene } from "./SelectCharacterMobileScene";
import {selectCharacterSceneVisibleStore} from "../../Stores/SelectCharacterStore";
import {customCharacterSceneVisibleStore} from "../../Stores/CustomCharacterStore";
import {waScaleManager} from "../Services/WaScaleManager";
import {isMobile} from "../../Enum/EnvironmentVariable";
//todo: put this constants in a dedicated file
export const SelectCharacterSceneName = "SelectCharacterScene";
@ -29,6 +31,10 @@ export class SelectCharacterScene extends AbstractCharacterScene {
protected selectCharacterSceneElement!: Phaser.GameObjects.DOMElement;
protected currentSelectUser = 0;
protected pointerClicked: boolean = false;
protected pointerTimer: number = 0;
protected lazyloadingAttempt = true; //permit to update texture loaded after renderer
constructor() {
super({
@ -43,44 +49,36 @@ export class SelectCharacterScene extends AbstractCharacterScene {
bodyResourceDescriptions.forEach((bodyResourceDescription) => {
this.playerModels.push(bodyResourceDescription);
});
})
this.lazyloadingAttempt = true;
});
this.playerModels = loadAllDefaultModels(this.load);
this.lazyloadingAttempt = false;
//this function must stay at the end of preload function
addLoader(this);
}
create() {
this.selectCharacterSceneElement = this.add.dom(-1000, 0).createFromCache(selectCharacterKey);
this.centerXDomElement(this.selectCharacterSceneElement, 150);
MenuScene.revealMenusAfterInit(this.selectCharacterSceneElement, selectCharacterKey);
this.selectCharacterSceneElement.addListener('click');
this.selectCharacterSceneElement.on('click', (event:MouseEvent) => {
event.preventDefault();
if((event?.target as HTMLInputElement).id === 'selectCharacterButtonLeft') {
this.moveToLeft();
}else if((event?.target as HTMLInputElement).id === 'selectCharacterButtonRight') {
this.moveToRight();
}else if((event?.target as HTMLInputElement).id === 'selectCharacterSceneFormSubmit') {
this.nextSceneToCameraScene();
}else if((event?.target as HTMLInputElement).id === 'selectCharacterSceneFormCustomYourOwnSubmit') {
this.nextSceneToCustomizeScene();
}
selectCharacterSceneVisibleStore.set(true);
this.events.addListener('wake', () => {
waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 2 : 1;
selectCharacterSceneVisibleStore.set(true);
});
if (touchScreenManager.supportTouchScreen) {
new PinchManager(this);
}
waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 2 : 1;
const rectangleXStart = this.game.renderer.width / 2 - (this.nbCharactersPerRow / 2) * 32 + 16;
this.selectedRectangle = this.add.rectangle(rectangleXStart, 90, 32, 32).setStrokeStyle(2, 0xFFFFFF);
this.selectedRectangle.setDepth(2);
/*create user*/
this.createCurrentPlayer();
const playerNumber = localUserStore.getPlayerCharacterIndex();
this.input.keyboard.on('keyup-ENTER', () => {
return this.nextSceneToCameraScene();
@ -108,9 +106,12 @@ export class SelectCharacterScene extends AbstractCharacterScene {
return;
}
this.scene.stop(SelectCharacterSceneName);
waScaleManager.restoreZoom();
gameManager.setCharacterLayers([this.selectedPlayer.texture.key]);
gameManager.tryResumingGame(this, EnableCameraSceneName);
this.scene.remove(SelectCharacterSceneName);
this.players = [];
selectCharacterSceneVisibleStore.set(false);
this.events.removeListener('wake');
}
protected nextSceneToCustomizeScene(): void {
@ -118,13 +119,20 @@ export class SelectCharacterScene extends AbstractCharacterScene {
return;
}
this.scene.sleep(SelectCharacterSceneName);
waScaleManager.restoreZoom();
this.scene.run(CustomizeSceneName);
selectCharacterSceneVisibleStore.set(false);
}
createCurrentPlayer(): void {
createCurrentPlayer(): void {
for (let i = 0; i <this.playerModels.length; i++) {
const playerResource = this.playerModels[i];
//check already exist texture
if(this.players.find((c) => c.texture.key === playerResource.name)){
continue;
}
const [middleX, middleY] = this.getCharacterPosition();
const player = this.physics.add.sprite(middleX, middleY, playerResource.name, 0);
this.setUpPlayer(player, i);
@ -135,15 +143,22 @@ export class SelectCharacterScene extends AbstractCharacterScene {
repeat: -1
});
player.setInteractive().on("pointerdown", () => {
if(this.currentSelectUser === i){
if (this.pointerClicked) {
return;
}
if (this.currentSelectUser === i) {
return;
}
//To not trigger two time the pointerdown events :
// We set a boolean to true so that pointerdown events does nothing when the boolean is true
// We set a timer that we decrease in update function to not trigger the pointerdown events twice
this.pointerClicked = true;
this.pointerTimer = 250;
this.currentSelectUser = i;
this.moveUser();
});
this.players.push(player);
}
this.selectedPlayer = this.players[this.currentSelectUser];
this.selectedPlayer.play(this.playerModels[this.currentSelectUser].name);
}
@ -188,35 +203,35 @@ export class SelectCharacterScene extends AbstractCharacterScene {
this.moveUser();
}
protected defineSetupPlayer(numero: number){
protected defineSetupPlayer(num: number){
const deltaX = 32;
const deltaY = 32;
let [playerX, playerY] = this.getCharacterPosition(); // player X and player y are middle of the
playerX = ( (playerX - (deltaX * 2.5)) + ((deltaX) * (numero % this.nbCharactersPerRow)) ); // calcul position on line users
playerY = ( (playerY - (deltaY * 2)) + ((deltaY) * ( Math.floor(numero / this.nbCharactersPerRow) )) ); // calcul position on column users
playerX = ( (playerX - (deltaX * 2.5)) + ((deltaX) * (num % this.nbCharactersPerRow)) ); // calcul position on line users
playerY = ( (playerY - (deltaY * 2)) + ((deltaY) * ( Math.floor(num / this.nbCharactersPerRow) )) ); // calcul position on column users
const playerVisible = true;
const playerScale = 1;
const playserOpactity = 1;
const playerOpacity = 1;
// if selected
if( numero === this.currentSelectUser ){
if( num === this.currentSelectUser ){
this.selectedRectangle.setX(playerX);
this.selectedRectangle.setY(playerY);
}
return {playerX, playerY, playerScale, playserOpactity, playerVisible}
return {playerX, playerY, playerScale, playerOpacity, playerVisible}
}
protected setUpPlayer(player: Phaser.Physics.Arcade.Sprite, numero: number){
protected setUpPlayer(player: Phaser.Physics.Arcade.Sprite, num: number){
const {playerX, playerY, playerScale, playserOpactity, playerVisible} = this.defineSetupPlayer(numero);
const {playerX, playerY, playerScale, playerOpacity, playerVisible} = this.defineSetupPlayer(num);
player.setBounce(0.2);
player.setCollideWorldBounds(true);
player.setCollideWorldBounds(false);
player.setVisible( playerVisible );
player.setScale(playerScale, playerScale);
player.setAlpha(playserOpactity);
player.setAlpha(playerOpacity);
player.setX(playerX);
player.setY(playerY);
}
@ -240,12 +255,23 @@ export class SelectCharacterScene extends AbstractCharacterScene {
}
update(time: number, delta: number): void {
// pointerTimer is set to 250 when pointerdown events is trigger
// After 250ms, pointerClicked is set to false and the pointerdown events can be trigger again
this.pointerTimer -= delta;
if (this.pointerTimer <= 0) {
this.pointerClicked = false;
}
if(this.lazyloadingAttempt){
//re-render players list
this.createCurrentPlayer();
this.moveUser();
this.lazyloadingAttempt = false;
}
}
public onResize(ev: UIEvent): void {
public onResize(): void {
//move position of user
this.moveUser();
this.centerXDomElement(this.selectCharacterSceneElement, 150);
}
}

View file

@ -5,23 +5,26 @@ import { gameManager} from "../Game/GameManager";
import { ResizableScene } from "./ResizableScene";
import { EnableCameraSceneName } from "./EnableCameraScene";
import { localUserStore } from "../../Connexion/LocalUserStore";
import { CompanionResourceDescriptionInterface } from "../Companion/CompanionTextures";
import type { CompanionResourceDescriptionInterface } from "../Companion/CompanionTextures";
import { getAllCompanionResources } from "../Companion/CompanionTexturesLoadingManager";
import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {PinchManager} from "../UserInput/PinchManager";
import { MenuScene } from "../Menu/MenuScene";
import {selectCompanionSceneVisibleStore} from "../../Stores/SelectCompanionStore";
import {waScaleManager} from "../Services/WaScaleManager";
import {isMobile} from "../../Enum/EnvironmentVariable";
export const SelectCompanionSceneName = "SelectCompanionScene";
const selectCompanionSceneKey = 'selectCompanionScene';
export class SelectCompanionScene extends ResizableScene {
private selectedCompanion!: Phaser.Physics.Arcade.Sprite;
private companions: Array<Phaser.Physics.Arcade.Sprite> = new Array<Phaser.Physics.Arcade.Sprite>();
private companionModels: Array<CompanionResourceDescriptionInterface> = [];
private saveZoom: number = 0;
private selectCompanionSceneElement!: Phaser.GameObjects.DOMElement;
private currentCompanion = 0;
private pointerClicked: boolean = false;
private pointerTimer: number = 0;
constructor() {
super({
@ -30,8 +33,6 @@ export class SelectCompanionScene extends ResizableScene {
}
preload() {
this.load.html(selectCompanionSceneKey, 'resources/html/SelectCompanionScene.html');
getAllCompanionResources(this.load).forEach(model => {
this.companionModels.push(model);
});
@ -42,30 +43,17 @@ export class SelectCompanionScene extends ResizableScene {
create() {
this.selectCompanionSceneElement = this.add.dom(-1000, 0).createFromCache(selectCompanionSceneKey);
this.centerXDomElement(this.selectCompanionSceneElement, 150);
MenuScene.revealMenusAfterInit(this.selectCompanionSceneElement, selectCompanionSceneKey);
selectCompanionSceneVisibleStore.set(true);
this.selectCompanionSceneElement.addListener('click');
this.selectCompanionSceneElement.on('click', (event:MouseEvent) => {
event.preventDefault();
if((event?.target as HTMLInputElement).id === 'selectCharacterButtonLeft') {
this.moveToLeft();
}else if((event?.target as HTMLInputElement).id === 'selectCharacterButtonRight') {
this.moveToRight();
}else if((event?.target as HTMLInputElement).id === 'selectCompanionSceneFormSubmit') {
this.nextScene();
}else if((event?.target as HTMLInputElement).id === 'selectCompanionSceneFormBack') {
this._nextScene();
}
});
waScaleManager.saveZoom();
waScaleManager.zoomModifier = isMobile() ? 2 : 1;
if (touchScreenManager.supportTouchScreen) {
new PinchManager(this);
}
// input events
this.input.keyboard.on('keyup-ENTER', this.nextScene.bind(this));
this.input.keyboard.on('keyup-ENTER', this.selectCompanion.bind(this));
this.input.keyboard.on('keydown-RIGHT', this.moveToRight.bind(this));
this.input.keyboard.on('keydown-LEFT', this.moveToLeft.bind(this));
@ -86,21 +74,28 @@ export class SelectCompanionScene extends ResizableScene {
}
update(time: number, delta: number): void {
// pointerTimer is set to 250 when pointerdown events is trigger
// After 250ms, pointerClicked is set to false and the pointerdown events can be trigger again
this.pointerTimer -= delta;
if (this.pointerTimer <= 0) {
this.pointerClicked = false;
}
}
private nextScene(): void {
public selectCompanion(): void {
localUserStore.setCompanion(this.companionModels[this.currentCompanion].name);
gameManager.setCompanion(this.companionModels[this.currentCompanion].name);
this._nextScene();
this.closeScene();
}
private _nextScene(){
public closeScene(){
// next scene
this.scene.stop(SelectCompanionSceneName);
waScaleManager.restoreZoom();
gameManager.tryResumingGame(this, EnableCameraSceneName);
this.scene.remove(SelectCompanionSceneName);
selectCompanionSceneVisibleStore.set(false);
}
private createCurrentCompanion(): void {
@ -117,6 +112,14 @@ export class SelectCompanionScene extends ResizableScene {
});
companion.setInteractive().on("pointerdown", () => {
if (this.pointerClicked) {
return;
}
//To not trigger two time the pointerdown events :
// We set a boolean to true so that pointerdown events does nothing when the boolean is true
// We set a timer that we decrease in update function to not trigger the pointerdown events twice
this.pointerClicked = true;
this.pointerTimer = 250;
this.currentCompanion = i;
this.moveCompanion();
});
@ -126,10 +129,8 @@ export class SelectCompanionScene extends ResizableScene {
this.selectedCompanion = this.companions[this.currentCompanion];
}
public onResize(ev: UIEvent): void {
public onResize(): void {
this.moveCompanion();
this.centerXDomElement(this.selectCompanionSceneElement, 150);
}
private updateSelectedCompanion(): void {
@ -147,15 +148,7 @@ export class SelectCompanionScene extends ResizableScene {
this.updateSelectedCompanion();
}
private moveToLeft(){
if(this.currentCompanion === 0){
return;
}
this.currentCompanion -= 1;
this.moveCompanion();
}
private moveToRight(){
public moveToRight(){
if(this.currentCompanion === (this.companions.length - 1)){
return;
}
@ -163,38 +156,46 @@ export class SelectCompanionScene extends ResizableScene {
this.moveCompanion();
}
private defineSetupCompanion(numero: number){
public moveToLeft(){
if(this.currentCompanion === 0){
return;
}
this.currentCompanion -= 1;
this.moveCompanion();
}
private defineSetupCompanion(num: number){
const deltaX = 30;
const deltaY = 2;
let [companionX, companionY] = this.getCompanionPosition();
let companionVisible = true;
let companionScale = 1.5;
let companionOpactity = 1;
if( this.currentCompanion !== numero ){
if( this.currentCompanion !== num ){
companionVisible = false;
}
if( numero === (this.currentCompanion + 1) ){
if( num === (this.currentCompanion + 1) ){
companionY -= deltaY;
companionX += deltaX;
companionScale = 0.8;
companionOpactity = 0.6;
companionVisible = true;
}
if( numero === (this.currentCompanion + 2) ){
if( num === (this.currentCompanion + 2) ){
companionY -= deltaY;
companionX += (deltaX * 2);
companionScale = 0.8;
companionOpactity = 0.6;
companionVisible = true;
}
if( numero === (this.currentCompanion - 1) ){
if( num === (this.currentCompanion - 1) ){
companionY -= deltaY;
companionX -= deltaX;
companionScale = 0.8;
companionOpactity = 0.6;
companionVisible = true;
}
if( numero === (this.currentCompanion - 2) ){
if( num === (this.currentCompanion - 2) ){
companionY -= deltaY;
companionX -= (deltaX * 2);
companionScale = 0.8;

View file

@ -1,4 +1,4 @@
import {ITiledMap, ITiledMapLayer} from "./ITiledMap";
import type {ITiledMap, ITiledMapLayer} from "./ITiledMap";
/**
* Iterates over the layers of a map, flattening the grouped layers

View file

@ -1,147 +0,0 @@
import {mediaManager} from "../../WebRtc/MediaManager";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
import {localUserStore} from "../../Connexion/LocalUserStore";
import {DirtyScene} from "../Game/DirtyScene";
export const HelpCameraSettingsSceneName = 'HelpCameraSettingsScene';
const helpCameraSettings = 'helpCameraSettings';
/**
* The scene that show how to permit Camera and Microphone access if there are not already allowed
*/
export class HelpCameraSettingsScene extends DirtyScene {
private helpCameraSettingsElement!: Phaser.GameObjects.DOMElement;
private helpCameraSettingsOpened: boolean = false;
constructor() {
super({key: HelpCameraSettingsSceneName});
}
preload() {
this.load.html(helpCameraSettings, 'resources/html/helpCameraSettings.html');
}
create(){
this.createHelpCameraSettings();
}
private createHelpCameraSettings() : void {
const middleX = this.getMiddleX();
this.helpCameraSettingsElement = this.add.dom(middleX, -800, undefined, {overflow: 'scroll'}).createFromCache(helpCameraSettings);
this.revealMenusAfterInit(this.helpCameraSettingsElement, helpCameraSettings);
this.helpCameraSettingsElement.addListener('click');
this.helpCameraSettingsElement.on('click', (event:MouseEvent) => {
if((event?.target as HTMLInputElement).id === 'mailto') {
return;
}
event.preventDefault();
if((event?.target as HTMLInputElement).id === 'helpCameraSettingsFormRefresh') {
window.location.reload();
}else if((event?.target as HTMLInputElement).id === 'helpCameraSettingsFormContinue') {
this.closeHelpCameraSettingsOpened();
}
});
if(!localUserStore.getHelpCameraSettingsShown() && (!mediaManager.constraintsMedia.audio || !mediaManager.constraintsMedia.video)){
this.openHelpCameraSettingsOpened();
localUserStore.setHelpCameraSettingsShown();
}
mediaManager.setHelpCameraSettingsCallBack(() => {
this.openHelpCameraSettingsOpened();
});
}
private openHelpCameraSettingsOpened(): void{
HtmlUtils.getElementByIdOrFail<HTMLDivElement>('webRtcSetup').style.display = 'none';
this.helpCameraSettingsOpened = true;
try{
if(window.navigator.userAgent.includes('Firefox')){
HtmlUtils.getElementByIdOrFail<HTMLParagraphElement>('browserHelpSetting').innerHTML ='<img src="/resources/objects/help-setting-camera-permission-firefox.png"/>';
}else if(window.navigator.userAgent.includes('Chrome')){
HtmlUtils.getElementByIdOrFail<HTMLParagraphElement>('browserHelpSetting').innerHTML ='<img src="/resources/objects/help-setting-camera-permission-chrome.png"/>';
}
}catch(err) {
console.error('openHelpCameraSettingsOpened => getElementByIdOrFail => error', err);
}
const middleY = this.getMiddleY();
const middleX = this.getMiddleX();
this.tweens.add({
targets: this.helpCameraSettingsElement,
y: middleY,
x: middleX,
duration: 1000,
ease: 'Power3',
overflow: 'scroll'
});
this.dirty = true;
}
private closeHelpCameraSettingsOpened(): void{
const middleX = this.getMiddleX();
/*const helpCameraSettingsInfo = this.helpCameraSettingsElement.getChildByID('helpCameraSettings') as HTMLParagraphElement;
helpCameraSettingsInfo.innerText = '';
helpCameraSettingsInfo.style.display = 'none';*/
this.helpCameraSettingsOpened = false;
this.tweens.add({
targets: this.helpCameraSettingsElement,
y: -1000,
x: middleX,
duration: 1000,
ease: 'Power3',
overflow: 'scroll'
});
this.dirty = true;
}
private revealMenusAfterInit(menuElement: Phaser.GameObjects.DOMElement, rootDomId: string) {
//Dom elements will appear inside the viewer screen when creating before being moved out of it, which create a flicker effect.
//To prevent this, we put a 'hidden' attribute on the root element, we remove it only after the init is done.
setTimeout(() => {
(menuElement.getChildByID(rootDomId) as HTMLElement).hidden = false;
}, 250);
}
update(time: number, delta: number): void {
this.dirty = false;
}
public onResize(ev: UIEvent): void {
super.onResize(ev);
const middleX = this.getMiddleX();
const middleY = this.getMiddleY();
this.tweens.add({
targets: this.helpCameraSettingsElement,
x: middleX,
y: middleY,
duration: 1000,
ease: 'Power3'
});
}
private getMiddleX() : number{
return (this.scale.width / 2) -
(
this.helpCameraSettingsElement
&& this.helpCameraSettingsElement.node
&& this.helpCameraSettingsElement.node.getBoundingClientRect().width > 0
? (this.helpCameraSettingsElement.node.getBoundingClientRect().width / (2 * this.scale.zoom))
: (400 / 2)
);
}
private getMiddleY() : number{
const middleY = ((this.scale.height) - (
(this.helpCameraSettingsElement
&& this.helpCameraSettingsElement.node
&& this.helpCameraSettingsElement.node.getBoundingClientRect().height > 0
? this.helpCameraSettingsElement.node.getBoundingClientRect().height : 400 /*FIXME to use a const will be injected in HTMLElement*/)/this.scale.zoom)) / 2;
return (middleY > 0 ? middleY : 0);
}
public isDirty(): boolean {
return this.dirty;
}
}

View file

@ -9,6 +9,8 @@ import {connectionManager} from "../../Connexion/ConnectionManager";
import {GameConnexionTypes} from "../../Url/UrlManager";
import {WarningContainer, warningContainerHtml, warningContainerKey} from "../Components/WarningContainer";
import {worldFullWarningStream} from "../../Connexion/WorldFullWarningStream";
import {menuIconVisible} from "../../Stores/MenuStore";
import {videoConstraintStore} from "../../Stores/MediaStore";
export const MenuSceneName = 'MenuScene';
const gameMenuKey = 'gameMenu';
@ -53,6 +55,7 @@ export class MenuScene extends Phaser.Scene {
}
create() {
menuIconVisible.set(true);
this.menuElement = this.add.dom(closedSideMenuX, 30).createFromCache(gameMenuKey);
this.menuElement.setOrigin(0);
MenuScene.revealMenusAfterInit(this.menuElement, 'gameMenu');
@ -322,7 +325,7 @@ export class MenuScene extends Phaser.Scene {
if (valueVideo !== this.videoQualityValue) {
this.videoQualityValue = valueVideo;
localUserStore.setVideoQualityValue(valueVideo);
mediaManager.updateCameraQuality(valueVideo);
videoConstraintStore.setFrameRate(valueVideo);
}
this.closeGameQualityMenu();
}

View file

@ -1,18 +1,18 @@
import {PlayerAnimationDirections} from "./Animation";
import {GameScene} from "../Game/GameScene";
import type {GameScene} from "../Game/GameScene";
import {UserInputEvent, UserInputManager} from "../UserInput/UserInputManager";
import {Character} from "../Entity/Character";
import {userMovingStore} from "../../Stores/GameStore";
import {RadialMenu, RadialMenuClickEvent, RadialMenuItem} from "../Components/RadialMenu";
export const hasMovedEventName = "hasMoved";
export interface CurrentGamerInterface extends Character{
moveUser(delta: number) : void;
say(text : string) : void;
isMoving(): boolean;
}
export const requestEmoteEventName = "requestEmote";
export class Player extends Character implements CurrentGamerInterface {
export class Player extends Character {
private previousDirection: string = PlayerAnimationDirections.Down;
private wasMoving: boolean = false;
private emoteMenu: RadialMenu|null = null;
private updateListener: () => void;
constructor(
Scene: GameScene,
@ -26,14 +26,18 @@ export class Player extends Character implements CurrentGamerInterface {
companion: string|null,
companionTexturePromise?: Promise<string>
) {
super(Scene, x, y, texturesPromise, name, direction, moving, 1);
super(Scene, x, y, texturesPromise, name, direction, moving, 1, true, companion, companionTexturePromise);
//the current player model should be push away by other players to prevent conflict
this.getBody().setImmovable(false);
if (typeof companion === 'string') {
this.addCompanion(companion, companionTexturePromise);
}
this.updateListener = () => {
if (this.emoteMenu) {
this.emoteMenu.x = this.x;
this.emoteMenu.y = this.y;
}
};
this.scene.events.addListener('postupdate', this.updateListener);
}
moveUser(delta: number): void {
@ -83,9 +87,39 @@ export class Player extends Character implements CurrentGamerInterface {
this.previousDirection = direction;
}
this.wasMoving = moving;
userMovingStore.set(moving);
}
public isMoving(): boolean {
return this.wasMoving;
}
openOrCloseEmoteMenu(emotes:RadialMenuItem[]) {
if(this.emoteMenu) {
this.closeEmoteMenu();
} else {
this.openEmoteMenu(emotes);
}
}
openEmoteMenu(emotes:RadialMenuItem[]): void {
this.cancelPreviousEmote();
this.emoteMenu = new RadialMenu(this.scene, this.x, this.y, emotes)
this.emoteMenu.on(RadialMenuClickEvent, (item: RadialMenuItem) => {
this.closeEmoteMenu();
this.emit(requestEmoteEventName, item.name);
this.playEmote(item.name);
});
}
closeEmoteMenu(): void {
if (!this.emoteMenu) return;
this.emoteMenu.destroy();
this.emoteMenu = null;
}
destroy() {
this.scene.events.removeListener('postupdate', this.updateListener);
super.destroy();
}
}

View file

@ -1,4 +1,3 @@
import ScaleManager = Phaser.Scale.ScaleManager;
interface Size {
width: number;
@ -13,8 +12,7 @@ export class HdpiManager {
* @param minRecommendedGamePixelsNumber The minimum number of pixels we want to display "by default" to the user
* @param absoluteMinPixelNumber The very minimum of game pixels to display. Below, we forbid zooming more
*/
public constructor(private minRecommendedGamePixelsNumber: number, private absoluteMinPixelNumber: number) {
}
public constructor(private minRecommendedGamePixelsNumber: number, private absoluteMinPixelNumber: number) {}
/**
* Returns the optimal size in "game pixels" based on the screen size in "real pixels".
@ -36,16 +34,12 @@ export class HdpiManager {
};
}
let i = 1;
while (realPixelNumber > this.minRecommendedGamePixelsNumber * i * i) {
i++;
}
const optimalZoomLevel = this.getOptimalZoomLevel(realPixelNumber);
// Has the canvas more pixels than the screen? This is forbidden
if ((i - 1) * this._zoomModifier < 1) {
if (optimalZoomLevel * this._zoomModifier < 1) {
// Let's reset the zoom modifier (WARNING this is a SIDE EFFECT in a getter)
this._zoomModifier = 1 / (i - 1);
this._zoomModifier = 1 / optimalZoomLevel;
return {
game: {
@ -59,8 +53,8 @@ export class HdpiManager {
}
}
const gameWidth = Math.ceil(realPixelScreenSize.width / (i - 1) / this._zoomModifier);
const gameHeight = Math.ceil(realPixelScreenSize.height / (i - 1) / this._zoomModifier);
const gameWidth = Math.ceil(realPixelScreenSize.width / optimalZoomLevel / this._zoomModifier);
const gameHeight = Math.ceil(realPixelScreenSize.height / optimalZoomLevel / this._zoomModifier);
// Let's ensure we display a minimum of pixels, even if crazily zoomed in.
if (gameWidth * gameHeight < this.absoluteMinPixelNumber) {
@ -68,7 +62,7 @@ export class HdpiManager {
const minGameWidth = Math.sqrt(this.absoluteMinPixelNumber * realPixelScreenSize.width / realPixelScreenSize.height);
// Let's reset the zoom modifier (WARNING this is a SIDE EFFECT in a getter)
this._zoomModifier = realPixelScreenSize.width / minGameWidth / (i - 1);
this._zoomModifier = realPixelScreenSize.width / minGameWidth / optimalZoomLevel;
return {
game: {
@ -89,12 +83,24 @@ export class HdpiManager {
height: gameHeight,
},
real: {
width: Math.ceil(realPixelScreenSize.width / (i - 1)) * (i - 1),
height: Math.ceil(realPixelScreenSize.height / (i - 1)) * (i - 1),
width: Math.ceil(realPixelScreenSize.width / optimalZoomLevel) * optimalZoomLevel,
height: Math.ceil(realPixelScreenSize.height / optimalZoomLevel) * optimalZoomLevel,
}
}
}
/**
* We only accept integer but we make an exception for 1.5
*/
private getOptimalZoomLevel(realPixelNumber: number): number {
const result = Math.sqrt(realPixelNumber / this.minRecommendedGamePixelsNumber);
if (1.5 <= result && result < 2) {
return 1.5
} else {
return Math.floor(result);
}
}
public get zoomModifier(): number {
return this._zoomModifier;
}

View file

@ -1,18 +1,25 @@
import {HdpiManager} from "./HdpiManager";
import ScaleManager = Phaser.Scale.ScaleManager;
import {coWebsiteManager} from "../../WebRtc/CoWebsiteManager";
import type {Game} from "../Game/Game";
import {ResizableScene} from "../Login/ResizableScene";
import {HtmlUtils} from "../../WebRtc/HtmlUtils";
class WaScaleManager {
private hdpiManager: HdpiManager;
private scaleManager!: ScaleManager;
private game!: Game;
private actualZoom: number = 1;
private _saveZoom: number = 1;
public constructor(private minGamePixelsNumber: number, private absoluteMinPixelNumber: number) {
this.hdpiManager = new HdpiManager(minGamePixelsNumber, absoluteMinPixelNumber);
}
public setScaleManager(scaleManager: ScaleManager) {
this.scaleManager = scaleManager;
public setGame(game: Game): void {
this.scaleManager = game.scale;
this.game = game;
}
public applyNewSize() {
@ -25,13 +32,29 @@ class WaScaleManager {
const { game: gameSize, real: realSize } = this.hdpiManager.getOptimalGameSize({width: width * devicePixelRatio, height: height * devicePixelRatio});
this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio);
this.actualZoom = realSize.width / gameSize.width / devicePixelRatio;
this.scaleManager.setZoom(realSize.width / gameSize.width / devicePixelRatio)
this.scaleManager.resize(gameSize.width, gameSize.height);
// Override bug in canvas resizing in Phaser. Let's resize the canvas ourselves
const style = this.scaleManager.canvas.style;
style.width = Math.ceil(realSize.width / devicePixelRatio) + 'px';
style.height = Math.ceil(realSize.height / devicePixelRatio) + 'px';
// Resize the game element at the same size at the canvas
const gameStyle = HtmlUtils.getElementByIdOrFail<HTMLDivElement>('game').style;
gameStyle.width = style.width;
gameStyle.height = style.height;
// Note: onResize will be called twice (once here and once in Game.ts), but we have no better way.
for (const scene of this.game.scene.getScenes(true)) {
if (scene instanceof ResizableScene) {
// We are delaying the call to the "render" event because otherwise, the "camera" coordinates are not correctly updated.
scene.events.once(Phaser.Scenes.Events.RENDER, () => scene.onResize());
}
}
this.game.markDirty();
}
public get zoomModifier(): number {
@ -42,6 +65,23 @@ class WaScaleManager {
this.hdpiManager.zoomModifier = zoomModifier;
this.applyNewSize();
}
public saveZoom(): void {
this._saveZoom = this.hdpiManager.zoomModifier;
}
public restoreZoom(): void{
this.hdpiManager.zoomModifier = this._saveZoom;
this.applyNewSize();
}
/**
* This is used to scale back the ui components to counter-act the zoom.
*/
public get uiScalingFactor(): number {
return this.actualZoom > 1 ? 1 : 1.2;
}
}
export const waScaleManager = new WaScaleManager(640*480, 196*196);

View file

@ -1,5 +1,5 @@
import { Direction } from "../../types";
import {GameScene} from "../Game/GameScene";
import type { Direction } from "../../types";
import type {GameScene} from "../Game/GameScene";
import {touchScreenManager} from "../../Touch/TouchScreenManager";
import {MobileJoystick} from "../Components/MobileJoystick";
@ -173,7 +173,7 @@ export class UserInputManager {
}
destroy(): void {
this.joystick.destroy();
this.joystick?.destroy();
}
private initMouseWheel() {