From 9895a286a31bfe677699878eaeb3d0a210f38145 Mon Sep 17 00:00:00 2001 From: aronmal Date: Wed, 7 Jun 2023 11:51:33 +0200 Subject: [PATCH] Working Eventbar --- leaky-ships/components/Gamefield/EventBar.tsx | 232 ++++++++++++++++-- .../components/Gamefield/Gamefield.tsx | 36 +-- leaky-ships/components/Gamefield/Item.tsx | 63 ++++- leaky-ships/components/Lobby/Player.tsx | 1 - leaky-ships/hooks/useDraw.ts | 40 ++- leaky-ships/hooks/useDrawProps.ts | 40 +++ leaky-ships/interfaces/frontend.ts | 18 +- leaky-ships/package.json | 1 + leaky-ships/pnpm-lock.yaml | 13 + leaky-ships/styles/App.scss | 36 ++- 10 files changed, 403 insertions(+), 77 deletions(-) create mode 100644 leaky-ships/hooks/useDrawProps.ts diff --git a/leaky-ships/components/Gamefield/EventBar.tsx b/leaky-ships/components/Gamefield/EventBar.tsx index 3805910..45c630a 100644 --- a/leaky-ships/components/Gamefield/EventBar.tsx +++ b/leaky-ships/components/Gamefield/EventBar.tsx @@ -1,35 +1,225 @@ -import { Items, Target } from "../../interfaces/frontend" +import { EventBarModes, Target } from "../../interfaces/frontend" import Item from "./Item" -import React, { Dispatch, SetStateAction } from "react" +import { GameSettings } from "@components/Lobby/SettingsFrame/Setting" +import { + faArrowRightFromBracket, + faBroomWide, + faComments, + faEye, + faEyeSlash, + faGlasses, + faPalette, + faReply, + faScribble, + faSparkles, + faSwords, +} from "@fortawesome/pro-solid-svg-icons" +import { useDrawProps } from "@hooks/useDrawProps" +import { useGameProps } from "@hooks/useGameProps" +import { socket } from "@lib/socket" +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useMemo, + useState, +} from "react" function EventBar({ - props: { setMode, setTarget }, + props: { setMode, setTarget, clear }, }: { props: { setMode: Dispatch> setTarget: Dispatch> + clear: () => void } }) { - const items: Items[] = [ - { icon: "burger-menu", text: "Menu" }, - { icon: "radar", text: "Radar scan", mode: 0, amount: 1 }, - { icon: "torpedo", text: "Fire torpedo", mode: 1, amount: 1 }, - { icon: "scope", text: "Fire missile", mode: 3 }, - { icon: "gear", text: "Settings" }, - ] + const [menu, setMenu] = useState("main") + const { shouldHide, color } = useDrawProps() + const { payload, setSetting, full } = useGameProps() + + const gameSetting = useCallback( + (payload: GameSettings) => { + const hash = setSetting(payload) + socket.emit("gameSetting", payload, (newHash) => { + if (newHash === hash) return + console.log("hash", hash, newHash) + socket.emit("update", full) + }) + }, + [full, setSetting] + ) + const items = useMemo( + () => ({ + main: [ + { + icon: "burger-menu", + text: "Menu", + callback: () => { + setMenu("menu") + }, + }, + { + icon: faSwords, + text: "Attack", + callback: () => { + setMenu("attack") + }, + }, + { + icon: "pen", + text: "Draw", + callback: () => { + setMenu("draw") + }, + }, + { + icon: "gear", + text: "Settings", + callback: () => { + setMenu("settings") + }, + }, + ], + menu: [ + { + icon: faReply, + text: "Return", + iconColor: "#555", + callback: () => { + setMenu("main") + }, + }, + { + icon: faArrowRightFromBracket, + text: "Leave", + iconColor: "darkred", + callback: () => { + // router.push() + }, + }, + ], + attack: [ + { + icon: faReply, + text: "Return", + iconColor: "#555", + callback: () => { + setMenu("main") + }, + }, + { + icon: "radar", + text: "Radar scan", + amount: 1, + callback: () => { + setMode(0) + setTarget((e) => ({ ...e, show: false })) + }, + }, + { + icon: "torpedo", + text: "Fire torpedo", + amount: 1, + callback: () => { + setMode(1) + setTarget((e) => ({ ...e, show: false })) + }, + }, + { + icon: "scope", + text: "Fire missile", + callback: () => { + setMode(3) + setTarget((e) => ({ ...e, show: false })) + }, + }, + ], + draw: [ + { + icon: faReply, + text: "Return", + iconColor: "#555", + callback: () => { + setMenu("main") + }, + }, + { icon: faBroomWide, text: "Clear", callback: clear }, + { icon: faPalette, text: "Color", iconColor: color }, + { + icon: shouldHide ? faEye : faEyeSlash, + text: shouldHide ? "Show" : "Hide", + callback: () => { + useDrawProps.setState({ shouldHide: !shouldHide }) + }, + }, + ], + settings: [ + { + icon: faReply, + text: "Return", + iconColor: "#555", + callback: () => { + setMenu("main") + }, + }, + { + icon: faGlasses, + text: "Spectators", + disabled: !payload?.game?.allowSpectators, + callback: () => { + gameSetting({ allowSpectators: !payload?.game?.allowSpectators }) + }, + }, + { + icon: faSparkles, + text: "Specials", + disabled: !payload?.game?.allowSpecials, + callback: () => { + gameSetting({ allowSpecials: !payload?.game?.allowSpecials }) + }, + }, + { + icon: faComments, + text: "Chat", + disabled: !payload?.game?.allowChat, + callback: () => { + gameSetting({ allowChat: !payload?.game?.allowChat }) + }, + }, + { + icon: faScribble, + text: "Mark/Draw", + disabled: !payload?.game?.allowMarkDraw, + callback: () => { + gameSetting({ allowMarkDraw: !payload?.game?.allowMarkDraw }) + }, + }, + ], + }), + [ + clear, + color, + gameSetting, + payload?.game?.allowChat, + payload?.game?.allowMarkDraw, + payload?.game?.allowSpecials, + payload?.game?.allowSpectators, + setMode, + setTarget, + shouldHide, + ] + ) + + useEffect(() => { + useDrawProps.setState({ enable: menu === "draw" }) + }, [menu]) + return (
- {items.map((e, i) => ( - { - if (e.mode !== undefined) setMode(e.mode) - setTarget((e) => ({ ...e, show: false })) - }, - }} - /> + {items[menu].map((e, i) => ( + ))}
) diff --git a/leaky-ships/components/Gamefield/Gamefield.tsx b/leaky-ships/components/Gamefield/Gamefield.tsx index acd750b..754f9aa 100644 --- a/leaky-ships/components/Gamefield/Gamefield.tsx +++ b/leaky-ships/components/Gamefield/Gamefield.tsx @@ -1,4 +1,4 @@ -import { Draw, Hit, MouseCursor, Target } from "../../interfaces/frontend" +import { Hit, MouseCursor, Target } from "../../interfaces/frontend" // import Bluetooth from "./Bluetooth" // import FogImages from "./FogImages" import Labeling from "./Labeling" @@ -8,6 +8,7 @@ import EventBar from "@components/Gamefield/EventBar" import HitElems from "@components/Gamefield/HitElems" import Targets from "@components/Gamefield/Targets" import { useDraw } from "@hooks/useDraw" +import { useDrawProps } from "@hooks/useDrawProps" import { hitReducer, initlialTarget, @@ -69,28 +70,8 @@ function Gamefield() { } }, [mode, mouseCursor, target]) - const [color, setColor] = useState("#f00") - const [disable, setDisable] = useState(false) - const { canvasRef, onMouseDown, clear } = useDraw(drawLine) - - function drawLine({ prevPoint, currentPoint, ctx }: Draw) { - const { x: currX, y: currY } = currentPoint - const lineColor = color - const lineWidth = 5 - - let startPoint = prevPoint ?? currentPoint - ctx.beginPath() - ctx.lineWidth = lineWidth - ctx.strokeStyle = lineColor - ctx.moveTo(startPoint.x, startPoint.y) - ctx.lineTo(currX, currY) - ctx.stroke() - - ctx.fillStyle = lineColor - ctx.beginPath() - ctx.arc(startPoint.x, startPoint.y, 2, 0, 2 * Math.PI) - ctx.fill() - } + const { canvasRef, onMouseDown, clear } = useDraw() + const { enable, color, shouldHide } = useDrawProps() return (
@@ -117,10 +98,13 @@ function Gamefield() { {/* Debug */} +
- - - + ) } diff --git a/leaky-ships/components/Gamefield/Item.tsx b/leaky-ships/components/Gamefield/Item.tsx index 13b7cb3..9ed5274 100644 --- a/leaky-ships/components/Gamefield/Item.tsx +++ b/leaky-ships/components/Gamefield/Item.tsx @@ -1,20 +1,53 @@ +import { ItemProps } from "../../interfaces/frontend" +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" +import { useDrawProps } from "@hooks/useDrawProps" import classNames from "classnames" -import React, { CSSProperties } from "react" +import React, { CSSProperties, useEffect, useRef, useState } from "react" +import { HexColorPicker } from "react-colorful" function Item({ - props: { icon, text, amount, callback }, + props: { icon, text, amount, iconColor, disabled, callback }, }: { - props: { - icon: string - text: string - amount?: number - callback: () => void - } + props: ItemProps }) { + const isColor = text === "Color" + const { color, setColor } = useDrawProps() + const [active, setActive] = useState(false) + const cpRef = useRef(null) + + useEffect(() => { + const inActive = (e: MouseEvent) => { + if (cpRef.current && !cpRef.current.contains(e.target as Node)) + setActive(false) + } + + // Add event listeners + if (!isColor) return + setTimeout(() => window.addEventListener("click", inActive), 200) + + // Remove event listeners + return () => { + window.removeEventListener("click", inActive) + } + }, [active, isColor]) + return ( -
+
setActive(true) : callback}> + {isColor ? ( +
+ +
+ ) : ( + <> + )}
- {`${icon}.png`} + {typeof icon === "string" ? ( + {`${icon}.png`} + ) : ( + + )}
{text}
diff --git a/leaky-ships/components/Lobby/Player.tsx b/leaky-ships/components/Lobby/Player.tsx index 35e28db..a600d2b 100644 --- a/leaky-ships/components/Lobby/Player.tsx +++ b/leaky-ships/components/Lobby/Player.tsx @@ -88,7 +88,6 @@ function Player({ isLatched={!!isReady} onClick={() => { if (!player) return - console.log(i, !isReady) setIsReady({ i, isReady: !isReady, diff --git a/leaky-ships/hooks/useDraw.ts b/leaky-ships/hooks/useDraw.ts index f07b813..d8d8ce6 100644 --- a/leaky-ships/hooks/useDraw.ts +++ b/leaky-ships/hooks/useDraw.ts @@ -1,14 +1,34 @@ import { Draw, Point } from "../interfaces/frontend" +import { useDrawProps } from "./useDrawProps" import { useEffect, useRef, useState } from "react" -export const useDraw = ( - onDraw: ({ ctx, currentPoint, prevPoint }: Draw) => void -) => { +function onDraw({ prevPoint, currentPoint, ctx, color }: Draw) { + const { x: currX, y: currY } = currentPoint + const lineColor = color + const lineWidth = 5 + + let startPoint = prevPoint ?? currentPoint + ctx.beginPath() + ctx.lineWidth = lineWidth + ctx.strokeStyle = lineColor + ctx.moveTo(startPoint.x, startPoint.y) + ctx.lineTo(currX, currY) + ctx.stroke() + + ctx.fillStyle = lineColor + ctx.beginPath() + ctx.arc(startPoint.x, startPoint.y, 2, 0, 2 * Math.PI) + ctx.fill() +} + +export const useDraw = () => { const [mouseDown, setMouseDown] = useState(false) const canvasRef = useRef(null) const prevPoint = useRef(null) + const { color } = useDrawProps() + const onMouseDown = () => setMouseDown(true) const clear = () => { @@ -22,6 +42,9 @@ export const useDraw = ( } useEffect(() => { + const canvas = canvasRef.current + if (!canvas) return + const handler = (e: MouseEvent) => { if (!mouseDown) return const currentPoint = computePointInCanvas(e) @@ -29,14 +52,11 @@ export const useDraw = ( const ctx = canvasRef.current?.getContext("2d") if (!ctx || !currentPoint) return - onDraw({ ctx, currentPoint, prevPoint: prevPoint.current }) + onDraw({ ctx, currentPoint, prevPoint: prevPoint.current, color }) prevPoint.current = currentPoint } const computePointInCanvas = (e: MouseEvent) => { - const canvas = canvasRef.current - if (!canvas) return - const rect = canvas.getBoundingClientRect() const x = e.clientX - rect.left const y = e.clientY - rect.top @@ -50,15 +70,15 @@ export const useDraw = ( } // Add event listeners - canvasRef.current?.addEventListener("mousemove", handler) + canvas.addEventListener("mousemove", handler) window.addEventListener("mouseup", mouseUpHandler) // Remove event listeners return () => { - canvasRef.current?.removeEventListener("mousemove", handler) + canvas.removeEventListener("mousemove", handler) window.removeEventListener("mouseup", mouseUpHandler) } - }, [onDraw]) + }, [color, mouseDown]) return { canvasRef, onMouseDown, clear } } diff --git a/leaky-ships/hooks/useDrawProps.ts b/leaky-ships/hooks/useDrawProps.ts new file mode 100644 index 0000000..af96f3a --- /dev/null +++ b/leaky-ships/hooks/useDrawProps.ts @@ -0,0 +1,40 @@ +import { produce } from "immer" +import { create } from "zustand" +import { devtools } from "zustand/middleware" + +const initialState: { + enable: boolean + shouldHide: boolean + color: string +} = { + enable: false, + shouldHide: false, + color: "#b32aa9", +} + +export type State = typeof initialState + +export type Action = { + setColor: (color: string) => void + reset: () => void +} + +export const useDrawProps = create()( + devtools( + (set) => ({ + ...initialState, + setColor: (color) => + set( + produce((state) => { + state.color = color + }) + ), + reset: () => { + set(initialState) + }, + }), + { + name: "gameState", + } + ) +) diff --git a/leaky-ships/interfaces/frontend.ts b/leaky-ships/interfaces/frontend.ts index 7f74958..d3f4be8 100644 --- a/leaky-ships/interfaces/frontend.ts +++ b/leaky-ships/interfaces/frontend.ts @@ -1,3 +1,5 @@ +import { IconDefinition } from "@fortawesome/pro-solid-svg-icons" + export interface Position { x: number y: number @@ -16,11 +18,20 @@ export interface Mode { pointerGrid: any[][] type: string } -export interface Items { - icon: string +export interface ItemProps { + icon: string | IconDefinition text: string - mode?: number amount?: number + iconColor?: string + disabled?: boolean + callback?: () => void +} +export interface EventBarModes { + main: ItemProps[] + menu: ItemProps[] + attack: ItemProps[] + draw: ItemProps[] + settings: ItemProps[] } export interface Field extends Position { field: string @@ -57,4 +68,5 @@ export interface Draw { ctx: CanvasRenderingContext2D currentPoint: Point prevPoint: Point | null + color: string } diff --git a/leaky-ships/package.json b/leaky-ships/package.json index a15fddb..c6df94b 100644 --- a/leaky-ships/package.json +++ b/leaky-ships/package.json @@ -31,6 +31,7 @@ "nodemailer": "^6.9.3", "prisma": "^4.15.0", "react": "18.2.0", + "react-colorful": "^5.6.1", "react-dom": "18.2.0", "react-otp-input": "^3.0.2", "react-toastify": "^9.1.3", diff --git a/leaky-ships/pnpm-lock.yaml b/leaky-ships/pnpm-lock.yaml index 094fded..c878cb3 100644 --- a/leaky-ships/pnpm-lock.yaml +++ b/leaky-ships/pnpm-lock.yaml @@ -71,6 +71,9 @@ dependencies: react: specifier: 18.2.0 version: 18.2.0 + react-colorful: + specifier: ^5.6.1 + version: 5.6.1(react-dom@18.2.0)(react@18.2.0) react-dom: specifier: 18.2.0 version: 18.2.0(react@18.2.0) @@ -2877,6 +2880,16 @@ packages: /queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + /react-colorful@5.6.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-dom@18.2.0(react@18.2.0): resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: diff --git a/leaky-ships/styles/App.scss b/leaky-ships/styles/App.scss index 463201b..5d761d3 100644 --- a/leaky-ships/styles/App.scss +++ b/leaky-ships/styles/App.scss @@ -337,7 +337,8 @@ body { canvas { grid-area: 1 / 1 / -1 / -1; border: 1px solid #000; - display: absolute; + border-radius: 0.5rem; + z-index: 1; } } @@ -352,10 +353,35 @@ body { align-items: center; gap: 0.5rem; width: 128px; + position: relative; + + .react-colorful-wrapper { + pointer-events: none; + opacity: 0; + transition: 0.2s; + position: absolute; + top: -225px; + left: 50%; + transform: translateX(-50%); + z-index: 1; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + + &.active { + pointer-events: auto; + opacity: 1; + } + } .container { width: initial; position: relative; + padding: 8px; + background-color: white; + border-radius: 1rem; + + &.disabled { + box-shadow: inset 0 0 1rem 1rem #888; + } &.amount::after { content: var(--amount); @@ -372,10 +398,12 @@ body { img { width: 64px; - padding: 8px; @include pixelart; - background-color: white; - border-radius: 1rem; + } + svg { + margin: 8px; + height: 48px; + display: inherit; } }