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