Skip to content

Commit

Permalink
Rework leaderboard
Browse files Browse the repository at this point in the history
  • Loading branch information
phisn committed Jul 21, 2024
1 parent 669cb85 commit 3e47a2b
Show file tree
Hide file tree
Showing 16 changed files with 730 additions and 340 deletions.
21 changes: 13 additions & 8 deletions packages/server/src/worker/api/leaderboard/leaderboard-router.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TRPCError } from "@trpc/server"
import { and, eq } from "drizzle-orm"
import { Buffer } from "node:buffer"
import { WorldLeaderboard } from "shared/src/worker-api/world-leaderboard"
import { z } from "zod"
import { leaderboard, users } from "../../framework/db-schema"
import { publicProcedure, router } from "../../framework/trpc"
Expand Down Expand Up @@ -81,15 +82,19 @@ export const leaderboardRouter = router({
),
)
.orderBy(leaderboard.ticks)
.limit(10)
.limit(50)
.execute()

return replays.map(replay => ({
leaderboardId: replay.leaderboard.id,
username: replay.users.username,
deaths: replay.leaderboard.deaths,
ticks: replay.leaderboard.ticks,
model: Buffer.from(replay.leaderboard.model).toString("base64"),
}))
const response: WorldLeaderboard = {
entries: replays.map((replay, i) => ({
leaderboardId: replay.leaderboard.id,
place: i + 1,
deaths: replay.leaderboard.deaths,
ticks: replay.leaderboard.ticks,
username: replay.users.username,
})),
}

return response
}),
})
11 changes: 11 additions & 0 deletions packages/shared/src/worker-api/world-leaderboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface WorldLeaderboard {
entries: WorldLeaderboardEntry[]
}

export interface WorldLeaderboardEntry {
leaderboardId: number
place: number
username: string
ticks: number
deaths: number
}
159 changes: 159 additions & 0 deletions packages/web/src/components/common/DraggableList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { animated, useResize, useSpring } from "@react-spring/web"
import { useGesture } from "@use-gesture/react"
import { useMemo, useRef } from "react"

export function DraggableList(props: {
length: number
className?: string
children: (index: number) => React.ReactNode
onSwipeHorizontal?: (side: "left" | "right") => void
}) {
const firstChildRef = useRef<HTMLDivElement>(null)
const relativeContainerRef = useRef<HTMLDivElement>(null)
const absoluteContainerRef = useRef<HTMLDivElement>(null)

const oldElementIndex = useRef(0)
const newElementIndex = useRef(0)

const elements = useMemo(() => Array.from({ length: props.length }), [props.length])

function clampElementIndex(index: number) {
return Math.max(0, Math.min(props.length - 1, index))
}

const { height: elementHeight } = useResize({
container: firstChildRef,
})

const { height: relativeContainerHeight } = useResize({
container: relativeContainerRef,
})

const { height: absoluteContainerHeight } = useResize({
container: absoluteContainerRef,
})

const [springs, api] = useSpring(() => ({
y: 0,
config: {
tension: 210,
friction: 20,
},
}))

function applyScrollCap(moveDownBy: number, smoothed?: number) {
if (moveDownBy > 0) {
return Math.pow(moveDownBy, smoothed ?? 1)
}

const largerChild = relativeContainerHeight.get() - absoluteContainerHeight.get()

if (largerChild > 0) {
return Math.sign(moveDownBy) * Math.pow(Math.abs(moveDownBy), smoothed ?? 1)
}

const overscrollDown = largerChild - moveDownBy

if (overscrollDown > 0) {
moveDownBy += overscrollDown

if (smoothed) {
moveDownBy -= Math.pow(overscrollDown, smoothed)
}
}

return moveDownBy
}

function handleGesture(props: { active: boolean; my: number; swipeY: number; vy: number }) {
// allow swiping
if (props.swipeY && oldElementIndex.current === newElementIndex.current) {
const t = newElementIndex.current + (props.swipeY < 0 ? 1 : -1)
newElementIndex.current = clampElementIndex(t)
}

if (props.active === false) {
if (oldElementIndex.current !== newElementIndex.current) {
console.log("old", oldElementIndex.current, "new", newElementIndex.current)
}
oldElementIndex.current = newElementIndex.current
}

// calculate movement
let moveDownBy = -oldElementIndex.current * elementHeight.get()
moveDownBy = applyScrollCap(moveDownBy)

// apply change of gesture
if (props.active) {
moveDownBy += props.my
moveDownBy = applyScrollCap(moveDownBy, 0.8)
}

// stop at element
if (props.active) {
const moveDownTarget = moveDownBy - Math.sign(props.my) * props.vy * 50
const moveDownTargetBounded = applyScrollCap(moveDownTarget, 0)

newElementIndex.current = clampElementIndex(
-Math.round(moveDownTargetBounded / elementHeight.get()),
)
}

api.start({
y: moveDownBy,
config: {
velocity: props.active ? props.vy : 0,
},
})
}

const binds = useGesture(
{
onDrag: ({
event,
active,
intentional,
movement: [, my],
swipe: [swipeX, swipeY],
velocity: [, vy],
}) => {
event.preventDefault()
handleGesture({ active, my, swipeY, vy })

if (intentional && swipeX && !swipeY) {
props.onSwipeHorizontal?.(swipeX > 0 ? "right" : "left")
}
},
onWheel: ({ active, movement: [, my] }) => {
handleGesture({ active, my, swipeY: 0, vy: 0 })
},
},
{
drag: {
filterTaps: true,
},
wheel: {},
},
)

return (
<div ref={relativeContainerRef} className={"relative h-full " + props.className}>
<animated.div
ref={absoluteContainerRef}
className="absolute inset-0 flex h-max touch-none select-none flex-col items-center "
{...binds()}
style={springs}
>
{elements.map((_, index) => (
<div
ref={index === 0 ? firstChildRef : undefined}
key={index}
className="w-full"
>
{props.children(index)}
</div>
))}
</animated.div>
</div>
)
}
4 changes: 2 additions & 2 deletions packages/web/src/components/common/svg/Base.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export function newSvgFrom(children: JSX.Element) {
export function newSvgFrom(children: JSX.Element, size: number = 16) {
function Svg(props: { className?: string; width: string; height: string; fill?: string }) {
return (
<svg
Expand All @@ -7,7 +7,7 @@ export function newSvgFrom(children: JSX.Element) {
height={props.height}
fill={props.fill || "currentColor"}
className={props.className}
viewBox="0 0 16 16"
viewBox={`0 0 ${size} ${size}`}
>
{children}
</svg>
Expand Down
10 changes: 10 additions & 0 deletions packages/web/src/components/common/svg/Bullseye.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { newSvgFrom } from "./Base"

export const Bullseye = newSvgFrom(
<>
<path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14m0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16" />
<path d="M8 13A5 5 0 1 1 8 3a5 5 0 0 1 0 10m0 1A6 6 0 1 0 8 2a6 6 0 0 0 0 12" />
<path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6m0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8" />
<path d="M9.5 8a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0" />
</>,
)
8 changes: 8 additions & 0 deletions packages/web/src/components/common/svg/Clock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { newSvgFrom } from "./Base"

export const Clock = newSvgFrom(
<>
<path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71z" />
<path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0" />
</>,
)
Loading

0 comments on commit 3e47a2b

Please sign in to comment.