diff --git a/app/src/components/game/editors/toolboxIcons.js b/app/src/components/game/editors/toolboxIcons.js
index 0180236..d5aaf5d 100644
--- a/app/src/components/game/editors/toolboxIcons.js
+++ b/app/src/components/game/editors/toolboxIcons.js
@@ -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";
}
diff --git a/app/src/hooks/useVolume.js b/app/src/hooks/useVolume.js
new file mode 100644
index 0000000..53b5b0a
--- /dev/null
+++ b/app/src/hooks/useVolume.js
@@ -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 };
+}
\ No newline at end of file
diff --git a/app/src/shared/gameController.js b/app/src/shared/gameController.js
index 8d3b93e..acdd4a1 100644
--- a/app/src/shared/gameController.js
+++ b/app/src/shared/gameController.js
@@ -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;
diff --git a/app/src/utils/__tests__/gameEvents.test.js b/app/src/utils/__tests__/gameEvents.test.js
index b8cab60..e59ce78 100644
--- a/app/src/utils/__tests__/gameEvents.test.js
+++ b/app/src/utils/__tests__/gameEvents.test.js
@@ -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);
});
diff --git a/app/src/utils/gameEvents.js b/app/src/utils/gameEvents.js
index 18972e0..d43148b 100644
--- a/app/src/utils/gameEvents.js
+++ b/app/src/utils/gameEvents.js
@@ -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 } }));
+ }
}
/**