use emote button

This commit is contained in:
Lurkars 2021-07-21 09:41:22 +02:00
parent 83bd19c8dc
commit f43deff626
9 changed files with 329 additions and 202 deletions

View file

@ -1,63 +1,50 @@
import Sprite = Phaser.GameObjects.Sprite;
import Text = Phaser.GameObjects.Text;
import {DEPTH_UI_INDEX} from "../Game/DepthIndexes";
import {waScaleManager} from "../Services/WaScaleManager";
import DOMElement = Phaser.GameObjects.DOMElement;
import { DEPTH_UI_INDEX } from "../Game/DepthIndexes";
import { waScaleManager } from "../Services/WaScaleManager";
import { EmojiButton } from "@joeattardi/emoji-button";
import { HtmlUtils } from "../../WebRtc/HtmlUtils";
export const EmoteMenuClickEvent = 'emoteClick';
export const EmoteMenuClickEvent = "emoteClick";
export class EmoteMenu extends Phaser.GameObjects.Container {
private resizeCallback: OmitThisParameter<() => void>;
private container: DOMElement;
private picker: EmojiButton;
constructor(scene: Phaser.Scene, x: number, y: number, private items: string[]) {
constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y);
this.setDepth(DEPTH_UI_INDEX)
this.setDepth(DEPTH_UI_INDEX);
this.scene.add.existing(this);
this.initItems();
this.container = new DOMElement(this.scene, 0, 0, "div", "", "");
this.container.setClassName("emoji-container");
const scalingFactor = waScaleManager.uiScalingFactor * 0.5;
this.container.setScale(scalingFactor);
this.add(this.container);
const emojiContainer = HtmlUtils.querySelectorOrFail(".emoji-container");
this.picker = new EmojiButton({ rootElement: emojiContainer });
this.picker.on("emoji", (selection) => {
this.emit(EmoteMenuClickEvent, selection.emoji);
});
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.createEmoteElement(item, index, itemsNumber, menuRadius))
public isOpen(): boolean {
return this.picker.isPickerVisible();
}
private createEmoteElement(item: string, index: number, itemsNumber: number, menuRadius: number) {
// const image = new Sprite(this.scene, 0, menuRadius, item.image);
const image = new Text(this.scene, -12, menuRadius, item, {fontFamily: '"twemoji"', fontSize:'75px'});
this.add(image);
// this.scene.sys.updateList.add(image);
const scalingFactor = waScaleManager.uiScalingFactor * 0.3;
image.setScale(scalingFactor)
image.setInteractive({
useHandCursor: true,
});
image.on('pointerdown', () => this.emit(EmoteMenuClickEvent, item));
image.on('pointerover', () => {
this.scene.tweens.add({
targets: image,
props: {
scale: 1.5 * 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: -12, y: -12}, angle, menuRadius);
public openPicker() {
const emojiContainer = HtmlUtils.querySelectorOrFail(".emoji-container");
this.picker.showPicker(emojiContainer);
}
public closePicker() {
this.picker.hidePicker();
}
private resize() {
@ -68,4 +55,4 @@ export class EmoteMenu extends Phaser.GameObjects.Container {
this.scene.scale.removeListener(Phaser.Scale.Events.RESIZE, this.resizeCallback);
super.destroy();
}
}
}

View file

@ -1,29 +1,29 @@
import {PlayerAnimationDirections, PlayerAnimationTypes} from "../Player/Animation";
import {SpeechBubble} from "./SpeechBubble";
import { PlayerAnimationDirections, PlayerAnimationTypes } from "../Player/Animation";
import { SpeechBubble } from "./SpeechBubble";
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";
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";
import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipeline-plugin.js";
const playerNameY = - 25;
const playerNameY = -25;
interface AnimationData {
key: string;
frameRate: number;
repeat: number;
frameModel: string; //todo use an enum
frames : number[]
frames: number[];
}
const interactiveRadius = 35;
export abstract class Character extends Container {
private bubble: SpeechBubble|null = null;
private bubble: SpeechBubble | null = null;
private readonly playerName: Text;
public PlayerValue: string;
public sprites: Map<string, Sprite>;
@ -32,35 +32,41 @@ export abstract class Character extends Container {
private invisible: boolean;
public companion?: Companion;
private emote: Phaser.GameObjects.Text | null = null;
private emoteTween: Phaser.Tweens.Tween|null = null;
private emoteTween: Phaser.Tweens.Tween | null = null;
scene: GameScene;
constructor(scene: GameScene,
x: number,
y: number,
texturesPromise: Promise<string[]>,
name: string,
direction: PlayerAnimationDirections,
moving: boolean,
frame: string | number,
isClickable: boolean,
companion: string|null,
companionTexturePromise?: Promise<string>
constructor(
scene: GameScene,
x: number,
y: number,
texturesPromise: Promise<string[]>,
name: string,
direction: PlayerAnimationDirections,
moving: boolean,
frame: string | number,
isClickable: boolean,
companion: string | null,
companionTexturePromise?: Promise<string>
) {
super(scene, x, y/*, texture, frame*/);
super(scene, x, y /*, texture, frame*/);
this.scene = scene;
this.PlayerValue = name;
this.invisible = true
this.invisible = true;
this.sprites = new Map<string, Sprite>();
//textures are inside a Promise in case they need to be lazyloaded before use.
texturesPromise.then((textures) => {
this.addTextures(textures, frame);
this.invisible = false
})
this.invisible = false;
});
this.playerName = new Text(scene, 0, playerNameY, name, {fontFamily: '"Press Start 2P"', fontSize: '8px', strokeThickness: 2, stroke: "gray"});
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);
@ -71,18 +77,17 @@ export abstract class Character extends Container {
useHandCursor: true,
});
this.on('pointerover',() => {
this.on("pointerover", () => {
this.getOutlinePlugin()?.add(this.playerName, {
thickness: 2,
outlineColor: 0xffff00
outlineColor: 0xffff00,
});
this.scene.markDirty();
});
this.on('pointerout',() => {
this.on("pointerout", () => {
this.getOutlinePlugin()?.remove(this.playerName);
this.scene.markDirty();
})
});
}
scene.add.existing(this);
@ -97,38 +102,38 @@ export abstract class Character extends Container {
this.playAnimation(direction, moving);
if (typeof companion === 'string') {
if (typeof companion === "string") {
this.addCompanion(companion, companionTexturePromise);
}
}
private getOutlinePlugin(): OutlinePipelinePlugin|undefined {
return this.scene.plugins.get('rexOutlinePipeline') as unknown as OutlinePipelinePlugin|undefined;
private getOutlinePlugin(): OutlinePipelinePlugin | undefined {
return this.scene.plugins.get("rexOutlinePipeline") as unknown as OutlinePipelinePlugin | undefined;
}
public addCompanion(name: string, texturePromise?: Promise<string>): void {
if (typeof texturePromise !== 'undefined') {
if (typeof texturePromise !== "undefined") {
this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise);
}
}
public addTextures(textures: string[], frame?: string | number): void {
for (const texture of textures) {
if(this.scene && !this.scene.textures.exists(texture)){
throw new TextureError('texture not found');
if (this.scene && !this.scene.textures.exists(texture)) {
throw new TextureError("texture not found");
}
const sprite = new Sprite(this.scene, 0, 0, texture, frame);
this.add(sprite);
this.getPlayerAnimations(texture).forEach(d => {
this.getPlayerAnimations(texture).forEach((d) => {
this.scene.anims.create({
key: d.key,
frames: this.scene.anims.generateFrameNumbers(d.frameModel, {frames: d.frames}),
frames: this.scene.anims.generateFrameNumbers(d.frameModel, { frames: d.frames }),
frameRate: d.frameRate,
repeat: d.repeat
repeat: d.repeat,
});
})
});
// Needed, otherwise, animations are not handled correctly.
if(this.scene) {
if (this.scene) {
this.scene.sys.updateList.add(sprite);
}
this.sprites.set(texture, sprite);
@ -136,68 +141,77 @@ export abstract class Character extends Container {
}
private getPlayerAnimations(name: string): AnimationData[] {
return [{
key: `${name}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Walk}`,
frameModel: name,
frames: [0, 1, 2, 1],
frameRate: 10,
repeat: -1
}, {
key: `${name}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Walk}`,
frameModel: name,
frames: [3, 4, 5, 4],
frameRate: 10,
repeat: -1
}, {
key: `${name}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Walk}`,
frameModel: name,
frames: [6, 7, 8, 7],
frameRate: 10,
repeat: -1
}, {
key: `${name}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Walk}`,
frameModel: name,
frames: [9, 10, 11, 10],
frameRate: 10,
repeat: -1
},{
key: `${name}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Idle}`,
frameModel: name,
frames: [1],
frameRate: 10,
repeat: 1
}, {
key: `${name}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Idle}`,
frameModel: name,
frames: [4],
frameRate: 10,
repeat: 1
}, {
key: `${name}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Idle}`,
frameModel: name,
frames: [7],
frameRate: 10,
repeat: 1
}, {
key: `${name}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Idle}`,
frameModel: name,
frames: [10],
frameRate: 10,
repeat: 1
}];
return [
{
key: `${name}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Walk}`,
frameModel: name,
frames: [0, 1, 2, 1],
frameRate: 10,
repeat: -1,
},
{
key: `${name}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Walk}`,
frameModel: name,
frames: [3, 4, 5, 4],
frameRate: 10,
repeat: -1,
},
{
key: `${name}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Walk}`,
frameModel: name,
frames: [6, 7, 8, 7],
frameRate: 10,
repeat: -1,
},
{
key: `${name}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Walk}`,
frameModel: name,
frames: [9, 10, 11, 10],
frameRate: 10,
repeat: -1,
},
{
key: `${name}-${PlayerAnimationDirections.Down}-${PlayerAnimationTypes.Idle}`,
frameModel: name,
frames: [1],
frameRate: 10,
repeat: 1,
},
{
key: `${name}-${PlayerAnimationDirections.Left}-${PlayerAnimationTypes.Idle}`,
frameModel: name,
frames: [4],
frameRate: 10,
repeat: 1,
},
{
key: `${name}-${PlayerAnimationDirections.Right}-${PlayerAnimationTypes.Idle}`,
frameModel: name,
frames: [7],
frameRate: 10,
repeat: 1,
},
{
key: `${name}-${PlayerAnimationDirections.Up}-${PlayerAnimationTypes.Idle}`,
frameModel: name,
frames: [10],
frameRate: 10,
repeat: 1,
},
];
}
protected playAnimation(direction : PlayerAnimationDirections, moving: boolean): void {
protected playAnimation(direction: PlayerAnimationDirections, moving: boolean): void {
if (this.invisible) return;
for (const [texture, sprite] of this.sprites.entries()) {
if (!sprite.anims) {
console.error('ANIMS IS NOT DEFINED!!!');
console.error("ANIMS IS NOT DEFINED!!!");
return;
}
if (moving && (!sprite.anims.currentAnim || sprite.anims.currentAnim.key !== direction)) {
sprite.play(texture+'-'+direction+'-'+PlayerAnimationTypes.Walk, true);
sprite.play(texture + "-" + direction + "-" + PlayerAnimationTypes.Walk, true);
} else if (!moving) {
sprite.anims.play(texture + '-' + direction + '-'+PlayerAnimationTypes.Idle, true);
sprite.anims.play(texture + "-" + direction + "-" + PlayerAnimationTypes.Idle, true);
}
}
}
@ -205,7 +219,7 @@ export abstract class Character extends Container {
protected getBody(): Phaser.Physics.Arcade.Body {
const body = this.body;
if (!(body instanceof Phaser.Physics.Arcade.Body)) {
throw new Error('Container does not have arcade body');
throw new Error("Container does not have arcade body");
}
return body;
}
@ -216,16 +230,20 @@ export abstract class Character extends Container {
body.setVelocity(x, y);
// up or down animations are prioritized over left and right
if (body.velocity.y < 0) { //moving up
if (body.velocity.y < 0) {
//moving up
this.lastDirection = PlayerAnimationDirections.Up;
this.playAnimation(PlayerAnimationDirections.Up, true);
} else if (body.velocity.y > 0) { //moving down
} else if (body.velocity.y > 0) {
//moving down
this.lastDirection = PlayerAnimationDirections.Down;
this.playAnimation(PlayerAnimationDirections.Down, true);
} else if (body.velocity.x > 0) { //moving right
} else if (body.velocity.x > 0) {
//moving right
this.lastDirection = PlayerAnimationDirections.Right;
this.playAnimation(PlayerAnimationDirections.Right, true);
} else if (body.velocity.x < 0) { //moving left
} else if (body.velocity.x < 0) {
//moving left
this.lastDirection = PlayerAnimationDirections.Left;
this.playAnimation(PlayerAnimationDirections.Left, true);
}
@ -237,45 +255,42 @@ export abstract class Character extends Container {
}
}
stop(){
stop() {
this.getBody().setVelocity(0, 0);
this.playAnimation(this.lastDirection, false);
}
say(text: string) {
if (this.bubble) return;
this.bubble = new SpeechBubble(this.scene, this, text)
this.bubble = new SpeechBubble(this.scene, this, text);
setTimeout(() => {
if (this.bubble !== null) {
this.bubble.destroy();
this.bubble = null;
}
}, 3000)
}, 3000);
}
destroy(): void {
for (const sprite of this.sprites.values()) {
if(this.scene) {
if (this.scene) {
this.scene.sys.updateList.remove(sprite);
}
}
this.list.forEach(objectContaining => objectContaining.destroy())
this.list.forEach((objectContaining) => objectContaining.destroy());
super.destroy();
}
playEmote(emote: string) {
this.cancelPreviousEmote();
const scalingFactor = waScaleManager.uiScalingFactor * 0.5;
const emoteY = -60 - scalingFactor * 10;
const scalingFactor = waScaleManager.uiScalingFactor;
const emoteY = -60;
this.playerName.setVisible(false);
this.emote = new Text(this.scene, -12, 0, emote, {fontFamily: '"twemoji"', fontSize:'55px'});
this.emote = new Text(this.scene, -12, 0, emote, { fontFamily: '"Twemoji Mozilla"', fontSize: "24px" });
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);
}
@ -287,11 +302,11 @@ export abstract class Character extends Container {
alpha: 1,
y: emoteY,
},
ease: 'Power2',
ease: "Power2",
duration: 500,
onComplete: () => {
this.startPulseTransition(emoteY, scalingFactor);
}
},
});
}
@ -300,7 +315,7 @@ export abstract class Character extends Container {
targets: this.emote,
props: {
y: emoteY * 1.3,
scale: scalingFactor * 1.1
scale: scalingFactor * 1.1,
},
duration: 250,
yoyo: true,
@ -308,7 +323,7 @@ export abstract class Character extends Container {
completeDelay: 200,
onComplete: () => {
this.startExitTransition(emoteY);
}
},
});
}
@ -319,11 +334,11 @@ export abstract class Character extends Container {
alpha: 0,
y: 2 * emoteY,
},
ease: 'Power2',
ease: "Power2",
duration: 500,
onComplete: () => {
this.destroyEmote();
}
},
});
}
@ -331,7 +346,7 @@ export abstract class Character extends Container {
if (!this.emote) return;
this.emoteTween?.remove();
this.destroyEmote()
this.destroyEmote();
}
private destroyEmote() {

View file

@ -1,8 +1,6 @@
import {emoteEventStream} from "../../Connexion/EmoteEventStream";
import type {GameScene} from "./GameScene";
import type {Subscription} from "rxjs";
export const emotes: string[] = ['❤️', '👏', '✋', '🙏', '👍', '👎'];
import { emoteEventStream } from "../../Connexion/EmoteEventStream";
import type { GameScene } from "./GameScene";
import type { Subscription } from "rxjs";
export class EmoteManager {
private subscription: Subscription;
@ -10,18 +8,13 @@ export class EmoteManager {
constructor(private scene: GameScene) {
this.subscription = emoteEventStream.stream.subscribe((event) => {
const actor = this.scene.MapPlayersByKey.get(event.userId);
if(actor) {
if (actor) {
actor.playEmote(event.emote);
}
})
}
getEmotes(): string[] {
// TODO: localstorage + management
return emotes;
});
}
destroy() {
this.subscription.unsubscribe();
}
}
}

View file

@ -1319,9 +1319,9 @@ ${escapedMessage}
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.CurrentPlayer.openOrCloseEmoteMenu( this.emoteManager.getEmotes());
})
this.CurrentPlayer.openOrCloseEmoteMenu();
});
this.CurrentPlayer.on(requestEmoteEventName, (emoteKey: string) => {
this.connection?.emitEmoteEvent(emoteKey);
});

View file

@ -1,9 +1,9 @@
import {PlayerAnimationDirections} from "./Animation";
import type {GameScene} from "../Game/GameScene";
import {UserInputEvent, UserInputManager} from "../UserInput/UserInputManager";
import {Character} from "../Entity/Character";
import {userMovingStore} from "../../Stores/GameStore";
import {EmoteMenu, EmoteMenuClickEvent} from "../Components/EmoteMenu";
import { PlayerAnimationDirections } from "./Animation";
import type { GameScene } from "../Game/GameScene";
import { UserInputEvent, UserInputManager } from "../UserInput/UserInputManager";
import { Character } from "../Entity/Character";
import { userMovingStore } from "../../Stores/GameStore";
import { EmoteMenu, EmoteMenuClickEvent } from "../Components/EmoteMenu";
export const hasMovedEventName = "hasMoved";
export const requestEmoteEventName = "requestEmote";
@ -11,7 +11,7 @@ export const requestEmoteEventName = "requestEmote";
export class Player extends Character {
private previousDirection: string = PlayerAnimationDirections.Down;
private wasMoving: boolean = false;
private emoteMenu: EmoteMenu|null = null;
private emoteMenu: EmoteMenu | null = null;
private updateListener: () => void;
constructor(
@ -23,7 +23,7 @@ export class Player extends Character {
direction: PlayerAnimationDirections,
moving: boolean,
private userInputManager: UserInputManager,
companion: string|null,
companion: string | null,
companionTexturePromise?: Promise<string>
) {
super(Scene, x, y, texturesPromise, name, direction, moving, 1, true, companion, companionTexturePromise);
@ -37,7 +37,7 @@ export class Player extends Character {
this.emoteMenu.y = this.y;
}
};
this.scene.events.addListener('postupdate', this.updateListener);
this.scene.events.addListener("postupdate", this.updateListener);
}
moveUser(delta: number): void {
@ -73,14 +73,14 @@ export class Player extends Character {
if (x !== 0 || y !== 0) {
this.move(x, y);
this.emit(hasMovedEventName, {moving, direction, x: this.x, y: this.y});
this.emit(hasMovedEventName, { moving, direction, x: this.x, y: this.y });
} else if (this.wasMoving && moving) {
// slow joystick movement
this.move(0, 0);
this.emit(hasMovedEventName, {moving, direction: this.previousDirection, x: this.x, y: this.y});
this.emit(hasMovedEventName, { moving, direction: this.previousDirection, x: this.x, y: this.y });
} else if (this.wasMoving && !moving) {
this.stop();
this.emit(hasMovedEventName, {moving, direction: this.previousDirection, x: this.x, y: this.y});
this.emit(hasMovedEventName, { moving, direction: this.previousDirection, x: this.x, y: this.y });
}
if (direction !== null) {
@ -94,17 +94,23 @@ export class Player extends Character {
return this.wasMoving;
}
openOrCloseEmoteMenu(emotes:string[]) {
if(this.emoteMenu) {
openOrCloseEmoteMenu() {
if (!this.emoteMenu) {
this.emoteMenu = new EmoteMenu(this.scene, this.x, this.y);
}
if (this.emoteMenu.isOpen()) {
this.closeEmoteMenu();
} else {
this.openEmoteMenu(emotes);
this.openEmoteMenu();
}
}
openEmoteMenu(emotes:string[]): void {
openEmoteMenu(): void {
this.cancelPreviousEmote();
this.emoteMenu = new EmoteMenu(this.scene, this.x, this.y, emotes)
if (!this.emoteMenu) return;
this.userInputManager.disableControls();
this.emoteMenu.openPicker();
this.emoteMenu.on(EmoteMenuClickEvent, (emote: string) => {
this.closeEmoteMenu();
this.emit(requestEmoteEventName, emote);
@ -113,13 +119,13 @@ export class Player extends Character {
}
closeEmoteMenu(): void {
this.userInputManager.restoreControls();
if (!this.emoteMenu) return;
this.emoteMenu.destroy();
this.emoteMenu = null;
this.emoteMenu.closePicker();
}
destroy() {
this.scene.events.removeListener('postupdate', this.updateListener);
this.scene.events.removeListener("postupdate", this.updateListener);
super.destroy();
}
}