Twemoji Emote Menu
This commit is contained in:
parent
4c8ee41cf2
commit
c264364752
@ -2,7 +2,7 @@ import {Subject} from "rxjs";
|
|||||||
|
|
||||||
interface EmoteEvent {
|
interface EmoteEvent {
|
||||||
userId: number,
|
userId: number,
|
||||||
emoteName: string,
|
emote: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
class EmoteEventStream {
|
class EmoteEventStream {
|
||||||
@ -11,8 +11,8 @@ class EmoteEventStream {
|
|||||||
public stream = this._stream.asObservable();
|
public stream = this._stream.asObservable();
|
||||||
|
|
||||||
|
|
||||||
fire(userId: number, emoteName:string) {
|
fire(userId: number, emote:string) {
|
||||||
this._stream.next({userId, emoteName});
|
this._stream.next({userId, emote});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
71
front/src/Phaser/Components/EmoteMenu.ts
Normal file
71
front/src/Phaser/Components/EmoteMenu.ts
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import Sprite = Phaser.GameObjects.Sprite;
|
||||||
|
import Text = Phaser.GameObjects.Text;
|
||||||
|
import {DEPTH_UI_INDEX} from "../Game/DepthIndexes";
|
||||||
|
import {waScaleManager} from "../Services/WaScaleManager";
|
||||||
|
|
||||||
|
export const EmoteMenuClickEvent = 'emoteClick';
|
||||||
|
|
||||||
|
export class EmoteMenu extends Phaser.GameObjects.Container {
|
||||||
|
private resizeCallback: OmitThisParameter<() => void>;
|
||||||
|
|
||||||
|
constructor(scene: Phaser.Scene, x: number, y: number, private items: string[]) {
|
||||||
|
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.createEmoteElement(item, index, itemsNumber, menuRadius))
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private resize() {
|
||||||
|
this.setScale(waScaleManager.uiScalingFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
this.scene.scale.removeListener(Phaser.Scale.Events.RESIZE, this.resizeCallback);
|
||||||
|
super.destroy();
|
||||||
|
}
|
||||||
|
}
|
@ -31,7 +31,7 @@ export abstract class Character extends Container {
|
|||||||
//private teleportation: Sprite;
|
//private teleportation: Sprite;
|
||||||
private invisible: boolean;
|
private invisible: boolean;
|
||||||
public companion?: Companion;
|
public companion?: Companion;
|
||||||
private emote: Phaser.GameObjects.Sprite | null = null;
|
private emote: Phaser.GameObjects.Text | null = null;
|
||||||
private emoteTween: Phaser.Tweens.Tween|null = null;
|
private emoteTween: Phaser.Tweens.Tween|null = null;
|
||||||
scene: GameScene;
|
scene: GameScene;
|
||||||
|
|
||||||
@ -263,18 +263,18 @@ export abstract class Character extends Container {
|
|||||||
super.destroy();
|
super.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
playEmote(emoteKey: string) {
|
playEmote(emote: string) {
|
||||||
this.cancelPreviousEmote();
|
this.cancelPreviousEmote();
|
||||||
|
|
||||||
const scalingFactor = waScaleManager.uiScalingFactor * 0.05;
|
const scalingFactor = waScaleManager.uiScalingFactor * 0.5;
|
||||||
const emoteY = -30 - scalingFactor * 10;
|
const emoteY = -60 - scalingFactor * 10;
|
||||||
|
|
||||||
this.playerName.setVisible(false);
|
this.playerName.setVisible(false);
|
||||||
this.emote = new Sprite(this.scene, 0, 0, emoteKey);
|
this.emote = new Text(this.scene, -12, 0, emote, {fontFamily: '"twemoji"', fontSize:'55px'});
|
||||||
this.emote.setAlpha(0);
|
this.emote.setAlpha(0);
|
||||||
this.emote.setScale(0.1 * scalingFactor);
|
this.emote.setScale(0.1 * scalingFactor);
|
||||||
this.add(this.emote);
|
this.add(this.emote);
|
||||||
this.scene.sys.updateList.add(this.emote);
|
// this.scene.sys.updateList.add(this.emote);
|
||||||
|
|
||||||
this.createStartTransition(scalingFactor, emoteY);
|
this.createStartTransition(scalingFactor, emoteY);
|
||||||
}
|
}
|
||||||
|
@ -1,72 +1,26 @@
|
|||||||
import type {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
|
|
||||||
import {emoteEventStream} from "../../Connexion/EmoteEventStream";
|
import {emoteEventStream} from "../../Connexion/EmoteEventStream";
|
||||||
import type {GameScene} from "./GameScene";
|
import type {GameScene} from "./GameScene";
|
||||||
import type {RadialMenuItem} from "../Components/RadialMenu";
|
|
||||||
import LoaderPlugin = Phaser.Loader.LoaderPlugin;
|
|
||||||
import type {Subscription} from "rxjs";
|
import type {Subscription} from "rxjs";
|
||||||
|
|
||||||
|
export const emotes: string[] = ['❤️', '👏', '✋', '🙏', '👍', '👎'];
|
||||||
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 {
|
export class EmoteManager {
|
||||||
private subscription: Subscription;
|
private subscription: Subscription;
|
||||||
|
|
||||||
constructor(private scene: GameScene) {
|
constructor(private scene: GameScene) {
|
||||||
this.subscription = emoteEventStream.stream.subscribe((event) => {
|
this.subscription = emoteEventStream.stream.subscribe((event) => {
|
||||||
const actor = this.scene.MapPlayersByKey.get(event.userId);
|
const actor = this.scene.MapPlayersByKey.get(event.userId);
|
||||||
if (actor) {
|
if(actor) {
|
||||||
this.lazyLoadEmoteTexture(event.emoteName).then(emoteKey => {
|
actor.playEmote(event.emote);
|
||||||
actor.playEmote(emoteKey);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
createLoadingPromise(loadPlugin: LoaderPlugin, playerResourceDescriptor: BodyResourceDescriptionInterface) {
|
|
||||||
return new Promise<string>((res) => {
|
getEmotes(): string[] {
|
||||||
if (loadPlugin.textureManager.exists(playerResourceDescriptor.name)) {
|
// TODO: localstorage + management
|
||||||
return res(playerResourceDescriptor.name);
|
return emotes;
|
||||||
}
|
|
||||||
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() {
|
destroy() {
|
||||||
this.subscription.unsubscribe();
|
this.subscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
@ -1202,7 +1202,8 @@ ${escapedMessage}
|
|||||||
if (pointer.wasTouch && (pointer.event as TouchEvent).touches.length > 1) {
|
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.
|
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.openOrCloseEmoteMenu( this.emoteManager.getEmotes());
|
||||||
})
|
})
|
||||||
this.CurrentPlayer.on(requestEmoteEventName, (emoteKey: string) => {
|
this.CurrentPlayer.on(requestEmoteEventName, (emoteKey: string) => {
|
||||||
this.connection?.emitEmoteEvent(emoteKey);
|
this.connection?.emitEmoteEvent(emoteKey);
|
||||||
|
@ -3,7 +3,7 @@ import type {GameScene} from "../Game/GameScene";
|
|||||||
import {UserInputEvent, UserInputManager} from "../UserInput/UserInputManager";
|
import {UserInputEvent, UserInputManager} from "../UserInput/UserInputManager";
|
||||||
import {Character} from "../Entity/Character";
|
import {Character} from "../Entity/Character";
|
||||||
import {userMovingStore} from "../../Stores/GameStore";
|
import {userMovingStore} from "../../Stores/GameStore";
|
||||||
import {RadialMenu, RadialMenuClickEvent, RadialMenuItem} from "../Components/RadialMenu";
|
import {EmoteMenu, EmoteMenuClickEvent} from "../Components/EmoteMenu";
|
||||||
|
|
||||||
export const hasMovedEventName = "hasMoved";
|
export const hasMovedEventName = "hasMoved";
|
||||||
export const requestEmoteEventName = "requestEmote";
|
export const requestEmoteEventName = "requestEmote";
|
||||||
@ -11,7 +11,7 @@ export const requestEmoteEventName = "requestEmote";
|
|||||||
export class Player extends Character {
|
export class Player extends Character {
|
||||||
private previousDirection: string = PlayerAnimationDirections.Down;
|
private previousDirection: string = PlayerAnimationDirections.Down;
|
||||||
private wasMoving: boolean = false;
|
private wasMoving: boolean = false;
|
||||||
private emoteMenu: RadialMenu|null = null;
|
private emoteMenu: EmoteMenu|null = null;
|
||||||
private updateListener: () => void;
|
private updateListener: () => void;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -94,7 +94,7 @@ export class Player extends Character {
|
|||||||
return this.wasMoving;
|
return this.wasMoving;
|
||||||
}
|
}
|
||||||
|
|
||||||
openOrCloseEmoteMenu(emotes:RadialMenuItem[]) {
|
openOrCloseEmoteMenu(emotes:string[]) {
|
||||||
if(this.emoteMenu) {
|
if(this.emoteMenu) {
|
||||||
this.closeEmoteMenu();
|
this.closeEmoteMenu();
|
||||||
} else {
|
} else {
|
||||||
@ -102,13 +102,13 @@ export class Player extends Character {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
openEmoteMenu(emotes:RadialMenuItem[]): void {
|
openEmoteMenu(emotes:string[]): void {
|
||||||
this.cancelPreviousEmote();
|
this.cancelPreviousEmote();
|
||||||
this.emoteMenu = new RadialMenu(this.scene, this.x, this.y, emotes)
|
this.emoteMenu = new EmoteMenu(this.scene, this.x, this.y, emotes)
|
||||||
this.emoteMenu.on(RadialMenuClickEvent, (item: RadialMenuItem) => {
|
this.emoteMenu.on(EmoteMenuClickEvent, (emote: string) => {
|
||||||
this.closeEmoteMenu();
|
this.closeEmoteMenu();
|
||||||
this.emit(requestEmoteEventName, item.name);
|
this.emit(requestEmoteEventName, emote);
|
||||||
this.playEmote(item.name);
|
this.playEmote(emote);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
@import "~@fontsource/press-start-2p/index.css";
|
@import "~@fontsource/press-start-2p/index.css";
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Twemoji Mozilla";
|
||||||
|
src: url("./fonts/TwemojiMozilla.ttf") format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
*{
|
*{
|
||||||
font-family: PixelFont-7,monospace;
|
font-family: "Twemoji Mozilla",PixelFont-7,monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nes-btn {
|
.nes-btn {
|
||||||
|
BIN
front/style/fonts/TwemojiMozilla.ttf
Normal file
BIN
front/style/fonts/TwemojiMozilla.ttf
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user