diff --git a/front/package.json b/front/package.json index a0b31cb3..eb24773b 100644 --- a/front/package.json +++ b/front/package.json @@ -5,6 +5,7 @@ "license": "SEE LICENSE IN LICENSE.txt", "devDependencies": { "@geprog/vite-plugin-env-config": "^4.0.3", + "@home-based-studio/phaser3-utils": "^0.4.2", "@sveltejs/vite-plugin-svelte": "^1.0.0-next.36", "@tsconfig/svelte": "^1.0.10", "@types/google-protobuf": "^3.7.3", @@ -45,7 +46,7 @@ "easystarjs": "^0.4.4", "generic-type-guard": "^3.4.2", "google-protobuf": "^3.13.0", - "phaser": "^3.54.0", + "phaser": "3.55.1", "phaser-animated-tiles": "workadventure/phaser-animated-tiles#da68bbededd605925621dd4f03bd27e69284b254", "phaser3-rex-plugins": "^1.1.42", "posthog-js": "^1.14.1", diff --git a/front/public/resources/icons/icon_accessory.png b/front/public/resources/icons/icon_accessory.png new file mode 100644 index 00000000..9ca5f341 Binary files /dev/null and b/front/public/resources/icons/icon_accessory.png differ diff --git a/front/public/resources/icons/icon_body.png b/front/public/resources/icons/icon_body.png new file mode 100644 index 00000000..c2d4983b Binary files /dev/null and b/front/public/resources/icons/icon_body.png differ diff --git a/front/public/resources/icons/icon_clothes.png b/front/public/resources/icons/icon_clothes.png new file mode 100644 index 00000000..0e9c39a0 Binary files /dev/null and b/front/public/resources/icons/icon_clothes.png differ diff --git a/front/public/resources/icons/icon_eyes.png b/front/public/resources/icons/icon_eyes.png new file mode 100644 index 00000000..8d37005e Binary files /dev/null and b/front/public/resources/icons/icon_eyes.png differ diff --git a/front/public/resources/icons/icon_hair.png b/front/public/resources/icons/icon_hair.png new file mode 100644 index 00000000..eee503d3 Binary files /dev/null and b/front/public/resources/icons/icon_hair.png differ diff --git a/front/public/resources/icons/icon_hat.png b/front/public/resources/icons/icon_hat.png new file mode 100644 index 00000000..36ed26ae Binary files /dev/null and b/front/public/resources/icons/icon_hat.png differ diff --git a/front/public/resources/icons/icon_turn.png b/front/public/resources/icons/icon_turn.png new file mode 100644 index 00000000..8e1b367e Binary files /dev/null and b/front/public/resources/icons/icon_turn.png differ diff --git a/front/public/resources/tilesets/floor_tiles.png b/front/public/resources/tilesets/floor_tiles.png new file mode 100644 index 00000000..0cdc7fde Binary files /dev/null and b/front/public/resources/tilesets/floor_tiles.png differ diff --git a/front/src/Components/App.svelte b/front/src/Components/App.svelte index e9fc0bfa..c596c8ee 100644 --- a/front/src/Components/App.svelte +++ b/front/src/Components/App.svelte @@ -1,14 +1,12 @@ - -
-
-

{$LL.woka.customWoka.title()}

-
-
- - -
-
- {#if $activeRowStore === 0} - - {/if} - {#if $activeRowStore !== 0} - - {/if} - {#if $activeRowStore === 5} - - {/if} - {#if $activeRowStore !== 5} - - {/if} -
-
- - diff --git a/front/src/Phaser/Components/CustomizeWoka/CustomWokaPreviewer.ts b/front/src/Phaser/Components/CustomizeWoka/CustomWokaPreviewer.ts new file mode 100644 index 00000000..68de3880 --- /dev/null +++ b/front/src/Phaser/Components/CustomizeWoka/CustomWokaPreviewer.ts @@ -0,0 +1,184 @@ +import { Easing } from "../../../types"; +import { getPlayerAnimations, PlayerAnimationDirections, PlayerAnimationTypes } from "../../Player/Animation"; + +export enum CustomWokaBodyPart { + Body = "Body", + Eyes = "Eyes", + Hair = "Hair", + Clothes = "Clothes", + Hat = "Hat", + Accessory = "Accessory", +} + +export enum CustomWokaBodyPartOrder { + Body, + Eyes, + Hair, + Clothes, + Hat, + Accessory, +} + +export interface CustomWokaPreviewerConfig { + color: number; + borderThickness: number; + borderColor: number; + bodyPartsOffsetX: number; +} + +export class CustomWokaPreviewer extends Phaser.GameObjects.Container { + private background: Phaser.GameObjects.Image; + private frame: Phaser.GameObjects.Graphics; + private sprites: Record; + private turnIcon: Phaser.GameObjects.Image; + + private animationDirection: PlayerAnimationDirections = PlayerAnimationDirections.Down; + private moving: boolean = true; + + private turnIconTween?: Phaser.Tweens.Tween; + + private config: CustomWokaPreviewerConfig; + + public readonly SIZE: number = 50; + + constructor(scene: Phaser.Scene, x: number, y: number, config: CustomWokaPreviewerConfig) { + super(scene, x, y); + + this.config = config; + + this.sprites = { + [CustomWokaBodyPart.Accessory]: this.scene.add + .sprite(this.config.bodyPartsOffsetX, 0, "") + .setVisible(false), + [CustomWokaBodyPart.Body]: this.scene.add.sprite(this.config.bodyPartsOffsetX, 0, "").setVisible(false), + [CustomWokaBodyPart.Clothes]: this.scene.add.sprite(this.config.bodyPartsOffsetX, 0, "").setVisible(false), + [CustomWokaBodyPart.Eyes]: this.scene.add.sprite(this.config.bodyPartsOffsetX, 0, "").setVisible(false), + [CustomWokaBodyPart.Hair]: this.scene.add.sprite(this.config.bodyPartsOffsetX, 0, "").setVisible(false), + [CustomWokaBodyPart.Hat]: this.scene.add.sprite(this.config.bodyPartsOffsetX, 0, "").setVisible(false), + }; + + this.background = this.scene.add.image(0, 0, "floorTexture1"); + this.frame = this.scene.add.graphics(); + this.turnIcon = this.scene.add + .image(this.background.displayWidth * 0.35, this.background.displayHeight * 0.35, "iconTurn") + .setScale(0.25) + .setTintFill(0xffffff) + .setAlpha(0.5); + + this.drawFrame(); + this.setSize(this.SIZE, this.SIZE); + this.setInteractive({ cursor: "pointer" }); + + this.add([ + this.background, + this.frame, + this.sprites.Body, + this.sprites.Eyes, + this.sprites.Hair, + this.sprites.Clothes, + this.sprites.Hat, + this.sprites.Accessory, + this.turnIcon, + ]); + + this.bindEventHandlers(); + + this.scene.add.existing(this); + } + + public update(): void { + this.animate(); + } + + public changeAnimation(direction: PlayerAnimationDirections, moving: boolean): void { + this.animationDirection = direction; + this.moving = moving; + } + + public updateSprite(textureKey: string, bodyPart: CustomWokaBodyPart): void { + this.sprites[bodyPart].anims.stop(); + this.sprites[bodyPart].setTexture(textureKey).setVisible(textureKey !== ""); + if (textureKey === "") { + return; + } + getPlayerAnimations(textureKey).forEach((d) => { + this.scene.anims.create({ + key: d.key, + frames: this.scene.anims.generateFrameNumbers(d.frameModel, { frames: d.frames }), + frameRate: d.frameRate, + repeat: d.repeat, + }); + }); + // Needed, otherwise, animations are not handled correctly. + if (this.scene) { + this.scene.sys.updateList.add(this.sprites[bodyPart]); + } + } + + public isMoving(): boolean { + return this.moving; + } + + public getAnimationDirection(): PlayerAnimationDirections { + return this.animationDirection; + } + + private bindEventHandlers(): void { + this.on(Phaser.Input.Events.POINTER_UP, () => { + const direction = this.getNextAnimationDirection(); + const moving = direction === PlayerAnimationDirections.Down ? !this.moving : this.moving; + this.changeAnimation(direction, moving); + + this.turnIconTween?.stop(); + this.turnIcon.setScale(0.25); + this.turnIconTween = this.scene.tweens.add({ + targets: [this.turnIcon], + duration: 100, + scale: 0.2, + yoyo: true, + ease: Easing.SineEaseIn, + }); + }); + } + + private drawFrame(): void { + this.frame.clear(); + this.frame.lineStyle(this.config.borderThickness, 0xadafbc); + this.frame.strokeRect(-this.SIZE / 2, -this.SIZE / 2, this.SIZE, this.SIZE); + } + + private animate(): void { + for (const bodyPartKey in this.sprites) { + const sprite = this.sprites[bodyPartKey as CustomWokaBodyPart]; + if (!sprite.anims) { + console.error("ANIMS IS NOT DEFINED!!!"); + return; + } + const textureKey = sprite.texture.key; + if (textureKey === "__MISSING") { + continue; + } + if ( + this.moving && + (!sprite.anims.currentAnim || sprite.anims.currentAnim.key !== this.animationDirection) + ) { + sprite.play(textureKey + "-" + this.animationDirection + "-" + PlayerAnimationTypes.Walk, true); + } else if (!this.moving) { + sprite.anims.play(textureKey + "-" + this.animationDirection + "-" + PlayerAnimationTypes.Idle, true); + } + } + } + + private getNextAnimationDirection(): PlayerAnimationDirections { + switch (this.animationDirection) { + case PlayerAnimationDirections.Down: + return PlayerAnimationDirections.Left; + case PlayerAnimationDirections.Left: + return PlayerAnimationDirections.Up; + case PlayerAnimationDirections.Up: + return PlayerAnimationDirections.Right; + case PlayerAnimationDirections.Right: + return PlayerAnimationDirections.Down; + } + } +} diff --git a/front/src/Phaser/Components/CustomizeWoka/WokaBodyPartSlot.ts b/front/src/Phaser/Components/CustomizeWoka/WokaBodyPartSlot.ts new file mode 100644 index 00000000..94e3bb33 --- /dev/null +++ b/front/src/Phaser/Components/CustomizeWoka/WokaBodyPartSlot.ts @@ -0,0 +1,143 @@ +import { GridItem } from "@home-based-studio/phaser3-utils"; +import { GridItemEvent } from "@home-based-studio/phaser3-utils/lib/utils/gui/containers/grids/GridItem"; +import { CustomWokaBodyPart } from "./CustomWokaPreviewer"; + +export interface WokaBodyPartSlotConfig { + color: number; + borderThickness: number; + borderColor: number; + borderSelectedColor: number; + offsetX: number; + offsetY: number; + textureKeys: Record; + categoryImageKey?: string; + selected?: boolean; +} + +export enum WokaBodyPartSlotEvent { + Clicked = "WokaBodyPartSlotEvent:Clicked", +} + +export class WokaBodyPartSlot extends GridItem { + private background: Phaser.GameObjects.Image; + private frame: Phaser.GameObjects.Graphics; + private categoryImage?: Phaser.GameObjects.Image; + private sprites: Record; + + private config: WokaBodyPartSlotConfig; + + private selected: boolean; + + public static readonly SIZE: number = 50; + + constructor(scene: Phaser.Scene, x: number, y: number, config: WokaBodyPartSlotConfig, id?: number) { + super(scene, `${id}`, { x, y }); + + this.config = config; + + const textures = this.config.textureKeys; + this.sprites = { + [CustomWokaBodyPart.Accessory]: this.scene.add + .sprite(this.config.offsetX, this.config.offsetY, textures.Accessory) + .setVisible(textures.Accessory !== ""), + [CustomWokaBodyPart.Body]: this.scene.add + .sprite(this.config.offsetX, this.config.offsetY, textures.Body) + .setVisible(textures.Body !== ""), + [CustomWokaBodyPart.Clothes]: this.scene.add + .sprite(this.config.offsetX, this.config.offsetY, textures.Clothes) + .setVisible(textures.Clothes !== ""), + [CustomWokaBodyPart.Eyes]: this.scene.add + .sprite(this.config.offsetX, this.config.offsetY, textures.Eyes) + .setVisible(textures.Eyes !== ""), + [CustomWokaBodyPart.Hair]: this.scene.add + .sprite(this.config.offsetX, this.config.offsetY, textures.Hair) + .setVisible(textures.Hair !== ""), + [CustomWokaBodyPart.Hat]: this.scene.add + .sprite(this.config.offsetX, this.config.offsetY, textures.Hat) + .setVisible(textures.Hat !== ""), + }; + + this.selected = this.config.selected ?? false; + + this.background = this.background = this.scene.add.image(0, 0, `floorTexture1`); + this.frame = this.scene.add.graphics(); + this.drawFrame(); + this.add([ + this.background, + this.frame, + this.sprites.Body, + this.sprites.Eyes, + this.sprites.Hair, + this.sprites.Clothes, + this.sprites.Hat, + this.sprites.Accessory, + ]); + + if (this.config.categoryImageKey) { + this.categoryImage = this.scene.add + .image(WokaBodyPartSlot.SIZE / 2 - 1, -WokaBodyPartSlot.SIZE / 2 + 1, this.config.categoryImageKey) + .setDisplaySize(16, 16) + .setAlpha(0.75) + .setOrigin(1, 0); + this.add(this.categoryImage); + } + + this.setSize(WokaBodyPartSlot.SIZE, WokaBodyPartSlot.SIZE); + this.setInteractive({ cursor: "pointer" }); + this.scene.input.setDraggable(this); + + this.bindEventHandlers(); + + this.scene.add.existing(this); + } + + public getContentData(): Record { + return this.config.textureKeys; + } + + public setTextures(textureKeys: Record): void { + this.config.textureKeys = textureKeys; + this.sprites.Accessory.setTexture(textureKeys.Accessory).setVisible(textureKeys.Accessory !== ""); + this.sprites.Body.setTexture(textureKeys.Body).setVisible(textureKeys.Body !== ""); + this.sprites.Clothes.setTexture(textureKeys.Clothes).setVisible(textureKeys.Clothes !== ""); + this.sprites.Eyes.setTexture(textureKeys.Eyes).setVisible(textureKeys.Eyes !== ""); + this.sprites.Hair.setTexture(textureKeys.Hair).setVisible(textureKeys.Hair !== ""); + this.sprites.Hat.setTexture(textureKeys.Hat).setVisible(textureKeys.Hat !== ""); + } + + public select(select: boolean = true): void { + if (this.selected === select) { + return; + } + this.selected = select; + this.updateSelected(); + } + + public isSelected(): boolean { + return this.selected; + } + + protected bindEventHandlers(): void { + super.bindEventHandlers(); + + this.on(GridItemEvent.Clicked, () => { + this.emit(WokaBodyPartSlotEvent.Clicked, this.selected); + }); + } + + private drawFrame(): void { + this.frame.clear(); + this.frame.lineStyle( + this.config.borderThickness, + this.selected ? this.config.borderSelectedColor : this.config.borderColor + ); + + const size = WokaBodyPartSlot.SIZE; + + this.frame.strokeRect(-size / 2, -size / 2, size, size); + } + + private updateSelected(): void { + this.drawFrame(); + } +} diff --git a/front/src/Phaser/Components/Ui/Button.ts b/front/src/Phaser/Components/Ui/Button.ts new file mode 100644 index 00000000..02219555 --- /dev/null +++ b/front/src/Phaser/Components/Ui/Button.ts @@ -0,0 +1,99 @@ +export interface ButtonConfig { + width: number; + height: number; + idle: ButtonAppearanceConfig; + hover: ButtonAppearanceConfig; + pressed: ButtonAppearanceConfig; +} + +export interface ButtonAppearanceConfig { + textColor: string; + color: number; + borderThickness: number; + borderColor: number; +} + +export class Button extends Phaser.GameObjects.Container { + private background: Phaser.GameObjects.Graphics; + private text: Phaser.GameObjects.Text; + + private config: ButtonConfig; + + private hovered: boolean = false; + private pressed: boolean = false; + + constructor(scene: Phaser.Scene, x: number, y: number, config: ButtonConfig) { + super(scene, x, y); + + this.config = config; + + this.background = this.scene.add.graphics(); + this.text = this.scene.add + .text(0, 0, "", { + color: "0x000000", + fontFamily: '"Press Start 2P"', + fontSize: "9px", + }) + .setOrigin(0.5, 0.45); + this.drawBackground(this.config.idle); + + this.add([this.background, this.text]); + + this.setSize(this.config.width, this.config.height); + this.setInteractive({ cursor: "pointer" }); + + this.bindEventHandlers(); + + this.scene.add.existing(this); + } + + public setText(text: string): void { + this.text.setText(text); + } + + private updateButtonAppearance(): void { + if (this.pressed) { + this.drawBackground(this.config.pressed); + return; + } + if (this.hovered) { + this.drawBackground(this.config.hover); + return; + } + this.drawBackground(this.config.idle); + } + + private drawBackground(appearance: ButtonAppearanceConfig): void { + this.background.clear(); + this.background.fillStyle(appearance.color); + this.background.lineStyle(appearance.borderThickness, appearance.borderColor); + + const w = this.config.width; + const h = this.config.height; + + this.background.fillRect(-w / 2, -h / 2, w, h); + this.background.strokeRect(-w / 2, -h / 2, w, h); + + this.text.setColor(appearance.textColor); + } + + private bindEventHandlers(): void { + this.on(Phaser.Input.Events.POINTER_OVER, () => { + this.hovered = true; + this.updateButtonAppearance(); + }); + this.on(Phaser.Input.Events.POINTER_OUT, () => { + this.hovered = false; + this.pressed = false; + this.updateButtonAppearance(); + }); + this.on(Phaser.Input.Events.POINTER_DOWN, () => { + this.pressed = true; + this.updateButtonAppearance(); + }); + this.on(Phaser.Input.Events.POINTER_UP, () => { + this.pressed = false; + this.updateButtonAppearance(); + }); + } +} diff --git a/front/src/Phaser/Components/Ui/IconButton.ts b/front/src/Phaser/Components/Ui/IconButton.ts new file mode 100644 index 00000000..2fdc9cfd --- /dev/null +++ b/front/src/Phaser/Components/Ui/IconButton.ts @@ -0,0 +1,115 @@ +export interface IconButtonConfig { + width: number; + height: number; + iconTextureKey: string; + idle: IconButtonAppearanceConfig; + hover: IconButtonAppearanceConfig; + pressed: IconButtonAppearanceConfig; + selected: IconButtonAppearanceConfig; +} + +export interface IconButtonAppearanceConfig { + color: number; + borderThickness: number; + borderColor: number; +} + +export enum IconButtonEvent { + Clicked = "IconButton:Clicked", +} + +export class IconButton extends Phaser.GameObjects.Container { + private background: Phaser.GameObjects.Graphics; + private icon: Phaser.GameObjects.Image; + + private config: IconButtonConfig; + + private hovered: boolean = false; + private pressed: boolean = false; + private selected: boolean = false; + + constructor(scene: Phaser.Scene, x: number, y: number, config: IconButtonConfig) { + super(scene, x, y); + + this.config = config; + + this.background = this.scene.add.graphics(); + this.icon = this.scene.add.image(0, 0, this.config.iconTextureKey); + this.drawBackground(this.config.idle); + + this.add([this.background, this.icon]); + + this.setSize(this.config.width, this.config.height); + this.setInteractive({ cursor: "pointer" }); + + this.bindEventHandlers(); + + this.scene.add.existing(this); + } + + public select(select: boolean = true): void { + if (this.selected === select) { + return; + } + this.selected = select; + this.updateButtonAppearance(); + } + + private updateButtonAppearance(): void { + if (this.selected) { + this.drawBackground(this.config.selected); + return; + } + if (this.pressed) { + this.drawBackground(this.config.pressed); + return; + } + if (this.hovered) { + this.drawBackground(this.config.hover); + return; + } + this.drawBackground(this.config.idle); + } + + private drawBackground(appearance: IconButtonAppearanceConfig): void { + this.background.clear(); + this.background.fillStyle(appearance.color); + this.background.lineStyle(appearance.borderThickness, appearance.borderColor); + + const w = this.config.width; + const h = this.config.height; + + this.background.fillRect(-w / 2, -h / 2, w, h); + this.background.strokeRect(-w / 2, -h / 2, w, h); + } + + private bindEventHandlers(): void { + this.on(Phaser.Input.Events.POINTER_OVER, () => { + if (this.selected) { + return; + } + this.hovered = true; + this.updateButtonAppearance(); + }); + this.on(Phaser.Input.Events.POINTER_OUT, () => { + this.hovered = false; + this.pressed = false; + this.updateButtonAppearance(); + }); + this.on(Phaser.Input.Events.POINTER_DOWN, () => { + if (this.selected) { + return; + } + this.pressed = true; + this.updateButtonAppearance(); + }); + this.on(Phaser.Input.Events.POINTER_UP, () => { + if (this.selected) { + return; + } + this.pressed = false; + this.updateButtonAppearance(); + this.emit(IconButtonEvent.Clicked, this.selected); + }); + } +} diff --git a/front/src/Phaser/Entity/Character.ts b/front/src/Phaser/Entity/Character.ts index 9551277a..fab18ce1 100644 --- a/front/src/Phaser/Entity/Character.ts +++ b/front/src/Phaser/Entity/Character.ts @@ -1,4 +1,9 @@ -import { PlayerAnimationDirections, PlayerAnimationTypes } from "../Player/Animation"; +import { + AnimationData, + getPlayerAnimations, + PlayerAnimationDirections, + PlayerAnimationTypes, +} from "../Player/Animation"; import { SpeechBubble } from "./SpeechBubble"; import Text = Phaser.GameObjects.Text; import Container = Phaser.GameObjects.Container; @@ -21,15 +26,6 @@ import { TalkIcon } from "../Components/TalkIcon"; import { Deferred } from "ts-deferred"; const playerNameY = -25; - -interface AnimationData { - key: string; - frameRate: number; - repeat: number; - frameModel: string; //todo use an enum - frames: number[]; -} - const interactiveRadius = 35; export abstract class Character extends Container implements OutlineableInterface { @@ -248,7 +244,7 @@ export abstract class Character extends Container implements OutlineableInterfac } } - public addTextures(textures: string[], frame?: string | number): void { + private addTextures(textures: string[], frame?: string | number): void { if (textures.length < 1) { throw new TextureError("no texture given"); } @@ -259,7 +255,7 @@ export abstract class Character extends Container implements OutlineableInterfac } const sprite = new Sprite(this.scene, 0, 0, texture, frame); this.add(sprite); - this.getPlayerAnimations(texture).forEach((d) => { + getPlayerAnimations(texture).forEach((d) => { this.scene.anims.create({ key: d.key, frames: this.scene.anims.generateFrameNumbers(d.frameModel, { frames: d.frames }), @@ -279,67 +275,6 @@ export abstract class Character extends Container implements OutlineableInterfac return this.scene.plugins.get("rexOutlinePipeline") as unknown as OutlinePipelinePlugin | undefined; } - 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, - }, - ]; - } - protected playAnimation(direction: PlayerAnimationDirections, moving: boolean): void { if (this.invisible) return; for (const [texture, sprite] of this.sprites.entries()) { diff --git a/front/src/Phaser/Entity/CustomizedCharacter.ts b/front/src/Phaser/Entity/CustomizedCharacter.ts deleted file mode 100644 index 79ac8ebc..00000000 --- a/front/src/Phaser/Entity/CustomizedCharacter.ts +++ /dev/null @@ -1,20 +0,0 @@ -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)); - } - } -} diff --git a/front/src/Phaser/Helpers/TexturesHelper.ts b/front/src/Phaser/Helpers/TexturesHelper.ts index 348e957a..6c0f1aab 100644 --- a/front/src/Phaser/Helpers/TexturesHelper.ts +++ b/front/src/Phaser/Helpers/TexturesHelper.ts @@ -31,4 +31,40 @@ export class TexturesHelper { throw new Error("Could not get the snapshot"); } } + + public static createFloorRectangleTexture( + scene: Phaser.Scene, + newTextureKey: string, + width: number, + height: number, + sourceTextureKey: string, + sourceTextureFrame?: number | string, + sourceTextureWidth: number = 32, + sourceTextureHeight: number = 32 + ): void { + const rt = scene.make.renderTexture({ x: 0, y: 0, width, height }, false); + const widthTiles = Math.ceil(width / sourceTextureWidth); + const heightTiles = Math.ceil(height / sourceTextureHeight); + + for (let x = 0; x < widthTiles; x += 1) { + for (let y = 0; y < heightTiles; y += 1) { + rt.drawFrame(sourceTextureKey, sourceTextureFrame, x * 32, y * 32); + } + } + + rt.saveTexture(newTextureKey); + rt.destroy(); + } + + public static createRectangleTexture( + scene: Phaser.Scene, + textureKey: string, + width: number, + height: number, + color: number + ): void { + const rectangleTexture = scene.add.graphics().fillStyle(color, 1).fillRect(0, 0, width, height); + rectangleTexture.generateTexture(textureKey, width, height); + rectangleTexture.destroy(); + } } diff --git a/front/src/Phaser/Login/AbstractCharacterScene.ts b/front/src/Phaser/Login/AbstractCharacterScene.ts index c762cbaa..27a4aeab 100644 --- a/front/src/Phaser/Login/AbstractCharacterScene.ts +++ b/front/src/Phaser/Login/AbstractCharacterScene.ts @@ -1,9 +1,5 @@ import { ResizableScene } from "./ResizableScene"; -import { BodyResourceDescriptionInterface, PlayerTexturesKey } from "../Entity/PlayerTextures"; -import { loadWokaTexture } from "../Entity/PlayerTexturesLoadingManager"; -import type CancelablePromise from "cancelable-promise"; import { PlayerTextures } from "../Entity/PlayerTextures"; -import Texture = Phaser.Textures.Texture; import { SuperLoaderPlugin } from "../Services/SuperLoaderPlugin"; export abstract class AbstractCharacterScene extends ResizableScene { diff --git a/front/src/Phaser/Login/CustomizeScene.ts b/front/src/Phaser/Login/CustomizeScene.ts index e7ef20b9..1aeb0684 100644 --- a/front/src/Phaser/Login/CustomizeScene.ts +++ b/front/src/Phaser/Login/CustomizeScene.ts @@ -1,7 +1,5 @@ import { EnableCameraSceneName } from "./EnableCameraScene"; -import Rectangle = Phaser.GameObjects.Rectangle; import { loadAllLayers } from "../Entity/PlayerTexturesLoadingManager"; -import Sprite = Phaser.GameObjects.Sprite; import { gameManager } from "../Game/GameManager"; import { localUserStore } from "../../Connexion/LocalUserStore"; import { Loader } from "../Components/Loader"; @@ -9,31 +7,50 @@ import type { BodyResourceDescriptionInterface } from "../Entity/PlayerTextures" import { AbstractCharacterScene } from "./AbstractCharacterScene"; import { areCharacterLayersValid } from "../../Connexion/LocalUser"; import { SelectCharacterSceneName } from "./SelectCharacterScene"; -import { activeRowStore, customCharacterSceneVisibleStore } from "../../Stores/CustomCharacterStore"; import { waScaleManager } from "../Services/WaScaleManager"; -import { CustomizedCharacter } from "../Entity/CustomizedCharacter"; -import { get } from "svelte/store"; import { analyticsClient } from "../../Administration/AnalyticsClient"; import { isMediaBreakpointUp } from "../../Utils/BreakpointsUtils"; import { PUSHER_URL } from "../../Enum/EnvironmentVariable"; +import { + CustomWokaBodyPart, + CustomWokaBodyPartOrder, + CustomWokaPreviewer, + CustomWokaPreviewerConfig, +} from "../Components/CustomizeWoka/CustomWokaPreviewer"; +import { DraggableGrid } from "@home-based-studio/phaser3-utils"; +import { + WokaBodyPartSlot, + WokaBodyPartSlotConfig, + WokaBodyPartSlotEvent, +} from "../Components/CustomizeWoka/WokaBodyPartSlot"; +import { DraggableGridEvent } from "@home-based-studio/phaser3-utils/lib/utils/gui/containers/grids/DraggableGrid"; +import { Button } from "../Components/Ui/Button"; import { wokaList } from "../../Messages/JsonMessages/PlayerTextures"; +import { TexturesHelper } from "../Helpers/TexturesHelper"; +import { IconButton, IconButtonConfig, IconButtonEvent } from "../Components/Ui/IconButton"; export const CustomizeSceneName = "CustomizeScene"; export class CustomizeScene extends AbstractCharacterScene { - private Rectangle!: Rectangle; + private customWokaPreviewer!: CustomWokaPreviewer; + private bodyPartsDraggableGridLeftShadow!: Phaser.GameObjects.Image; + private bodyPartsDraggableGridRightShadow!: Phaser.GameObjects.Image; + private bodyPartsDraggableGrid!: DraggableGrid; + private bodyPartsButtons!: Record; - private selectedLayers: number[] = [0]; - private containersRow: CustomizedCharacter[][] = []; + private randomizeButton!: Button; + private finishButton!: Button; + + private selectedLayers: number[] = [0, 0, 0, 0, 0, 0]; private layers: BodyResourceDescriptionInterface[][] = []; + private selectedBodyPartType?: CustomWokaBodyPart; protected lazyloadingAttempt = true; //permit to update texture loaded after renderer - private moveHorizontally: number = 0; - private moveVertically: number = 0; - private loader: Loader; + private readonly SLOT_DIMENSION = 100; + constructor() { super({ key: CustomizeSceneName, @@ -41,7 +58,20 @@ export class CustomizeScene extends AbstractCharacterScene { this.loader = new Loader(this); } - preload() { + public preload(): void { + this.input.dragDistanceThreshold = 10; + + this.load.image("iconClothes", "/resources/icons/icon_clothes.png"); + this.load.image("iconAccessory", "/resources/icons/icon_accessory.png"); + this.load.image("iconHat", "/resources/icons/icon_hat.png"); + this.load.image("iconHair", "/resources/icons/icon_hair.png"); + this.load.image("iconEyes", "/resources/icons/icon_eyes.png"); + this.load.image("iconBody", "/resources/icons/icon_body.png"); + this.load.image("iconTurn", "/resources/icons/icon_turn.png"); + this.load.spritesheet("floorTiles", "/resources/tilesets/floor_tiles.png", { frameWidth: 32, frameHeight: 32 }); + + TexturesHelper.createRectangleTexture(this, "gridEdgeShadow", this.cameras.main.width * 0.2, 115, 0x000000); + const wokaMetadataKey = "woka-list" + gameManager.currentStartedRoom.href; this.cache.json.remove(wokaMetadataKey); this.superLoad @@ -68,212 +98,87 @@ export class CustomizeScene extends AbstractCharacterScene { this.loader.addLoader(); } - create() { - customCharacterSceneVisibleStore.set(true); - this.events.addListener("wake", () => { - waScaleManager.saveZoom(); - waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 3 : 1; - customCharacterSceneVisibleStore.set(true); + public create(): void { + TexturesHelper.createFloorRectangleTexture(this, "floorTexture1", 50, 50, "floorTiles", 0); + TexturesHelper.createFloorRectangleTexture(this, "floorTexture2", 50, 50, "floorTiles", 1); + TexturesHelper.createFloorRectangleTexture(this, "floorTexture3", 50, 50, "floorTiles", 2); + TexturesHelper.createFloorRectangleTexture(this, "floorTexture4", 50, 50, "floorTiles", 3); + this.customWokaPreviewer = new CustomWokaPreviewer( + this, + 0, + 0, + this.getCustomWokaPreviewerConfig() + ).setDisplaySize(200, 200); + + this.bodyPartsDraggableGrid = new DraggableGrid(this, { + position: { x: 0, y: 0 }, + maskPosition: { x: 0, y: 0 }, + dimension: { x: 485, y: 165 }, + horizontal: true, + repositionToCenter: true, + itemsInRow: 1, + margin: { + left: (innerWidth / waScaleManager.getActualZoom() - this.SLOT_DIMENSION) * 0.5, + right: (innerWidth / waScaleManager.getActualZoom() - this.SLOT_DIMENSION) * 0.5, + }, + spacing: 5, + debug: { + showDraggableSpace: false, + }, }); - waScaleManager.saveZoom(); - waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 3 : 1; + this.bodyPartsDraggableGridLeftShadow = this.add + .image(0, this.cameras.main.worldView.y + this.cameras.main.height, "gridEdgeShadow") + .setAlpha(1, 0, 1, 0) + .setOrigin(0, 0.5); - 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); + this.bodyPartsDraggableGridRightShadow = this.add + .image( + this.cameras.main.worldView.x + this.cameras.main.width, + this.cameras.main.worldView.y + this.cameras.main.height, + "gridEdgeShadow" + ) + .setAlpha(1, 0, 1, 0) + .setFlipX(true) + .setOrigin(1, 0.5); - this.createCustomizeLayer(0, 0, 0); - this.createCustomizeLayer(0, 0, 1); - this.createCustomizeLayer(0, 0, 2); - this.createCustomizeLayer(0, 0, 3); - this.createCustomizeLayer(0, 0, 4); - this.createCustomizeLayer(0, 0, 5); + this.bodyPartsButtons = { + [CustomWokaBodyPart.Accessory]: new IconButton( + this, + 0, + 0, + this.getDefaultIconButtonConfig("iconAccessory") + ), + [CustomWokaBodyPart.Body]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconBody")), + [CustomWokaBodyPart.Clothes]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconClothes")), + [CustomWokaBodyPart.Eyes]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconEyes")), + [CustomWokaBodyPart.Hair]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconHair")), + [CustomWokaBodyPart.Hat]: new IconButton(this, 0, 0, this.getDefaultIconButtonConfig("iconHat")), + }; - this.moveLayers(); - this.input.keyboard.on("keyup-ENTER", () => { - this.nextSceneToCamera(); - }); - this.input.keyboard.on("keyup-BACKSPACE", () => { - this.backToPreviousScene(); - }); + this.selectedBodyPartType = CustomWokaBodyPart.Body; + this.bodyPartsButtons.Body.select(); - // 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)); + this.initializeRandomizeButton(); + this.initializeFinishButton(); - const customCursorPosition = localUserStore.getCustomCursorPosition(); - if (customCursorPosition) { - activeRowStore.set(customCursorPosition.activeRow); - this.selectedLayers = customCursorPosition.selectedLayers; - this.moveLayers(); - this.updateSelectedLayer(); - } + this.refreshPlayerCurrentOutfit(); this.onResize(); + + this.bindEventHandlers(); } - public moveCursorHorizontally(index: number): void { - this.moveHorizontally = index; - } - - public moveCursorVertically(index: number): void { - this.moveVertically = index; - } - - private doMoveCursorHorizontally(index: number): void { - this.selectedLayers[get(activeRowStore)] += index; - if (this.selectedLayers[get(activeRowStore)] < 0) { - this.selectedLayers[get(activeRowStore)] = 0; - } else if (this.selectedLayers[get(activeRowStore)] > this.layers[get(activeRowStore)].length - 1) { - this.selectedLayers[get(activeRowStore)] = this.layers[get(activeRowStore)].length - 1; - } - this.moveLayers(); - this.updateSelectedLayer(); - this.saveInLocalStorage(); - } - - private doMoveCursorVertically(index: number): void { - activeRowStore.set(get(activeRowStore) + index); - if (get(activeRowStore) < 0) { - activeRowStore.set(0); - } else if (get(activeRowStore) > this.layers.length - 1) { - activeRowStore.set(this.layers.length - 1); - } - this.moveLayers(); - this.saveInLocalStorage(); - } - - private saveInLocalStorage() { - localUserStore.setCustomCursorPosition(get(activeRowStore), this.selectedLayers); - } - - /** - * @param x, the layer's vertical position - * @param y, the layer's horizontal position - * @param layerNumber, index of the this.layers array - * create the layer and display it on the scene - */ - private createCustomizeLayer(x: number, y: number, layerNumber: number): void { - this.containersRow[layerNumber] = []; - this.selectedLayers[layerNumber] = 0; - let alpha = 0; - let layerPosX = 0; - for (let i = 0; i < this.layers[layerNumber].length; i++) { - const container = this.generateCharacter(300 + x + layerPosX, y, layerNumber, i); - - this.containersRow[layerNumber][i] = container; - this.add.existing(container); - layerPosX += 30; - alpha += 0.1; - } - } - - /** - * Generates a character from the current selected items BUT replaces - * one layer item with an item we pass in parameter. - * - * Current selected items are fetched from this.selectedLayers - * - * @param x, - * @param y, - * @param layerNumber, The selected layer number (0 for body...) - * @param selectedItem, The number of the item select (0 for black body...) - */ - private generateCharacter(x: number, y: number, layerNumber: number, selectedItem: number) { - return new CustomizedCharacter(this, x, y, this.getContainerChildren(layerNumber, selectedItem)); - } - - private getContainerChildren(layerNumber: number, selectedItem: number): Array { - const children: Array = new Array(); - for (let j = 0; j <= layerNumber; j++) { - if (j === layerNumber) { - children.push(this.layers[j][selectedItem].id); - } else { - const layer = this.selectedLayers[j]; - if (layer === undefined) { - continue; - } - children.push(this.layers[j][layer].id); - } - } - return children; - } - - /** - * Move the layer left, right, up and down and update the selected layer - */ - private moveLayers(): void { - const screenCenterX = this.cameras.main.worldView.x + this.cameras.main.width / 2; - const screenCenterY = this.cameras.main.worldView.y + this.cameras.main.height / 3; - const screenWidth = this.game.renderer.width; - const screenHeight = this.game.renderer.height; - for (let i = 0; i < this.containersRow.length; i++) { - for (let j = 0; j < this.containersRow[i].length; j++) { - let selectedX = this.selectedLayers[i]; - if (selectedX === undefined) { - selectedX = 0; - } - this.containersRow[i][j].x = screenCenterX + (j - selectedX) * 40; - this.containersRow[i][j].y = screenCenterY + (i - get(activeRowStore)) * 40; - const alpha1 = (Math.abs(selectedX - j) * 47 * 2) / screenWidth; - const alpha2 = (Math.abs(get(activeRowStore) - i) * 49 * 2) / screenHeight; - this.containersRow[i][j].setAlpha((1 - alpha1) * (1 - alpha2)); - } - } - } - - /** - * @param x, the sprite's vertical position - * @param y, the sprites's horizontal position - * @param name, the sprite's name - * @return a new sprite - */ - private generateLayers(x: number, y: number, name: string): Sprite { - //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].updateSprites(children); - } - } - } - - update(time: number, delta: number): void { - if (this.lazyloadingAttempt) { - this.moveLayers(); - this.doMoveCursorHorizontally(this.moveHorizontally); - 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 update(time: number, dt: number): void { + this.customWokaPreviewer.update(); } 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.handleCustomWokaPreviewerOnResize(); + this.handleBodyPartButtonsOnResize(); + this.handleRandomizeButtonOnResize(); + this.handleFinishButtonOnResize(); + this.handleBodyPartsDraggableGridOnResize(); } public nextSceneToCamera() { @@ -295,13 +200,464 @@ export class CustomizeScene extends AbstractCharacterScene { this.scene.stop(CustomizeSceneName); waScaleManager.restoreZoom(); gameManager.tryResumingGame(EnableCameraSceneName); - customCharacterSceneVisibleStore.set(false); } public backToPreviousScene() { this.scene.stop(CustomizeSceneName); waScaleManager.restoreZoom(); this.scene.run(SelectCharacterSceneName); - customCharacterSceneVisibleStore.set(false); + } + + private getDefaultIconButtonConfig(iconTextureKey: string): IconButtonConfig { + return { + iconTextureKey, + width: 25, + height: 25, + idle: { + color: 0xffffff, + borderThickness: 3, + borderColor: 0xe7e7e7, + }, + hover: { + color: 0xe7e7e7, + borderThickness: 3, + borderColor: 0xadafbc, + }, + pressed: { + color: 0xadafbc, + borderThickness: 3, + borderColor: 0xadafbc, + }, + selected: { + color: 0xadafbc, + borderThickness: 3, + borderColor: 0x209cee, + }, + }; + } + + private initializeRandomizeButton(): void { + this.randomizeButton = new Button(this, 50, 50, { + width: 95, + height: 50, + idle: { + color: 0xffffff, + textColor: "#000000", + borderThickness: 3, + borderColor: 0xe7e7e7, + }, + hover: { + color: 0xe7e7e7, + textColor: "#000000", + borderThickness: 3, + borderColor: 0xadafbc, + }, + pressed: { + color: 0xadafbc, + textColor: "#000000", + borderThickness: 3, + borderColor: 0xadafbc, + }, + }); + this.randomizeButton.setText("Randomize"); + } + + private initializeFinishButton(): void { + this.finishButton = new Button(this, 50, 50, { + width: 95, + height: 50, + idle: { + color: 0x209cee, + textColor: "#ffffff", + borderThickness: 3, + borderColor: 0x006bb3, + }, + hover: { + color: 0x0987db, + textColor: "#ffffff", + borderThickness: 3, + borderColor: 0x006bb3, + }, + pressed: { + color: 0x006bb3, + textColor: "#ffffff", + borderThickness: 3, + borderColor: 0x006bb3, + }, + }); + this.finishButton.setText("Finish"); + } + + private refreshPlayerCurrentOutfit(): void { + let i = 0; + for (const layerItem of this.selectedLayers) { + const bodyPart = CustomWokaBodyPart[CustomWokaBodyPartOrder[i] as CustomWokaBodyPart]; + this.customWokaPreviewer.updateSprite(this.layers[i][layerItem].id, bodyPart); + i += 1; + } + } + + private getCurrentlySelectedWokaTexturesRecord(): Record { + return { + [CustomWokaBodyPart.Accessory]: + this.layers[CustomWokaBodyPartOrder.Accessory][this.selectedLayers[CustomWokaBodyPartOrder.Accessory]] + .id, + [CustomWokaBodyPart.Body]: + this.layers[CustomWokaBodyPartOrder.Body][this.selectedLayers[CustomWokaBodyPartOrder.Body]].id, + [CustomWokaBodyPart.Clothes]: + this.layers[CustomWokaBodyPartOrder.Clothes][this.selectedLayers[CustomWokaBodyPartOrder.Clothes]].id, + [CustomWokaBodyPart.Eyes]: + this.layers[CustomWokaBodyPartOrder.Eyes][this.selectedLayers[CustomWokaBodyPartOrder.Eyes]].id, + [CustomWokaBodyPart.Hair]: + this.layers[CustomWokaBodyPartOrder.Hair][this.selectedLayers[CustomWokaBodyPartOrder.Hair]].id, + [CustomWokaBodyPart.Hat]: + this.layers[CustomWokaBodyPartOrder.Hat][this.selectedLayers[CustomWokaBodyPartOrder.Hat]].id, + }; + } + + private handleCustomWokaPreviewerOnResize(): void { + this.customWokaPreviewer.x = this.cameras.main.worldView.x + this.cameras.main.width / 2; + this.customWokaPreviewer.y = this.customWokaPreviewer.displayHeight * 0.5 + 10; + } + + private handleBodyPartButtonsOnResize(): void { + const ratio = innerHeight / innerWidth; + const slotDimension = 50; + + for (const part in this.bodyPartsButtons) { + this.bodyPartsButtons[part as CustomWokaBodyPart].setDisplaySize(slotDimension, slotDimension); + } + + const slotSize = this.bodyPartsButtons.Accessory.displayHeight; + + if (ratio > 1.6) { + const middle = Math.floor(this.customWokaPreviewer.x); + const left = Math.floor(middle - slotSize - 23); + const right = Math.floor(middle + slotSize + 23); + const top = Math.floor( + this.customWokaPreviewer.y + this.customWokaPreviewer.displayHeight * 0.5 + slotSize * 1.5 + 30 + ); + const bottom = Math.floor(top + slotSize + 23); + + this.bodyPartsButtons.Body.setPosition(left, top); + this.bodyPartsButtons.Eyes.setPosition(middle, top); + this.bodyPartsButtons.Hair.setPosition(right, top); + this.bodyPartsButtons.Clothes.setPosition(left, bottom); + this.bodyPartsButtons.Hat.setPosition(middle, bottom); + this.bodyPartsButtons.Accessory.setPosition(right, bottom); + + return; + } + + const left = Math.floor( + this.customWokaPreviewer.x - this.customWokaPreviewer.displayWidth * 0.5 - slotSize * 0.5 - 24 + ); + const right = Math.floor( + this.customWokaPreviewer.x + this.customWokaPreviewer.displayWidth * 0.5 + slotSize * 0.5 + 24 + ); + const top = Math.floor(0 + slotSize * 0.5 + 11); + const middle = Math.floor(top + slotSize + 24); + const bottom = Math.floor(middle + slotSize + 24); + + this.bodyPartsButtons.Body.setPosition(left, top); + this.bodyPartsButtons.Eyes.setPosition(left, middle); + this.bodyPartsButtons.Hair.setPosition(left, bottom); + this.bodyPartsButtons.Clothes.setPosition(right, top); + this.bodyPartsButtons.Hat.setPosition(right, middle); + this.bodyPartsButtons.Accessory.setPosition(right, bottom); + } + + private handleBodyPartsDraggableGridOnResize(): void { + const gridHeight = 110; + const gridWidth = innerWidth / waScaleManager.getActualZoom(); + + const gridTopMargin = Math.max( + this.finishButton.y + this.finishButton.displayHeight * 0.5, + this.bodyPartsButtons.Hair.y + this.bodyPartsButtons.Hair.displayHeight * 0.5 + ); + const gridBottomMargin = this.cameras.main.worldView.y + this.cameras.main.height; + + const yPos = gridTopMargin + (gridBottomMargin - gridTopMargin) * 0.5; + + const gridPos = { + x: this.cameras.main.worldView.x + this.cameras.main.width / 2, + y: yPos, + }; + + this.bodyPartsDraggableGridLeftShadow.setPosition(0, yPos); + this.bodyPartsDraggableGridRightShadow.setPosition( + this.cameras.main.worldView.x + this.cameras.main.width, + yPos + ); + + try { + this.bodyPartsDraggableGrid.changeDraggableSpacePosAndSize( + gridPos, + { x: gridWidth, y: gridHeight }, + gridPos + ); + } catch (error) { + console.warn(error); + } + + this.populateGrid(); + const selectedGridItem = this.selectGridItem(); + if (selectedGridItem) { + this.centerGridOnItem(selectedGridItem); + } + } + + private handleRandomizeButtonOnResize(): void { + const x = + this.customWokaPreviewer.x + + (this.customWokaPreviewer.displayWidth - this.randomizeButton.displayWidth) * 0.5; + const y = + this.customWokaPreviewer.y + + (this.customWokaPreviewer.displayHeight + this.randomizeButton.displayHeight) * 0.5 + + 10; + this.randomizeButton.setPosition(x, y); + } + + private handleFinishButtonOnResize(): void { + const x = + this.customWokaPreviewer.x - + (this.customWokaPreviewer.displayWidth - this.randomizeButton.displayWidth) * 0.5; + const y = + this.customWokaPreviewer.y + + (this.customWokaPreviewer.displayHeight + this.randomizeButton.displayHeight) * 0.5 + + 10; + this.finishButton.setPosition(x, y); + } + + private getCustomWokaPreviewerConfig(): CustomWokaPreviewerConfig { + return { + color: 0xffffff, + borderThickness: 1, + borderColor: 0xadafbc, + bodyPartsOffsetX: -1, + }; + } + + private getWokaBodyPartSlotConfig(bodyPart?: CustomWokaBodyPart, newTextureKey?: string): WokaBodyPartSlotConfig { + const textures = this.getCurrentlySelectedWokaTexturesRecord(); + if (bodyPart && newTextureKey) { + textures[bodyPart] = newTextureKey; + } + return { + color: 0xffffff, + borderThickness: 1, + borderColor: 0xadafbc, + borderSelectedColor: 0x209cee, + textureKeys: textures, + offsetX: -4, + offsetY: 2, + }; + } + + private bindEventHandlers(): void { + this.bindKeyboardEventHandlers(); + this.events.addListener("wake", () => { + waScaleManager.saveZoom(); + waScaleManager.zoomModifier = isMediaBreakpointUp("md") ? 3 : 1; + }); + + this.randomizeButton.on(Phaser.Input.Events.POINTER_UP, () => { + this.randomizeOutfit(); + this.clearGrid(); + this.deselectAllButtons(); + this.refreshPlayerCurrentOutfit(); + }); + + this.finishButton.on(Phaser.Input.Events.POINTER_UP, () => { + this.nextSceneToCamera(); + }); + + for (const bodyPart in CustomWokaBodyPart) { + const button = this.bodyPartsButtons[bodyPart as CustomWokaBodyPart]; + button.on(IconButtonEvent.Clicked, (selected: boolean) => { + if (!selected) { + this.selectBodyPartType(bodyPart as CustomWokaBodyPart); + } + }); + } + + this.bodyPartsDraggableGrid.on(DraggableGridEvent.ItemClicked, (item: WokaBodyPartSlot) => { + void this.bodyPartsDraggableGrid.centerOnItem(this.bodyPartsDraggableGrid.getAllItems().indexOf(item), 500); + this.deselectAllSlots(); + item.select(true); + this.setNewBodyPart(Number(item.getId())); + }); + } + + private selectBodyPartType(bodyPart: CustomWokaBodyPart): void { + this.selectedBodyPartType = bodyPart; + this.deselectAllButtons(); + const button = this.bodyPartsButtons[bodyPart]; + button.select(true); + this.populateGrid(); + const selectedGridItem = this.selectGridItem(); + if (!selectedGridItem) { + return; + } + this.bodyPartsDraggableGrid.moveContentToBeginning(); + this.centerGridOnItem(selectedGridItem); + } + + private bindKeyboardEventHandlers(): void { + this.input.keyboard.on("keyup-ENTER", () => { + this.nextSceneToCamera(); + }); + this.input.keyboard.on("keyup-BACKSPACE", () => { + this.backToPreviousScene(); + }); + this.input.keyboard.on("keydown-LEFT", () => { + this.selectNextGridItem(true); + }); + this.input.keyboard.on("keydown-RIGHT", () => { + this.selectNextGridItem(); + }); + this.input.keyboard.on("keydown-UP", () => { + this.selectNextCategory(true); + }); + this.input.keyboard.on("keydown-DOWN", () => { + this.selectNextCategory(); + }); + this.input.keyboard.on("keydown-W", () => { + this.selectNextCategory(true); + }); + this.input.keyboard.on("keydown-S", () => { + this.selectNextCategory(); + }); + this.input.keyboard.on("keydown-A", () => { + this.selectNextGridItem(true); + }); + this.input.keyboard.on("keydown-D", () => { + this.selectNextGridItem(); + }); + } + + private setNewBodyPart(bodyPartIndex: number) { + this.changeOutfitPart(bodyPartIndex); + this.refreshPlayerCurrentOutfit(); + } + + private selectGridItem(): WokaBodyPartSlot | undefined { + const bodyPartType = this.selectedBodyPartType; + if (!bodyPartType) { + return; + } + const items = this.bodyPartsDraggableGrid.getAllItems() as WokaBodyPartSlot[]; + const item = items.find( + (item) => item.getContentData()[bodyPartType] === this.getBodyPartSelectedItemId(bodyPartType) + ); + item?.select(); + return item; + } + + private getBodyPartSelectedItemId(bodyPartType: CustomWokaBodyPart): string { + const categoryIndex = CustomWokaBodyPartOrder[bodyPartType]; + return this.layers[categoryIndex][this.selectedLayers[categoryIndex]].id; + } + + private selectNextGridItem(previous: boolean = false): void { + if (!this.selectedBodyPartType) { + return; + } + const currentIndex = this.getCurrentlySelectedItemIndex(); + if (previous ? currentIndex > 0 : currentIndex < this.bodyPartsDraggableGrid.getAllItems().length - 1) { + this.deselectAllSlots(); + const item = this.bodyPartsDraggableGrid.getAllItems()[ + currentIndex + (previous ? -1 : 1) + ] as WokaBodyPartSlot; + if (item) { + item.select(); + this.setNewBodyPart(Number(item.getId())); + this.centerGridOnItem(item); + } + } + } + + private selectNextCategory(previous: boolean = false): void { + if (!this.selectedBodyPartType) { + this.selectBodyPartType(CustomWokaBodyPart.Body); + return; + } + if (previous && this.selectedBodyPartType === CustomWokaBodyPart.Body) { + return; + } + if (!previous && this.selectedBodyPartType === CustomWokaBodyPart.Accessory) { + return; + } + const index = CustomWokaBodyPartOrder[this.selectedBodyPartType] + (previous ? -1 : 1); + this.selectBodyPartType(CustomWokaBodyPart[CustomWokaBodyPartOrder[index] as CustomWokaBodyPart]); + } + + private getCurrentlySelectedItemIndex(): number { + const bodyPartType = this.selectedBodyPartType; + if (!bodyPartType) { + return -1; + } + const items = this.bodyPartsDraggableGrid.getAllItems() as WokaBodyPartSlot[]; + return items.findIndex( + (item) => item.getContentData()[bodyPartType] === this.getBodyPartSelectedItemId(bodyPartType) + ); + } + + private centerGridOnItem(item: WokaBodyPartSlot, duration: number = 500): void { + void this.bodyPartsDraggableGrid.centerOnItem( + this.bodyPartsDraggableGrid.getAllItems().indexOf(item), + duration + ); + } + + private randomizeOutfit(): void { + for (let i = 0; i < 6; i += 1) { + this.selectedLayers[i] = Math.floor(Math.random() * this.layers[i].length); + } + } + + private changeOutfitPart(index: number): void { + if (this.selectedBodyPartType === undefined) { + return; + } + this.selectedLayers[CustomWokaBodyPartOrder[this.selectedBodyPartType]] = index; + } + + private populateGrid(): void { + if (this.selectedBodyPartType === undefined) { + return; + } + + const bodyPartsLayer = this.layers[CustomWokaBodyPartOrder[this.selectedBodyPartType]]; + + this.clearGrid(); + for (let i = 0; i < bodyPartsLayer.length; i += 1) { + const slot = new WokaBodyPartSlot( + this, + 0, + 0, + { + ...this.getWokaBodyPartSlotConfig(this.selectedBodyPartType, bodyPartsLayer[i].id), + offsetX: 0, + offsetY: 0, + }, + i + ).setDisplaySize(this.SLOT_DIMENSION, this.SLOT_DIMENSION); + this.bodyPartsDraggableGrid.addItem(slot); + } + } + + private clearGrid(): void { + this.bodyPartsDraggableGrid.clearAllItems(); + } + + private deselectAllButtons(): void { + for (const bodyPart in CustomWokaBodyPart) { + this.bodyPartsButtons[bodyPart as CustomWokaBodyPart].select(false); + } + } + + private deselectAllSlots(): void { + this.bodyPartsDraggableGrid.getAllItems().forEach((slot) => (slot as WokaBodyPartSlot).select(false)); } } diff --git a/front/src/Phaser/Login/EntryScene.ts b/front/src/Phaser/Login/EntryScene.ts index d86e3a2e..75846bf7 100644 --- a/front/src/Phaser/Login/EntryScene.ts +++ b/front/src/Phaser/Login/EntryScene.ts @@ -1,6 +1,6 @@ import { gameManager } from "../Game/GameManager"; import { Scene } from "phaser"; -import { ErrorScene, ErrorSceneName } from "../Reconnecting/ErrorScene"; +import { ErrorScene } from "../Reconnecting/ErrorScene"; import { WAError } from "../Reconnecting/WAError"; import { waScaleManager } from "../Services/WaScaleManager"; import { ReconnectingTextures } from "../Reconnecting/ReconnectingScene"; diff --git a/front/src/Phaser/Login/SelectCharacterScene.ts b/front/src/Phaser/Login/SelectCharacterScene.ts index a6f9bcee..13bc1e75 100644 --- a/front/src/Phaser/Login/SelectCharacterScene.ts +++ b/front/src/Phaser/Login/SelectCharacterScene.ts @@ -145,7 +145,6 @@ export class SelectCharacterScene extends AbstractCharacterScene { 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.id)) { continue; diff --git a/front/src/Phaser/Player/Animation.ts b/front/src/Phaser/Player/Animation.ts index cf13e087..868f9243 100644 --- a/front/src/Phaser/Player/Animation.ts +++ b/front/src/Phaser/Player/Animation.ts @@ -8,3 +8,72 @@ export enum PlayerAnimationTypes { Walk = "walk", Idle = "idle", } + +export interface AnimationData { + key: string; + frameRate: number; + repeat: number; + frameModel: string; //todo use an enum + frames: number[]; +} + +export function 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, + }, + ]; +} diff --git a/front/src/Phaser/Services/WaScaleManager.ts b/front/src/Phaser/Services/WaScaleManager.ts index 7958e79d..27e8f2ba 100644 --- a/front/src/Phaser/Services/WaScaleManager.ts +++ b/front/src/Phaser/Services/WaScaleManager.ts @@ -128,6 +128,10 @@ export class WaScaleManager { this.applyNewSize(); } + public getActualZoom(): number { + return this.actualZoom; + } + /** * This is used to scale back the ui components to counter-act the zoom. */ diff --git a/front/src/Stores/CustomCharacterStore.ts b/front/src/Stores/CustomCharacterStore.ts index 2c7d75e8..0ade6ea8 100644 --- a/front/src/Stores/CustomCharacterStore.ts +++ b/front/src/Stores/CustomCharacterStore.ts @@ -1,5 +1,3 @@ import { derived, writable, Writable } from "svelte/store"; -export const customCharacterSceneVisibleStore = writable(false); - export const activeRowStore = writable(0); diff --git a/front/src/WebRtc/VideoPeer.ts b/front/src/WebRtc/VideoPeer.ts index 50c3e19f..97f855b7 100644 --- a/front/src/WebRtc/VideoPeer.ts +++ b/front/src/WebRtc/VideoPeer.ts @@ -10,8 +10,6 @@ import { chatMessagesStore, newChatMessageSubject } from "../Stores/ChatStore"; import { getIceServersConfig } from "../Components/Video/utils"; import { isMediaBreakpointUp } from "../Utils/BreakpointsUtils"; import { SoundMeter } from "../Phaser/Components/SoundMeter"; -import { AudioContext } from "standardized-audio-context"; -import { Console } from "console"; import Peer from "simple-peer/simplepeer.min.js"; import { Buffer } from "buffer"; diff --git a/front/yarn.lock b/front/yarn.lock index da49197a..0c19e0fc 100644 --- a/front/yarn.lock +++ b/front/yarn.lock @@ -77,6 +77,13 @@ resolved "https://registry.yarnpkg.com/@geprog/vite-plugin-env-config/-/vite-plugin-env-config-4.0.3.tgz#ca04bd9ad9f55fe568917db79266afe8e766e25e" integrity sha512-2HDCV+6XXJjSuBAhDWLRr111buMQ3bIZrKo3dymIhEJ4oJCC/3yDqg7HDQIn8Y8KKbsM0AtuHMZW4yz2tPBsYg== +"@home-based-studio/phaser3-utils@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@home-based-studio/phaser3-utils/-/phaser3-utils-0.4.2.tgz#b2c1815a6b51321ea8dab027b5badcf714d99fd6" + integrity sha512-S0VkAq3z0Kf0vEUUyCDes911icvc+nkUq7lGp23zD/5lk7LTGM51NswSAfel7Rm/DLY8IBxvDTBJADTf/De82w== + dependencies: + phaser "3.55.1" + "@humanwhocodes/config-array@^0.9.2": version "0.9.2" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.2.tgz#68be55c737023009dfc5fe245d51181bb6476914" @@ -2161,10 +2168,10 @@ phaser3-rex-plugins@^1.1.42: papaparse "^5.3.0" webfontloader "^1.6.28" -phaser@^3.54.0: - version "3.54.0" - resolved "https://registry.yarnpkg.com/phaser/-/phaser-3.54.0.tgz#46b191e46059aab2a9a57f78525c60b595767eee" - integrity sha512-/1XVI6J2siS0OGwJez7vLbRjars1zb//EvJdYMVyd3wNTUf5DHrvYUj1f6TsEISr4vjnbrNtS66QIuPbGU8x6A== +phaser@3.55.1: + version "3.55.1" + resolved "https://registry.yarnpkg.com/phaser/-/phaser-3.55.1.tgz#25923fe845f6598aec57cfb37a5641834e9943a7" + integrity sha512-A5J9/diRz05qc498UNJAaXp85JVkBAEMqxP8pmcRMu1RCLBs4Kx7axd7YxNbXnQuK58JBhTRucngLt8LSpsUlQ== dependencies: eventemitter3 "^4.0.7" path "^0.12.7" diff --git a/pusher/src/Enum/PlayerTextures.ts b/pusher/src/Enum/PlayerTextures.ts new file mode 100644 index 00000000..8c7407f9 --- /dev/null +++ b/pusher/src/Enum/PlayerTextures.ts @@ -0,0 +1,48 @@ +import * as tg from "generic-type-guard"; +import { z } from "zod"; + +//The list of all the player textures, both the default models and the partial textures used for customization + +const wokaTexture = z.object({ + id: z.string(), + name: z.string(), + url: z.string(), + tags: z.array(z.string()).optional(), + tintable: z.boolean().optional(), +}); + +export type WokaTexture = z.infer; + +const wokaTextureCollection = z.object({ + name: z.string(), + textures: z.array(wokaTexture), +}); + +export type WokaTextureCollection = z.infer; + +const wokaPartType = z.object({ + collections: z.array(wokaTextureCollection), + required: z.boolean().optional(), +}); + +export type WokaPartType = z.infer; + +export const wokaList = z.record(wokaPartType); + +export type WokaList = z.infer; + +export const wokaPartNames = ["woka", "body", "eyes", "hair", "clothes", "hat", "accessory"]; + +export const isWokaDetail = new tg.IsInterface() + .withProperties({ + id: tg.isString, + }) + .withOptionalProperties({ + url: tg.isString, + layer: tg.isString, + }) + .get(); + +export type WokaDetail = tg.GuardedType; + +export type WokaDetailsResult = WokaDetail[];