Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"iface",
"importas",
"INGAME",
"interactables",
"Ints",
"ireturn",
"irongut",
Expand Down
2 changes: 2 additions & 0 deletions TODO
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Line of action
Lint
☐ Fix all linter issues
☐ Disable unused lints (remove if default satisfies)
☐ Update README for frontend and move frontend to another repo
☐ Make taskfile to shim frontend for go to run tests without building using Wails. Remove the step from CI pipeline
Refactors
☐ Migrate to data-driven approach
☐ Benchmark performance for room and connection against direct data storage instead of using pointers
Expand Down
648 changes: 415 additions & 233 deletions pkg/client/frontend/package-lock.json

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions pkg/client/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"name": "nukeship",
"author": "Utkarsh Bhardwaj (Passeriform) <bhardwajutkarsh.ub@gmail.com>",
"private": true,
"version": "1.0.0",
"type": "module",
"version": "1.0.0",
"scripts": {
"start": "vite",
"dev": "vite",
Expand All @@ -18,15 +18,17 @@
"@tweenjs/tween.js": "^25.0.0",
"solid-js": "^1.9.3",
"solid-spinner": "^0.2.0",
"solid-three": "^0.2.0",
"solid-toast": "^0.5.0",
"tailwind-merge": "^2.6.0",
"three": "^0.172.0"
"three": "^0.175.0",
"three-stdlib": "^2.35.15"
},
"devDependencies": {
"@pyncz/tailwind-mask-image": "^2.0.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.1",
"@types/node": "^22.10.5",
"@types/three": "^0.172.0",
"@types/three": "^0.175.0",
"@xpd/tailwind-3dtransforms": "^1.0.3",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.49",
Expand Down
8 changes: 4 additions & 4 deletions pkg/client/frontend/src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,17 @@ interface ButtonProps {
onClick?: (event: MouseEvent) => void
}

const Button: VoidComponent<ButtonProps> = ({ onClick = () => undefined, class: cls = "", text = "" }) => {
const Button: VoidComponent<ButtonProps> = (props) => {
return (
<button
class={twMerge(
"group relative cursor-pointer appearance-none rounded-lg border border-dark-turquoise/30 bg-transparent px-3 py-5 text-base/tight font-medium uppercase tracking-wide text-dark-turquoise/50 outline-none transition-all duration-200 ease-in-out before:absolute before:left-1/10 before:top-0 before:h-px before:w-16 before:-translate-y-px before:bg-dark-turquoise before:transition-all before:duration-200 before:ease-in-out after:absolute after:bottom-0 after:right-1/10 after:h-px after:w-16 after:translate-y-px after:bg-dark-turquoise after:transition-all after:duration-200 after:ease-in-out hover:rounded-ee-none hover:rounded-ss-none hover:border-medium-slate-blue hover:text-dark-turquoise hover:shadow-sm hover:shadow-dark-turquoise/30 hover:text-shadow hover:before:left-0 hover:before:w-5 hover:after:right-0 hover:after:w-5",
cls,
props.class ?? "",
)}
on:click={onClick}
on:click={props.onClick}
>
<span class="before:absolute before:bottom-1/3 before:right-0 before:h-5 before:w-px before:translate-x-px before:bg-dark-turquoise before:transition-all before:duration-200 before:ease-in-out after:absolute after:left-0 after:top-1/3 after:h-5 after:w-px after:-translate-x-px after:bg-dark-turquoise after:transition-all after:duration-200 after:ease-in-out group-hover:before:bottom-0 group-hover:after:top-0" />
{text}
{props.text ?? ""}
</button>
)
}
Expand Down
16 changes: 7 additions & 9 deletions pkg/client/frontend/src/components/NavButton.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { VoidComponent } from "solid-js"
import { VoidComponent, mergeProps } from "solid-js"
import { twMerge } from "tailwind-merge"
import Button from "./Button"

Expand All @@ -9,15 +9,13 @@ interface NavButtonProps {
position: "left" | "right"
}

const NavButton: VoidComponent<NavButtonProps> = ({
onClick = () => undefined,
class: cls = "",
text = "",
position = "left",
}) => {
const NavButton: VoidComponent<NavButtonProps> = (_props) => {
const props = mergeProps({ onClick: () => undefined }, _props)
return (
<nav class={twMerge(`absolute ${position === "left" ? "left-16" : "right-16"} top-16`, cls)}>
<Button class="min-h-16 min-w-32 backdrop-blur-md" text={text} onClick={onClick} />
<nav
class={twMerge(`absolute ${props.position === "left" ? "left-16" : "right-16"} top-16`, props.class ?? "")}
>
<Button class="min-h-16 min-w-32 backdrop-blur-md" text={props.text ?? ""} onClick={props.onClick} />
</nav>
)
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/client/frontend/src/components/VideoBackground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ interface VideoBackgroundProps {
src: string
}

const VideoBackground: VoidComponent<VideoBackgroundProps> = ({ src }) => {
const VideoBackground: VoidComponent<VideoBackgroundProps> = (props) => {
return (
<div class="absolute inset-0 -z-10 h-full w-full overflow-hidden">
<video preload="auto" autoplay loop muted class="aspect-auto w-full blur-sm brightness-50 -hue-rotate-45">
<source src={src} type="video/mp4" />
<source src={props.src} type="video/mp4" />
</video>
</div>
)
Expand Down
4 changes: 2 additions & 2 deletions pkg/client/frontend/src/constants/sample.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TreeRawData } from "@game/tree"
import { RawDataStream } from "@game/Tree"

export const ExampleFS: TreeRawData = {
export const ExampleFS: RawDataStream = {
label: "C:",
children: [
{
Expand Down
19 changes: 1 addition & 18 deletions pkg/client/frontend/src/constants/statics.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,5 @@
import { Quaternion, Vector3 } from "three"
import { TweenTransform } from "@constants/types"
import { Vector3 } from "three"

export const X_AXIS = Object.freeze(new Vector3(1, 0, 0))
export const Y_AXIS = Object.freeze(new Vector3(0, 1, 0))
export const Z_AXIS = Object.freeze(new Vector3(0, 0, 1))

export const ELEVATION_FORWARD_QUATERNION = Object.freeze(new Quaternion(0, 0, 0, 1).normalize())

export const STATICS = {
DIRECTIONAL_LIGHT: {
position: Object.freeze(new Vector3(5, 5, 5)),
},
SELF: {
position: Object.freeze(new Vector3(2, 0, 0)),
rotation: Object.freeze(new Quaternion(0, 1, 0, 1).normalize()),
},
OPPONENT: {
position: Object.freeze(new Vector3(-2, 0, 0)),
rotation: Object.freeze(new Quaternion(0, -1, 0, 1).normalize()),
},
} satisfies Record<string, Partial<TweenTransform>>
216 changes: 216 additions & 0 deletions pkg/client/frontend/src/game/Game.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
import { TourControlsDrillEvent } from "@passeriform/three-tour-controls"
import { Group as TweenGroup } from "@tweenjs/tween.js"
import { VoidComponent, createEffect, extend, useFrame } from "solid-three"
import { Mesh, Object3D, Quaternion, QuaternionTuple, Vector3Tuple } from "three"
import { ExampleFS } from "@constants/sample"
import { FocusType, ViewType } from "@constants/types"
import { boundsFromObjects, boundsFromPoints } from "@utility/bounds"
import { tweenOpacity } from "@utility/tween"
import TourControls from "./TourControls"
import Tree from "./Tree"

const AMBIENT_LIGHT_COLOR = 0x193751
const AMBIENT_LIGHT_INTENSITY = 2
const DIRECTIONAL_LIGHT_COLOR = 0xffffff
const DIRECTIONAL_LIGHT_INTENSITY = 2
const DIRECTIONAL_LIGHT_POSITION = [5, 5, 5] as Vector3Tuple
const SELF_TREE_POSITION = [2, 0, 0] as Vector3Tuple
const SELF_TREE_ROTATION = [0, 1, 0, 1] as QuaternionTuple
const OPPONENT_TREE_POSITION = [-2, 0, 0] as Vector3Tuple
const OPPONENT_TREE_ROTATION = [0, -1, 0, 1] as QuaternionTuple
const ARCH_CONTROLS_ELEVATION_FORWARD_ROTATION = [0, 0, 0, 1] as QuaternionTuple
const ARCH_CONTROLS_OFFSET = {
[ViewType.ELEVATION]: 4,
[ViewType.PLAN]: 2,
}
const TARGET_CONTROLS_OFFSET = 1
const PLAN_MATCH_OPACITY_MAP: Record<number, number> = {
[-1]: 0.2,
[1]: 0,
[0]: 1,
}

extend({ TourControls })

type GameProps = {
view: ViewType
focus: FocusType
tweenGroup: TweenGroup
}

const Game: VoidComponent<GameProps> = (props) => {
let selfTree: Object3D
let opponentTree: Object3D
let pushTarget: (target: Object3D) => void
let getSelfTreeNodeMeshes: () => Mesh[][]
let getSelfTreeNodePositions: () => Vector3Tuple[][]
let getOpponentTreeNodeMeshes: () => Mesh[][]
let getOpponentTreeNodePositions: () => Vector3Tuple[][]

const getFocussedTree = () => {
if (!selfTree! || !opponentTree!) {
return undefined
}

return (
(props.focus === FocusType.SELF && selfTree!) ||
(props.focus === FocusType.OPPONENT && opponentTree!) ||
undefined
)
}

const getFocussedTreeNodeMeshes = () => {
if (!getSelfTreeNodeMeshes! || !getOpponentTreeNodeMeshes!) {
return undefined
}

return (
(props.focus === FocusType.SELF && getSelfTreeNodeMeshes!()) ||
(props.focus === FocusType.OPPONENT && getOpponentTreeNodeMeshes!()) ||
undefined
)
}

const getFocussedTreeNodePositions = () => {
if (!getSelfTreeNodePositions! || !getOpponentTreeNodePositions!) {
return undefined
}

return (
(props.focus === FocusType.SELF && getSelfTreeNodePositions!()) ||
(props.focus === FocusType.OPPONENT && getOpponentTreeNodePositions!()) ||
undefined
)
}

const getInteractables = () => {
const focusedTree = getFocussedTree()
return focusedTree ? [focusedTree] : []
}

const getBoundPoses = () => {
if (!selfTree! || !opponentTree! || !getSelfTreeNodeMeshes! || !getOpponentTreeNodeMeshes!) {
return
}

const focusedTree = getFocussedTree()

if (!focusedTree) {
return [
{
bounds: boundsFromObjects(selfTree, opponentTree),
quaternion: new Quaternion(...ARCH_CONTROLS_ELEVATION_FORWARD_ROTATION),
},
]
}

const targetTreeNodePositions = getFocussedTreeNodePositions()

if (!targetTreeNodePositions) {
return
}

return props.view === ViewType.ELEVATION
? [
{
bounds: boundsFromObjects(focusedTree),
quaternion: new Quaternion(...ARCH_CONTROLS_ELEVATION_FORWARD_ROTATION),
},
]
: targetTreeNodePositions.map((level) => ({
bounds: boundsFromPoints(...level),
quaternion: focusedTree.quaternion.clone(),
}))
}

const resetOpacities = () => {
if (!getSelfTreeNodeMeshes! || !getOpponentTreeNodeMeshes!) {
return
}

getSelfTreeNodeMeshes().forEach((level) => level.forEach((mesh) => tweenOpacity(props.tweenGroup, mesh, 1)))
getOpponentTreeNodeMeshes().forEach((level) => level.forEach((mesh) => tweenOpacity(props.tweenGroup, mesh, 1)))
}

useFrame(() => {
props.tweenGroup.update()
}, -1)

createEffect(() => {
if (props.focus === FocusType.NONE) {
resetOpacities()
return
}

if (props.view === ViewType.ELEVATION) {
resetOpacities()
return
}
})

return (
<>
<ambientLight color={AMBIENT_LIGHT_COLOR} intensity={AMBIENT_LIGHT_INTENSITY} />
<directionalLight
color={DIRECTIONAL_LIGHT_COLOR}
intensity={DIRECTIONAL_LIGHT_INTENSITY}
position={DIRECTIONAL_LIGHT_POSITION}
/>
<tourControls
cameraOffset={ARCH_CONTROLS_OFFSET[props.focus === FocusType.NONE ? ViewType.ELEVATION : props.view]}
boundPoses={getBoundPoses() ?? []}
onDrill={(event: TourControlsDrillEvent) =>
getFocussedTreeNodeMeshes()?.forEach((level, levelIdx) =>
level.forEach((mesh) =>
tweenOpacity(
props.tweenGroup,
mesh,
event.historyIdx === -1
? 1
: PLAN_MATCH_OPACITY_MAP[Math.sign(event.historyIdx - levelIdx)]!,
),
),
)
}
/>
{/* <TargetControls
cameraOffset={TARGET_CONTROLS_OFFSET}
enabled={props.focus !== FocusType.NONE}
interactables={getInteractables()}
pushTargetRef={pushTarget!}
onSelect={(event) => {
resetOpacities()
tweenOpacity(event.tweenGroup, event.intersect as any, 1)
}}
/> */}
<Tree
dataStream={ExampleFS}
colorSeed={1}
position={SELF_TREE_POSITION}
rotation={SELF_TREE_ROTATION}
ref={selfTree!}
getNodeMeshesRef={getSelfTreeNodeMeshes!}
getNodePositionsRef={getSelfTreeNodePositions!}
onClick={(event) => props.focus === FocusType.SELF && pushTarget!(event.object as any)}
onPointerOver={() => undefined}
onPointerOut={() => undefined}
onPointerMissed={() => undefined}
/>
<Tree
dataStream={ExampleFS}
colorSeed={2}
position={OPPONENT_TREE_POSITION}
rotation={OPPONENT_TREE_ROTATION}
ref={opponentTree!}
getNodeMeshesRef={getOpponentTreeNodeMeshes!}
getNodePositionsRef={getOpponentTreeNodePositions!}
onClick={(event) => props.focus === FocusType.OPPONENT && pushTarget!(event.object as any)}
onPointerOver={() => undefined}
onPointerOut={() => undefined}
onPointerMissed={() => undefined}
/>
</>
)
}

export default Game
Loading