diff --git a/client/package.json b/client/package.json index 01ba945c..a72bfc2d 100644 --- a/client/package.json +++ b/client/package.json @@ -22,6 +22,7 @@ "phaser": "^3.55.2", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-joystick-component": "^6.0.0", "react-redux": "^7.2.5", "sass": "^1.42.1", "styled-components": "^5.3.5", diff --git a/client/src/App.tsx b/client/src/App.tsx index e35a5e7c..38316812 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -10,6 +10,7 @@ import WhiteboardDialog from './components/WhiteboardDialog' import VideoConnectionDialog from './components/VideoConnectionDialog' import Chat from './components/Chat' import HelperButtonGroup from './components/HelperButtonGroup' +import MobileVirtualJoystick from './components/MobileVirtualJoystick' const Backdrop = styled.div` position: absolute; @@ -39,6 +40,7 @@ function App() { {/* Render VideoConnectionDialog if user is not connected to a webcam. */} {!videoConnected && } + ) } diff --git a/client/src/characters/MyPlayer.ts b/client/src/characters/MyPlayer.ts index 16236a37..b11ab658 100644 --- a/client/src/characters/MyPlayer.ts +++ b/client/src/characters/MyPlayer.ts @@ -13,10 +13,12 @@ import store from '../stores' import { pushPlayerJoinedMessage } from '../stores/ChatStore' import { ItemType } from '../../../types/Items' import { NavKeys } from '../../../types/KeyboardState' +import { JoystickMovement } from '../components/MobileVirtualJoystick' export default class MyPlayer extends Player { private playContainerBody: Phaser.Physics.Arcade.Body private chairOnSit?: Chair + public joystickMovement?: JoystickMovement constructor( scene: Phaser.Scene, x: number, @@ -41,6 +43,10 @@ export default class MyPlayer extends Player { phaserEvents.emit(Event.MY_PLAYER_TEXTURE_CHANGE, this.x, this.y, this.anims.currentAnim.key) } + handleJoystickMovement(movement: JoystickMovement) { + this.joystickMovement = movement + } + update( playerSelector: PlayerSelector, cursors: NavKeys, @@ -121,13 +127,26 @@ export default class MyPlayer extends Player { const speed = 200 let vx = 0 let vy = 0 - if (cursors.left?.isDown || cursors.A?.isDown) vx -= speed - if (cursors.right?.isDown || cursors.D?.isDown) vx += speed - if (cursors.up?.isDown || cursors.W?.isDown) { + + let joystickLeft = false + let joystickRight = false + let joystickUp = false + let joystickDown = false + + if (this.joystickMovement?.isMoving) { + joystickLeft = this.joystickMovement.direction.left + joystickRight = this.joystickMovement.direction.right + joystickUp = this.joystickMovement.direction.up + joystickDown = this.joystickMovement.direction.down + } + + if (cursors.left?.isDown || cursors.A?.isDown || joystickLeft) vx -= speed + if (cursors.right?.isDown || cursors.D?.isDown || joystickRight) vx += speed + if (cursors.up?.isDown || cursors.W?.isDown || joystickUp) { vy -= speed this.setDepth(this.y) //change player.depth if player.y changes } - if (cursors.down?.isDown || cursors.S?.isDown) { + if (cursors.down?.isDown || cursors.S?.isDown || joystickDown) { vy += speed this.setDepth(this.y) //change player.depth if player.y changes } diff --git a/client/src/characters/PlayerSelector.ts b/client/src/characters/PlayerSelector.ts index 9a0b7392..9efc7d01 100644 --- a/client/src/characters/PlayerSelector.ts +++ b/client/src/characters/PlayerSelector.ts @@ -24,13 +24,23 @@ export default class PlayerSelector extends Phaser.GameObjects.Zone { // update player selection box position so that it's always in front of the player const { x, y } = player - if (cursors.left?.isDown || cursors.A?.isDown) { + let joystickLeft = false + let joystickRight = false + let joystickUp = false + let joystickDown = false + if (player.joystickMovement?.isMoving) { + joystickLeft = player.joystickMovement?.direction.left + joystickRight = player.joystickMovement?.direction.right + joystickUp = player.joystickMovement?.direction.up + joystickDown = player.joystickMovement?.direction.down + } + if (cursors.left?.isDown || cursors.A?.isDown || joystickLeft) { this.setPosition(x - 32, y) - } else if (cursors.right?.isDown || cursors.D?.isDown) { + } else if (cursors.right?.isDown || cursors.D?.isDown || joystickRight) { this.setPosition(x + 32, y) - } else if (cursors.up?.isDown || cursors.W?.isDown) { + } else if (cursors.up?.isDown || cursors.W?.isDown || joystickUp) { this.setPosition(x, y - 32) - } else if (cursors.down?.isDown || cursors.S?.isDown) { + } else if (cursors.down?.isDown || cursors.S?.isDown || joystickDown) { this.setPosition(x, y + 32) } diff --git a/client/src/components/Chat.tsx b/client/src/components/Chat.tsx index 3f166c2f..b1a03185 100644 --- a/client/src/components/Chat.tsx +++ b/client/src/components/Chat.tsx @@ -20,12 +20,12 @@ import { MessageType, setFocused, setShowChat } from '../stores/ChatStore' const Backdrop = styled.div` position: fixed; - bottom: 0; + bottom: 60px; left: 0; height: 400px; width: 500px; max-height: 50%; - max-width: 50%; + max-width: 100%; ` const Wrapper = styled.div` diff --git a/client/src/components/ComputerDialog.tsx b/client/src/components/ComputerDialog.tsx index 617693a2..3467f081 100644 --- a/client/src/components/ComputerDialog.tsx +++ b/client/src/components/ComputerDialog.tsx @@ -32,8 +32,8 @@ const Wrapper = styled.div` .close { position: absolute; - top: 16px; - right: 16px; + top: 0px; + right: 0px; } ` diff --git a/client/src/components/HelperButtonGroup.tsx b/client/src/components/HelperButtonGroup.tsx index 74857b34..21b73976 100644 --- a/client/src/components/HelperButtonGroup.tsx +++ b/client/src/components/HelperButtonGroup.tsx @@ -13,9 +13,11 @@ import LightbulbIcon from '@mui/icons-material/Lightbulb' import ArrowRightIcon from '@mui/icons-material/ArrowRight' import GitHubIcon from '@mui/icons-material/GitHub' import TwitterIcon from '@mui/icons-material/Twitter' +import VideogameAssetIcon from '@mui/icons-material/VideogameAsset' +import VideogameAssetOffIcon from '@mui/icons-material/VideogameAssetOff' import { BackgroundMode } from '../../../types/BackgroundMode' -import { toggleBackgroundMode } from '../stores/UserStore' +import { setShowJoystick, toggleBackgroundMode } from '../stores/UserStore' import { useAppSelector, useAppDispatch } from '../hooks' import { getAvatarString, getColorByString } from '../util' @@ -106,6 +108,7 @@ const StyledFab = styled(Fab)<{ target?: string }>` export default function HelperButtonGroup() { const [showControlGuide, setShowControlGuide] = useState(false) const [showRoomInfo, setShowRoomInfo] = useState(false) + const showJoystick = useAppSelector((state) => state.user.showJoystick) const backgroundMode = useAppSelector((state) => state.user.backgroundMode) const roomJoined = useAppSelector((state) => state.room.roomJoined) const roomId = useAppSelector((state) => state.room.roomId) @@ -116,6 +119,13 @@ export default function HelperButtonGroup() { return (
+ {roomJoined && ( + + dispatch(setShowJoystick(!showJoystick))}> + {showJoystick ? : } + + + )} {showRoomInfo && ( setShowRoomInfo(false)} size="small"> diff --git a/client/src/components/Joystick.jsx b/client/src/components/Joystick.jsx new file mode 100644 index 00000000..be737f38 --- /dev/null +++ b/client/src/components/Joystick.jsx @@ -0,0 +1,79 @@ +import { Joystick } from 'react-joystick-component' +import React from 'react' + +var AngleToDirections = function (angle, out) { + if (out === undefined) { + out = {} + } else if (out === true) { + out = globOut + } + + out.left = false + out.right = false + out.up = false + out.down = false + + angle = (angle + 360) % 360 + + if (angle > 22.5 && angle <= 67.5) { + out.down = true + out.right = true + } else if (angle > 67.5 && angle <= 112.5) { + out.down = true + } else if (angle > 112.5 && angle <= 157.5) { + out.down = true + out.left = true + } else if (angle > 157.5 && angle <= 202.5) { + out.left = true + } else if (angle > 202.5 && angle <= 247.5) { + out.left = true + out.up = true + } else if (angle > 247.5 && angle <= 292.5) { + out.up = true + } else if (angle > 292.5 && angle <= 337.5) { + out.up = true + out.right = true + } else { + out.right = true + } + return out +} + +var globOut = {} + +function JoystickItem(props) { + return ( + <> + { + props.onDirectionChange({ + isMoving: false, + direction: { + left: false, + right: false, + up: false, + down: false, + }, + }) + }} + move={(event) => { + const x1 = 0 + const y1 = event.y + const x2 = event.x + const y2 = 0 + var deltaX = x2 - x1 // distance between joystick and center + var deltaY = y2 - y1 // distance between joystick and center + var rad = Math.atan2(deltaY, deltaX) // In radians + var deg = (rad * 180) / Math.PI // In degrees + var direction = AngleToDirections(deg, true) // Convert degrees to direction + props.onDirectionChange({ isMoving: true, direction: direction }) + }} + /> + + ) +} + +export default JoystickItem diff --git a/client/src/components/MobileVirtualJoystick.tsx b/client/src/components/MobileVirtualJoystick.tsx new file mode 100644 index 00000000..89286fef --- /dev/null +++ b/client/src/components/MobileVirtualJoystick.tsx @@ -0,0 +1,73 @@ +import React, { useRef, useState, useEffect } from 'react' +import styled from 'styled-components' +import 'emoji-mart/css/emoji-mart.css' +import JoystickItem from './Joystick' + +import phaserGame from '../PhaserGame' +import Game from '../scenes/Game' + +import { useAppDispatch, useAppSelector } from '../hooks' + +export interface JoystickMovement { + isMoving: boolean + direction: { + left: boolean + right: boolean + up: boolean + down: boolean + } +} + +const Backdrop = styled.div` + position: fixed; + bottom: 100px; + right: 32px; + max-height: 50%; + max-width: 100%; +` + +const Wrapper = styled.div` + position: relative; + height: 100%; + padding: 16px; + display: flex; + flex-direction: column; +` + +const JoystickWrapper = styled.div` + margin-top: auto; + align-self: flex-end; +` + +export default function MobileVirtualJoystick() { + const inputRef = useRef(null) + + const showJoystick = useAppSelector((state) => state.user.showJoystick) + const focused = useAppSelector((state) => state.chat.focused) + const showChat = useAppSelector((state) => state.chat.showChat) + const game = phaserGame.scene.keys.game as Game + + useEffect(() => { + if (focused) { + inputRef.current?.focus() + } + }, [focused]) + + useEffect(() => {}, [showJoystick, showChat]) + + const handleMovement = (movement: JoystickMovement) => { + game.myPlayer?.handleJoystickMovement(movement) + } + + return ( + + + {(!showChat || window.innerWidth > 650) && showJoystick && ( + + + + )} + + + ) +} diff --git a/client/src/components/WhiteboardDialog.tsx b/client/src/components/WhiteboardDialog.tsx index 317f8fc0..dc43b73c 100644 --- a/client/src/components/WhiteboardDialog.tsx +++ b/client/src/components/WhiteboardDialog.tsx @@ -14,6 +14,8 @@ const Backdrop = styled.div` height: 100vh; overflow: hidden; padding: 16px 180px 16px 16px; + width: 100%; + height: 100%; ` const Wrapper = styled.div` width: 100%; @@ -25,11 +27,12 @@ const Wrapper = styled.div` position: relative; display: flex; flex-direction: column; + min-width: max-content; .close { position: absolute; - top: 16px; - right: 16px; + top: 0px; + right: 0px; } ` @@ -37,7 +40,7 @@ const WhiteboardWrapper = styled.div` flex: 1; border-radius: 25px; overflow: hidden; - margin-right: 50px; + margin-right: 25px; iframe { width: 100%; diff --git a/client/src/stores/UserStore.ts b/client/src/stores/UserStore.ts index a51116e1..8c158579 100644 --- a/client/src/stores/UserStore.ts +++ b/client/src/stores/UserStore.ts @@ -18,6 +18,7 @@ export const userSlice = createSlice({ videoConnected: false, loggedIn: false, playerNameMap: new Map(), + showJoystick: window.innerWidth < 650, }, reducers: { toggleBackgroundMode: (state) => { @@ -43,6 +44,9 @@ export const userSlice = createSlice({ removePlayerNameMap: (state, action: PayloadAction) => { state.playerNameMap.delete(sanitizeId(action.payload)) }, + setShowJoystick: (state, action: PayloadAction) => { + state.showJoystick = action.payload + }, }, }) @@ -53,6 +57,7 @@ export const { setLoggedIn, setPlayerNameMap, removePlayerNameMap, + setShowJoystick, } = userSlice.actions export default userSlice.reducer diff --git a/client/yarn.lock b/client/yarn.lock index e59156fa..5a8d9349 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -1549,6 +1549,11 @@ react-is@^17.0.1, react-is@^17.0.2: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +react-joystick-component@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/react-joystick-component/-/react-joystick-component-6.0.0.tgz#5768762a6c52de57215a74ef54438221afedf36b" + integrity sha512-kbhDrPbasELoQcUxG+YNqsjsZDV3xo0TBnGXjv/TekoLZHZmmj/X/Y85YfvEPzWos6C3YmOjQKc5gK59o+jI2w== + react-redux@^7.2.5: version "7.2.8" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.8.tgz#a894068315e65de5b1b68899f9c6ee0923dd28de"