feat: adiciona botão de ativar e desativar o volume no fooder dos jogos #6

Open
lrafaelz wants to merge 1 commits from feature/sound-toggle into main
6 changed files with 78 additions and 1 deletions

View File

@@ -7,6 +7,8 @@
import React from "react";
import PropTypes from "prop-types";
import { getCategoryIcon } from "./editors/toolboxIcons"
import { useVolume } from "../../hooks/useVolume";
export default function GameFooter({
gameConfig,
@@ -17,6 +19,8 @@ export default function GameFooter({
const totalPhases = gameConfig.fases.length;
const displayPhase = currentPhase ?? currentPhase;
const { isMuted, toggleVolume } = useVolume();
const ajuda = () => {
if (onHelpClick) {
onHelpClick();
@@ -25,11 +29,12 @@ export default function GameFooter({
alert("Recurso de ajuda será implementado em breve!");
};
return (
<div className="bg-gray-900">
<div className="flex items-center justify-between px-6 py-3">
{/* Lado esquerdo - Botão de Ajuda (desativado temporariamente, tour será reimplementado) */}
<div className="flex items-center invisible">
{/*<div className="flex items-center invisible">
<button
onClick={ajuda}
data-tour="help-button"
@@ -38,7 +43,19 @@ export default function GameFooter({
>
Ajuda
</button>
</div>*/}
<div className="flex items-center">
<button
onClick={toggleVolume}
data-tour="volume-button"
title={isMuted ? "Ligar som" : "Desligar som"}
aria-label={isMuted ? "Ligar som" : "Desligar som"}
className="bg-green-100 text-black font-medium py-2 px-6 lg:py-3 lg:px-9 text-sm lg:text-base rounded-full transition-all duration-200 hover:bg-green-200 hover:scale-105 hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-green-300"
>
<i className={isMuted ? getCategoryIcon("Volume Desligado") : getCategoryIcon("Volume Ligado")} />
</button>
</div>
{/* Centro - Indicador de Fase Atual/Total */}
<div className="flex items-center space-x-4">
<div className="phase-indicator">

View File

@@ -42,6 +42,10 @@ export function getCategoryIcon(name) {
return "fa fa-database";
case "Caneta":
return "fa fa-pencil-alt";
case "Volume Desligado":
return "fa-solid fa-volume-xmark";
case "Volume Ligado":
return "fa-solid fa-volume-high";
default:
return "fa fa-cube";
}

View File

@@ -0,0 +1,23 @@
// hooks/useVolume.js
import { useCallback, useEffect, useState } from "react";
import { gameEventBus } from "../utils/gameEvents";
const STORAGE_KEY = "decoda-volume-muted";
export function useVolume() {
const [isMuted, setIsMuted] = useState(() => localStorage.getItem(STORAGE_KEY) === "true");
useEffect(() => {
gameEventBus.setVolumeMuted(isMuted);
}, [isMuted]);
const toggleVolume = useCallback(() => {
setIsMuted((prev) => {
const next = !prev;
localStorage.setItem(STORAGE_KEY, String(next));
return next;
});
}, []);
return { isMuted, toggleVolume };
}

View File

@@ -49,14 +49,26 @@ export function setupGameController(
}
};
const setVolumeMutedHandler = (event) => {
if (scene.game?.sound) {
scene.game.sound.mute = event.detail.muted;
}
};
gameEventBus.addEventListener("executeCode", executeCodeHandler);
gameEventBus.addEventListener("resetGame", resetGameHandler);
gameEventBus.addEventListener("stopExecution", stopExecutionHandler);
gameEventBus.addEventListener("setVolumeMuted", setVolumeMutedHandler);
if (scene.game?.sound) {
scene.game.sound.mute = localStorage.getItem("decoda-volume-muted") === "true";
}
const cleanup = () => {
gameEventBus.removeEventListener("executeCode", executeCodeHandler);
gameEventBus.removeEventListener("resetGame", resetGameHandler);
gameEventBus.removeEventListener("stopExecution", stopExecutionHandler);
gameEventBus.removeEventListener("setVolumeMuted", setVolumeMutedHandler);
};
scene._cleanupController = cleanup;

View File

@@ -86,6 +86,15 @@ describe("gameEventBus", () => {
});
});
describe("setVolumeMuted", () => {
it("dispatches setVolumeMuted with muted flag in detail", async () => {
const p = listenOnce("setVolumeMuted");
gameEventBus.setVolumeMuted(true);
const e = await p;
expect(e.detail).toEqual({ muted: true });
});
});
it("is an EventTarget", () => {
expect(gameEventBus).toBeInstanceOf(EventTarget);
});

View File

@@ -103,6 +103,18 @@ class GameEventBus extends EventTarget {
stopExecution() {
this.dispatchEvent(new CustomEvent("stopExecution"));
}
/**
* React define mute global do áudio no Phaser.
*
* @function setVolumeMuted
* @param {boolean} muted - `true` para silenciar, `false` para restaurar
* @returns {void}
* @fires setVolumeMuted - CustomEvent com detail: { muted }
*/
setVolumeMuted(muted) {
this.dispatchEvent(new CustomEvent("setVolumeMuted", { detail: { muted } }));
}
}
/**