Compare commits

..

3 Commits

Author SHA1 Message Date
yellow
0e691e46af session wallet
Some checks failed
CI / Foundry project (push) Has been cancelled
2024-11-01 16:45:31 +01:00
yellow
4c8f2250b7 add to boss fight 2024-11-01 16:13:41 +01:00
yellow
ef1c242471 session wallet
Some checks are pending
CI / Foundry project (push) Waiting to run
2024-11-01 13:30:56 +01:00
20 changed files with 346 additions and 902 deletions

15
app/package-lock.json generated
View File

@ -11,8 +11,6 @@
"@next/eslint-plugin-next": "^14.2.15", "@next/eslint-plugin-next": "^14.2.15",
"@rainbow-me/rainbowkit": "^2.2.0", "@rainbow-me/rainbowkit": "^2.2.0",
"@tanstack/react-query": "^5.55.3", "@tanstack/react-query": "^5.55.3",
"howler": "^2.2.4",
"jsfxr": "^1.2.2",
"next": "^14.2.10", "next": "^14.2.10",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -8976,11 +8974,6 @@
"minimalistic-crypto-utils": "^1.0.1" "minimalistic-crypto-utils": "^1.0.1"
} }
}, },
"node_modules/howler": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/howler/-/howler-2.2.4.tgz",
"integrity": "sha512-iARIBPgcQrwtEr+tALF+rapJ8qSc+Set2GJQl7xT1MQzWaVkFebdJhR3alVlSiUf5U7nAANKuj3aWpwerocD5w=="
},
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@ -10124,14 +10117,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/jsfxr": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/jsfxr/-/jsfxr-1.2.2.tgz",
"integrity": "sha512-aBtNHZ/eJVZ3Q12HLj6F0eF20bRJTar6fjHf14zZ/Co5GzcVsEBujJO7IKwAhZS3Pe0xIvUOD3O1BoZ6ij0xhA==",
"bin": {
"sfxr-to-wav": "sfxr-to-wav"
}
},
"node_modules/json-buffer": { "node_modules/json-buffer": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",

View File

@ -12,8 +12,6 @@
"@next/eslint-plugin-next": "^14.2.15", "@next/eslint-plugin-next": "^14.2.15",
"@rainbow-me/rainbowkit": "^2.2.0", "@rainbow-me/rainbowkit": "^2.2.0",
"@tanstack/react-query": "^5.55.3", "@tanstack/react-query": "^5.55.3",
"howler": "^2.2.4",
"jsfxr": "^1.2.2",
"next": "^14.2.10", "next": "^14.2.10",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",

Binary file not shown.

Binary file not shown.

View File

@ -4,67 +4,30 @@ import styles from "../styles/Modal.module.css";
import bgStyles from "../styles/Background.module.css"; import bgStyles from "../styles/Background.module.css";
import { bossToName, bossToReward } from "./BossInfo"; import { bossToName, bossToReward } from "./BossInfo";
import { bossLevelToClass } from "./Boss"; import { bossLevelToClass } from "./Boss";
import { lostSound, wonSound } from "../utils/soundsEffect";
import { useEffect } from "react";
interface BossOutcomeModalProps { interface BossOutcomeModalProps {
setIsOpen: (val: boolean) => void; setIsOpen: (val: boolean) => void,
} }
const BossOutcomeModal = ({ setIsOpen }: BossOutcomeModalProps) => { const BossOutcomeModal = ({ setIsOpen }: BossOutcomeModalProps) => {
const { lastBossResult } = usePlayer(); const { lastBossResult } = usePlayer();
const outcome = lastBossResult?.reward != BigInt(0);
const ascended = lastBossResult?.prestigeGained;
useEffect(() => {
if (lastBossResult != null) {
if (outcome) {
wonSound();
} else {
lostSound();
}
}
}, [outcome, lastBossResult])
if (lastBossResult == null) return null; if (lastBossResult == null) return null;
const text = outcome ? ( const outcome = lastBossResult.reward != BigInt(0);
<span> const ascended = lastBossResult.prestigeGained;
and you <strong className={styles.won}>won!</strong> 🤩
</span> const text = outcome ? <span>and you <strong className={styles.won}>won!</strong> 🤩</span> : <span>and you <strong className={styles.lost}>lost</strong> 😔</span>;
) : ( const rewardAmount = parseFloat(parseFloat(formatUnits(bossToReward[lastBossResult.level], 18).toString()).toFixed(4));
<span> const rewardText =
and you <strong className={styles.lost}>lost</strong> 😔 ascended ? <p>You won <strong>{rewardAmount} RGCVII</strong> and <strong>ASCENDED!!!</strong>. This means you beat the bosses and gained a <strong>Prestige level</strong>. Your GELD is now forfeit, but your legend lives on.</p>
</span> : outcome ? <p>You won <strong>{rewardAmount} RGCVII</strong></p>
); : <p>Your GELD is now forfeit.<br />Try again 💪 we know you can do it!</p>
const rewardAmount = parseFloat(
parseFloat(
formatUnits(bossToReward[lastBossResult.level], 18).toString()
).toFixed(4)
);
const rewardText = ascended ? (
<p>
You won <strong>{rewardAmount} RGCVII</strong> and{" "}
<strong>ASCENDED!!!</strong>. This means you beat the bosses and gained a{" "}
<strong>Prestige level</strong>. Your GELD is now forfeit, but your legend
lives on.
</p>
) : outcome ? (
<p>
You won <strong>{rewardAmount} RGCVII</strong>
</p>
) : (
<p>
Your GELD is now forfeit.
<br />
Try again 💪 we know you can do it!
</p>
);
const bossName = bossToName[lastBossResult.variant]; const bossName = bossToName[lastBossResult.variant];
const bossClass = bossLevelToClass[lastBossResult.variant]; const bossClass = bossLevelToClass[lastBossResult.variant];
return ( return <div className={`${styles.modal} ${styles.bossModal}`}>
<div className={`${styles.modal} ${styles.bossModal}`}>
<h2>You battled {bossName} Moloch!</h2> <h2>You battled {bossName} Moloch!</h2>
<div className={`${bgStyles.boss} ${bossClass} ${styles.image}`} /> <div className={`${bgStyles.boss} ${bossClass} ${styles.image}`} />
<p className={styles.outcome}>{text}</p> <p className={styles.outcome}>{text}</p>
@ -73,7 +36,6 @@ const BossOutcomeModal = ({ setIsOpen }: BossOutcomeModalProps) => {
<button onClick={() => setIsOpen(false)}>Onward!</button> <button onClick={() => setIsOpen(false)}>Onward!</button>
</div> </div>
</div> </div>
); }
};
export default BossOutcomeModal; export default BossOutcomeModal

View File

@ -1,85 +0,0 @@
import { useEffect, useState } from 'react';
import styles from '../styles/Dashboard.module.css';
interface MetricsData {
totalPlayers: number;
totalRuns: number;
activePlayers24h: number;
totalBossesDefeated: number;
totalPrestigeLevels: number;
}
const SUBGRAPH_URL = "https://api.studio.thegraph.com/query/75782/slay-the-moloch-base-mainnet/version/latest";
const Dashboard = () => {
const [metrics, setMetrics] = useState<MetricsData>();
useEffect(() => {
const fetchMetrics = async () => {
try {
const response = await fetch(SUBGRAPH_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `{
# Get global stats
globalStat(id: "1") {
totalPlayers
totalBossesDefeated
totalPrestigeLevels
totalRuns
}
# Get active players in last 24h
players(where: { lastRaidedAt_gt: "${Math.floor(Date.now() / 1000) - 86400}" }) {
id
}
}`
})
});
const data = await response.json();
setMetrics({
totalPlayers: parseInt(data.data.globalStat.totalPlayers),
totalRuns: parseInt(data.data.globalStat.totalRuns),
activePlayers24h: data.data.players.length,
totalBossesDefeated: parseInt(data.data.globalStat.totalBossesDefeated),
totalPrestigeLevels: parseInt(data.data.globalStat.totalPrestigeLevels)
});
} catch (error) {
console.error("Error fetching metrics:", error);
}
};
fetchMetrics();
const interval = setInterval(fetchMetrics, 30000); // Refresh every 30 seconds
return () => clearInterval(interval);
}, []);
return (
<div className={styles.dashboard}>
<div className={styles.metric}>
<h3>Total Players</h3>
<p>{metrics?.totalPlayers || 0}</p>
</div>
<div className={styles.metric}>
<h3>Total Game Runs</h3>
<p>{metrics?.totalRuns || 0}</p>
</div>
<div className={styles.metric}>
<h3>Active Players (24h)</h3>
<p>{metrics?.activePlayers24h || 0}</p>
</div>
<div className={styles.metric}>
<h3>Total Bosses Defeated</h3>
<p>{metrics?.totalBossesDefeated || 0}</p>
</div>
<div className={styles.metric}>
<h3>Total Prestige Levels</h3>
<p>{metrics?.totalPrestigeLevels || 0}</p>
</div>
</div>
);
};
export default Dashboard;

View File

@ -1,31 +1,22 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from 'react'
import styles from "../styles/Leaderboard.module.css"; import styles from '../styles/Leaderboard.module.css'
import { import { TopEarnersResponse, TopRaidersResponse } from '../types/leaderboard'
TopEarnersResponse, import { formatUnits } from 'viem'
TopRaidersResponse,
PlayerResponse,
} from "../types/leaderboard";
import { formatUnits } from "viem";
const SUBGRAPH_URL = const SUBGRAPH_URL = 'https://api.studio.thegraph.com/query/75782/slay-the-moloch-base-sepolia/version/latest'
"https://api.studio.thegraph.com/query/75782/slay-the-moloch-base-mainnet/version/latest";
const Leaderboard = () => { const Leaderboard = () => {
const [topEarners, setTopEarners] = useState<TopEarnersResponse>(); const [topEarners, setTopEarners] = useState<TopEarnersResponse>()
const [topRaiders, setTopRaiders] = useState<TopRaidersResponse>(); const [topRaiders, setTopRaiders] = useState<TopRaidersResponse>()
const [activeTab, setActiveTab] = useState< const [activeTab, setActiveTab] = useState<'earners' | 'raiders'>('earners')
"earners" | "raiders" | "bosses" | "prestige"
>("earners");
const [bossesDefeated, setBossesDefeated] = useState<PlayerResponse>();
const [playerPrestige, setPlayerPrestige] = useState<PlayerResponse>();
useEffect(() => { useEffect(() => {
const fetchLeaderboards = async () => { const fetchLeaderboards = async () => {
try { try {
// Fetch top earners // Fetch top earners
const earnersResponse = await fetch(SUBGRAPH_URL, { const earnersResponse = await fetch(SUBGRAPH_URL, {
method: "POST", method: 'POST',
headers: { "Content-Type": "application/json" }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
query: `{ query: `{
armies(first: 10, orderBy: profitPerSecond, orderDirection: desc) { armies(first: 10, orderBy: profitPerSecond, orderDirection: desc) {
@ -41,16 +32,16 @@ const Leaderboard = () => {
anointedLevel anointedLevel
championLevel championLevel
} }
}`, }`
}), })
}); })
const earnersData = await earnersResponse.json(); const earnersData = await earnersResponse.json()
setTopEarners({ armies: earnersData.data.armies }); setTopEarners({ armies: earnersData.data.armies })
// Fetch top raiders // Fetch top raiders
const raidersResponse = await fetch(SUBGRAPH_URL, { const raidersResponse = await fetch(SUBGRAPH_URL, {
method: "POST", method: 'POST',
headers: { "Content-Type": "application/json" }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
query: `{ query: `{
players(first: 10, orderBy: numberOfRaids, orderDirection: desc) { players(first: 10, orderBy: numberOfRaids, orderDirection: desc) {
@ -59,52 +50,20 @@ const Leaderboard = () => {
totalMinted totalMinted
currentBalance currentBalance
} }
}`, }`
}), })
}); })
const raidersData = await raidersResponse.json(); const raidersData = await raidersResponse.json()
setTopRaiders({ players: raidersData.data.players }); setTopRaiders({ players: raidersData.data.players })
// Fetch bosses defeated
const bossesDefeatedResponse = await fetch(SUBGRAPH_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `{
players(first: 10, orderBy: bossesDefeated, orderDirection: desc) {
id
bossesDefeated
}
}`,
}),
});
const bossesDefeatedData = await bossesDefeatedResponse.json();
setBossesDefeated({ players: bossesDefeatedData.data.players });
// Fetch player prestige
const playerPrestigeResponse = await fetch(SUBGRAPH_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `{
players(first: 10, orderBy: prestigeLevel, orderDirection: desc) {
id
prestigeLevel
}
}`,
}),
});
const playerPrestigeData = await playerPrestigeResponse.json();
setPlayerPrestige({ players: playerPrestigeData.data.players });
} catch (error) { } catch (error) {
console.error("Error fetching leaderboard:", error); console.error('Error fetching leaderboard:', error)
}
} }
};
fetchLeaderboards(); fetchLeaderboards()
const interval = setInterval(fetchLeaderboards, 30000); // Refresh every 30 seconds const interval = setInterval(fetchLeaderboards, 30000) // Refresh every 30 seconds
return () => clearInterval(interval); return () => clearInterval(interval)
}, []); }, [])
return ( return (
<div className={styles.leaderboard}> <div className={styles.leaderboard}>
@ -112,106 +71,44 @@ const Leaderboard = () => {
<div className={styles.tabs}> <div className={styles.tabs}>
<button <button
className={`${styles.tab} ${ className={`${styles.tab} ${activeTab === 'earners' ? styles.active : ''}`}
activeTab === "earners" ? styles.active : "" onClick={() => setActiveTab('earners')}
}`}
onClick={() => setActiveTab("earners")}
> >
Top Earners Top Earners
</button> </button>
<button <button
className={`${styles.tab} ${ className={`${styles.tab} ${activeTab === 'raiders' ? styles.active : ''}`}
activeTab === "raiders" ? styles.active : "" onClick={() => setActiveTab('raiders')}
}`}
onClick={() => setActiveTab("raiders")}
> >
Top Raiders Top Raiders
</button> </button>
<button
className={`${styles.tab} ${
activeTab === "bosses" ? styles.active : ""
}`}
onClick={() => setActiveTab("bosses")}
>
Top Boss Slayers
</button>
<button
className={`${styles.tab} ${
activeTab === "prestige" ? styles.active : ""
}`}
onClick={() => setActiveTab("prestige")}
>
Top Players by Prestige
</button>
</div> </div>
{activeTab === "earners" && ( {activeTab === 'earners' && (
<div className={styles.list}> <div className={styles.list}>
{topEarners?.armies.map((army, index) => ( {topEarners?.armies.map((army, index) => (
<div key={army.player.id} className={styles.item}> <div key={army.player.id} className={styles.item}>
<span className={styles.rank}>#{index + 1}</span> <span className={styles.rank}>#{index + 1}</span>
<span className={styles.address}> <span className={styles.address}>{army.player.id.slice(0, 6)}...{army.player.id.slice(-4)}</span>
{army.player.id.slice(0, 6)}... <span className={styles.stat}>{formatUnits(BigInt(army.profitPerSecond), 4)} GELD/s</span>
{army.player.id.slice(-4)}
</span>
<span className={styles.stat}>
{formatUnits(BigInt(army.profitPerSecond), 4)}{" "}
GELD/s
</span>
</div> </div>
))} ))}
</div> </div>
)} )}
{activeTab === "raiders" && ( {activeTab === 'raiders' && (
<div className={styles.list}> <div className={styles.list}>
{topRaiders?.players.map((player, index) => ( {topRaiders?.players.map((player, index) => (
<div key={player.id} className={styles.item}> <div key={player.id} className={styles.item}>
<span className={styles.rank}>#{index + 1}</span> <span className={styles.rank}>#{index + 1}</span>
<span className={styles.address}> <span className={styles.address}>{player.id.slice(0, 6)}...{player.id.slice(-4)}</span>
{player.id.slice(0, 6)}...{player.id.slice(-4)} <span className={styles.stat}>{player.numberOfRaids} raids</span>
</span>
<span className={styles.stat}>
{player.numberOfRaids} raids
</span>
</div>
))}
</div>
)}
{activeTab === "bosses" && (
<div className={styles.list}>
{bossesDefeated?.players.map((player, index) => (
<div key={player.id} className={styles.item}>
<span className={styles.rank}>#{index + 1}</span>
<span className={styles.address}>
{player.id.slice(0, 6)}...{player.id.slice(-4)}
</span>
<span className={styles.stat}>
{player.bossesDefeated} Bosses Slain
</span>
</div>
))}
</div>
)}
{activeTab === "prestige" && (
<div className={styles.list}>
{playerPrestige?.players.map((player, index) => (
<div key={player.id} className={styles.item}>
<span className={styles.rank}>#{index + 1}</span>
<span className={styles.address}>
{player.id.slice(0, 6)}...{player.id.slice(-4)}
</span>
<span className={styles.stat}>
{player.prestigeLevel} Prestige Level
</span>
</div> </div>
))} ))}
</div> </div>
)} )}
</div> </div>
); )
}; }
export default Leaderboard; export default Leaderboard

View File

@ -8,7 +8,6 @@ import Leaderboard from "./Leaderboard";
import { usePlayer } from "../providers/PlayerProvider"; import { usePlayer } from "../providers/PlayerProvider";
import Boss from "./Boss"; import Boss from "./Boss";
import BossInfo from "./BossInfo"; import BossInfo from "./BossInfo";
import Link from "next/link";
const bossToMountainsClass = { const bossToMountainsClass = {
0: styles.mountains0, 0: styles.mountains0,
@ -54,7 +53,6 @@ const Scene = () => {
> >
🏆 <span className={styles.hideMobile}>Top players</span> 🏆 <span className={styles.hideMobile}>Top players</span>
</button> </button>
<Link href="/metrics" className={styles.metricsButton}>📈 <span className={styles.hideMobile}>Game metrics</span></Link>
{isLeaderboardOpen && ( {isLeaderboardOpen && (
<div className={styles.leaderboardOverlay}> <div className={styles.leaderboardOverlay}>
<div className={styles.leaderboardContent}> <div className={styles.leaderboardContent}>

View File

@ -8,10 +8,8 @@ import { RainbowKitProvider, midnightTheme } from "@rainbow-me/rainbowkit";
import { config } from "../wagmi"; import { config } from "../wagmi";
import { Press_Start_2P, Texturina } from "next/font/google"; import { Press_Start_2P, Texturina } from "next/font/google";
import PlayerProvider from "../providers/PlayerProvider"; import PlayerProvider from "../providers/PlayerProvider";
import ModalProvider from "../providers/ModalProvider"; import ModalProvider from '../providers/ModalProvider';
import Script from "next/script";
import { useEffect } from "react";
import { clickSound } from "../utils/soundsEffect";
const client = new QueryClient(); const client = new QueryClient();
const font = Texturina({ weight: ["400"], subsets: ["latin"] }); const font = Texturina({ weight: ["400"], subsets: ["latin"] });
@ -20,18 +18,6 @@ const font = Texturina({ weight: ["400"], subsets: ["latin"] });
const fontPixel = Press_Start_2P({ weight: ["400"], subsets: ["latin"] }); const fontPixel = Press_Start_2P({ weight: ["400"], subsets: ["latin"] });
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
useEffect(() => {
window.addEventListener("click", () => {
clickSound();
});
return () => {
window.removeEventListener("click", () => {
clickSound();
});
};
}, []);
return ( return (
<WagmiProvider config={config}> <WagmiProvider config={config}>
<QueryClientProvider client={client}> <QueryClientProvider client={client}>
@ -60,14 +46,6 @@ function MyApp({ Component, pageProps }: AppProps) {
`}</style> `}</style>
<PlayerProvider> <PlayerProvider>
<ModalProvider> <ModalProvider>
<Script
src="https://sfxr.me/riffwave.js"
strategy="beforeInteractive"
/>
<Script
src="https://sfxr.me/sfxr.js"
strategy="beforeInteractive"
/>
<Component {...pageProps} /> <Component {...pageProps} />
</ModalProvider> </ModalProvider>
</PlayerProvider> </PlayerProvider>

View File

@ -1,15 +0,0 @@
import Dashboard from '../components/Dashboard';
import styles from '../styles/Metrics.module.css';
import Link from 'next/link';
const MetricsPage = () => {
return (
<div className={styles.metricsPage}>
<Link href="/" className={styles.backLink}> Back to game</Link>
<h1>Game Metrics</h1>
<Dashboard />
</div>
);
};
export default MetricsPage;

View File

@ -1,55 +1,38 @@
import React, { import React, { createContext, ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react'
createContext, import { useAccount, useReadContract, useWriteContract } from 'wagmi'
ReactNode, import contractAbi from "../../../out/RaidGeld.sol/RaidGeld.json"
useCallback, import { Hash, parseEther } from 'viem'
useContext, import contracts from '../../contract_address'
useEffect, import WaitingForTxModal from '../components/WaitingForTxModal'
useRef, import BossOutcomeModal from '../components/BossOutcomeModal'
useState, import styles from "../styles/Background.module.css"
} from "react";
import { useAccount, useReadContract, useWriteContract } from "wagmi";
import contractAbi from "../../../out/RaidGeld.sol/RaidGeld.json";
import { Hash, parseEther } from "viem";
import contracts from "../../contract_address";
import WaitingForTxModal from "../components/WaitingForTxModal";
import BossOutcomeModal from "../components/BossOutcomeModal";
import styles from "../styles/Background.module.css";
import { coinSound } from "../utils/soundsEffect";
const { contractAddress, daoTokenAddress } = contracts; const { contractAddress, daoTokenAddress } = contracts
const abi = contractAbi.abi; const abi = contractAbi.abi
export type UnitType = 0 | 1 | 2 | 3; export type UnitType = 0 | 1 | 2 | 3
export type BossLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6; export type BossLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6
export interface Player { export interface Player {
created_at: bigint; created_at: bigint,
last_raided_at: bigint; last_raided_at: bigint,
total_minted: bigint; total_minted: bigint
total_rewards: bigint; total_rewards: bigint,
n_runs: number; n_runs: number,
prestige_level: number; prestige_level: number,
is_registered: boolean; is_registered: boolean,
has_active_session: boolean; has_active_session: boolean,
} }
export interface Army { export interface Army {
anointed: { level: number }; anointed: { level: number }
apprentice: { level: number }; apprentice: { level: number }
champion: { level: number }; champion: { level: number }
moloch_denier: { level: number }; moloch_denier: { level: number }
profit_per_second: bigint; profit_per_second: bigint
} }
export interface Boss { export interface Boss {
level: BossLevel; level: BossLevel;
variants: [ variants: [BossLevel, BossLevel, BossLevel, BossLevel, BossLevel, BossLevel, BossLevel]
BossLevel,
BossLevel,
BossLevel,
BossLevel,
BossLevel,
BossLevel,
BossLevel
];
} }
export interface LastBossResult { export interface LastBossResult {
@ -61,14 +44,14 @@ export interface LastBossResult {
} }
export interface PlayerContextType { export interface PlayerContextType {
isRegistered: boolean; isRegistered: boolean,
player: null | Player; player: null | Player,
army: null | Army; army: null | Army,
boss: null | Boss; boss: null | Boss,
lastBossResult: null | LastBossResult; lastBossResult: null | LastBossResult,
balance: bigint; balance: bigint,
register: (arg: "ETH" | "RGCVII") => void; register: (arg: "ETH" | "RGCVII") => void,
raid: () => void; raid: () => void,
battleWithBoss: () => void; battleWithBoss: () => void;
addUnit: (unit: UnitType, amount?: number) => void; addUnit: (unit: UnitType, amount?: number) => void;
} }
@ -80,199 +63,169 @@ const PlayerContext = createContext<PlayerContextType>({
boss: null, boss: null,
lastBossResult: null, lastBossResult: null,
balance: BigInt(0), balance: BigInt(0),
register: () => {}, register: () => { },
raid: () => {}, raid: () => { },
battleWithBoss: () => {}, battleWithBoss: () => { },
addUnit: () => {}, addUnit: () => { }
}); });
const PlayerProvider = ({ children }: { children: ReactNode }) => { const PlayerProvider = ({ children }: { children: ReactNode }) => {
const { address, isConnected } = useAccount(); const { address, isConnected } = useAccount();
const { writeContract, error } = useWriteContract(); const { writeContract, error } = useWriteContract();
const [[txHash, callbackFn], setHashAndCallback] = useState< const [[txHash, callbackFn], setHashAndCallback] = useState<[Hash | null, () => void]>([null, () => { }])
[Hash | null, () => void]
>([null, () => {}]);
const [bossBattledModalOpen, setBossBattlesModalOpen] = useState(false); const [bossBattledModalOpen, setBossBattlesModalOpen] = useState(false);
const hasFetchedLastBossFirstTime = useRef(false); const hasFetchedLastBossFirstTime = useRef(false);
useEffect(() => { useEffect(() => {
console.warn(error); console.warn(error)
}, [error]); }, [error])
const resetHashAndCallback = useCallback(() => { const resetHashAndCallback = useCallback(() => {
setHashAndCallback([null, () => {}]); setHashAndCallback([null, () => { }])
}, []); }, [])
const { data: isRegistered } = useReadContract({ const { data: isRegistered } = useReadContract({
address: contractAddress, address: contractAddress,
abi, abi,
functionName: "isRegistered", functionName: 'isRegistered',
args: [address], args: [address],
query: { query: {
enabled: isConnected, enabled: isConnected,
refetchInterval: 10000, refetchInterval: 15,
}, }
}); });
const { data: balance } = useReadContract({ const { data: balance, } = useReadContract({
address: contractAddress, address: contractAddress,
abi, abi,
functionName: "balanceOf", functionName: 'balanceOf',
args: [address], args: [address],
query: { query: {
refetchInterval: 10000, refetchInterval: 15,
enabled: isConnected, enabled: isConnected
}, }
}); });
const { data: player } = useReadContract({ const { data: player } = useReadContract({
address: contractAddress, address: contractAddress,
abi, abi,
functionName: "getPlayer", functionName: 'getPlayer',
args: [address], args: [address],
query: { query: {
enabled: isConnected, enabled: isConnected,
refetchInterval: 10000, refetchInterval: 15
}, }
}); });
const { data: army } = useReadContract({ const { data: army } = useReadContract({
address: contractAddress, address: contractAddress,
abi, abi,
functionName: "getArmy", functionName: 'getArmy',
args: [address], args: [address],
query: { query: {
enabled: isConnected, enabled: isConnected,
refetchInterval: 10000, refetchInterval: 15
}, }
}); });
const { data: boss } = useReadContract({ const { data: boss } = useReadContract({
address: contractAddress, address: contractAddress,
abi, abi,
functionName: "getBoss", functionName: 'getBoss',
args: [address], args: [address],
query: { query: {
enabled: isConnected, enabled: isConnected,
refetchInterval: 10000, refetchInterval: 15
}, }
}); });
const { data: lastBossResult } = useReadContract({ const { data: lastBossResult } = useReadContract({
address: contractAddress, address: contractAddress,
abi, abi,
functionName: "getLastBossResult", functionName: 'getLastBossResult',
args: [address], args: [address],
query: { query: {
enabled: isConnected, enabled: isConnected,
refetchInterval: 10000, refetchInterval: 15
}, }
}); });
const register = useCallback( console.log(balance, player, army, boss)
(arg: "RGCVII" | "ETH") => {
if (arg === "ETH") { const register = useCallback((arg: "RGCVII" | "ETH") => {
writeContract( if (arg === 'ETH') {
{ writeContract({
abi, abi,
address: contractAddress, address: contractAddress,
functionName: "register_eth", functionName: 'register_eth',
value: parseEther("0.00045"), value: parseEther("0.00045"),
}, }, {
{
onSuccess: (hash) => { onSuccess: (hash) => {
setHashAndCallback([hash, resetHashAndCallback]); setHashAndCallback([hash, resetHashAndCallback])
}, },
onError: () => resetHashAndCallback(), onError: () => resetHashAndCallback()
} })
);
} else if (arg === "RGCVII") { } else if (arg === "RGCVII") {
writeContract( writeContract({
{
abi, abi,
address: daoTokenAddress, address: daoTokenAddress,
functionName: "approve", functionName: 'approve',
args: [contractAddress, parseEther("400")], args: [contractAddress, parseEther("400")],
}, }, {
{
onSuccess: (hash) => { onSuccess: (hash) => {
setHashAndCallback([ setHashAndCallback([
hash, hash,
() => () => writeContract({
writeContract(
{
abi, abi,
address: contractAddress, address: contractAddress,
functionName: "register_dao", functionName: 'register_dao',
}, }, {
{
onSuccess: (hash) => { onSuccess: (hash) => {
setHashAndCallback([hash, resetHashAndCallback]); setHashAndCallback([hash, resetHashAndCallback])
}, },
onError: () => resetHashAndCallback(), onError: () => resetHashAndCallback()
} })
), ])
]);
}, },
onError: () => resetHashAndCallback(), onError: () => resetHashAndCallback()
});
} }
); }, [writeContract, resetHashAndCallback])
}
},
[writeContract, resetHashAndCallback]
);
const raid = useCallback(() => { const raid = useCallback(() => {
writeContract( writeContract({
{
abi, abi,
address: contractAddress, address: contractAddress,
functionName: "raid", functionName: 'raid',
}, }, {
{
onSuccess: (hash) => { onSuccess: (hash) => {
setHashAndCallback([hash, resetHashAndCallback]); setHashAndCallback([hash, resetHashAndCallback])
}, },
onError: () => resetHashAndCallback(), onError: () => resetHashAndCallback()
} })
); }, [writeContract, resetHashAndCallback])
}, [writeContract, resetHashAndCallback]);
const addUnit = useCallback( const addUnit = useCallback((unit: UnitType) => {
(unit: UnitType) => { writeContract({
writeContract(
{
abi, abi,
address: contractAddress, address: contractAddress,
functionName: "addUnit", functionName: 'addUnit',
args: [unit, 1], args: [unit, 1]
}, })
{ }, [writeContract])
onSuccess: () => {
coinSound();
},
onError: () => resetHashAndCallback(),
}
);
},
[writeContract, resetHashAndCallback]
);
const battleWithBoss = useCallback(() => { const battleWithBoss = useCallback(() => {
writeContract( writeContract({
{
abi, abi,
address: contractAddress, address: contractAddress,
functionName: "battle_with_boss", functionName: 'battle_with_boss',
}, }, {
{
onSuccess: (hash) => { onSuccess: (hash) => {
setHashAndCallback([hash, () => resetHashAndCallback()]); setHashAndCallback([hash, () => resetHashAndCallback()])
}, },
onError: () => resetHashAndCallback(), onError: () => resetHashAndCallback()
} })
); }, [writeContract, resetHashAndCallback])
}, [writeContract, resetHashAndCallback]);
useEffect(() => { useEffect(() => {
if (lastBossResult != null) { if (lastBossResult != null) {
@ -282,11 +235,10 @@ const PlayerProvider = ({ children }: { children: ReactNode }) => {
hasFetchedLastBossFirstTime.current = true; hasFetchedLastBossFirstTime.current = true;
} }
} }
}, [lastBossResult]); }, [lastBossResult])
return ( return (
<PlayerContext.Provider <PlayerContext.Provider value={{
value={{
isRegistered: isRegistered as boolean, isRegistered: isRegistered as boolean,
player: player as Player, player: player as Player,
army: army as Army, army: army as Army,
@ -296,26 +248,20 @@ const PlayerProvider = ({ children }: { children: ReactNode }) => {
register, register,
raid, raid,
addUnit, addUnit,
battleWithBoss, battleWithBoss
}} }}>
>
{children} {children}
<div <div className={`${(txHash || bossBattledModalOpen) ? styles.leaderboardOverlay : ""}`}>
className={`${
txHash || bossBattledModalOpen ? styles.leaderboardOverlay : ""
}`}
>
{txHash && <WaitingForTxModal hash={txHash} callbackFn={callbackFn} />} {txHash && <WaitingForTxModal hash={txHash} callbackFn={callbackFn} />}
{bossBattledModalOpen && ( {bossBattledModalOpen && <BossOutcomeModal setIsOpen={setBossBattlesModalOpen} />}
<BossOutcomeModal setIsOpen={setBossBattlesModalOpen} />
)}
</div> </div>
</PlayerContext.Provider> </PlayerContext.Provider>
); );
}; }
export const usePlayer = () => { export const usePlayer = () => {
return useContext(PlayerContext); return useContext(PlayerContext);
}; }
export default PlayerProvider
export default PlayerProvider;

View File

@ -402,8 +402,7 @@
} }
} }
.leaderboardButton, .leaderboardButton {
.metricsButton {
position: absolute; position: absolute;
top: 30px; top: 30px;
left: 80px; left: 80px;
@ -426,12 +425,6 @@
} }
} }
} }
.metricsButton {
left: auto;
top: auto;
right: 32px;
bottom: 32px;
}
.leaderboardButton:hover { .leaderboardButton:hover {
transform: scale(1.1); transform: scale(1.1);

View File

@ -1,29 +0,0 @@
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
padding: 2rem;
max-width: 1200px;
margin: 0 auto;
}
.metric {
background: rgba(0, 0, 0, 0.8);
border-radius: 8px;
padding: 1.5rem;
text-align: center;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.metric h3 {
color: #888;
margin: 0 0 1rem 0;
font-size: 1.1rem;
}
.metric p {
color: #fff;
font-size: 2rem;
margin: 0;
font-weight: bold;
}

View File

@ -1,23 +0,0 @@
.metricsPage {
min-height: 100vh;
padding: 2rem;
background: #1a1a1a;
color: white;
}
.metricsPage h1 {
text-align: center;
margin-bottom: 3rem;
}
.backLink {
display: inline-block;
color: #888;
text-decoration: none;
margin-bottom: 2rem;
transition: color 0.2s;
}
.backLink:hover {
color: white;
}

View File

@ -1,31 +1,25 @@
export interface Player { export interface Player {
id: string; id: string
totalMinted: string; totalMinted: string
currentBalance: string; currentBalance: string
numberOfRaids: string; numberOfRaids: string
army?: Army; army?: Army
bossesDefeated?: string; }
prestigeLevel: string;
}
export interface Army { export interface Army {
player: Player; player: Player
profitPerSecond: string; profitPerSecond: string
projectedDailyEarnings: string; projectedDailyEarnings: string
molochDenierLevel: string; molochDenierLevel: string
apprenticeLevel: string; apprenticeLevel: string
anointedLevel: string; anointedLevel: string
championLevel: string; championLevel: string
} }
export interface TopEarnersResponse { export interface TopEarnersResponse {
armies: Army[]; armies: Army[]
} }
export interface TopRaidersResponse { export interface TopRaidersResponse {
players: Player[]; players: Player[]
} }
export interface PlayerResponse {
players: Player[];
}

View File

@ -1,44 +0,0 @@
// https://sfxr.me/
// https://github.com/chr15m/jsfxr?tab=readme-ov-file#library
// https://github.com/goldfire/howler.js
import { sfxr } from "jsfxr";
import { Howl } from "howler";
export const coinSound = () => {
const coin = sfxr.toAudio(
"34T6PkpUzHPSs4CvSaNXpQ6fftpW1yrCDSda6oECJ5CH6BEaokmoZC7aAra2xef61iP6srxUaRZUk8Z2DRJHNMEvtWCLjgKkUCFtpxPe9o8AvYBCJZG1YNNPR"
);
coin.play();
};
export const errorSound = () => {
const fail = sfxr.toAudio(
"3mLuemG9nym33ak7ot6gTcNTFBBnNkcF4rmQFkh1zRhrvJ6totmE1EX61m9LTW9KWGuQpQEMqnVopubShwmxqQK7vAZYMXKbJCxYE9bcTh2qMm9JbMRJAKD5a"
);
fail.play();
};
export const clickSound = () => {
const click = sfxr.toAudio(
"7BMHBGPkXasqBZ54qHeMQTKSwDs2Y176H4hQVNkvQPg5eZyckEhyzKTnAZfqnp9ayL5iPVRNXFNjXAXBbUKhT7U6c1hKZBgWzaWkWTQvmcrCwikKi3RoF7wbd"
);
click.play();
};
export const lostSound = () => {
const fail = new Howl({
src: ["/sounds/lost.wav"],
volume: 0.7
});
fail.play();
};
export const wonSound = () => {
const won = new Howl({
src: ["/sounds/arcade_win.wav"],
});
won.play();
};

View File

@ -9,9 +9,9 @@ export const config = getDefaultConfig({
appName: 'RainbowKit App', appName: 'RainbowKit App',
projectId: 'YOUR_PROJECT_ID', projectId: 'YOUR_PROJECT_ID',
chains: [ chains: [
base,
foundry,
baseSepolia, baseSepolia,
foundry,
base,
], ],
ssr: true, ssr: true,
}); });

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -20,11 +20,16 @@ contract RaidGeld is ERC20, Ownable, Constants {
uint256 public BUY_IN_DAO_TOKEN_AMOUNT; uint256 public BUY_IN_DAO_TOKEN_AMOUNT;
uint256 public constant INITIAL_GELD = 50 * MANTISSA; uint256 public constant INITIAL_GELD = 50 * MANTISSA;
uint256 public constant SACRIFICE_SHARE = 1e3; // 10% uint256 public constant SACRIFICE_SHARE = 1e3; // 10%
uint256 public constant SESSION_WALLET_FUNDING_CAP = 0.001 ether;
mapping(address => Player) private players; mapping(address => Player) private players;
mapping(address => Army) private armies; mapping(address => Army) private armies;
mapping(address => Boss) private bosses; mapping(address => Boss) private bosses;
mapping(address => LastBossResult) private lastBossResults; mapping(address => LastBossResult) private lastBossResults;
mapping(address => address) private playerToSessionWallet;
mapping(address => address) private sessionWalletToPlayer;
mapping(address => address) private proposedSessionWallets;
// WETH // WETH
IWETH public immutable weth = IWETH(WETH); IWETH public immutable weth = IWETH(WETH);
// RGCVII token // RGCVII token
@ -92,7 +97,10 @@ contract RaidGeld is ERC20, Ownable, Constants {
} }
modifier onlyActiveSession() { modifier onlyActiveSession() {
require(players[msg.sender].has_active_session, "Session is not active, you need to buy into the game first"); address delegatedWallet = playerToSessionWallet[msg.sender];
require(
players[msg.sender].has_active_session ||
players[delegatedWallet].has_active_session, "Session is not active, you need to buy into the game first");
_; _;
} }
@ -200,7 +208,7 @@ contract RaidGeld is ERC20, Ownable, Constants {
// Manual minting for itchy fingers // Manual minting for itchy fingers
function raid() external onlyActiveSession { function raid() external onlyActiveSession {
performRaid(msg.sender); performRaid(_player());
} }
// Helper so we can use it when buying units too // Helper so we can use it when buying units too
@ -240,9 +248,10 @@ contract RaidGeld is ERC20, Ownable, Constants {
// Add a unit to your army // Add a unit to your army
function addUnit(uint8 unit, uint16 n_units) external onlyActiveSession { function addUnit(uint8 unit, uint16 n_units) external onlyActiveSession {
address player = _player();
require(unit <= 3, "Unknown unit"); require(unit <= 3, "Unknown unit");
Army storage army = armies[msg.sender]; Army storage army = armies[player];
uint16 currentLevel = 0; uint16 currentLevel = 0;
if (unit == 0) { if (unit == 0) {
// moloch_denier // moloch_denier
@ -261,13 +270,13 @@ contract RaidGeld is ERC20, Ownable, Constants {
uint256 cost = RaidGeldUtils.calculateUnitPrice(unit, currentLevel, n_units); uint256 cost = RaidGeldUtils.calculateUnitPrice(unit, currentLevel, n_units);
performRaid(msg.sender); performRaid(player);
// TODO: Since we are first minting then burning the token, this could be simplified // TODO: Since we are first minting then burning the token, this could be simplified
require(balanceOf(msg.sender) >= cost, "Not enough GELD to add this unit"); require(balanceOf(player) >= cost, "Not enough GELD to add this unit");
// then burn the cost of the new army // then burn the cost of the new army
_burn(msg.sender, cost); _burn(player, cost);
// Increase level // Increase level
if (unit == 0) { if (unit == 0) {
@ -289,7 +298,7 @@ contract RaidGeld is ERC20, Ownable, Constants {
// Emit event // Emit event
emit UnitAdded( emit UnitAdded(
msg.sender, player,
unit, unit,
n_units, n_units,
cost, cost,
@ -303,15 +312,16 @@ contract RaidGeld is ERC20, Ownable, Constants {
function battle_with_boss() external onlyActiveSession returns (bool[2] memory hasWonOrAscended) { function battle_with_boss() external onlyActiveSession returns (bool[2] memory hasWonOrAscended) {
// first perform raid // first perform raid
performRaid(msg.sender); address player = _player();
Boss memory boss_to_attack = bosses[msg.sender]; performRaid(player);
Boss memory boss_to_attack = bosses[player];
// calculate how much the player will put into battle // calculate how much the player will put into battle
uint256 geld_to_burn = balanceOf(msg.sender) >= RaidGeldUtils.getBossPower(boss_to_attack.level) uint256 geld_to_burn = balanceOf(player) >= RaidGeldUtils.getBossPower(boss_to_attack.level)
? RaidGeldUtils.getBossPower(boss_to_attack.level) ? RaidGeldUtils.getBossPower(boss_to_attack.level)
: balanceOf(msg.sender); : balanceOf(player);
bool hasWonBattle = RaidGeldUtils.calculateBossFight(boss_to_attack.level, geld_to_burn, block.prevrandao); bool hasWonBattle = RaidGeldUtils.calculateBossFight(boss_to_attack.level, geld_to_burn, block.prevrandao);
emit BossBattle(msg.sender, boss_to_attack.level, hasWonBattle); emit BossBattle(player, boss_to_attack.level, hasWonBattle);
lastBossResults[msg.sender] = LastBossResult({ lastBossResults[player] = LastBossResult({
battled_at: block.timestamp, battled_at: block.timestamp,
level: boss_to_attack.level, level: boss_to_attack.level,
variant: boss_to_attack.variants[boss_to_attack.level], variant: boss_to_attack.variants[boss_to_attack.level],
@ -320,7 +330,7 @@ contract RaidGeld is ERC20, Ownable, Constants {
}); });
if (hasWonBattle) { if (hasWonBattle) {
// Burn geld, send some sweet DAO Token and continue // Burn geld, send some sweet DAO Token and continue
_burn(msg.sender, geld_to_burn); _burn(player, geld_to_burn);
uint256 baseReward = (BUY_IN_DAO_TOKEN_AMOUNT - BUY_IN_DAO_TOKEN_AMOUNT * SACRIFICE_SHARE / MANTISSA); uint256 baseReward = (BUY_IN_DAO_TOKEN_AMOUNT - BUY_IN_DAO_TOKEN_AMOUNT * SACRIFICE_SHARE / MANTISSA);
uint256 wholeReward = RaidGeldUtils.calculateBossReward(boss_to_attack.level, baseReward); uint256 wholeReward = RaidGeldUtils.calculateBossReward(boss_to_attack.level, baseReward);
uint256 treasuryShare = wholeReward * SACRIFICE_SHARE / MANTISSA; uint256 treasuryShare = wholeReward * SACRIFICE_SHARE / MANTISSA;
@ -328,29 +338,29 @@ contract RaidGeld is ERC20, Ownable, Constants {
// send a share to dao treasury // send a share to dao treasury
daoToken.transfer(DAO_TREASURY, treasuryShare); daoToken.transfer(DAO_TREASURY, treasuryShare);
players[msg.sender].total_rewards += reward; players[player].total_rewards += reward;
// send user its reward // send user its reward
daoToken.transfer(msg.sender, reward); daoToken.transfer(player, reward);
emit BossDefeated(msg.sender, boss_to_attack.level, reward); emit BossDefeated(player, boss_to_attack.level, reward);
lastBossResults[msg.sender].reward = reward; lastBossResults[player].reward = reward;
if (boss_to_attack.level == 6) { if (boss_to_attack.level == 6) {
// User ascends! Moloch is defeated, user can start a new run // User ascends! Moloch is defeated, user can start a new run
players[msg.sender].prestige_level += 1; players[player].prestige_level += 1;
emit PrestigeGained(msg.sender, players[msg.sender].prestige_level); emit PrestigeGained(player, players[player].prestige_level);
player_dies(msg.sender); player_dies(player);
lastBossResults[msg.sender].prestigeGained = true; lastBossResults[player].prestigeGained = true;
return [hasWonBattle, true /* New prestige level! */ ]; return [hasWonBattle, true /* New prestige level! */ ];
} else { } else {
// else go to next boss // else go to next boss
bosses[msg.sender].level += 1; bosses[player].level += 1;
} }
} else { } else {
// Whoops u died, boss defeated you // Whoops u died, boss defeated you
lastBossResults[msg.sender].reward = 0; lastBossResults[player].reward = 0;
player_dies(msg.sender); player_dies(player);
} }
return [hasWonBattle, false /* hasnt gotten prestige level */ ]; return [hasWonBattle, false /* hasnt gotten prestige level */ ];
} }
@ -428,6 +438,31 @@ contract RaidGeld is ERC20, Ownable, Constants {
DAO = _dao; DAO = _dao;
} }
function proposeSessionWallet(address _wallet) payable external onlyPlayer {
require(!isRegistered(_wallet), "Wallet belongs to other player");
require(msg.value <= SESSION_WALLET_FUNDING_CAP, "Too high funding amoount");
require(sessionWalletToPlayer[_wallet] == address(0), "Wallet already in use");
playerToSessionWallet[msg.sender] = _wallet;
sessionWalletToPlayer[_wallet] = msg.sender;
payable(_wallet).call{value: msg.value}("");
}
function acceptSessionWallet(address _player) external {
require(proposedSessionWallets[_player] == msg.sender, "Not the proposed session wallet");
sessionWalletToPlayer[msg.sender] = _player;
playerToSessionWallet[_player] = msg.sender;
proposedSessionWallets[msg.sender] = address(0);
}
function fundSessionWallet() payable external {
require(sessionWalletToPlayer[msg.sender] != address(0), "No session wallet found");
require(msg.value <= SESSION_WALLET_FUNDING_CAP, "Too high funding amoount");
payable(sessionWalletToPlayer[msg.sender]).call{value: msg.value}("");
}
function _player() internal view returns (address) {
address delegatedWallet = sessionWalletToPlayer[msg.sender];
return delegatedWallet == address(0) ? msg.sender : delegatedWallet;
}
receive() external payable { receive() external payable {
revert("No plain Ether accepted, use register() function to check in :)"); revert("No plain Ether accepted, use register() function to check in :)");
} }