diff --git a/client/src/game/scripts/core/game.ts b/client/src/game/scripts/core/game.ts index 1dc219b..3607109 100644 --- a/client/src/game/scripts/core/game.ts +++ b/client/src/game/scripts/core/game.ts @@ -1,4 +1,5 @@ import "phaser"; +import Phaser from "phaser"; import MouseWheelScrollerPlugin from "phaser3-rex-plugins/plugins/mousewheelscroller-plugin.js"; import RotateToPlugin from "phaser3-rex-plugins/plugins/rotateto-plugin.js"; import SoundFadePlugin from "phaser3-rex-plugins/plugins/soundfade-plugin.js"; @@ -22,7 +23,7 @@ const DEFAULT_HEIGHT = 1080; // const WIDTH = Math.round(Math.max(width, height) * DPR); // const HEIGHT = Math.round(Math.min(width, height) * DPR); -export const gameConfig = { +export const gameConfig: Phaser.Types.Core.GameConfig = { type: Phaser.AUTO, transparent: true, scale: { @@ -80,25 +81,40 @@ export const gameConfig = { }, }; -class Game { +export class Game { config; - game; + game: Phaser.Game; constructor(config) { this.config = config; } - init = (settings = {}) => { - this.game = new Phaser.Game(this.config); - this.game.settings = settings; - }; + init = async (settings) => { + const whenIsBooted = new Promise((resolve) => { + this.game = new Phaser.Game({ + ...this.config, + callbacks: { postBoot: () => resolve(true) }, + }); + this.game["settings"] = settings; + }); + await whenIsBooted; - getScene = (): MainScene => { - return this.game?.scene?.keys?.MainScene ?? null; - }; + const whenSceneCreated = new Promise((resolve) => { + const MainScene = this.game.scene.keys.MainScene as MainScene; + MainScene.events.on("create", resolve); + }); + await whenSceneCreated; - getPlayer = (): Spaceship | null => { - return this.getScene()?.player ?? null; + return this; + }; + get scene(): MainScene | null { + return (this.game?.scene?.keys?.MainScene as MainScene) ?? null; + } + get player(): Spaceship | null { + return this.scene?.player ?? null; + } + destroy = () => { + this.game.destroy(false); }; } diff --git a/client/src/ui/App.tsx b/client/src/ui/App.tsx index a852efa..0bd9f13 100644 --- a/client/src/ui/App.tsx +++ b/client/src/ui/App.tsx @@ -2,13 +2,13 @@ import React, { useEffect } from "react"; import { Group } from "@mantine/core"; import styled from "@emotion/styled"; -import { game } from "~/game"; +import { syncSettingsToSession, useGame } from "./hooks"; import { TopLeft } from "./scenes/TopLeft"; import { TopRight } from "./scenes/TopRight"; +import { Center } from "./scenes/Center"; +import { Right } from "./scenes/Right"; import { BottomLeft } from "./scenes/BottomLeft"; import { BottomRight } from "./scenes/BottomRight"; -import { Right } from "./scenes/Right"; -import { syncSettingsToSession, useSettings } from "./hooks"; const StyledUI = styled.div` position: absolute; @@ -22,14 +22,14 @@ const StyledUI = styled.div` grid-template-columns: repeat(12, 1fr); grid-template-rows: repeat(8, 1fr); grid-template-areas: - "top-l top-l top-l . . . . . . . top-r top-r" - " . . . . . . . . . . . ." - " . . . . . . . . . . . ." - " . . . . . . . . . right right right" - " . . . . . . . . . right right right" - " . . . . . . . . . . . ." - " . . . . . . . . . . . ." - "bot-l bot-l bot-l . . . . . . . bot-r bot-r"; + "top-l top-l top-l . . . . . . . top-r top-r" + " . . . . . . . . . . . ." + " . . . . cent cent cent cent . . . ." + " . . . . cent cent cent cent . right right right" + " . . . . cent cent cent cent . right right right" + " . . . . cent cent cent cent . . . ." + " . . . . . . . . . . . ." + "bot-l bot-l bot-l . . . . . . . bot-r bot-r"; & > * { margin: 1rem; @@ -41,12 +41,31 @@ const StyledTopLeftGroup = styled(Group)` grid-area: top-l; justify-self: start; align-self: start; + + display: flex; + flex-wrap: nowrap; `; const StyledTopRightGroup = styled(Group)` grid-area: top-r; justify-self: end; align-self: start; `; +const StyledCenterGroup = styled(Group)` + grid-area: cent; + justify-self: stretch; + align-self: center; + + display: flex; + flex-direction: column; + & > { + flex-grow: 1; + } +`; +const StyledRightGroup = styled(Group)` + grid-area: right; + justify-self: end; + align-self: center; +`; const StyledBottomLeftGroup = styled(Group)` grid-area: bot-l; justify-self: start; @@ -57,29 +76,26 @@ const StyledBottomRightGroup = styled(Group)` justify-self: end; align-self: end; `; -const StyledRightGroup = styled(Group)` - grid-area: right; - justify-self: end; - align-self: center; -`; export const App = () => { - const { settings } = useSettings(); - useEffect(() => { - game.init(settings); const unsub = syncSettingsToSession(); - return unsub; }, []); + const { + mode, + computed: { isLoaded }, + } = useGame(); + return ( - - - - + {isLoaded && } + {isLoaded && } + {mode === "mainMenu" &&
} + {isLoaded && } + ); }; diff --git a/client/src/ui/hooks/index.ts b/client/src/ui/hooks/index.ts index dbaf4ac..1eaf7f6 100644 --- a/client/src/ui/hooks/index.ts +++ b/client/src/ui/hooks/index.ts @@ -1 +1,2 @@ +export * from "./useGame"; export * from "./useSettings"; diff --git a/client/src/ui/hooks/useGame.ts b/client/src/ui/hooks/useGame.ts new file mode 100644 index 0000000..7c08495 --- /dev/null +++ b/client/src/ui/hooks/useGame.ts @@ -0,0 +1,80 @@ +import { MainScene } from "~/scenes"; +import type { Spaceship } from "~/objects"; +import { produce } from "immer"; +import { create } from "zustand"; + +import { game } from "~/game/core/game"; +import type { Game } from "~/game/core/game"; + +interface GameStore { + game: Game | null; + mode: "mainMenu" | "singleplayer" | "multiplayer"; + computed: { + isLoading: boolean; + isLoaded: boolean; + player: Spaceship | null; + scene: MainScene | null; + }; + loadSingleplayer: (settings) => Promise; + loadMultiplayer: (settings) => Promise; + loadMainMenu: () => void; +} + +export const useGame = create((set, get) => ({ + game: null, + mode: "mainMenu", + + // https://github.com/pmndrs/zustand/issues/132 + computed: { + get isLoading() { + return get().mode !== "mainMenu" && !get().computed.isLoaded; + }, + get isLoaded() { + return !!get().game?.player; + }, + + get player() { + return get().game?.player; + }, + get scene() { + return get().game?.scene; + }, + }, + + loadSingleplayer: async (settings) => { + set( + produce((state) => { + state.mode = "singleplayer"; + }) + ); + const singleplayerGame = await game.init(settings); + + set( + produce((state) => { + state.game = singleplayerGame; + }) + ); + }, + loadMultiplayer: async (settings) => { + set( + produce((state) => { + state.mode = "multiplayer"; + }) + ); + const multiplayerGame = await game.init(settings); + set( + produce((state) => { + state.game = multiplayerGame; + }) + ); + }, + loadMainMenu: () => { + get().game.destroy(); + set( + produce((state) => { + state.game = null; + state.mode = "mainMenu"; + }) + ); + }, +})); diff --git a/client/src/ui/hooks/useSettings.ts b/client/src/ui/hooks/useSettings.ts index d778bb4..a1dd312 100644 --- a/client/src/ui/hooks/useSettings.ts +++ b/client/src/ui/hooks/useSettings.ts @@ -48,6 +48,7 @@ const defaultSettings = { musicVolume: 0.05, graphicsSettings: 1, enableTouchControls: isTouchDevice(), + showDeviceInfo: false, }; const key = "settings"; diff --git a/client/src/ui/scenes/BottomLeft/scenes/OutfittingDrawer/OutfittingDrawer.tsx b/client/src/ui/scenes/BottomLeft/scenes/OutfittingDrawer/OutfittingDrawer.tsx index 3d6142e..dd581c2 100644 --- a/client/src/ui/scenes/BottomLeft/scenes/OutfittingDrawer/OutfittingDrawer.tsx +++ b/client/src/ui/scenes/BottomLeft/scenes/OutfittingDrawer/OutfittingDrawer.tsx @@ -1,19 +1,22 @@ import React, { useState } from "react"; -import { Divider, Drawer, Title } from "@mantine/core"; +import { Drawer, Title } from "@mantine/core"; import { useDidUpdate, useSetState } from "@mantine/hooks"; import { DndContext, DragOverlay } from "@dnd-kit/core"; -import { game } from "~/game"; import { InventorySection } from "./scenes/InventorySection"; import { InventoryItem } from "./scenes/InventorySection/scenes/ItemSlot/components/InventoryItem"; +import { useGame } from "~/ui/hooks"; export const OutfittingDrawer = ({ shouldBeOpened, close }) => { const [didLoad, setDidLoad] = useState(false); + const { + computed: { player }, + } = useGame(); useDidUpdate(() => { if (!didLoad) { - const player = game.getPlayer(); - const activeOutfit = player?.outfitting.getOutfit(); + // TODO: to not mutate state + const activeOutfit = player.outfitting.getOutfit(); if (activeOutfit) { setDidLoad(() => true); setOutfit(activeOutfit); @@ -81,9 +84,9 @@ export const OutfittingDrawer = ({ shouldBeOpened, close }) => { }; const reoutfit = () => { - const player = game.getPlayer(); const activeOutfit = player?.outfitting.getOutfit(); - if (player && activeOutfit) { + if (activeOutfit) { + // TODO: to not mutate state player.outfitting.reoutfit(outfit); } }; diff --git a/client/src/ui/scenes/BottomRight/BottomRight.tsx b/client/src/ui/scenes/BottomRight/BottomRight.tsx index 777ad80..fc1c4a3 100644 --- a/client/src/ui/scenes/BottomRight/BottomRight.tsx +++ b/client/src/ui/scenes/BottomRight/BottomRight.tsx @@ -1,26 +1,36 @@ import React from "react"; import { useToggle } from "@mantine/hooks"; -import { User } from "tabler-icons-react"; +import { Home, User } from "tabler-icons-react"; -import { game } from "~/game"; import { Button } from "~/ui/components"; import { ProfileModal } from "./scenes/ProfileModal"; +import { useGame } from "~/ui/hooks"; export const BottomRight = ({ GroupComponent }) => { + const { + loadMainMenu, + computed: { isLoaded, player }, + } = useGame(); const [openedProfileModal, toggleProfileModal] = useToggle([false, true]); const toggleProfile = () => { - const player = game.getPlayer(); - if (player) { - toggleProfileModal(); + toggleProfileModal(); + if (isLoaded) { // todo this will enable you to shoot and move in dying animation player.active = openedProfileModal; } }; + const handleLoadMainMenu = () => { + loadMainMenu(); + }; + return ( <> + diff --git a/client/src/ui/scenes/BottomRight/scenes/ProfileModal/scenes/Me/Me.tsx b/client/src/ui/scenes/BottomRight/scenes/ProfileModal/scenes/Me/Me.tsx index 6df82dc..50d32fb 100644 --- a/client/src/ui/scenes/BottomRight/scenes/ProfileModal/scenes/Me/Me.tsx +++ b/client/src/ui/scenes/BottomRight/scenes/ProfileModal/scenes/Me/Me.tsx @@ -2,13 +2,16 @@ import React, { useState } from "react"; import { Button, Group, Stack, Text } from "@mantine/core"; import { useSessionStorage } from "@mantine/hooks"; -import { game } from "~/game"; import { NonFieldErrors } from "../../components"; import { useSaveMutation } from "./hooks"; import { useProfile } from "../../hooks"; +import { useGame } from "~/ui/hooks"; export const Me = ({ onLogout }) => { const { me, meStatus } = useProfile(); + const { + computed: { isLoaded, player }, + } = useGame(); const [accessToken, setAccessToken] = useSessionStorage({ key: "accessToken", @@ -16,16 +19,13 @@ export const Me = ({ onLogout }) => { }); const handleSave = () => { - const player = game.getPlayer(); - if (player && accessToken) { + if (isLoaded && accessToken) { const { x, y } = player; save(me.id, { x, y }, accessToken); } }; const handleLoad = () => { - const player = game.getPlayer(); - - if (player) { + if (isLoaded) { player.respawn(me.x, me.y); player.followText.setText(me.username); } diff --git a/client/src/ui/scenes/Center/Center.tsx b/client/src/ui/scenes/Center/Center.tsx new file mode 100644 index 0000000..06548f2 --- /dev/null +++ b/client/src/ui/scenes/Center/Center.tsx @@ -0,0 +1,31 @@ +import React from "react"; + +import { Button } from "~/ui/components"; +import { useGame, useSettings } from "~/ui/hooks"; + +export const Center = ({ GroupComponent }) => { + const { settings } = useSettings(); + const { mode, loadSingleplayer, loadMultiplayer } = useGame(); + + const handleSingleplayer = () => { + if (mode === "mainMenu") { + loadSingleplayer(settings); + } + }; + const handleMultiplayer = () => { + if (mode === "mainMenu") { + loadMultiplayer(settings); + } + }; + + return ( + + + + + ); +}; diff --git a/client/src/ui/scenes/Center/index.ts b/client/src/ui/scenes/Center/index.ts new file mode 100644 index 0000000..306d4b5 --- /dev/null +++ b/client/src/ui/scenes/Center/index.ts @@ -0,0 +1 @@ +export * from "./Center"; diff --git a/client/src/ui/scenes/Right/Right.tsx b/client/src/ui/scenes/Right/Right.tsx index 2b72ff7..fa469f5 100644 --- a/client/src/ui/scenes/Right/Right.tsx +++ b/client/src/ui/scenes/Right/Right.tsx @@ -3,6 +3,8 @@ import { Device } from "@capacitor/device"; import { Stack, Text } from "@mantine/core"; import styled from "@emotion/styled"; +import { useSettings } from "~/ui/hooks"; + const StyledStack = styled(Stack)` gap: 0; min-width: 12rem; @@ -11,6 +13,8 @@ const StyledStack = styled(Stack)` `; export const Right = ({ GroupComponent }) => { + const { settings } = useSettings(); + const [deviceInfo, setDeviceInfo] = useState({}); const logDeviceInfo = async () => { const info = await Device.getInfo(); @@ -25,11 +29,12 @@ export const Right = ({ GroupComponent }) => { <> - {Object.entries(deviceInfo).map(([key, value]) => ( - - {`${key}: ${value}`} - - ))} + {settings.showDeviceInfo && + Object.entries(deviceInfo).map(([key, value]) => ( + + {`${key}: ${value}`} + + ))} diff --git a/client/src/ui/scenes/TopLeft/TopLeft.tsx b/client/src/ui/scenes/TopLeft/TopLeft.tsx index 863897d..2765940 100644 --- a/client/src/ui/scenes/TopLeft/TopLeft.tsx +++ b/client/src/ui/scenes/TopLeft/TopLeft.tsx @@ -7,17 +7,18 @@ import { Button } from "~/ui/components"; import { SettingsModal } from "./scenes/SettingsModal"; import { EffectsMuteBtn } from "./scenes/EffectsMuteBtn"; import { MusicMuteBtn } from "./scenes/MusicMuteBtn"; +import { useGame } from "~/ui/hooks"; export const TopLeft = ({ GroupComponent }) => { + const { + computed: { player }, + } = useGame(); const [openedSettings, toggleSettingsModal] = useToggle([false, true]); const toggleSettings = () => { - const player = game.getPlayer(); - if (player) { - toggleSettingsModal(); - // todo this will enable you to shoot and move in dying animation - player.active = openedSettings; - } + toggleSettingsModal(); + // todo this will enable you to shoot and move in dying animation + player.active = openedSettings; }; return ( <> diff --git a/client/src/ui/scenes/TopLeft/scenes/EffectsMuteBtn/EffectsMuteBtn.tsx b/client/src/ui/scenes/TopLeft/scenes/EffectsMuteBtn/EffectsMuteBtn.tsx index a112ab6..a561b31 100644 --- a/client/src/ui/scenes/TopLeft/scenes/EffectsMuteBtn/EffectsMuteBtn.tsx +++ b/client/src/ui/scenes/TopLeft/scenes/EffectsMuteBtn/EffectsMuteBtn.tsx @@ -3,23 +3,23 @@ import { useToggle } from "@mantine/hooks"; import { Volume, VolumeOff } from "tabler-icons-react"; import { ToggleButton } from "~/ui/components"; -import { useSettings } from "~/ui/hooks"; -import { game } from "~/game"; +import { useGame, useSettings } from "~/ui/hooks"; const settingName = "effectsMute"; export const EffectsMuteBtn = () => { + const { + computed: { + scene: { soundManager }, + }, + } = useGame(); const { settings, toggleEffectsSetting } = useSettings(); const [on, toggle] = useToggle([!settings[settingName], settings[settingName]]); const handleClick = () => { - const { soundManager } = game.getScene(); - - if (soundManager) { - soundManager.toggleMute(settingName); - toggleEffectsSetting(); - toggle(); - } + soundManager.toggleMute(settingName); + toggleEffectsSetting(); + toggle(); }; return ( diff --git a/client/src/ui/scenes/TopLeft/scenes/MusicMuteBtn/MusicMuteBtn.tsx b/client/src/ui/scenes/TopLeft/scenes/MusicMuteBtn/MusicMuteBtn.tsx index 95f45d5..cb89d12 100644 --- a/client/src/ui/scenes/TopLeft/scenes/MusicMuteBtn/MusicMuteBtn.tsx +++ b/client/src/ui/scenes/TopLeft/scenes/MusicMuteBtn/MusicMuteBtn.tsx @@ -3,23 +3,23 @@ import { useToggle } from "@mantine/hooks"; import { Music, MusicOff } from "tabler-icons-react"; import { ToggleButton } from "~/ui/components"; -import { useSettings } from "~/ui/hooks"; -import { game } from "~/game"; +import { useGame, useSettings } from "~/ui/hooks"; const settingName = "musicMute"; export const MusicMuteBtn = () => { + const { + computed: { + scene: { soundManager }, + }, + } = useGame(); const { settings, toggleMusicSetting } = useSettings(); const [on, toggle] = useToggle([!settings[settingName], settings[settingName]]); const handleClick = () => { - const { soundManager } = game.getScene(); - - if (soundManager) { - soundManager.toggleMute(settingName); - toggleMusicSetting(); - toggle(); - } + soundManager.toggleMute(settingName); + toggleMusicSetting(); + toggle(); }; return } iconOff={} onClick={handleClick} />; diff --git a/client/src/ui/scenes/TopLeft/scenes/SettingsModal/SettingsModal.tsx b/client/src/ui/scenes/TopLeft/scenes/SettingsModal/SettingsModal.tsx index db56507..57cf0ec 100644 --- a/client/src/ui/scenes/TopLeft/scenes/SettingsModal/SettingsModal.tsx +++ b/client/src/ui/scenes/TopLeft/scenes/SettingsModal/SettingsModal.tsx @@ -12,12 +12,17 @@ import { import { useDisclosure } from "@mantine/hooks"; import React, { useState } from "react"; -import { game } from "~/game"; import { Button } from "~/ui/components"; import { SliderInput } from "./components"; -import { useSettings } from "~/ui/hooks"; +import { useGame, useSettings } from "~/ui/hooks"; export const SettingsModal = ({ opened, onClose }) => { + const { + computed: { + player, + scene: { soundManager, inputManager, mobManager }, + }, + } = useGame(); const { settings, setMasterVolumeSetting, @@ -26,38 +31,25 @@ export const SettingsModal = ({ opened, onClose }) => { setGraphicsSettingsSetting, } = useSettings(); + // TODELETE: Mutating state! const addEngine = () => { - const player = game.getPlayer(); - if (player) { - player.exhausts.createExhaust(); - } + player.exhausts.createExhaust(); }; const removeEngine = () => { - const player = game.getPlayer(); - if (player) { - player.exhausts.removeExhaust(); - } + player.exhausts.removeExhaust(); }; const addLaser = (slot) => { - const player = game.getPlayer(); - if (player) { - player.weapons.createLaser(slot); - } + player.weapons.createLaser(slot); }; - const addGatling = (slot) => { - const player = game.getPlayer(); - if (player) { - player.weapons.createGatling(slot); - } + player.weapons.createGatling(slot); }; const setVolume = (key, volume) => { - const { soundManager } = game.getScene(); const isValidKey = key === "masterVolume" || key === "musicVolume" || key === "effectsVolume"; - if (soundManager && isValidKey) { + if (isValidKey) { soundManager.setVolume(key, volume); if (key === "masterVolume") { setMasterVolumeSetting(volume); @@ -73,20 +65,16 @@ export const SettingsModal = ({ opened, onClose }) => { }; const toggleTouchControls = () => { - const { inputManager } = game.getScene(); - if (inputManager) { - inputManager.toggleTouchControls(); - handleTouchControls.toggle(); - } + inputManager.toggleTouchControls(); + handleTouchControls.toggle(); }; const [touchControlChecked, handleTouchControls] = useDisclosure(settings.enableTouchControls); const [activeTab, setActiveTab] = useState("audio"); - // TODELETE + // TODELETE: Mutating state! const sendMobs = (e) => { e.preventDefault(); - const { mobManager, player } = game.getScene(); - const mobs = mobManager.mobs; + const { mobs } = mobManager; mobManager.spawnMobs(mobsCount, [player]); mobs.forEach((mob) => { @@ -95,15 +83,12 @@ export const SettingsModal = ({ opened, onClose }) => { }); }; + // TODELETE: Mutating state! const teleport = () => { - const player = game.getPlayer(); - - if (player) { - player.x = x; - player.y = y; - player.shields.x = x; - player.shields.y = y; - } + player.x = x; + player.y = y; + player.shields.x = x; + player.shields.y = y; }; const [x, setx] = useState(120);