diff --git a/app/public/dist/client/changelog/patch-4.10.md b/app/public/dist/client/changelog/patch-4.10.md
index 92a94e1be2..58ef55c550 100644
--- a/app/public/dist/client/changelog/patch-4.10.md
+++ b/app/public/dist/client/changelog/patch-4.10.md
@@ -67,3 +67,4 @@
- New title: Stargazer - Get Solgaleo or Lunala
- Changed Scribble "Rare is Expensive": Buying XP now costs 8 instead of 4. Units are bought and sold for 1 gold less.
- New Scribble rule: Free Market
+- New keybiding for emotes: maintain Ctrl to show emote menu, Ctrl+1..9 to trigger emote 1..9
diff --git a/app/public/dist/client/locales/en/translation.json b/app/public/dist/client/locales/en/translation.json
index 1c85d8c4e3..9f9f190209 100644
--- a/app/public/dist/client/locales/en/translation.json
+++ b/app/public/dist/client/locales/en/translation.json
@@ -2558,6 +2558,7 @@
"key_desc_refresh": "Refresh shop",
"key_desc_avatar_anim": "Play avatar animation",
"key_desc_avatar_emotes": "Toggle emote menu",
+ "key_desc_avatar_show_emote": "Trigger emote",
"jukebox": "Jukebox",
"gadgets": "Gadgets",
"gadgets_unlocked": "gadgets unlocked",
diff --git a/app/public/dist/client/locales/fr/translation.json b/app/public/dist/client/locales/fr/translation.json
index 89c58ceece..86fb1037d0 100644
--- a/app/public/dist/client/locales/fr/translation.json
+++ b/app/public/dist/client/locales/fr/translation.json
@@ -2507,6 +2507,7 @@
"key_desc_refresh": "Relancer la boutique",
"key_desc_avatar_anim": "Jouer l'emote de l'avatar",
"key_desc_avatar_emotes": "Voir le menu des emotes",
+ "key_desc_avatar_show_emote": "Lancer l'emote",
"jukebox": "Jukebox",
"gadgets": "Gadgets",
"gadgets_unlocked": "gadgets débloqués",
diff --git a/app/public/src/game/components/board-manager.ts b/app/public/src/game/components/board-manager.ts
index f958b10736..bad5576153 100644
--- a/app/public/src/game/components/board-manager.ts
+++ b/app/public/src/game/components/board-manager.ts
@@ -510,7 +510,7 @@ export default class BoardManager {
return benchSize
}
- toggleAnimation(playerId: string, emote?: string) {
+ showEmote(playerId: string, emote?: string) {
const player =
this.playerAvatar.playerId === playerId
? this.playerAvatar
diff --git a/app/public/src/game/components/emote-menu.css b/app/public/src/game/components/emote-menu.css
index 8bd77d4134..3a197cac15 100644
--- a/app/public/src/game/components/emote-menu.css
+++ b/app/public/src/game/components/emote-menu.css
@@ -9,6 +9,7 @@
.emote-menu li {
display: block;
list-style: none;
+ position: relative;
}
.emote-menu li img {
@@ -20,6 +21,17 @@
box-shadow: 2px 2px #00000060;
}
+.emote-menu li .counter {
+ position: absolute;
+ bottom: 0;
+ left: 0.25em;
+ opacity: 0.5;
+ color: black;
+ font-size: 1em;
+ text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff,
+ 1px 1px 0 #fff;
+}
+
.emote-menu li img.locked {
filter: grayscale(1) contrast(0.5);
}
diff --git a/app/public/src/game/components/emote-menu.tsx b/app/public/src/game/components/emote-menu.tsx
index fb050426a0..5509f16da3 100644
--- a/app/public/src/game/components/emote-menu.tsx
+++ b/app/public/src/game/components/emote-menu.tsx
@@ -4,41 +4,21 @@ import ReactDOM from "react-dom/client"
import { useTranslation } from "react-i18next"
import { PRECOMPUTED_EMOTIONS_PER_POKEMON_INDEX } from "../../../../models/precomputed"
import { IPlayer } from "../../../../types"
-import { Emotion } from "../../../../types/enum/Emotion"
-import { throttle } from "../../../../utils/function"
+import { AvatarEmotions, Emotion } from "../../../../types/enum/Emotion"
import { logger } from "../../../../utils/logger"
import { cc } from "../../pages/utils/jsx"
import store from "../../stores"
-import { toggleAnimation } from "../../stores/NetworkStore"
-import { getAvatarString, getPortraitSrc } from "../../utils"
+import { getPortraitSrc } from "../../utils"
import "./emote-menu.css"
-const sendEmote = throttle(function (
- index: string,
- shiny: boolean,
- emotion: Emotion
-) {
- store.dispatch(toggleAnimation(getAvatarString(index, shiny, emotion)))
-},
-3000)
-
export function EmoteMenuComponent(props: {
player: IPlayer
index: string
shiny: boolean
+ sendEmote: (emotion: Emotion) => void
}) {
const { t } = useTranslation()
- const emotions: Emotion[] = [
- Emotion.HAPPY,
- Emotion.JOYOUS,
- Emotion.DETERMINED,
- Emotion.PAIN,
- Emotion.ANGRY,
- Emotion.CRYING,
- Emotion.SURPRISED,
- Emotion.STUNNED,
- Emotion.DIZZY
- ].filter((emotion) => {
+ const emotions: Emotion[] = AvatarEmotions.filter((emotion) => {
const indexEmotion = Object.values(Emotion).indexOf(emotion)
return (
PRECOMPUTED_EMOTIONS_PER_POKEMON_INDEX[props.index]?.[indexEmotion] === 1
@@ -52,7 +32,7 @@ export function EmoteMenuComponent(props: {
{t("no_emotions_available")}
) : (
- {emotions.map((emotion) => {
+ {emotions.map((emotion, i) => {
const unlocked = pConfig && pConfig.emotions.includes(emotion)
return (
-
@@ -60,10 +40,9 @@ export function EmoteMenuComponent(props: {
src={getPortraitSrc(props.index, props.shiny, emotion)}
title={emotion + (!unlocked ? " (locked)" : "")}
className={cc({ locked: !unlocked })}
- onClick={() =>
- unlocked && sendEmote(props.index, props.shiny, emotion)
- }
+ onClick={() => unlocked && props.sendEmote(emotion)}
/>
+ {i + 1}
)
})}
@@ -73,7 +52,12 @@ export function EmoteMenuComponent(props: {
export default class EmoteMenu extends GameObjects.DOMElement {
dom: HTMLDivElement
- constructor(scene: Phaser.Scene, avatarIndex: string, shiny: boolean) {
+ constructor(
+ scene: Phaser.Scene,
+ avatarIndex: string,
+ shiny: boolean,
+ sendEmote: (emotion: Emotion) => void
+ ) {
super(scene, -350, -150)
const state = store.getState()
const player = state.game.players.find(
@@ -85,7 +69,12 @@ export default class EmoteMenu extends GameObjects.DOMElement {
const root = ReactDOM.createRoot(this.dom)
if (player) {
root.render(
-
+
)
} else {
logger.error(`Cant' find player bound to EmoteMenu`)
diff --git a/app/public/src/game/components/minigame-manager.ts b/app/public/src/game/components/minigame-manager.ts
index 93e54d8d7b..ce558566cf 100644
--- a/app/public/src/game/components/minigame-manager.ts
+++ b/app/public/src/game/components/minigame-manager.ts
@@ -1,10 +1,12 @@
import { t } from "i18next"
import {
+ Emotion,
IFloatingItem,
IPokemonAvatar,
IPortal,
ISynergySymbol
} from "../../../../types"
+import { PokemonActionState } from "../../../../types/enum/Game"
import { Pkm } from "../../../../types/enum/Pokemon"
import { SpecialGameRule } from "../../../../types/enum/SpecialGameRule"
import { logger } from "../../../../utils/logger"
@@ -16,13 +18,12 @@ import {
import AnimationManager from "../animation-manager"
import GameScene from "../scenes/game-scene"
import { FloatingItem } from "./floating-item"
-import PokemonSprite from "./pokemon"
import PokemonAvatar from "./pokemon-avatar"
import PokemonSpecial from "./pokemon-special"
import { Portal, SynergySymbol } from "./portal"
export default class MinigameManager {
- pokemons: Map
+ pokemons: Map
items: Map
portals: Map
symbols: Map
@@ -39,7 +40,7 @@ export default class MinigameManager {
avatars: Map,
items: Map
) {
- this.pokemons = new Map()
+ this.pokemons = new Map()
this.items = new Map()
this.portals = new Map()
this.symbols = new Map()
@@ -357,4 +358,17 @@ export default class MinigameManager {
)
}
}
+
+ showEmote(id: string, emote: Emotion) {
+ const pokemonAvatar = this.pokemons.get(id)
+ if (pokemonAvatar) {
+ pokemonAvatar.action = PokemonActionState.EMOTE
+ this.animationManager.animatePokemon(
+ pokemonAvatar,
+ PokemonActionState.EMOTE,
+ false
+ )
+ pokemonAvatar.drawSpeechBubble(emote, false)
+ }
+ }
}
diff --git a/app/public/src/game/components/pokemon-avatar.ts b/app/public/src/game/components/pokemon-avatar.ts
index 49da2d3954..a6e42cd76b 100644
--- a/app/public/src/game/components/pokemon-avatar.ts
+++ b/app/public/src/game/components/pokemon-avatar.ts
@@ -1,15 +1,16 @@
import { GameObjects } from "phaser"
import PokemonFactory from "../../../../models/pokemon-factory"
-import { IPokemonAvatar } from "../../../../types"
+import { AvatarEmotions, Emotion, IPokemonAvatar } from "../../../../types"
import { GamePhaseState } from "../../../../types/enum/Game"
import { playSound, SOUNDS } from "../../pages/utils/audio"
import store from "../../stores"
-import { toggleAnimation } from "../../stores/NetworkStore"
-import { getAvatarSrc } from "../../utils"
+import { showEmote } from "../../stores/NetworkStore"
+import { getAvatarSrc, getAvatarString } from "../../utils"
import GameScene from "../scenes/game-scene"
import EmoteMenu from "./emote-menu"
import LifeBar from "./life-bar"
import PokemonSprite from "./pokemon"
+import { throttle } from "../../../../utils/function"
export default class PokemonAvatar extends PokemonSprite {
circleHitbox: GameObjects.Ellipse | undefined
@@ -47,6 +48,7 @@ export default class PokemonAvatar extends PokemonSprite {
this.drawLifebar()
}
this.registerKeys()
+ this.sendEmote = throttle(this.sendEmote, 1000).bind(this)
}
registerKeys() {
@@ -55,17 +57,52 @@ export default class PokemonAvatar extends PokemonSprite {
this.playAnimation()
}
})
- this.scene.input.keyboard!.on("keydown-S", () => {
- const scene = this.scene as GameScene
- if (
- this.isCurrentPlayerAvatar &&
- this.scene &&
- scene.room?.state.phase !== GamePhaseState.MINIGAME &&
- this.scene.game
- ) {
- this.toggleEmoteMenu()
+ this.scene.input.keyboard!.on("keydown-CTRL", () => {
+ if (this.isCurrentPlayerAvatar && this.scene?.game) {
+ this.showEmoteMenu()
}
})
+
+ this.scene.input.keyboard!.on("keyup-CTRL", () => {
+ this.hideEmoteMenu()
+ })
+
+ const NUM_KEYS = [
+ "ONE",
+ "TWO",
+ "THREE",
+ "FOUR",
+ "FIVE",
+ "SIX",
+ "SEVEN",
+ "EIGHT",
+ "NINE"
+ ]
+ NUM_KEYS.forEach((keycode, i) => {
+ const onKeydown = (event) => {
+ console.log("onkeydown", event, {
+ cpa: this.isCurrentPlayerAvatar,
+ sg: this.scene?.game,
+ ctrl: event.ctrlKey
+ })
+ if (this.isCurrentPlayerAvatar && this.scene?.game && event.ctrlKey) {
+ this.sendEmote(AvatarEmotions[i])
+ }
+ }
+ this.scene.input.keyboard!.on("keydown-" + keycode, onKeydown)
+ this.scene.input.keyboard!.on("keydown-NUMPAD_" + keycode, onKeydown)
+ })
+
+ // do not forget to clean up parent listeners after destroy
+ this.sprite.once("destroy", () => {
+ this.scene.input.keyboard!.off("keydown-A")
+ this.scene.input.keyboard!.off("keydown-CTRL")
+ this.scene.input.keyboard!.off("keyup-CTRL")
+ NUM_KEYS.forEach((keycode) => {
+ this.scene.input.keyboard!.off("keydown-" + keycode)
+ this.scene.input.keyboard!.off("keydown-NUMPAD_" + keycode)
+ })
+ })
}
drawCircles() {
@@ -146,19 +183,49 @@ export default class PokemonAvatar extends PokemonSprite {
this.add(this.lifebar)
}
- toggleEmoteMenu() {
+ showEmoteMenu() {
+ if (this.isCurrentPlayerAvatar && !this.emoteMenu) {
+ this.emoteMenu = new EmoteMenu(
+ this.scene,
+ this.index,
+ this.shiny,
+ this.sendEmote
+ )
+ this.add(this.emoteMenu)
+ }
+ }
+
+ hideEmoteMenu() {
if (this.emoteMenu) {
this.emoteMenu.destroy()
this.emoteMenu = null
- } else if (this.isCurrentPlayerAvatar) {
- this.emoteMenu = new EmoteMenu(this.scene, this.index, this.shiny)
- this.add(this.emoteMenu)
+ }
+ }
+
+ toggleEmoteMenu() {
+ if (this.emoteMenu) this.hideEmoteMenu()
+ else this.showEmoteMenu()
+ }
+
+ sendEmote(emotion: Emotion) {
+ const state = store.getState()
+ const player = state.game.players.find(
+ (p) => p.id === state.game.currentPlayerId
+ )
+ const pokemonCollection = player?.pokemonCollection
+ const pConfig = pokemonCollection?.[this.index]
+ const unlocked = pConfig && pConfig.emotions.includes(emotion)
+ if (unlocked) {
+ store.dispatch(
+ showEmote(getAvatarString(this.index, this.shiny, emotion))
+ )
+ this.hideEmoteMenu()
}
}
playAnimation() {
try {
- store.dispatch(toggleAnimation())
+ store.dispatch(showEmote())
} catch (err) {
console.error("could not play animation", err)
}
diff --git a/app/public/src/pages/component/keybind-info/keybind-info.css b/app/public/src/pages/component/keybind-info/keybind-info.css
index cf7818c3ab..be077dd474 100644
--- a/app/public/src/pages/component/keybind-info/keybind-info.css
+++ b/app/public/src/pages/component/keybind-info/keybind-info.css
@@ -3,27 +3,19 @@
align-items: center;
justify-content: center;
flex-direction: column;
- font-size: 1.5rem;
- width: 400px;
- padding: 1.5rem 2rem;
- color: white;
}
-.keybind-table {
- width: 100%;
+.keybind-container dl {
+ display: grid;
+ grid-template-columns: max-content auto;
+ gap: 0.5em;
}
-.keybind-table-header {
- border-bottom: 4px solid #4f5160;
+.keybind-container dt {
+ grid-column-start: 1;
+ text-align: right;
}
-.keybind-table th:first-child,
-.keybind-table td:first-child {
- text-align: center;
- border-right: 4px solid #4f5160;
-}
-
-.keybind-table th:last-child,
-.keybind-table td:last-child {
- padding-left: 12px;
+.keybind-container dd {
+ grid-column-start: 2;
}
diff --git a/app/public/src/pages/component/keybind-info/keybind-info.tsx b/app/public/src/pages/component/keybind-info/keybind-info.tsx
index 86843b234e..6d68ff1543 100644
--- a/app/public/src/pages/component/keybind-info/keybind-info.tsx
+++ b/app/public/src/pages/component/keybind-info/keybind-info.tsx
@@ -5,37 +5,40 @@ import "./keybind-info.css"
export default function KeybindInfo() {
const { t } = useTranslation()
- const ALL_KEYBINDS = {
- E: t("key_desc_sell"),
- F: t("key_desc_buy_xp"),
- D: t("key_desc_refresh"),
- A: t("key_desc_avatar_anim"),
- S: t("key_desc_avatar_emotes")
- }
-
return (
{t("key_bindings")}
-
-
-
- {t("key")} |
- {t("action")} |
-
-
-
- {Object.entries(ALL_KEYBINDS).map(([key, description]) => {
- return (
-
-
- {key}
- |
- {description} |
-
- )
- })}
-
-
+
+ -
+ E
+
+ - {t("key_desc_sell")}
+
+ -
+ F
+
+ - {t("key_desc_buy_xp")}
+
+ -
+ D
+
+ - {t("key_desc_refresh")}
+
+ -
+ A
+
+ - {t("key_desc_avatar_anim")}
+
+ -
+ Ctrl
+
+ - {t("key_desc_avatar_emotes")}
+
+ -
+ Ctrl+1..9
+
+ - {t("key_desc_avatar_show_emote")} 1..9
+
)
}
diff --git a/app/public/src/pages/game.tsx b/app/public/src/pages/game.tsx
index 0d72d79d24..5054423881 100644
--- a/app/public/src/pages/game.tsx
+++ b/app/public/src/pages/game.tsx
@@ -303,19 +303,15 @@ export default function Game() {
room.onMessage(Transfer.REQUEST_TILEMAP, (tilemap) => {
gameContainer.setTilemap(tilemap)
})
- room.onMessage(Transfer.TOGGLE_ANIMATION, (message) => {
+ room.onMessage(Transfer.SHOW_EMOTE, (message) => {
const g = getGameScene()
if (g && g.minigameManager.pokemons.size > 0) {
- // early return here to prevent toggling animation twice
- return g.minigameManager.changePokemon(
- message,
- "action",
- PokemonActionState.EMOTE
- )
+ // early return here to prevent showing animation twice
+ return g.minigameManager.showEmote(message.id, message?.emote)
}
if (g && g.board) {
- g.board.toggleAnimation(message.id, message?.emote)
+ g.board.showEmote(message.id, message?.emote)
}
})
diff --git a/app/public/src/stores/NetworkStore.ts b/app/public/src/stores/NetworkStore.ts
index 3f02c9754c..519e3bd2f3 100644
--- a/app/public/src/stores/NetworkStore.ts
+++ b/app/public/src/stores/NetworkStore.ts
@@ -227,8 +227,8 @@ export const networkSlice = createSlice({
openBooster: (state) => {
state.lobby?.send(Transfer.OPEN_BOOSTER)
},
- toggleAnimation: (state, action: PayloadAction) => {
- state.game?.send(Transfer.TOGGLE_ANIMATION, action.payload)
+ showEmote: (state, action: PayloadAction) => {
+ state.game?.send(Transfer.SHOW_EMOTE, action.payload)
},
searchById: (state, action: PayloadAction) => {
state.lobby?.send(Transfer.SEARCH_BY_ID, action.payload)
@@ -319,7 +319,7 @@ export const {
createTournamentLobbies,
participateInTournament,
giveBooster,
- toggleAnimation,
+ showEmote,
openBooster,
changeSelectedEmotion,
buyEmotion,
diff --git a/app/rooms/game-room.ts b/app/rooms/game-room.ts
index a42faa4800..1888d49c34 100644
--- a/app/rooms/game-room.ts
+++ b/app/rooms/game-room.ts
@@ -99,8 +99,8 @@ export default class GameRoom extends Room {
selectedMap: DungeonPMDO | "random"
gameMode: GameMode
minRank: EloRank | null
- tournamentId: string | null,
- bracketId: string | null,
+ tournamentId: string | null
+ bracketId: string | null
whenReady: (room: GameRoom) => void
}) {
logger.trace("create game room")
@@ -389,17 +389,14 @@ export default class GameRoom extends Room {
}
})
- this.onMessage(
- Transfer.TOGGLE_ANIMATION,
- (client: Client, message?: string) => {
- if (client.auth) {
- this.broadcast(Transfer.TOGGLE_ANIMATION, {
- id: client.auth.uid,
- emote: message
- })
- }
+ this.onMessage(Transfer.SHOW_EMOTE, (client: Client, message?: string) => {
+ if (client.auth) {
+ this.broadcast(Transfer.SHOW_EMOTE, {
+ id: client.auth.uid,
+ emote: message
+ })
}
- )
+ })
this.onMessage(Transfer.UNOWN_WANDERING, async (client, unownIndex) => {
try {
@@ -743,7 +740,7 @@ export default class GameRoom extends Room {
}
}
- if(this.state.gameMode === GameMode.TOURNAMENT){
+ if (this.state.gameMode === GameMode.TOURNAMENT) {
this.presence.publish("tournament-match-end", {
tournamentId: this.metadata?.tournamentId,
bracketId: this.metadata?.bracketId,
diff --git a/app/types/enum/Emotion.ts b/app/types/enum/Emotion.ts
index 18ddb28082..d9cae27a50 100644
--- a/app/types/enum/Emotion.ts
+++ b/app/types/enum/Emotion.ts
@@ -20,3 +20,15 @@ export enum Emotion {
SPECIAL2 = "Special2",
SPECIAL3 = "Special3"
}
+
+export const AvatarEmotions: Emotion[] = [
+ Emotion.HAPPY,
+ Emotion.JOYOUS,
+ Emotion.DETERMINED,
+ Emotion.PAIN,
+ Emotion.ANGRY,
+ Emotion.CRYING,
+ Emotion.SURPRISED,
+ Emotion.STUNNED,
+ Emotion.DIZZY
+]
diff --git a/app/types/enum/Item.ts b/app/types/enum/Item.ts
index e6db8615f0..434fcd356b 100644
--- a/app/types/enum/Item.ts
+++ b/app/types/enum/Item.ts
@@ -94,7 +94,7 @@ export enum Item {
export const AllItems: Item[] = Object.values(Item)
// should be excluded from carousels
-export const SpecialItems: Item[] = [Item.COMFEY]
+export const SpecialItems: Item[] = [Item.COMFEY, Item.METEORITE]
export const BasicItems: Item[] = [
Item.FOSSIL_STONE,
diff --git a/app/types/index.ts b/app/types/index.ts
index 6893cb1d95..6be9a8b3a2 100644
--- a/app/types/index.ts
+++ b/app/types/index.ts
@@ -108,7 +108,7 @@ export enum Transfer {
PASTEBIN_URL = "PASTEBIN_URL",
USER = "USER",
DRAG_DROP_FAILED = "DRAG_DROP_FAILED",
- TOGGLE_ANIMATION = "TOGGLE_ANIMATION",
+ SHOW_EMOTE = "SHOW_EMOTE",
BROADCAST_INFO = "BROADCAST_INFO",
SEARCH_BY_ID = "SEARCH_BY_ID",
SUGGESTIONS = "SUGGESTIONS",