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);