Working ship placement

This commit is contained in:
aronmal 2023-06-11 22:09:36 +02:00
parent 0317a3343c
commit c2af2dffa2
Signed by: aronmal
GPG key ID: 816B7707426FC612
14 changed files with 1155 additions and 281 deletions

View file

@ -1,9 +1,15 @@
import { EventBarModes } from "../../interfaces/frontend"
import Item from "./Item"
import { GameSettings } from "@components/Lobby/SettingsFrame/Setting"
import {
faSquare2,
faSquare3,
faSquare4,
} from "@fortawesome/pro-regular-svg-icons"
import {
faArrowRightFromBracket,
faBroomWide,
faCheck,
faComments,
faEye,
faEyeSlash,
@ -11,6 +17,7 @@ import {
faPalette,
faReply,
faScribble,
faShip,
faSparkles,
faSwords,
} from "@fortawesome/pro-solid-svg-icons"
@ -18,13 +25,13 @@ import { useDrawProps } from "@hooks/useDrawProps"
import { useGameProps } from "@hooks/useGameProps"
import { socket } from "@lib/socket"
import { GamePropsSchema } from "@lib/zodSchemas"
import { useSession } from "next-auth/react"
import {
Dispatch,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from "react"
export function setGameSetting(
@ -42,23 +49,35 @@ export function setGameSetting(
}
}
function EventBar({
props: { setMode, clear },
}: {
props: {
setMode: Dispatch<SetStateAction<number>>
clear: () => void
}
}) {
const [menu, setMenu] = useState<keyof EventBarModes>("main")
function EventBar({ clear }: { clear: () => void }) {
const { shouldHide, color } = useDrawProps()
const { payload, setSetting, full, setTarget } = useGameProps()
const { data: session } = useSession()
const {
ships,
payload,
menu,
mode,
setSetting,
full,
setTarget,
setIsReady,
} = useGameProps()
const gameSetting = useCallback(
(payload: GameSettings) => setGameSetting(payload, setSetting, full),
[full, setSetting]
)
const setMenu = useCallback(
(menu: keyof EventBarModes) => useGameProps.setState({ menu }),
[]
)
const setMode = useCallback(
(mode: number) => useGameProps.setState({ mode }),
[]
)
const self = useMemo(
() => payload?.users.find((e) => e?.id === session?.user.id),
[payload?.users, session?.user.id]
)
const items = useMemo<EventBarModes>(
() => ({
main: [
@ -69,11 +88,19 @@ function EventBar({
setMenu("menu")
},
},
{
payload?.game?.state === "running"
? {
icon: faSwords,
text: "Attack",
callback: () => {
setMenu("attack")
setMenu("actions")
},
}
: {
icon: faShip,
text: "Ships",
callback: () => {
setMenu("actions")
},
},
{
@ -92,14 +119,6 @@ function EventBar({
},
],
menu: [
{
icon: faReply,
text: "Return",
iconColor: "#555",
callback: () => {
setMenu("main")
},
},
{
icon: faArrowRightFromBracket,
text: "Leave",
@ -109,33 +128,9 @@ function EventBar({
},
},
],
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 }))
},
},
actions:
payload?.game?.state === "running"
? [
{
icon: "scope",
text: "Fire missile",
@ -144,16 +139,73 @@ function EventBar({
setTarget((e) => ({ ...e, show: false }))
},
},
{
icon: "torpedo",
text: "Fire torpedo",
amount:
2 -
(self?.moves.filter(
(e) => e.action === "htorpedo" || e.action === "vtorpedo"
).length ?? 0),
callback: () => {
setMode(1)
setTarget((e) => ({ ...e, show: false }))
},
},
{
icon: "radar",
text: "Radar scan",
amount:
1 -
(self?.moves.filter((e) => e.action === "radar").length ?? 0),
callback: () => {
setMode(0)
setTarget((e) => ({ ...e, show: false }))
},
},
]
: [
{
icon: faSquare2,
text: "Minensucher",
amount: 1 - ships.filter((e) => e.size === 2).length,
callback: () => {
if (1 - ships.filter((e) => e.size === 2).length === 0) return
setMode(0)
},
},
{
icon: faSquare3,
text: "Kreuzer",
amount: 3 - ships.filter((e) => e.size === 3).length,
callback: () => {
if (3 - ships.filter((e) => e.size === 3).length === 0) return
setMode(1)
},
},
{
icon: faSquare4,
text: "Schlachtschiff",
amount: 2 - ships.filter((e) => e.size === 4).length,
callback: () => {
if (2 - ships.filter((e) => e.size === 4).length === 0) return
setMode(2)
},
},
{
icon: faCheck,
text: "Done",
disabled: mode >= 0,
callback: () => {
const i = payload?.users.findIndex(
(user) => session?.user?.id === user?.id
)
if (!i) return
setIsReady({ isReady: true, i })
},
},
],
draw: [
{
icon: faReply,
text: "Return",
iconColor: "#555",
callback: () => {
setMenu("main")
},
},
{ icon: faBroomWide, text: "Clear", callback: clear },
{ icon: faPalette, text: "Color", iconColor: color },
{
@ -165,14 +217,6 @@ function EventBar({
},
],
settings: [
{
icon: faReply,
text: "Return",
iconColor: "#555",
callback: () => {
setMenu("main")
},
},
{
icon: faGlasses,
text: "Spectators",
@ -209,22 +253,54 @@ function EventBar({
clear,
color,
gameSetting,
mode,
payload?.game?.allowChat,
payload?.game?.allowMarkDraw,
payload?.game?.allowSpecials,
payload?.game?.allowSpectators,
payload?.game?.state,
payload?.users,
self?.moves,
session?.user?.id,
setIsReady,
setMenu,
setMode,
setTarget,
ships,
shouldHide,
]
)
useEffect(() => {
if (
menu !== "actions" ||
payload?.game?.state !== "starting" ||
mode < 0 ||
items.actions[mode].amount
)
return
const index = items.actions.findIndex((e) => e.amount)
useGameProps.setState({ mode: index })
}, [items.actions, menu, mode, payload?.game?.state])
useEffect(() => {
useDrawProps.setState({ enable: menu === "draw" })
}, [menu])
return (
<div className="event-bar">
{menu !== "main" && (
<Item
props={{
icon: faReply,
text: "Return",
iconColor: "#555",
callback: () => {
setMenu("main")
},
}}
></Item>
)}
{items[menu].map((e, i) => (
<Item key={i} props={e} />
))}

View file

@ -10,11 +10,14 @@ import Targets from "@components/Gamefield/Targets"
import { useDraw } from "@hooks/useDraw"
import { useDrawProps } from "@hooks/useDrawProps"
import { useGameProps } from "@hooks/useGameProps"
import useSocket from "@hooks/useSocket"
import { socket } from "@lib/socket"
import {
initlialMouseCursor,
overlapsWithAnyBorder,
isAlreadyHit,
targetList,
shipProps,
} from "@lib/utils/helpers"
import { CSSProperties, useCallback } from "react"
import { useEffect, useState } from "react"
@ -25,17 +28,28 @@ function Gamefield() {
const [mouseCursor, setMouseCursor] =
useState<MouseCursor>(initlialMouseCursor)
const {
mode,
hits,
target,
targetPreview,
DispatchHits,
ships,
addShip,
payload,
DispatchAction,
setTarget,
setTargetPreview,
full,
} = useGameProps()
const [mode, setMode] = useState(0)
const { isConnected } = useSocket()
useEffect(() => {
if (payload?.game?.id || !isConnected) return
socket.emit("update", full)
}, [full, payload?.game?.id, isConnected])
const settingTarget = useCallback(
(isGameTile: boolean, x: number, y: number) => {
if (payload?.game?.state === "running") {
const list = targetList(targetPreview, mode)
if (
!isGameTile ||
@ -43,22 +57,32 @@ function Gamefield() {
)
return
if (target.show && target.x == x && target.y == y) {
DispatchHits({
type: "fireMissile",
payload: list.map(({ x, y }) => ({
hit: isAlreadyHit(x, y, hits),
x,
y,
})),
DispatchAction({
action: "missile",
...target,
})
setTarget((t) => ({ ...t, show: false }))
} else if (!overlapsWithAnyBorder(targetPreview, mode))
setTarget({ show: true, x, y })
} else if (payload?.game?.state === "starting") {
addShip(shipProps(ships, mode, targetPreview))
}
},
[DispatchHits, hits, mode, setTarget, target, targetPreview]
[
DispatchAction,
addShip,
hits,
mode,
payload?.game?.state,
setTarget,
ships,
target,
targetPreview,
]
)
useEffect(() => {
if (mode < 0) return
const { x, y, show } = target
const { shouldShow, ...position } = mouseCursor
if (!shouldShow || overlapsWithAnyBorder(position, mode))
@ -97,8 +121,7 @@ function Gamefield() {
{/* Fog images */}
{/* <FogImages /> */}
<Targets props={{ target, targetPreview, mode, hits }} />
{/* <span id='dev-debug' style={{gridArea: '1 / 12 / 1 / 15', backgroundColor: 'red', zIndex: 3} as CSSProperties}>Debug</span> */}
<Targets />
<canvas
style={
@ -114,7 +137,7 @@ function Gamefield() {
height="648"
/>
</div>
<EventBar props={{ setMode, clear }} />
<EventBar clear={clear} />
</div>
)
}

View file

@ -45,11 +45,12 @@ function Item({
)}
<div
className={classNames("container", {
amount: amount,
disabled: disabled,
amount: typeof amount !== "undefined",
disabled: disabled || amount === 0,
enabled: disabled === false,
})}
style={
amount
typeof amount !== "undefined"
? ({
"--amount": JSON.stringify(amount.toString()),
} as CSSProperties)

View file

@ -0,0 +1,34 @@
import { ShipProps } from "../../interfaces/frontend"
import { useGameProps } from "@hooks/useGameProps"
import classNames from "classnames"
import React, { CSSProperties } from "react"
function Ship({
props: { size, variant, x, y },
preview,
}: {
props: ShipProps
preview?: boolean
}) {
const { payload, removeShip } = useGameProps()
const filename = `ship_blue_${size}x_${variant}.gif`
return (
<div
className={classNames("ship", "s" + size, {
preview: preview,
interactive: payload?.game?.state === "starting",
})}
style={{ "--x": x, "--y": y } as CSSProperties}
onClick={() => {
if (payload?.game?.state !== "starting") return
removeShip({ size, variant, x, y })
useGameProps.setState({ mode: size - 2 })
}}
>
<img src={"/assets/" + filename} alt={filename} />
</div>
)
}
export default Ship

View file

@ -1,31 +1,23 @@
import { Ship } from "../../interfaces/frontend"
import classNames from "classnames"
import { CSSProperties } from "react"
import Ship from "./Ship"
import { useGameProps } from "@hooks/useGameProps"
function Ships() {
const shipIndexes: Ship[] = [
{ size: 2, variant: 1, x: 3, y: 3 },
{ size: 3, variant: 1, x: 4, y: 3 },
{ size: 3, variant: 2, x: 5, y: 3 },
{ size: 3, variant: 3, x: 6, y: 3 },
{ size: 4, variant: 1, x: 7, y: 3 },
{ size: 4, variant: 2, x: 8, y: 3 },
]
// const shipIndexes: Ship[] = [
// { size: 2, variant: 1, x: 3, y: 3 },
// { size: 3, variant: 1, x: 4, y: 3 },
// { size: 3, variant: 2, x: 5, y: 3 },
// { size: 3, variant: 3, x: 6, y: 3 },
// { size: 4, variant: 1, x: 7, y: 3 },
// { size: 4, variant: 2, x: 8, y: 3 },
// ]
const { ships } = useGameProps()
return (
<>
{shipIndexes.map(({ size, variant, x, y }, i) => {
const filename = `ship_blue_${size}x_${variant}.gif`
return (
<div
key={i}
className={classNames("ship", "s" + size)}
style={{ "--x": x, "--y": y } as CSSProperties}
>
<img src={"/assets/" + filename} alt={filename} />
</div>
)
})}
{ships.map((props, i) => (
<Ship key={i} props={props} />
))}
</>
)
}

View file

@ -1,18 +1,12 @@
import { Hit, Target } from "../../interfaces/frontend"
import GamefieldPointer from "./GamefieldPointer"
import { composeTargetTiles } from "@lib/utils/helpers"
import React from "react"
import Ship from "./Ship"
import { useGameProps } from "@hooks/useGameProps"
import { composeTargetTiles, shipProps } from "@lib/utils/helpers"
function Targets({
props: { target, targetPreview, mode, hits },
}: {
props: {
target: Target
targetPreview: Target
mode: number
hits: Hit[]
}
}) {
function Targets() {
const { payload, target, targetPreview, mode, hits, ships } = useGameProps()
if (payload?.game?.state === "running")
return (
<>
{[
@ -25,6 +19,9 @@ function Targets({
]}
</>
)
if (payload?.game?.state === "starting" && mode >= 0 && targetPreview.show)
return <Ship preview props={shipProps(ships, mode, targetPreview)} />
}
export default Targets

View file

@ -1,4 +1,10 @@
import { Hit, HitDispatch, Target } from "../interfaces/frontend"
import {
ActionDispatchProps,
EventBarModes,
Hit,
ShipProps,
Target,
} from "../interfaces/frontend"
import { GameSettings } from "@components/Lobby/SettingsFrame/Setting"
import { getPayloadwithChecksum } from "@lib/getPayloadwithChecksum"
import { socket } from "@lib/socket"
@ -19,12 +25,18 @@ const initialState: optionalGamePropsSchema & {
isReady: boolean
isConnected: boolean
}[]
menu: keyof EventBarModes
mode: number
ships: ShipProps[]
hits: Hit[]
target: Target
targetPreview: Target
} = {
menu: "actions",
mode: 0,
payload: null,
hash: null,
ships: [],
hits: [],
target: initlialTarget,
targetPreview: initlialTargetPreview,
@ -37,7 +49,7 @@ const initialState: optionalGamePropsSchema & {
export type State = typeof initialState
export type Action = {
DispatchHits: (action: HitDispatch) => void
DispatchAction: (props: ActionDispatchProps) => void
setTarget: (target: SetStateAction<Target>) => void
setTargetPreview: (targetPreview: SetStateAction<Target>) => void
setPlayer: (payload: { users: PlayerSchema[] }) => string | null
@ -46,6 +58,8 @@ export type Action = {
leave: (cb: () => void) => void
setIsReady: (payload: { i: number; isReady: boolean }) => void
starting: () => void
addShip: (props: ShipProps) => void
removeShip: (props: ShipProps) => void
setIsConnected: (payload: { i: number; isConnected: boolean }) => void
reset: () => void
}
@ -54,16 +68,16 @@ export const useGameProps = create<State & Action>()(
devtools(
(set) => ({
...initialState,
DispatchHits: (action) =>
DispatchAction: (action) =>
set(
produce((state: State) => {
switch (action.type) {
case "fireMissile":
case "htorpedo":
case "vtorpedo": {
state.hits.push(...action.payload)
}
}
// switch (action.type) {
// case "fireMissile":
// case "htorpedo":
// case "vtorpedo": {
// state.hits.push(...action.payload)
// }
// }
})
),
setTarget: (target) =>
@ -82,6 +96,25 @@ export const useGameProps = create<State & Action>()(
else state.targetPreview = targetPreview
})
),
addShip: (props) =>
set(
produce((state: State) => {
state.ships.push(props)
})
),
removeShip: ({ size, variant, x, y }) =>
set(
produce((state: State) => {
const indexToRemove = state.ships.findIndex(
(ship) =>
ship.size === size &&
ship.variant === variant &&
ship.x === x &&
ship.y === y
)
state.ships.splice(indexToRemove, 1)
})
),
setPlayer: (payload) => {
let hash: string | null = null
set(
@ -103,7 +136,6 @@ export const useGameProps = create<State & Action>()(
return hash
},
setSetting: (settings) => {
const payload = JSON.stringify(settings)
let hash: string | null = null
set(
produce((state: State) => {

View file

@ -1,4 +1,5 @@
import { IconDefinition } from "@fortawesome/pro-solid-svg-icons"
import { MoveType } from "@prisma/client"
export interface Position {
x: number
@ -16,7 +17,7 @@ export interface TargetList extends Position {
}
export interface Mode {
pointerGrid: any[][]
type: string
type: MoveType
}
export interface ItemProps {
icon: string | IconDefinition
@ -29,7 +30,7 @@ export interface ItemProps {
export interface EventBarModes {
main: ItemProps[]
menu: ItemProps[]
attack: ItemProps[]
actions: ItemProps[]
draw: ItemProps[]
settings: ItemProps[]
}
@ -40,29 +41,7 @@ export interface Hit extends Position {
hit: boolean
}
interface fireMissile {
type: "fireMissile" | "htorpedo" | "vtorpedo"
payload: {
x: number
y: number
hit: boolean
}[]
}
interface removeMissile {
type: "removeMissile"
payload: {
x: number
y: number
hit: boolean
}
}
export type HitDispatch = fireMissile | removeMissile
export interface Point {
x: number
y: number
}
export interface Point extends Position {}
export interface DrawLineProps {
currentPoint: Point
@ -74,7 +53,12 @@ export interface Draw extends DrawLineProps {
ctx: CanvasRenderingContext2D
}
export interface Ship extends Position {
export interface ShipProps extends Position {
size: number
variant: number
}
export interface ActionDispatchProps extends Position {
index?: number
action: MoveType
}

View file

@ -1,13 +1,14 @@
import type {
Hit,
HitDispatch,
Mode,
Position,
ShipProps,
Target,
TargetList,
} from "../../interfaces/frontend"
import { count } from "@components/Gamefield/Gamefield"
import { PointerProps } from "@components/Gamefield/GamefieldPointer"
import { useGameProps } from "@hooks/useGameProps"
export function borderCN(count: number, x: number, y: number) {
if (x === 0) return "left"
@ -54,13 +55,6 @@ export function isAlreadyHit(x: number, y: number, hits: Hit[]) {
return !!hits.filter((h) => h.x === x && h.y === y).length
}
function isSet(x: number, y: number, targetList: TargetList[], show: boolean) {
return (
!!targetList.filter((field) => x === field.x && y === field.y).length &&
show
)
}
export function targetList(
{ x: targetX, y: targetY }: Position,
mode: number
@ -129,3 +123,21 @@ export const initlialMouseCursor = {
x: 0,
y: 0,
}
export const shipProps = (
ships: ShipProps[],
mode: number,
targetPreview: Target
) => ({
size: mode + 2,
variant:
ships
.filter((e) => e.size === mode + 2)
.sort((a, b) => a.variant - b.variant)
.reduce((prev, curr) => {
console.log(curr.variant - prev)
return curr.variant - prev < 2 ? curr.variant : prev
}, 0) + 1,
x: targetPreview.x - Math.floor((mode + 2) / 2),
y: targetPreview.y,
})

View file

@ -1,3 +1,4 @@
import { ShipSchema } from "../prisma/generated/zod"
import { GameState } from "@prisma/client"
import { z } from "zod"
@ -18,6 +19,9 @@ export const PlayerSchema = z
.object({
id: z.string(),
index: z.number(),
action: z.string(),
x: z.number(),
y: z.number(),
})
.array(),
})
@ -34,10 +38,20 @@ export const CreateSchema = z.object({
allowSpecials: z.boolean(),
allowChat: z.boolean(),
allowMarkDraw: z.boolean(),
ships: z
.object({
id: z.string(),
size: z.number(),
variant: z.number(),
x: z.number(),
y: z.number(),
})
.array(),
})
.nullable(),
gamePin: z.string().nullable(),
users: PlayerSchema.array(),
activeIndex: z.number().optional(),
})
export const GamePropsSchema = z.object({

View file

@ -15,6 +15,15 @@ export const gameSelects = {
allowSpecials: true,
allowSpectators: true,
state: true,
ships: {
select: {
id: true,
size: true,
variant: true,
x: true,
y: true,
},
},
gamePin: {
select: {
pin: true,
@ -36,6 +45,9 @@ export const gameSelects = {
select: {
id: true,
index: true,
action: true,
x: true,
y: true,
},
},
user: {

File diff suppressed because it is too large Load diff

View file

@ -75,11 +75,22 @@ enum GameState {
ended
}
model Ship {
id String @id @default(cuid())
size Int
variant Int
x Int
y Int
gameId String
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
}
model Game {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
state GameState @default(lobby)
ships Ship[]
allowSpectators Boolean @default(true)
allowSpecials Boolean @default(true)
allowChat Boolean @default(true)
@ -111,14 +122,25 @@ model User_Game {
@@unique([gameId, userId])
}
enum MoveType {
radar
htorpedo
vtorpedo
missile
}
model Move {
id String @id @default(cuid())
createdAt DateTime @default(now())
index Int
action MoveType
x Int
y Int
user_game_id String
user_game User_Game @relation(fields: [user_game_id], references: [id], onDelete: Cascade)
@@unique([user_game_id, index])
@@unique([action, x, y])
}
model Chat {

View file

@ -108,7 +108,7 @@ body {
position: relative;
@include flex-col;
align-items: center;
grid-row: var(--x);
grid-row: var(--y);
pointer-events: none;
img {
@ -119,16 +119,31 @@ body {
// object-fit: cover;
}
&.interactive:not(.preview) {
pointer-events: auto;
}
&.preview {
border: 2px dashed orange;
border-radius: 0.5rem;
animation: blink 0.5s ease-in-out alternate infinite;
@keyframes blink {
from {
opacity: 0.2;
}
}
}
&.s2 {
grid-column: var(--y) / 5;
grid-column: var(--x) / calc(var(--x) + 2);
}
&.s3 {
grid-column: var(--y) / 6;
grid-column: var(--x) / calc(var(--x) + 3);
}
&.s4 {
grid-column: var(--y) / 7;
grid-column: var(--x) / calc(var(--x) + 4);
}
}
@ -285,8 +300,8 @@ body {
&.left {
transform: rotate(90deg);
animation: floatInl 3s ease-out;
@keyframes floatInl {
animation: floatInLe 3s ease-out;
@keyframes floatInLe {
from {
transform: scale(2) rotate(90deg);
margin-left: -324px;
@ -297,8 +312,8 @@ body {
&.right {
transform: rotate(270deg);
animation: floatInr 3s ease-out;
@keyframes floatInr {
animation: floatInRi 3s ease-out;
@keyframes floatInRi {
from {
transform: scale(2) rotate(270deg);
margin-left: 324px;
@ -307,8 +322,8 @@ body {
}
}
&.top {
animation: floatInt 3s ease-out;
@keyframes floatInt {
animation: floatInTop 3s ease-out;
@keyframes floatInTop {
from {
transform: scale(2);
margin-top: 648px;
@ -319,8 +334,8 @@ body {
&.bottom {
transform: rotate(180deg);
animation: floatInb 3s ease-out;
@keyframes floatInb {
animation: floatInBot 3s ease-out;
@keyframes floatInBot {
from {
transform: scale(2) rotate(180deg);
margin-top: 0;
@ -331,12 +346,18 @@ body {
&.middle {
grid-area: 4 / 4 / -4 / -4;
transform: scale(1.5);
animation: floatInMid 3s ease-in-out;
@keyframes floatInMid {
from {
opacity: 0;
}
}
}
}
canvas {
grid-area: 1 / 1 / -1 / -1;
border: 1px solid #000;
border-radius: 0.5rem;
z-index: 1;
}
@ -379,6 +400,9 @@ body {
background-color: white;
border-radius: 1rem;
&.enabled {
box-shadow: 0 0 0.5rem 0.25rem royalblue;
}
&.disabled {
box-shadow: inset 0 0 1rem 1rem #888;
}