Web3 RPG Prototype Slice - James Atlas
James AtlasWeb3 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