Skip to content
Merged
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
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
# 🔥⚒️🎮 **Forge: Token Crafting Game**

*(README in progress...)*

---

## **Game Overview**

**Forge** is a Web3 token crafting game where players mint, burn, and trade tokens to create rare items. Basic tokens can be minted directly, while forged tokens require burning combinations of basic tokens.

---

## **Live Demo 🌐**
👉 **Play now:** [https://forge-tokens.vercel.app/](https://forge-tokens.vercel.app)

👉 [Play now](https://forge-tokens.vercel.app)

---

## **Game Rules**

### **1. Token Categories**

| Token IDs | Type | Minting Rule |
|-----------|-----------|---------------------------------------|
| 0, 1, 2 | Basic | Mint directly (1-minute cooldown). |
| 3, 4, 5, 6| Forged | Burn specific basic tokens to mint. |

### **2. Minting Rules**

- **Basic Tokens (0, 1, 2):**
- **Action:** `mint(_tokenId)`
- **Cooldown:** 1 minute per user.
Expand All @@ -36,17 +41,20 @@
- **Token 6:** Burn 1x token 0 + 1x token 1 + 1x token 2.

### **3. Burning & Trading**

- **Burning:** Only tokens 3-6 can be burned directly.
- **Trading:** Burn any token to mint tokens 0-2 (cannot trade a token for itself).

### **Key Constraints**

- **Cooldown:** Applies only to minting tokens 0-2.
- **Single Mint:** Only 1 token per function call.
- **Ownership:** Only the Forge contract can mint/burn tokens.

---

## **How to Play**

1. **Connect your wallet** to the DApp.
2. **Mint basic tokens** (0, 1, 2) to start crafting.
3. **Burn combinations** of basic tokens to forge rare tokens (3, 4, 5, 6).
Expand All @@ -55,14 +63,17 @@
---

## **Tech Stack**

- **Frontend:** Next.js, shadcn/ui, Wagmi, Viem, TanStack Query
- **Blockchain:** Solidity, Foundry, Alchemy (WebSocket + HTTP RPC)
- **State Management:** React Context, React Query

---

## **Contracts**

### **Deployed on Sepolia**

- **Forge Game Contract**
[0xd4922b783f762feb81ceb08d6f1f4c45a8caa148](https://sepolia.etherscan.io/address/0xd4922b783f762feb81ceb08d6f1f4c45a8caa148#code)
*(Verified on Etherscan)*
Expand All @@ -72,6 +83,7 @@
*(Verified on Etherscan)*

### **Testing**

- **100% Coverage:** Both Solidity contracts (Forge + FToken) are tested with **Foundry**.

---
Expand Down
50 changes: 50 additions & 0 deletions fe/app/error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"use client";

import Link from "next/link";
import { useEffect } from "react";
import { TypographyH3 } from "@/app/_components/typography/h3";
import { Button } from "@/components/ui/button";
import {
ButtonGroup,
ButtonGroupSeparator,
} from "@/components/ui/button-group";
import { TypographyH5 } from "./_components/typography/h5";

export default function ErrorPage({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
console.error(error);
}, [error]);

return (
<div className="min-h-screen flex items-center justify-center p-6 sm:p-16 text-center">
<div className="w-full">
<TypographyH3>Something went wrong!</TypographyH3>
<TypographyH5>
An unexpected error occurred. You can try reloading this page.
</TypographyH5>

<div className="flex flex-col items-center justify-center gap-2 p-4">
<ButtonGroup>
<Button onClick={() => reset()} variant="outline">
Try Again
</Button>
<ButtonGroupSeparator />
<Button onClick={() => location.reload()} variant="default">
Reload Page
</Button>
</ButtonGroup>

<Button asChild variant="link">
<Link href="/">Go Home</Link>
</Button>
</div>
</div>
</div>
);
}
46 changes: 46 additions & 0 deletions fe/app/global-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use client";

import Link from "next/link";

export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
console.error(error);

return (
<html lang="en">
<body
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "100vh",
textAlign: "center",
fontFamily: "system-ui, sans-serif",
padding: "1rem",
}}
>
<div style={{ maxWidth: 400 }}>
<h2 style={{ fontSize: "1.5rem", marginBottom: "0.5rem" }}>
Something went wrong!
</h2>
<p style={{ fontSize: "1rem", marginBottom: "1rem" }}>
An unexpected error occurred. You can try reloading or go home.
</p>
<div
style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}
>
<button type="button" onClick={() => reset()}>
Try Again
</button>
<Link href="/">Go Home</Link>
</div>
</div>
</body>
</html>
);
}
83 changes: 83 additions & 0 deletions fe/components/ui/button-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"

const buttonGroupVariants = cva(
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
{
variants: {
orientation: {
horizontal:
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
vertical:
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
},
},
defaultVariants: {
orientation: "horizontal",
},
}
)

function ButtonGroup({
className,
orientation,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
return (
<div
role="group"
data-slot="button-group"
data-orientation={orientation}
className={cn(buttonGroupVariants({ orientation }), className)}
{...props}
/>
)
}

function ButtonGroupText({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "div"

return (
<Comp
className={cn(
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}

function ButtonGroupSeparator({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="button-group-separator"
orientation={orientation}
className={cn(
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
className
)}
{...props}
/>
)
}

export {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
buttonGroupVariants,
}
28 changes: 28 additions & 0 deletions fe/components/ui/separator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client"

import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"

import { cn } from "@/lib/utils"

function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
}

export { Separator }
1 change: 1 addition & 0 deletions fe/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8",
"@rainbow-me/rainbowkit": "^2.2.9",
Expand Down
Loading