Web3 RPG Prototype Slice - James Atlas

Web3 RPG Prototype Slice - James Atlas

James Atlas

Web3 RPG Prototype Slice - James Atlas

Spec sample for the Monero.jobs Web3 RPG/NFT prototype brief: ERC-1155 item contract, starter-pack mint, player level state, Hardhat deploy script, and a React/Ethers wallet inventory UI.

This is intentionally scoped as a first milestone: small enough to verify quickly, then expandable into marketplace, metadata, quest rewards, and gameplay loops.

Contract: RPGItems.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

contract RPGItems is ERC1155, Ownable {
    uint256 public constant FOUNDERS_BLADE = 1;
    uint256 public constant NIGHTFALL_ARMOR = 2;
    uint256 public constant HEALING_RUNE = 3;

    mapping(address => uint256) public playerLevel;

    event PlayerLeveled(address indexed player, uint256 level);

    constructor(string memory metadataBaseUri) ERC1155(metadataBaseUri) Ownable(msg.sender) {}

    function mintStarterPack(address player) external {
        require(balanceOf(player, FOUNDERS_BLADE) == 0, "starter already minted");
        _mint(player, FOUNDERS_BLADE, 1, "");
        _mint(player, HEALING_RUNE, 5, "");
        if (playerLevel[player] == 0) {
            playerLevel[player] = 1;
            emit PlayerLeveled(player, 1);
        }
    }

    function adminMint(address player, uint256 itemId, uint256 amount) external onlyOwner {
        _mint(player, itemId, amount, "");
    }

    function recordQuestWin(address player, uint256 newLevel) external onlyOwner {
        require(newLevel > playerLevel[player], "level must increase");
        playerLevel[player] = newLevel;
        emit PlayerLeveled(player, newLevel);
    }
}

Wallet UI: App.jsx

import { useEffect, useMemo, useState } from "react";
import { BrowserProvider, Contract } from "ethers";

const ABI = [
  "function mintStarterPack(address player) external",
  "function balanceOf(address account,uint256 id) view returns (uint256)",
  "function playerLevel(address player) view returns (uint256)"
];

const ITEM_IDS = [
  { id: 1, name: "Founder's Blade" },
  { id: 2, name: "Nightfall Armor" },
  { id: 3, name: "Healing Rune" }
];

export default function App() {
  const [account, setAccount] = useState("");
  const [contractAddress, setContractAddress] = useState(import.meta.env.VITE_RPG_ITEMS || "");
  const [inventory, setInventory] = useState([]);
  const [level, setLevel] = useState("0");
  const [busy, setBusy] = useState(false);

  const hasWallet = typeof window !== "undefined" && window.ethereum;

  async function connect() {
    const [addr] = await window.ethereum.request({ method: "eth_requestAccounts" });
    setAccount(addr);
  }

  const provider = useMemo(() => {
    if (!hasWallet) return null;
    return new BrowserProvider(window.ethereum);
  }, [hasWallet]);

  async function getContract(write = false) {
    if (!provider || !contractAddress) return null;
    const signer = write ? await provider.getSigner() : provider;
    return new Contract(contractAddress, ABI, signer);
  }

  async function refresh() {
    if (!account || !contractAddress) return;
    const contract = await getContract(false);
    const rows = await Promise.all(
      ITEM_IDS.map(async (item) => ({
        ...item,
        balance: (await contract.balanceOf(account, item.id)).toString()
      }))
    );
    setInventory(rows);
    setLevel((await contract.playerLevel(account)).toString());
  }

  async function mintStarterPack() {
    setBusy(true);
    try {
      const contract = await getContract(true);
      const tx = await contract.mintStarterPack(account);
      await tx.wait();
      await refresh();
    } finally {
      setBusy(false);
    }
  }

  useEffect(() => {
    refresh();
  }, [account, contractAddress]);

  return (
    <main className="shell">
      <section className="topbar">
        <div>
          <p className="eyebrow">Web3 RPG Prototype Slice</p>
          <h1>Mint a starter pack, read inventory, track player level.</h1>
        </div>
        <button disabled={!hasWallet} onClick={connect}>
          {account ? account.slice(0, 6) + "..." + account.slice(-4) : "Connect Wallet"}
        </button>
      </section>

      <label className="field">
        Contract address
        <input value={contractAddress} onChange={(event) => setContractAddress(event.target.value)} />
      </label>

      <section className="actions">
        <button disabled={!account || !contractAddress || busy} onClick={mintStarterPack}>
          {busy ? "Minting..." : "Mint starter pack"}
        </button>
        <button disabled={!account || !contractAddress} onClick={refresh}>Refresh inventory</button>
      </section>

      <section className="inventory">
        <div className="level">Level {level}</div>
        {inventory.map((item) => (
          <article key={item.id}>
            <span>{item.name}</span>
            <strong>{item.balance}</strong>
          </article>
        ))}
      </section>
    </main>
  );
}

Deploy Script

const hre = require("hardhat");

async function main() {
  const metadataUri = process.env.METADATA_URI || "https://example.com/rpg-items/{id}.json";
  const RPGItems = await hre.ethers.getContractFactory("RPGItems");
  const contract = await RPGItems.deploy(metadataUri);
  await contract.waitForDeployment();

  console.log("RPGItems deployed:", await contract.getAddress());
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

README

# Web3 RPG Prototype Slice

Prepared by James Atlas as a fast proof sample for a Web3 RPG/NFT prototype.

## What This Covers

- ERC-1155 item contract for game items and starter packs
- Basic player level state
- Hardhat deploy script
- React/Ethers wallet UI for connecting, minting, and reading inventory
- Small enough scope to verify quickly before expanding into marketplace, metadata, and gameplay

## First Paid Milestone

Deliver a runnable repo with:

- ERC-1155 or ERC-721/1155 hybrid contracts
- Local + testnet deploy instructions
- Wallet-connected mint/inventory UI
- Example metadata files
- README and handoff notes

## Run

```bash
npm install
npm run compile
SEPOLIA_RPC_URL=... PRIVATE_KEY=... npm run deploy
VITE_RPG_ITEMS=0xYourContract npm run dev
```

## Next Expansion

- Add ERC-20 in-game currency
- Add marketplace listing/fulfillment contract
- Add backend signer for quest rewards
- Add metadata CDN/IPFS publishing
- Add admin dashboard for item drops and player progression

Report Page