diff --git a/README.md b/README.md index 2544bb2..f572c44 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,10 @@ Find instructions if you wish to setup your own game server. - [x] right click modal, taps card +## Missing Cards +- [ ] [Winter river](https://curiosa.io/cards/winter_river) + + ## Feedback - [ ] diff --git a/notes/2024-10-04.md b/notes/2024-10-04.md new file mode 100644 index 0000000..fcd06a0 --- /dev/null +++ b/notes/2024-10-04.md @@ -0,0 +1,74 @@ + +#### 00:52 + +# Beta Box Pull Rate +Need to find some statistics on box openings to get booster pack statistics + + +# beta +https://www.youtube.com/watch?v=fcmxdMPGLuA + +1:14 +3x exc +1x elite +10x ord +ord site + +1:59 +exc site +2x exc +elite +10x ord +sorcerer sparkcaster + +2:22 +exc +exc +exc +elite +10x ord +ord site + +2:44 +exc +exc +exc site +sorcerer enchantress +10x ord +ord site + +3:18 +exc +exc +exc +elite +ord site + +3:42 +exc +exc +exc site +elite +10x ord +ord site + +4:02 +exc +exc +exc site +unique + + +| Timestamp | Exceptional | Elite | Ordinary | Unique | Site (Rarity) | Special Card | +|-----------|-------------|-------|----------|--------|------------------------|------------------------| +| 1:14 | 3 | 1 | 10 | - | Ordinary | - | +| 1:59 | 3 | 1 | 10 | - | Exceptional | Sorcerer Sparkcaster | +| 2:22 | 3 | 1 | 10 | - | Ordinary | - | +| 2:44 | 3 | - | 10 | - | Exceptional, Ordinary | Sorcerer Enchantress | +| 3:18 | 3 | 1 | 10 | - | Ordinary | - | +| 3:42 | 2 | 1 | 10 | - | Exceptional, Ordinary | - | +| 4:02 | 3 | - | 10 | 1 | Exceptional | - | +| 4:27 | 3 | - | 10 | 1 | Unique, Exceptional, Ordinary | - | +| 4:52 | 3 | 1 | 10 | - | Exceptional, Exceptional, Ordinary | - | +| 5:18 | 3 | 1 | 10 | - | (Foil)Ord, Ordinary | - | +| 0:00 | - | - | 10 | - | Ordinary | - | diff --git a/package.json b/package.json index 07dfca2..7cf2ff5 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "react-dom": "^18", "react-hot-toast": "^2.4.1", "react-icons": "^5.3.0", + "react-parallax-tilt": "^1.7.245", "uuid": "^10.0.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 49bb628..4d7d2d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: react-icons: specifier: ^5.3.0 version: 5.3.0(react@18.3.1) + react-parallax-tilt: + specifier: ^1.7.245 + version: 1.7.245(react-dom@18.3.1(react@18.3.1))(react@18.3.1) uuid: specifier: ^10.0.0 version: 10.0.0 @@ -2323,6 +2326,12 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-parallax-tilt@1.7.245: + resolution: {integrity: sha512-WTyACpCvZgfM2FyN3Yf66stxe+B5L0E6mPoimCOzrBQykJFDE3ojwlhleuEqxFZ44Y289W2PR0gCDapX+N9/Aw==} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-remove-scroll-bar@2.3.6: resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==} engines: {node: '>=10'} @@ -5156,6 +5165,11 @@ snapshots: react-is@16.13.1: {} + react-parallax-tilt@1.7.245(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll-bar@2.3.6(@types/react@18.3.5)(react@18.3.1): dependencies: react: 18.3.1 diff --git a/src/components/atoms/Tabs/index.tsx b/src/components/atoms/Tabs/index.tsx index c51718e..34b7686 100644 --- a/src/components/atoms/Tabs/index.tsx +++ b/src/components/atoms/Tabs/index.tsx @@ -8,20 +8,37 @@ import { ReactNode } from "react"; type Props = { tabs: ReactNode[]; - content: ReactNode[]; + content?: ReactNode[]; + selectedIndex?: number; + onSelect?(index: number): void; }; -export const Tabs = ({ tabs, content }: Props) => { +export const Tabs = ({ + tabs, + content, + selectedIndex, + onSelect = (index: number) => { + console.info(index + " selected"); + }, +}: Props) => { + const value = + selectedIndex !== undefined ? selectedIndex.toString() : undefined; + const options = value !== undefined ? { value } : {}; + return ( - + {tabs.map((tab, index) => ( - + onSelect(index)} + > {tab} ))} - {content.map((content, index) => ( + {content?.map((content, index) => ( {content} diff --git a/src/components/organisms/Draft/Card/index.tsx b/src/components/organisms/Draft/Card/index.tsx new file mode 100644 index 0000000..ae6215a --- /dev/null +++ b/src/components/organisms/Draft/Card/index.tsx @@ -0,0 +1,63 @@ +import { CardImage } from "@/components/atoms/card-view/card"; +import { Box, Flex } from "styled-system/jsx"; +import Tilt, { GlareProps } from "react-parallax-tilt"; +import { useState } from "react"; +import { CardDTO } from "@/utils/api/cardData/CardDataType"; + +export const DraftCard = (cardDTO: CardDTO) => { + const [isOver, setIsOver] = useState(false); + function over() { + setIsOver(true); + } + function out() { + setIsOver(false); + } + + const rarityColor: Record = { + Ordinary: "#fff", + Exceptional: "rgba(0,100,150,1)", + Elite: "rgba(150,0,250,1)", + Unique: "rgba(230,180,50,1)", + }; + + return ( + + + + + + + + ); +}; + +const glareOptions: GlareProps = { + glareEnable: true, + glareColor: "lightblue", + glareMaxOpacity: 0.25, + glarePosition: "all", +}; +const tiltOptions = { + ...glareOptions, +}; diff --git a/src/components/organisms/Draft/Ribbon/index.tsx b/src/components/organisms/Draft/Ribbon/index.tsx new file mode 100644 index 0000000..344e1e8 --- /dev/null +++ b/src/components/organisms/Draft/Ribbon/index.tsx @@ -0,0 +1,54 @@ +import { Box, Grid, HStack } from "styled-system/jsx"; +import { Button } from "@/components/ui/button"; +import { Tabs } from "@/components/atoms/Tabs"; + +import { useCardFullData } from "@/utils/api/cardData/useCardData"; +import { DraftProps } from "../types"; +import { generateBoosterPack } from "../helpers"; + +export const Ribbon = ( + props: DraftProps & { + activeViewIndex: number; + setActiveView(index: number): void; + }, +) => { + const { data: cardData = [] } = useCardFullData(); + + function crackBooster() { + const newBooster = generateBoosterPack({ + cardData, + expansionSlug: "bet", + }); + const { finishedPacks } = props.player; + const isEmpty = finishedPacks.length === 0; + props.setPlayerData({ + ...props.player, + finishedPacks: isEmpty ? [newBooster] : [...finishedPacks, newBooster], + }); + props.setActiveView(props.player.finishedPacks.length); + } + + const packTabs = + props.player?.finishedPacks?.length > 0 + ? props.player?.finishedPacks?.map((_, index) => `Pack ${index + 1}`) + : []; + + return ( + + + + + {props.player.finishedPacks.length > 0 && ( + + )} + + + + ); +}; diff --git a/src/components/organisms/Draft/Stats/index.tsx b/src/components/organisms/Draft/Stats/index.tsx new file mode 100644 index 0000000..5f72b2d --- /dev/null +++ b/src/components/organisms/Draft/Stats/index.tsx @@ -0,0 +1,47 @@ +import { Box, Flex, HStack } from "styled-system/jsx"; +import { DraftProps } from "../types"; +import { CardDTO } from "@/utils/api/cardData/CardDataType"; + +function filterRarity(rarity: CardDTO["guardian"]["rarity"]) { + return (card: CardDTO) => card?.guardian?.rarity === rarity; +} + +export const DraftStats = (props: DraftProps) => { + if (props.player.finishedPacks.length === 0) return
; + const flat = props.player.finishedPacks.flat(); + + const exceptionals = flat.filter(filterRarity("Exceptional")); + const elites = flat.filter(filterRarity("Elite")); + const uniques = flat.filter(filterRarity("Unique")); + + const sites = flat.filter((card) => card?.guardian?.type === "Site"); + const avatars = flat.filter((card) => card?.guardian?.type === "Avatar"); + + return ( + + + +

Exceptionals: {exceptionals.length.toString()}

+

Elites: {elites.length.toString()}

+

Uniques: {uniques.length.toString()}

+
+ +

Total Cards: {flat.length.toString()}

+

Sites: {sites.length.toString()}

+

Avatars: {avatars.length.toString()}

+
+
+ +

Online draft currently in development!

+
+
+ ); +}; diff --git a/src/components/organisms/Draft/helpers.ts b/src/components/organisms/Draft/helpers.ts new file mode 100644 index 0000000..192d6ce --- /dev/null +++ b/src/components/organisms/Draft/helpers.ts @@ -0,0 +1,68 @@ +import { CardDTO } from "@/utils/api/cardData/CardDataType"; + +function shuffleAndSelect(arr: CardDTO[], count = 15) { + const shuffled = arr.slice(); // Shallow copy to avoid mutating the original array + for (let i = arr.length - 1; i > arr.length - 1 - count; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; // Swap elements + } + return shuffled.slice(-count); // Return the last `count` items +} + +type Expansion = "alp" | "bet"; +export function generateBoosterPack(props: { + cardData: CardDTO[]; + expansionSlug: Expansion | "all"; +}) { + const cards = props.cardData.slice(); // shallow copy + const cardsInSet = cards.filter((card) => { + if (props.expansionSlug === "all") return true; + const setsFoundIn = card.sets.map((set) => set.slug); + return setsFoundIn.includes(props.expansionSlug); + }); + + // 3 exceptionals + const exceptionals = cardsInSet?.filter( + (card) => card.guardian.rarity === "Exceptional", + ); + + // special pull card + const pullCard = cardsInSet?.filter((card) => { + const weightedCoinFlip = Math.random() <= 0.2; // 20% chance of success + const rarity: CardDTO["guardian"]["rarity"] = weightedCoinFlip + ? "Unique" + : "Elite"; + + // const isSpecialRarity = !["Exceptional", "Ordinary"].includes( + // card.guardian.rarity, + // ); + // + const isSpecialRarity = card.guardian.rarity === rarity; + const isAvatar = card.guardian.type === "Avatar"; + return isSpecialRarity || isAvatar; + }); + + // 10 ordinaries + const ordinaries = cardsInSet?.filter((card) => { + const isOrdinary = card.guardian.rarity === "Ordinary"; + const isAvatar = card.guardian.type === "Avatar"; + const isSite = card.guardian.type === "Site"; + return isOrdinary && !isAvatar && !isSite; + }); + + // 1 ordinary land + const ordinarySite = cardsInSet?.filter((card) => { + const isOrdinary = card.guardian.rarity === "Ordinary"; + const isSite = card.guardian.type === "Site"; + return isOrdinary && isSite; + }); + + const newBooster = [ + ...shuffleAndSelect(exceptionals, 3), + ...shuffleAndSelect(pullCard, 1), + ...shuffleAndSelect(ordinaries, 10), + ...shuffleAndSelect(ordinarySite, 1), + ]; + + return newBooster; +} diff --git a/src/components/organisms/Draft/index.tsx b/src/components/organisms/Draft/index.tsx new file mode 100644 index 0000000..959ef14 --- /dev/null +++ b/src/components/organisms/Draft/index.tsx @@ -0,0 +1,54 @@ +import { DraftCard } from "@/components/organisms/Draft/Card"; +import { Grid } from "styled-system/jsx"; +import { DraftPlayerData } from "./types"; +import { Ribbon } from "./Ribbon"; +import { useMemo, useState } from "react"; +import { DraftStats } from "./Stats"; + +const hTop = "7vh"; +const hTabs = "5vh"; +const hCards = "88vh"; +export const gridHeight = { top: hTop, tabs: hTabs, cards: hCards }; + +export const DraftBoard = (props: { + player: DraftPlayerData; + setPlayerData(data: DraftPlayerData): void; +}) => { + const [activeView, setActiveView] = useState(0); + const cardView = useMemo(() => { + return props.player.finishedPacks?.[activeView]; + }, [activeView, props.player.finishedPacks.length]); + + return ( + + + + + {(!cardView || cardView?.length === 0) && ( +

No packs... yet! Click Crack a Pack!

+ )} + {cardView?.map((card, index) => ( + + ))} +
+
+ ); +}; diff --git a/src/components/organisms/Draft/types.ts b/src/components/organisms/Draft/types.ts new file mode 100644 index 0000000..210116a --- /dev/null +++ b/src/components/organisms/Draft/types.ts @@ -0,0 +1,24 @@ +import { CardDTO } from "@/utils/api/cardData/CardDataType"; + +export type BoosterPack = { + uuid: string; + playerName: string; + cards: CardDTO[]; +}; + +export type DraftPlayerData = { + joinedSessionTimestamp: number; // time of joining the session. Used for ordering + selectedCards: CardDTO[]; // cards you've picked + activePack: CardDTO[]; // pack actively picking from + pendingPacks: CardDTO[][]; // packs ready for pick (passed by other player) + finishedPacks: CardDTO[][]; // packs you've picked and ready to pass (ready to pass to other player) + deck?: CardDTO[]; // after draft, the deck you construct from selectedCards +}; + +/** + * Your individual state and setter to update it + * */ +export type DraftProps = { + player: DraftPlayerData; + setPlayerData(data: DraftPlayerData): void; +}; diff --git a/src/pages/draft/index.tsx b/src/pages/draft/index.tsx new file mode 100644 index 0000000..5a893f0 --- /dev/null +++ b/src/pages/draft/index.tsx @@ -0,0 +1,26 @@ +import { DraftBoard } from "@/components/organisms/Draft"; +import { DraftPlayerData } from "@/components/organisms/Draft/types"; +import { useState } from "react"; + +export default function DraftPage() { + const [players, setPlayers] = useState>({ + p1: { + joinedSessionTimestamp: 1, + selectedCards: [], + activePack: [], + pendingPacks: [[]], + finishedPacks: [], + }, + }); + + function setPlayer(data: DraftPlayerData) { + return setPlayers((prev) => ({ + ...prev, + p1: { + ...data, + }, + })); + } + + return ; +} diff --git a/src/pages/index.tsx b/src/pages/index.tsx index 9984c24..9a827bd 100644 --- a/src/pages/index.tsx +++ b/src/pages/index.tsx @@ -67,6 +67,14 @@ export default function Home() { }} /> + +

Draft Mode in development!

+ +

+ In the meantime, crack some packs! +

+ +
); diff --git a/src/utils/api/cardData/CardDataType.ts b/src/utils/api/cardData/CardDataType.ts new file mode 100644 index 0000000..501e513 --- /dev/null +++ b/src/utils/api/cardData/CardDataType.ts @@ -0,0 +1,55 @@ +type Thresholds = { + air: number; + earth: number; + fire: number; + water: number; +}; + +type Guardian = { + rarity: "Ordinary" | "Elite" | "Exceptional" | "Unique"; + type: "Minion" | "Magic" | "Aura" | "Artifact" | "Site" | "Avatar"; + typeText: string; + subType: string; + rulesText: string; + cost: number; + attack: number; + defence: number; + life: number | null; + thresholds: Thresholds; +}; + +type Variant = { + finish: string; + product: string; + artist: string; + flavorText: string; +}; + +type SetMetadata = { + rarity: string; + type: string; + typeText: string; + subType: string; + rulesText: string; + cost: number; + attack: number; + defence: number; + life: number | null; + thresholds: Thresholds; +}; + +type CardSet = { + name: string; + slug: string; + releasedAt: string; + type: string; + variants: Variant[]; + metadata: SetMetadata; +}; + +export type CardDTO = { + name: string; + slug: string; + guardian: Guardian; + sets: CardSet[]; +}; diff --git a/src/utils/api/cardData/api.ts b/src/utils/api/cardData/api.ts index 40f3f1f..6dc1d65 100644 --- a/src/utils/api/cardData/api.ts +++ b/src/utils/api/cardData/api.ts @@ -1,4 +1,5 @@ import axios from "axios"; +import { CardDTO } from "./CardDataType"; type CardDataMinimized = { name: string; @@ -45,3 +46,12 @@ export const TOKEN_RUBBLE: CardDataMinimized = { name: "Rubble", type: "site", }; + +/** + * FUNCTIONS FOR DRAFT + * */ + +export async function getCardsFullData() { + const res = await axios.get(`/card-data/cards.json`); + return res.data; +} diff --git a/src/utils/api/cardData/useCardData.ts b/src/utils/api/cardData/useCardData.ts index 452ca12..9b4d108 100644 --- a/src/utils/api/cardData/useCardData.ts +++ b/src/utils/api/cardData/useCardData.ts @@ -1,5 +1,5 @@ import { useQuery } from "@tanstack/react-query"; -import { getCardsData, getTokensData } from "./api"; +import { getCardsData, getCardsFullData, getTokensData } from "./api"; export const useCardData = () => { return useQuery({ @@ -16,3 +16,11 @@ export const useTokenData = () => { staleTime: Infinity, }); }; + +export const useCardFullData = () => { + return useQuery({ + queryFn: getCardsFullData, + queryKey: ["card-data-full"], + staleTime: Infinity, + }); +};