burn part of reg fee. deploy pool #12

Merged
mico merged 9 commits from burn into main 2024-11-01 10:55:05 +00:00
31 changed files with 2246 additions and 1572 deletions
Showing only changes of commit 1f24f68862 - Show all commits

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 433 KiB

After

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 178 KiB

After

Width:  |  Height:  |  Size: 178 KiB

View File

@ -80,7 +80,7 @@ const unitAvailableToDiscoverAt: Record<UnitType, bigint> = {
};
interface UnitProps {
addUnit: (unitType: UnitType) => void;
addUnit: (unitType: UnitType, amount?: number) => void;
unitType: UnitType;
canPurchase: boolean;
isShrouded: boolean;
@ -92,19 +92,20 @@ const Unit = ({
unitType,
canPurchase,
isShrouded,
n_units,
n_units: unitLevel,
}: UnitProps) => {
const [unitPrice, unitProfit] = useMemo(() => {
return [
toReadable(calculateUnitPrice(unitType, n_units, 1)),
toReadable(calculateProfitPerSecond(unitType, n_units)),
toReadable(calculateUnitPrice(unitType, unitLevel, 1)),
toReadable(calculateProfitPerSecond(unitType, unitLevel)),
];
}, [n_units, unitType]);
}, [unitLevel, unitType]);
return (
<div
onClick={() => addUnit(unitType)}
className={`${styles.armyUnit} ${canPurchase ? "" : styles.isUnavailable
}`}
className={`${styles.armyUnit} ${
canPurchase ? "" : styles.isUnavailable
}`}
>
<div
className={`
@ -122,12 +123,12 @@ const Unit = ({
{unitPrice} <small>GELD</small>
</span>
)}
{n_units > 0 ? (
{unitLevel > 0 ? (
<span className={`${styles.unitSupply} ${styles.uiElement}`}>
{n_units}
{`lvl ${unitLevel}`}
</span>
) : null}
{n_units > 0 ? (
{unitLevel > 0 ? (
<span className={`${styles.unitProfit} ${styles.uiElement}`}>
{unitProfit} <small>per sec</small>
</span>

View File

@ -2,7 +2,7 @@ import { formatUnits } from "viem"
import { BossLevel, usePlayer } from "../providers/PlayerProvider"
import styles from "../styles/Info.module.css"
import { useEffect, useReducer, useRef } from "react"
import { calculateBalance } from "./Counter"
import { calculateBalance, toReadable } from "./Counter"
export const bossLevelToClass: Record<BossLevel, string> = {
0: styles.boss0,
@ -14,7 +14,7 @@ export const bossLevelToClass: Record<BossLevel, string> = {
6: styles.boss6,
}
const bossToName: Record<BossLevel, string> = {
export const bossToName: Record<BossLevel, string> = {
0: "Gluttonous",
1: "Slothful",
2: "Lusty",
@ -24,7 +24,7 @@ const bossToName: Record<BossLevel, string> = {
6: "Greedy",
}
const bossToReward: Record<BossLevel, bigint> = {
export const bossToReward: Record<BossLevel, bigint> = {
0: BigInt("200000000000000000"),
1: BigInt("28274420000000000000"),
2: BigInt("174191628800000000000"),
@ -46,13 +46,13 @@ const bossToChance: Record<BossLevel, number> = {
}
const bossToBossPower: Record<BossLevel, bigint> = {
0: BigInt("9000000"),
1: BigInt("90000000"),
2: BigInt("900000000"),
3: BigInt("9000000000"),
4: BigInt("90000000000"),
5: BigInt("900000000000"),
6: BigInt("9000000000000"),
0: BigInt("3000000"),
1: BigInt("30000000"),
2: BigInt("300000000"),
3: BigInt("3000000000"),
4: BigInt("30000000000"),
5: BigInt("300000000000"),
6: BigInt("3000000000000"),
}
const getBossChanceToDefeat = (bossLevel: BossLevel, geld_balance: bigint) => {
@ -70,22 +70,24 @@ const BossInfo = () => {
const [, render] = useReducer(p => !p, false);
useEffect(() => {
const tickInterval = setInterval(() => {
chanceToDefeat.current = getBossChanceToDefeat(boss?.level ?? 0, calculateBalance(
const _balance = calculateBalance(
balance ?? BigInt(0),
army?.profit_per_second ?? BigInt(0),
player?.last_raided_at ?? BigInt(0)
))
);
chanceToDefeat.current = getBossChanceToDefeat(boss?.level ?? 0, _balance)
render();
}, 100);
return () => clearInterval(tickInterval)
}, [balance, army?.profit_per_second, player?.last_raided_at, boss?.level])
return <div className={styles.bossInfo}>
<p><strong className={bossLevelToClass[boss?.level || 0]}>{bossToName[variant]}</strong> Moloch <small>(lvl {boss ? boss.level + 1 : 0})</small></p>
<p><strong className={styles.reward}>{formatUnits(bossToReward[boss?.level || 0], 18)} RGCVII</strong> <small>reward</small></p>
<p><strong className={bossLevelToClass[variant]}>{bossToName[variant]}</strong> Moloch <small>(lvl {boss ? boss.level + 1 : 0})</small></p>
<p><strong className={styles.reward}>{parseFloat(parseFloat(formatUnits(bossToReward[boss?.level || 0], 18).toString()).toFixed(4))} RGCVII</strong> <small>reward</small></p>
<p>
<strong>{chanceToDefeat.current * 100} % to slay</strong>{" "}
<strong>{parseFloat((chanceToDefeat.current * 100).toFixed(2))} % to slay</strong>{" "}
{chanceToDefeat.current == maxChance ? <small className={styles.maxed}>(MAXED)</small> : <small>(Max {maxChance * 100}%)</small>}
</p>
<p><small>{toReadable(bossToBossPower[boss?.level ?? 0])} GELD = max chance</small></p>
</div>
}

View File

@ -0,0 +1,41 @@
import { formatUnits } from "viem";
import { usePlayer } from "../providers/PlayerProvider";
import styles from "../styles/Modal.module.css";
import bgStyles from "../styles/Background.module.css";
import { bossToName, bossToReward } from "./BossInfo";
import { bossLevelToClass } from "./Boss";
interface BossOutcomeModalProps {
setIsOpen: (val: boolean) => void,
}
const BossOutcomeModal = ({ setIsOpen }: BossOutcomeModalProps) => {
const { lastBossResult } = usePlayer();
if (lastBossResult == null) return null;
const outcome = lastBossResult.reward != BigInt(0);
const ascended = lastBossResult.prestigeGained;
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));
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 bossClass = bossLevelToClass[lastBossResult.variant];
return <div className={`${styles.modal} ${styles.bossModal}`}>
<h2>You battled {bossName} Moloch!</h2>
<div className={`${bgStyles.boss} ${bossClass} ${styles.image}`} />
<p className={styles.outcome}>{text}</p>
{rewardText}
<div>
<button onClick={() => setIsOpen(false)}>Onward!</button>
</div>
</div>
}
export default BossOutcomeModal

View File

@ -16,25 +16,26 @@ export const calculateBalance = (balance: bigint, perSecond: bigint, lastRaidedA
export const toReadable = (rawValue: bigint) => {
const value = rawValue / BigInt(10000);
const suffixes = [
{ value: BigInt('1000'), suffix: 'thousand' },
{ value: BigInt('1000000'), suffix: 'million' },
{ value: BigInt('1000000000'), suffix: 'billion' },
{ value: BigInt('1000000000000'), suffix: 'trillion' },
{ value: BigInt('1000000000000000'), suffix: 'quadrillion' },
{ value: BigInt('1000000000000000000'), suffix: 'quintillion' },
{ value: BigInt('1000000000000000000000'), suffix: 'sextillion' },
{ value: BigInt('1000000000000000000000000'), suffix: 'septillion' },
{ value: BigInt('1000000000000000000000000000'), suffix: 'octillion' },
{ value: BigInt('1000000000000000000000000000000'), suffix: 'nonillion' },
{ value: BigInt('1000000000000000000000000000000000'), suffix: 'decillion' },
{ value: BigInt('1000000000000000000000000000000000000'), suffix: 'undecillion' },
{ value: BigInt('1000000000000000000000000000000000000000'), suffix: 'duodecillion' },
{ value: BigInt('1000000000000000000000000000000000000000000'), suffix: 'tredecillion' },
{ value: BigInt('1000000000000000000000000000000000000000000000'), suffix: 'quattuordecillion' },
{ value: BigInt('1000000000000000000000000000000000000000000000000'), suffix: 'quindecillion' },
{ value: BigInt('1000000000000000000000000000000000000000000000000000'), suffix: 'sexdecillion' },
{ value: BigInt('1000000000000000000000000000000000000000000000000000000'), suffix: 'septendecillion' },
{ value: BigInt('1000'), suffix: 'k' }, // Thousand
{ value: BigInt('1000000'), suffix: 'M' }, // Million
{ value: BigInt('1000000000'), suffix: 'B' }, // Billion
{ value: BigInt('1000000000000'), suffix: 'T' }, // Trillion
{ value: BigInt('1000000000000000'), suffix: 'Qd' }, // Quadrillion
{ value: BigInt('1000000000000000000'), suffix: 'Qi' }, // Quintillion
{ value: BigInt('1000000000000000000000'), suffix: 'Sx' }, // Sextillion
{ value: BigInt('1000000000000000000000000'), suffix: 'Sp' }, // Septillion
{ value: BigInt('1000000000000000000000000000'), suffix: 'Oc' }, // Octillion
{ value: BigInt('1000000000000000000000000000000'), suffix: 'No' }, // Nonillion
{ value: BigInt('1000000000000000000000000000000000'), suffix: 'Dc' }, // Decillion
{ value: BigInt('1000000000000000000000000000000000000'), suffix: 'Ud' }, // Undecillion
{ value: BigInt('1000000000000000000000000000000000000000'), suffix: 'Dd' }, // Duodecillion
{ value: BigInt('1000000000000000000000000000000000000000000'), suffix: 'Td' }, // Tredecillion
{ value: BigInt('1000000000000000000000000000000000000000000000'), suffix: 'Qt' }, // Quattuordecillion
{ value: BigInt('1000000000000000000000000000000000000000000000000'), suffix: 'Qn' }, // Quindecillion
{ value: BigInt('1000000000000000000000000000000000000000000000000000'), suffix: 'Sd' }, // Sexdecillion
{ value: BigInt('1000000000000000000000000000000000000000000000000000000'), suffix: 'St' }, // Septendecillion
];
for (let i = 0; i < suffixes.length; i++) {
if (value < suffixes[i].value) {
@ -43,8 +44,8 @@ export const toReadable = (rawValue: bigint) => {
} else {
const divided = value / suffixes[i - 1].value;
const numStr = (value % suffixes[i - 1].value).toString().slice(0, 3);
const remainder = parseInt(numStr.replace(/0+$/, ''), 10);
return `${divided.toString()}.${remainder.toString()} ${suffixes[i - 1].suffix}`;
const remainder = numStr == "0" ? "" : "." + parseInt(numStr.replace(/0+$/, ''), 10);
return `${divided.toString()}${remainder.toString()} ${suffixes[i - 1].suffix}`;
}
}
}

View File

@ -1,49 +1,68 @@
import React, { useCallback, useMemo } from "react"
import styles from "../styles/Header.module.css"
import bgStyles from "../styles/Background.module.css"
import React, { useCallback, useMemo } from "react";
import styles from "../styles/Header.module.css";
import bgStyles from "../styles/Background.module.css";
import { usePlayer } from "../providers/PlayerProvider";
import { useAccount } from 'wagmi';
import { useAccount } from "wagmi";
import dynamic from "next/dynamic";
import Counter, { toReadable } from "./Counter";
import { useModal } from "../providers/ModalProvider";
const Header = () => {
const { isConnected } = useAccount();
const { isRegistered, army } = usePlayer();
const { isRegistered, player, army } = usePlayer();
const { openRegistrationModal } = useModal();
const title = useMemo(() => {
return isRegistered ? `SLAY THE MOLOCH` :
!isConnected ? "Connect your wallet traveler ☝️ and then ..." :
"Click here to start 😈"
}, [isConnected, isRegistered])
return isRegistered && !player?.has_active_session
? `You died 😇 Click here to start again and ...`
: isRegistered
? `SLAY THE MOLOCH`
: !isConnected
? "Connect your wallet traveler ☝️ and then ..."
: "Click here to start 😈";
}, [isConnected, isRegistered, player?.has_active_session]);
const subtitle = useMemo(() => {
if (isRegistered) {
return <Counter />
if (isRegistered && player?.has_active_session) {
return <Counter />;
} else {
return <p className={styles.counter}>SLAY THE MOLOCH</p>
return (
<p
className={`${styles.counter} ${
isConnected && !player?.has_active_session ? bgStyles.excited : ""
}`}
>
SLAY THE MOLOCH
</p>
);
}
}, [isRegistered])
}, [isRegistered, player?.has_active_session, isConnected]);
const perSecondParagraph = useMemo(() => {
const perSecond = toReadable(army?.profit_per_second ?? BigInt(0))
return (isRegistered) ?
const perSecond = toReadable(army?.profit_per_second ?? BigInt(0));
return isRegistered && player?.has_active_session ? (
<p className={styles.counter_per_seconds}>per second: {perSecond}</p>
: null
}, [isRegistered, army?.profit_per_second])
) : null;
}, [isRegistered, army?.profit_per_second, player?.has_active_session]);
const onRegister = useCallback(() => {
if (isRegistered) return
openRegistrationModal()
}, [isRegistered, openRegistrationModal])
if (player?.has_active_session) return;
openRegistrationModal();
}, [player?.has_active_session, openRegistrationModal]);
return <header onClick={onRegister} className={styles.header}>
<h1 className={`${styles.title} ${isConnected && !isRegistered ? bgStyles.excited : ""}`}>{title}</h1>
{subtitle}
{perSecondParagraph}
</header>
}
return (
<header
onClick={onRegister}
className={`${styles.header} ${
isConnected && !player?.has_active_session ? styles.clickable : ""
}`}
>
<h1 className={`${styles.title}`}>{title}</h1>
{subtitle}
{perSecondParagraph}
</header>
);
};
// export default Header

View File

@ -0,0 +1,114 @@
import { useEffect, useState } from 'react'
import styles from '../styles/Leaderboard.module.css'
import { TopEarnersResponse, TopRaidersResponse } from '../types/leaderboard'
import { formatUnits } from 'viem'
const SUBGRAPH_URL = 'https://api.studio.thegraph.com/query/75782/slay-the-moloch-base-sepolia/version/latest'
const Leaderboard = () => {
const [topEarners, setTopEarners] = useState<TopEarnersResponse>()
const [topRaiders, setTopRaiders] = useState<TopRaidersResponse>()
const [activeTab, setActiveTab] = useState<'earners' | 'raiders'>('earners')
useEffect(() => {
const fetchLeaderboards = async () => {
try {
// Fetch top earners
const earnersResponse = await fetch(SUBGRAPH_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `{
armies(first: 10, orderBy: profitPerSecond, orderDirection: desc) {
player {
id
totalMinted
currentBalance
numberOfRaids
}
profitPerSecond
molochDenierLevel
apprenticeLevel
anointedLevel
championLevel
}
}`
})
})
const earnersData = await earnersResponse.json()
setTopEarners({ armies: earnersData.data.armies })
// Fetch top raiders
const raidersResponse = await fetch(SUBGRAPH_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `{
players(first: 10, orderBy: numberOfRaids, orderDirection: desc) {
id
numberOfRaids
totalMinted
currentBalance
}
}`
})
})
const raidersData = await raidersResponse.json()
setTopRaiders({ players: raidersData.data.players })
} catch (error) {
console.error('Error fetching leaderboard:', error)
}
}
fetchLeaderboards()
const interval = setInterval(fetchLeaderboards, 30000) // Refresh every 30 seconds
return () => clearInterval(interval)
}, [])
return (
<div className={styles.leaderboard}>
<h2 className={styles.title}>Leaderboard</h2>
<div className={styles.tabs}>
<button
className={`${styles.tab} ${activeTab === 'earners' ? styles.active : ''}`}
onClick={() => setActiveTab('earners')}
>
Top Earners
</button>
<button
className={`${styles.tab} ${activeTab === 'raiders' ? styles.active : ''}`}
onClick={() => setActiveTab('raiders')}
>
Top Raiders
</button>
</div>
{activeTab === 'earners' && (
<div className={styles.list}>
{topEarners?.armies.map((army, index) => (
<div key={army.player.id} className={styles.item}>
<span className={styles.rank}>#{index + 1}</span>
<span className={styles.address}>{army.player.id.slice(0, 6)}...{army.player.id.slice(-4)}</span>
<span className={styles.stat}>{formatUnits(BigInt(army.profitPerSecond), 4)} GELD/s</span>
</div>
))}
</div>
)}
{activeTab === 'raiders' && (
<div className={styles.list}>
{topRaiders?.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.numberOfRaids} raids</span>
</div>
))}
</div>
)}
</div>
)
}
export default Leaderboard

View File

@ -1,51 +1,223 @@
import { useState, useEffect, useRef } from "react";
import styles from "../styles/Army.module.css"
import { usePlayer } from "../providers/PlayerProvider";
import styles from "../styles/Army.module.css";
const tavernerQuotes = [
// Quote categories based on total minted GELD
const PROGRESSION_TIERS = {
BEGINNER: BigInt("1000000"),
NOVICE: BigInt("10000000"),
INTERMEDIATE: BigInt("100000000"),
EXPERIENCED: BigInt("1000000000"),
EXPERT: BigInt("10000000000"),
MASTER: BigInt("100000000000"),
EPIC: BigInt("1000000000000"),
LEGENDARY: BigInt("10000000000000"),
GUILD_LEADER: BigInt("100000000000000"),
DIVINE: BigInt("1000000000000000"),
MAXIMUM: BigInt("10000000000000000"),
};
const quotes = {
BEGINNER: [
"Well, well... another fresh face looking to be a hero. At least you're holding your weapon the right way up.",
"Ha! Your armor's so shiny I can see my reflection. Give it a week, rookie.",
"Another raider? The rats in the cellar are trembling... with laughter.",
"Ah, the optimism of the inexperienced. Refreshing, like a cool drink before a hard fall.",
"Don't worry, we keep the bandages cheap for newcomers like yourself.",
"I've seen tougher looking raiders, but hey, everyone starts somewhere!",
"You, a raider? Well, beggars can't be choosers, I suppose.",
"Try not to dent your armor on the doorframe on your way out.",
"I remember when I was green as you. Actually, I don't.",
"The practice dummy out back is taking bets against you. Just thought you should know.",
],
NOVICE: [
"Back again? You're lasting longer than most of the fresh blood.",
"You're developing some calluses finally. Good, you'll need them.",
"Well, you haven't disappeared yet. That's... something.",
"Starting to walk with a bit of confidence, eh? Don't let it go to your head.",
"Your reputation precedes you... mind you, it's not saying much.",
"The local wolves don't laugh quite as hard when they see you coming now.",
"You're making progress! The practice dummy finally lost some coins.",
"I see your training is coming along. You only singed your eyebrows twice this week.",
"Getting better with that weapon. You actually hit what you're aiming at... occasionally.",
"The raiders have stopped taking bets on when you'll quit. Progress!",
],
INTERMEDIATE: [
"Now there's a familiar face that's earned their drink!",
"The usual? You've been around long enough to have a usual!",
"Looking more battle-worn these days. It suits you.",
"Your reputation's growing. Heard some kids playing 'mighty raider' in the street.",
"The local raiders actually know your name now. Well, most of it.",
"That's quite a collection of scars you're building. Each one's a lesson, eh?",
"You carry yourself like a real adventurer these days.",
"Those monsters in the forest? They're starting to learn your name.",
"Your deeds are becoming tavern tales. Small tales, but tales nonetheless.",
"That gleam in your eye... you're starting to believe in yourself, aren't you?",
],
EXPERIENCED: [
"Honor to serve you, friend. Your exploits bring customers to my humble establishment.",
"The bards are starting to write songs about you. Decent ones, too!",
"Ah, the hero of the hour! Your usual table awaits.",
"When they speak of a great raider these days, your name comes up!",
"Your presence honors us. What tales will you bring today?",
"The young ones whisper your name in awe. Remember when that was you?",
"Your legend grows with each passing moon. As does my profit from telling your tales!",
"The guild speak of your deeds with respect.",
"You've come so far from that nervous newcomer who first walked through my door.",
"They say you've met Moloch face to face. Now that's a tale worth hearing!",
],
EXPERT: [
"My most esteemed patron! The usual bottle of our finest?",
"Blessed are we to host a hero of your caliber!",
"Your very presence brings honor to this humble establishment.",
"The stuff of legends walks among us, friends!",
"They say you've slain dragons now. Dragons! In my tavern!",
"Your name echoes through the kingdoms. Yet you still honor us with your presence.",
"Ah, the mighty raider returns! Shall I dust off the good chalice?",
"The bards fight over who gets to sing your tales first.",
"Your achievements bring glory to all who follow your footsteps.",
"Even the ancient raiders speak of your deeds with reverence.",
],
MASTER: [
"You humble us with your magnificent presence!",
"Champions and heroes seek you out for guidance now. How far you've come!",
"They say your name in whispers in the dark kingdoms of Moloch. In fear.",
"Your legend spreads even to distant shores. Travelers speak of your deeds in awe.",
"The very mountains seem to bow in your presence, great one.",
"Your power radiates like the sun. You've mastered arts few can comprehend.",
"To think, I've watched you grow from newcomer to living legend!",
"They say you've went berserk naked in a raid. The gods must surely know your name.",
"Your mastery of the raider's arts is unmatched in all the realms.",
"The ancient prophecies speak of one such as you...",
],
EPIC: [
"They say you've mastered every weapon in the realm!",
"Is it true you've tamed the ancient dragons?",
"The Moloch speaks your name in terror!",
"Your magic reshapes the very world!",
"None have delved deeper into the ancient dungeons!",
],
LEGENDARY: [
"The greatest raider of our age graces us! All hail!",
"Your presence blesses this humble tavern, O mighty one!",
"Let it be known - a living legend walks among us!",
"The realm's greatest champion returns to honor us!",
"Your deeds will be told for a thousand years!",
"Gods walk among us this day! Welcome, mighty hero!",
"Champion of champions! Slayer of the Moloch!",
"Your very footsteps shake the foundations of reality!",
"None have achieved what you have, great one!",
"The stars themselves dim in your presence!",
],
GUILD_LEADER: [
"The guild masters seek your counsel now, don't they?",
"Your followers grow more numerous by the day!",
"Even kings bow to your wisdom in matters of war!",
"The greatest raider academy bears your name!",
"Your teachings will guide generations to come!",
"The realm was forever changed when you sealed the Dark Portal of Moloch!",
"Peace reigns because you guard our lands!",
"The ancient prophecies spoke true of your coming!",
"Even the gods watch your journey with interest!",
"The very fabric of magic bears your mark!",
],
DIVINE: [
"Even the Moloch fears your coming!",
"The Cohort VI champions pale before your glory!",
"The Moon shines brighter in your presence!",
"The Golden Lady herself seeks your blessing!",
"The elements themselves bend to your will!",
"The Sword of Ages chosen YOU as its wielder!",
"You've walked the Path of a Thousand Stars!",
"The Ancient Trials bow before your mastery!",
"Bearer of the Eternal Flame!",
"Even Sayonara uses your deeds as images!",
],
MAXIMUM: [
"Hamsterverse itself bends in your presence, great one!",
"Your power transcends mortal understanding!",
"Moloch trembles at your footsteps!",
"Your legend will outlive the gods themselves!",
"In all my centuries, none have achieved what you have, mighty raider!",
],
};
const EARLY_GAME_QUOTES = [
"There is always Moloch to be slain here...",
"We prioritize Shipping at All Costs!",
"Get out there RAIDER, Moloch won't Slay Himself!",
];
function PixelatedQuote() {
const { player } = usePlayer();
const [isShown, setIsShown] = useState(true);
const [currentQuote, setCurrentQuote] = useState(
"Welcome to the Dark Forest!"
);
const intervalIdRef = useRef<NodeJS.Timeout | null>(null); // Define the type for Node environment compatibility
const intervalIdRef = useRef<NodeJS.Timeout | null>(null);
const hasShownWelcome = useRef(false);
// Determine which tier of quotes to use based on total minted
const getQuoteTier = (totalMinted: bigint) => {
if (totalMinted >= PROGRESSION_TIERS.MAXIMUM) return "MAXIMUM";
if (totalMinted >= PROGRESSION_TIERS.DIVINE) return "DIVINE";
if (totalMinted >= PROGRESSION_TIERS.GUILD_LEADER) return "GUILD_LEADER";
if (totalMinted >= PROGRESSION_TIERS.LEGENDARY) return "LEGENDARY";
if (totalMinted >= PROGRESSION_TIERS.EPIC) return "EPIC";
if (totalMinted >= PROGRESSION_TIERS.MASTER) return "MASTER";
if (totalMinted >= PROGRESSION_TIERS.EXPERT) return "EXPERT";
if (totalMinted >= PROGRESSION_TIERS.EXPERIENCED) return "EXPERIENCED";
if (totalMinted >= PROGRESSION_TIERS.INTERMEDIATE) return "INTERMEDIATE";
if (totalMinted >= PROGRESSION_TIERS.NOVICE) return "NOVICE";
return "BEGINNER";
};
useEffect(() => {
if (intervalIdRef.current !== null) {
clearInterval(intervalIdRef.current);
} // Clear interval if it exists
}
// Set up an interval to show the toast every 10 seconds
intervalIdRef.current = setInterval(() => {
setCurrentQuote(
tavernerQuotes[Math.floor(Math.random() * tavernerQuotes.length)]
);
const totalMinted = player?.total_minted ?? BigInt(0);
// Show welcome message only once at the start
if (!hasShownWelcome.current) {
setCurrentQuote("Welcome to the Dark Forest!");
hasShownWelcome.current = true;
} else if (totalMinted < PROGRESSION_TIERS.BEGINNER) {
// Show early game quotes until player reaches beginner level
setCurrentQuote(
EARLY_GAME_QUOTES[
Math.floor(Math.random() * EARLY_GAME_QUOTES.length)
]
);
} else {
const tier = getQuoteTier(totalMinted);
const tierQuotes = quotes[tier];
setCurrentQuote(
tierQuotes[Math.floor(Math.random() * tierQuotes.length)]
);
}
setIsShown(true);
// Hide the toast after 4 seconds
setTimeout(() => {
setIsShown(false);
}, 4000);
}, 6000);
}, 8000);
}, 10000);
// Clean up the interval on component unmount
return () => {
if (intervalIdRef.current !== null) {
clearInterval(intervalIdRef.current); // Clear interval using correct reference
clearInterval(intervalIdRef.current);
intervalIdRef.current = null;
}
};
}, []);
}, [player?.total_minted]);
return (
<div>
<div
className={`pixel-borders pixel-borders--2 pixelFont ${styles.pixelQuote}`}
style={{ opacity: isShown ? 1 : 0 /* Control visibility with opacity */ }}
style={{ opacity: isShown ? 1 : 0 }}
>
{currentQuote}
</div>

View File

@ -1,6 +1,7 @@
import { useCallback } from "react";
import { usePlayer } from "../providers/PlayerProvider";
import styles from "../styles/Modal.module.css";
import bgStyles from "../styles/Background.module.css";
interface RegistrationModalProps {
isOpen: boolean;
@ -14,13 +15,14 @@ const RegistrationModal = ({ isOpen, setIsOpen }: RegistrationModalProps) => {
setIsOpen(false);
}, [register, setIsOpen])
if (!isOpen) return null;
return <div className={styles.modal}>
<h2>Insert coins to continue</h2>
return <div className={bgStyles.leaderboardOverlay}><div className={styles.modal}>
<span className={styles.closeBtn} onClick={() => setIsOpen(false)}>x</span>
<h2 className={styles.textCenter}>Insert coins to continue</h2>
<div>
<button onClick={() => onRegister("RGCVII")}>50 RGCVII</button>
<button onClick={() => onRegister("RGCVII")}>500 RGCVII</button>
<button onClick={() => onRegister("ETH")}>0.0005 ETH</button>
</div>
</div>
</div></div>
}
export default RegistrationModal

View File

@ -1,9 +1,10 @@
import React, { useCallback } from "react"
import styles from '../styles/Background.module.css';
import React, { useCallback, useState } from "react";
import styles from "../styles/Background.module.css";
import Tower from "./Tower";
import Army from "./Army";
import MarchingBand from "./MarchingBand";
import MusicPlayer from "./MusicPlayer";
import Leaderboard from "./Leaderboard";
import { usePlayer } from "../providers/PlayerProvider";
import Boss from "./Boss";
import BossInfo from "./BossInfo";
@ -16,15 +17,20 @@ const bossToMountainsClass = {
4: styles.mountains4,
5: styles.mountains5,
6: styles.mountains6,
}
};
const Scene = () => {
const { isRegistered, boss } = usePlayer();
const handleMusicReady = useCallback((unmute: () => void) => {
if (isRegistered) {
unmute();
}
}, [isRegistered]);
const { isRegistered, boss, player } = usePlayer();
const [isLeaderboardOpen, setIsLeaderboardOpen] = useState(false);
const handleMusicReady = useCallback(
(unmute: () => void) => {
if (isRegistered) {
unmute();
}
},
[isRegistered]
);
const variant = boss?.variants[boss.level] || 0;
return <div className={styles.frame}>
@ -35,12 +41,31 @@ const Scene = () => {
<div className={`${styles.clouds_small} ${styles.background_asset}`} />
<div className={`${styles.mountains} ${styles.background_asset} ${bossToMountainsClass[variant]}`} />
<div className={`${styles.village} ${styles.background_asset}`} />
<BossInfo />
{isRegistered && player?.has_active_session && <BossInfo />}
<MarchingBand />
<div className={`${styles.bonfire} ${styles.background_asset}`} />
<Army />
<MusicPlayer onReady={handleMusicReady} />
<button
onClick={() => setIsLeaderboardOpen(true)}
className={styles.leaderboardButton}
title="Leaderboard"
>
🏆 <span className={styles.hideMobile}>Top players</span>
</button>
{isLeaderboardOpen && (
<div className={styles.leaderboardOverlay}>
<div className={styles.leaderboardContent}>
<button
className={styles.closeButton}
onClick={() => setIsLeaderboardOpen(false)}
>
×
</button>
<Leaderboard />
</div></div>
)}
</div>
}
export default Scene
export default Scene;

View File

@ -24,7 +24,7 @@ const WaitingForTxModal = ({
<div className={styles.loadingHamsterWheel} />
<div className={styles.loadingHamster} />
</div>
<p className={styles.loadingText}>Writing contract ...</p>
<p className={styles.loadingText}>Spinning the chain ...</p>
</div>
}

View File

@ -36,6 +36,7 @@ function MyApp({ Component, pageProps }: AppProps) {
h4,
h5,
h6,
button,
.title {
font-family: ${font.style.fontFamily};
}

View File

@ -1,9 +1,9 @@
import { ConnectButton } from '@rainbow-me/rainbowkit';
import type { NextPage } from 'next';
import Head from 'next/head';
import styles from '../styles/Home.module.css';
import Header from '../components/Header';
import Scene from '../components/Scene';
import { ConnectButton } from "@rainbow-me/rainbowkit";
import type { NextPage } from "next";
import Head from "next/head";
import styles from "../styles/Home.module.css";
import Header from "../components/Header";
import Scene from "../components/Scene";
const Home: NextPage = () => {
return (
@ -26,7 +26,10 @@ const Home: NextPage = () => {
</main>
<footer className={styles.footer}>
Made with by your frens at 😈 Slay the Moloch team for Cohort VII of <a href="https://www.raidguild.org/" target="blank">RaidGuild</a>
Made with by your frens at 😈 Slay the Moloch team for Cohort VII of{" "}
<a href="https://www.raidguild.org/" target="blank">
RaidGuild
</a>
</footer>
</div>
);

View File

@ -1,9 +1,11 @@
import React, { createContext, ReactNode, useCallback, useContext, useEffect, useState } from 'react'
import React, { createContext, ReactNode, useCallback, useContext, useEffect, useRef, useState } 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"
const { contractAddress, daoTokenAddress } = contracts
const abi = contractAbi.abi
@ -33,16 +35,25 @@ export interface Boss {
variants: [BossLevel, BossLevel, BossLevel, BossLevel, BossLevel, BossLevel, BossLevel]
}
export interface LastBossResult {
level: BossLevel;
variant: BossLevel;
battled_at: bigint;
reward: bigint;
prestigeGained: boolean;
}
export interface PlayerContextType {
isRegistered: boolean,
player: null | Player,
army: null | Army,
boss: null | Boss,
lastBossResult: null | LastBossResult,
balance: bigint,
register: (arg: "ETH" | "RGCVII") => void,
raid: () => void,
battleWithBoss: () => void;
addUnit: (unit: UnitType) => void
addUnit: (unit: UnitType, amount?: number) => void;
}
const PlayerContext = createContext<PlayerContextType>({
@ -50,6 +61,7 @@ const PlayerContext = createContext<PlayerContextType>({
player: null,
army: null,
boss: null,
lastBossResult: null,
balance: BigInt(0),
register: () => { },
raid: () => { },
@ -61,6 +73,8 @@ const PlayerProvider = ({ children }: { children: ReactNode }) => {
const { address, isConnected } = useAccount();
const { writeContract, error } = useWriteContract();
const [[txHash, callbackFn], setHashAndCallback] = useState<[Hash | null, () => void]>([null, () => { }])
const [bossBattledModalOpen, setBossBattlesModalOpen] = useState(false);
const hasFetchedLastBossFirstTime = useRef(false);
useEffect(() => {
console.warn(error)
@ -125,6 +139,17 @@ const PlayerProvider = ({ children }: { children: ReactNode }) => {
}
});
const { data: lastBossResult } = useReadContract({
address: contractAddress,
abi,
functionName: 'getLastBossResult',
args: [address],
query: {
enabled: isConnected,
refetchInterval: 15
}
});
console.log(balance, player, army, boss)
const register = useCallback((arg: "RGCVII" | "ETH") => {
@ -133,11 +158,12 @@ const PlayerProvider = ({ children }: { children: ReactNode }) => {
abi,
address: contractAddress,
functionName: 'register_eth',
value: parseEther("0.0005"),
value: parseEther("0.00005"),
}, {
onSuccess: (hash) => {
setHashAndCallback([hash, resetHashAndCallback])
}
},
onError: () => resetHashAndCallback()
})
} else if (arg === "RGCVII") {
writeContract({
@ -156,10 +182,12 @@ const PlayerProvider = ({ children }: { children: ReactNode }) => {
}, {
onSuccess: (hash) => {
setHashAndCallback([hash, resetHashAndCallback])
}
},
onError: () => resetHashAndCallback()
})
])
}
},
onError: () => resetHashAndCallback()
});
}
}, [writeContract, resetHashAndCallback])
@ -169,8 +197,13 @@ const PlayerProvider = ({ children }: { children: ReactNode }) => {
abi,
address: contractAddress,
functionName: 'raid',
}, {
onSuccess: (hash) => {
setHashAndCallback([hash, resetHashAndCallback])
},
onError: () => resetHashAndCallback()
})
}, [writeContract])
}, [writeContract, resetHashAndCallback])
const addUnit = useCallback((unit: UnitType) => {
writeContract({
@ -186,8 +219,23 @@ const PlayerProvider = ({ children }: { children: ReactNode }) => {
abi,
address: contractAddress,
functionName: 'battle_with_boss',
}, {
onSuccess: (hash) => {
setHashAndCallback([hash, () => resetHashAndCallback()])
},
onError: () => resetHashAndCallback()
})
}, [writeContract])
}, [writeContract, resetHashAndCallback])
useEffect(() => {
if (lastBossResult != null) {
if (hasFetchedLastBossFirstTime.current) {
setBossBattlesModalOpen(true);
} else {
hasFetchedLastBossFirstTime.current = true;
}
}
}, [lastBossResult])
return (
<PlayerContext.Provider value={{
@ -196,13 +244,17 @@ const PlayerProvider = ({ children }: { children: ReactNode }) => {
army: army as Army,
boss: boss as Boss,
balance: balance as bigint,
lastBossResult: lastBossResult as LastBossResult,
register,
raid,
addUnit,
battleWithBoss
}}>
{children}
{txHash && <WaitingForTxModal hash={txHash} callbackFn={callbackFn} />}
<div className={`${(txHash || bossBattledModalOpen) ? styles.leaderboardOverlay : ""}`}>
{txHash && <WaitingForTxModal hash={txHash} callbackFn={callbackFn} />}
{bossBattledModalOpen && <BossOutcomeModal setIsOpen={setBossBattlesModalOpen} />}
</div>
</PlayerContext.Provider>
);
}

View File

@ -130,8 +130,17 @@
display: flex;
justify-content: center;
align-items: center;
@media only screen and (max-width: 600px) {
left: 10px;
bottom: 5px;
overflow: auto;
}
}
.armyUnit {
@media only screen and (max-width: 600px) {
transform: scale(0.75);
}
position: relative;
height: 100%;
width: 120px;
@ -152,6 +161,7 @@
}
}
.uiElement {
width: fit-content;
position: absolute;
border-radius: 10px;
background: rgba(0, 0, 0, 0.89);
@ -161,18 +171,22 @@
text-align: center;
}
.unitSupply {
top: 0;
top: -30px;
right: 0;
left: 2rem;
white-space: nowrap;
}
.unitName {
left: 0;
right: 0;
bottom: 45px;
white-space: nowrap;
}
.unitPrice {
left: 0;
right: 0;
bottom: 25px;
white-space: nowrap;
}
.unitProfit {
left: 0;
@ -201,18 +215,25 @@
height: 90px;
user-select: none;
.pixelQuote {
min-width: 150px;
z-index: 20;
min-width: 200px;
width: fit-content;
max-width: 300px;
color: black;
font-size: 0.7rem;
line-height: 0.9rem;
position: absolute;
bottom: 5.5rem;
left: -20px;
left: -70px;
right: 0;
padding: 0.7rem;
line-height: 0.8rem;
transition: opacity 1s ease-in-out;
box-shadow: 0px 5px 10px 5px rgba(0, 0, 0, 0.4);
}
@media only screen and (max-width: 600px) {
right: 60px;
}
}
.static.moloch_denier {
background-image: url("/roles/scribe2.png");
@ -257,6 +278,40 @@
}
}
@media only screen and (max-width: 600px) {
@keyframes marching {
0% {
transform: translate(-54px, -59px); /* -100px scaled to ~-54px */
}
8% {
/* approaches fire */
transform: translate(15px, -100px); /* 72px scaled to ~39px */
}
15% {
/* approaches road */
transform: translate(82px, -123px); /* 152px scaled to ~82px */
}
25% {
/* first road turn */
transform: translate(66px, -200px); /* 122px scaled to ~66px */
}
45% {
/* second road turn */
transform: translate(138px, -264px); /* 256px scaled to ~138px */
}
75% {
/* third road turn */
transform: translate(86px, -293px); /* 159px scaled to ~86px */
}
100% {
/* vanishes into distance */
transform: translate(97px, -300px); /* 180px scaled to ~97px */
}
}
}
@keyframes marchingPerson {
0% {
background-size: 100% 100%;

View File

@ -1,11 +1,14 @@
.frame {
position: absolute;
width: 100%;
height: 100%;
border-width: 8px;
border-image: url("/background/frame.png") 22 fill / auto space;
width: 720px;
height: 960px;
@media only screen and (max-width: 600px) {
max-height: 90vh;
overflow: hidden;
max-width: 100vw;
}
}
.background_asset {
@ -34,16 +37,27 @@
height: 150px;
background-image: url("/background/clouds_large.png");
animation:
scrollBackground 80s linear infinite,
scrollBackground 28s linear infinite,
thunder 4s linear infinite;
@media only screen and (max-width: 600px) {
animation:
scrollBackground 280s linear infinite,
thunder 4s linear infinite;
}
}
.clouds_small {
top: 270px;
top: 275px;
height: 82px;
background-image: url("/background/clouds_small.png");
animation:
scrollBackground 20s linear infinite,
thunder 12s linear infinite;
@media only screen and (max-width: 600px) {
top: 285px;
animation:
scrollBackground 200s linear infinite,
thunder 12s linear infinite;
}
}
.boss {
@ -54,6 +68,9 @@
top: 130px;
right: 10px;
left: auto;
@media only screen and (max-width: 600px) {
right: -20px;
}
animation: thunder_hue_hard 12s linear infinite;
transition: all 0.1s cubic-bezier(0.265, 1.4, 0.68, 1.65);
transform-origin: bottom center;
@ -113,6 +130,9 @@
height: 372px;
top: 90px;
left: -10px;
@media only screen and (max-width: 600px) {
left: -80px;
}
animation: thunder_hue_hard 12s linear infinite;
transition: all 0.1s cubic-bezier(0.265, 1.4, 0.68, 1.65);
transform-origin: bottom center;
@ -181,34 +201,45 @@
background-image: url("/background/mountains/0_gluttony.svg");
height: 181px;
top: 285px;
animation: thunder_hue 12s linear infinite;
pointer-events: none;
&.mountains0 {
background-image: url("/background/mountains/0_gluttony.svg");
filter: saturate(2) contrast(1.2);
}
&.mountains1 {
background-image: url("/background/mountains/1_sloth.svg");
filter: contrast(1.2);
}
&.mountains2 {
background-image: url("/background/mountains/2_lust.svg");
filter: contrast(1.1) hue-rotate(260deg);
}
&.mountains3 {
background-image: url("/background/mountains/3_wrath.svg");
filter: contrast(1.2);
}
&.mountains4 {
background-image: url("/background/mountains/4_envy.svg");
filter: saturate(1.5) contrast(1.4);
}
&.mountains5 {
background-image: url("/background/mountains/5_pride.svg");
filter: contrast(1.2);
}
&.mountains6 {
background-image: url("/background/mountains/6_greed.svg");
filter: contrast(1.2);
}
}
.village {
background-image: url("/background/village.png");
height: 540px;
bottom: 22px;
@media only screen and (max-width: 600px) {
height: 300px;
bottom: 80px;
}
}
.bonfire {
background-image: url("/background/bonfire.png");
@ -220,6 +251,12 @@
animation:
bonfire 12s linear infinite,
bonfire_skew 5s infinite linear;
@media only screen and (max-width: 600px) {
left: 80px;
bottom: 105px;
scale: 0.7;
}
}
.musicButton {
position: absolute;
@ -364,3 +401,77 @@
transform: scale(1.02, 1.03) skew(0deg, -1deg);
}
}
.leaderboardButton {
position: absolute;
top: 30px;
left: 80px;
background: rgba(0, 0, 0, 0.5);
border: none;
border-radius: 5px;
padding: 5px 10px;
font-size: 1.2rem;
cursor: pointer;
z-index: 1000;
transition: all 0.2s cubic-bezier(0.265, 1.4, 0.68, 1.65);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
color: var(--text-color);
& .hideMobile {
padding-left: 0.5rem;
}
@media only screen and (max-width: 600px) {
& .hideMobile {
display: none;
}
}
}
.leaderboardButton:hover {
transform: scale(1.1);
background: rgba(0, 0, 0, 0.7);
color: var(--hover-color);
}
.leaderboardButton:active {
transform: scale(0.95);
}
.leaderboardOverlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
z-index: 1000;
display: flex;
justify-content: center;
align-items: center;
}
.leaderboardContent {
background: var(--bg-color);
border-width: 8px;
border-image: url("/background/frame.png") 22 fill / auto space;
padding: 2rem;
max-width: 600px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
position: relative;
}
.closeButton {
position: absolute;
top: 1rem;
right: 1rem;
background: none;
border: none;
font-size: 2rem;
cursor: pointer;
color: var(--text-color);
}
.closeButton:hover {
color: var(--hover-color);
}

View File

@ -2,10 +2,21 @@
position: relative;
margin-top: 5rem;
z-index: 1;
&.clickable {
cursor: pointer;
}
@media only screen and (max-width: 600px) {
margin-top: 3rem;
}
}
.title {
font-size: 1.5rem;
margin: 0;
@media only screen and (max-width: 600px) {
display: none;
}
}
.counter {
font-size: 2rem;

View File

@ -18,12 +18,14 @@
max-width: 720px;
height: 100vh;
margin: 0 auto;
@media only screen and (max-width: 600px) {
max-height: 90vh;
}
}
.footer {
margin-top: 2rem;
padding: 2rem 0;
border-top: 1px solid #eaeaea;
padding: 0;
margin: 10px 0;
text-align: center;
}

View File

@ -1,5 +1,6 @@
.bossInfo {
position: absolute;
z-index: 10;
top: 350px;
right: 28px;
background: var(--bg-color);

View File

@ -0,0 +1,70 @@
.leaderboard {
padding: 1rem;
color: white;
}
.title {
text-align: center;
margin-bottom: 1.5rem;
font-size: 1.5rem;
color: var(--hover-color);
}
.tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.tab {
flex: 1;
background: transparent;
border: 1px solid #666;
color: white;
padding: 0.5rem;
border-radius: 5px;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
.tab.active {
background: var(--hover-color);
border-color: var(--hover-color);
color: #000;
}
.list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.5rem;
border: 1px solid #333;
border-radius: 5px;
background: rgba(0, 0, 0, 0.3);
}
.item:hover {
background: rgba(0, 0, 0, 0.5);
}
.rank {
min-width: 30px;
color: var(--hover-color);
}
.address {
flex: 1;
font-family: monospace;
}
.stat {
color: #0f0;
font-family: monospace;
}

View File

@ -47,6 +47,65 @@
margin-bottom: 0;
}
}
.bossModal {
padding: 32px;
z-index: 3;
max-width: 100%;
width: 500px;
text-align: center;
margin-top: 50px;
.outcome {
font-size: 1.7rem;
}
.image {
position: relative;
margin: 0 auto;
top: 0;
&::after {
display: none;
}
}
& p {
margin: 0.5rem 0;
}
& button {
margin: 1rem;
}
.lost {
color: var(--accent-color);
}
.won {
color: var(--hover-color);
}
.lost,
.won {
font-size: 2rem;
}
}
.textCenter {
text-align: center;
}
.closeBtn {
display: inline-block;
width: 32px;
height: 32px;
text-align: center;
line-height: 26px;
border-radius: 4px;
position: absolute;
right: 32px;
top: 32px;
background: black;
cursor: pointer;
transition: all 0.2s ease;
}
.closeBtn:hover {
color: var(--hover-color);
transform: scale(1.1);
}
.closeBtn::active {
transform: scale(1.2);
}
@keyframes spin {
0% {

View File

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

View File

@ -2,7 +2,6 @@ import { getDefaultConfig } from '@rainbow-me/rainbowkit';
import {
base,
baseSepolia,
sepolia,
foundry
} from 'wagmi/chains';
@ -13,7 +12,6 @@ export const config = getDefaultConfig({
baseSepolia,
foundry,
base,
...(process.env.NEXT_PUBLIC_ENABLE_TESTNETS === 'true' ? [sepolia] : []),
],
ssr: true,
});

View File

@ -11,7 +11,7 @@ import { INonfungiblePositionManager } from "./lib/INonfungiblePositionManager.s
import { CustomMath} from "./lib/CustomMath.sol";
import {RaidGeldUtils } from "../src/RaidGeldUtils.sol";
import { Army, Player, Raider, Boss } from "../src/RaidGeldStructs.sol";
import {Army, Player, Raider, Boss, LastBossResult} from "../src/RaidGeldStructs.sol";
import { Constants} from "../src/Constants.sol";
contract RaidGeld is ERC20, Ownable, Constants {
@ -23,6 +23,7 @@ contract RaidGeld is ERC20, Ownable, Constants {
mapping(address => Player) private players;
mapping(address => Army) private armies;
mapping(address => Boss) private bosses;
mapping(address => LastBossResult) private lastBossResults;
// WETH
IWETH public immutable weth = IWETH(WETH);
@ -43,7 +44,7 @@ contract RaidGeld is ERC20, Ownable, Constants {
// Events
event PlayerRegistered(address indexed player, uint256 initialGeld);
event PleyerStrikesAgain(address indexed player, uint256 totalRewards, uint256 initialGeld);
event PlayerStrikesAgain(address indexed player, uint256 totalRewards, uint256 initialGeld);
event RaidPerformed(address indexed player, uint256 totalMinted, uint256 geldBalance);
event UnitAdded(
address indexed player,
@ -57,7 +58,10 @@ contract RaidGeld is ERC20, Ownable, Constants {
uint16 championLevel
);
event DaoTokenBuyInAmountSet(address indexed owner, uint256 oldAmount, uint256 newAmount);
event PrestigeGained(address indexed player, uint32 prestigeLevel);
event BossDefeated(address indexed player, uint8 bossLevel, uint256 earnings);
event BossBattle(address indexed player, uint8 bossLevel, bool hasWon);
/* * @notice emitted when the UniV3 pool is created and the initial liquidity position is minted
* @param pool pool address
* @param positionId NFT position Id
@ -127,8 +131,7 @@ contract RaidGeld is ERC20, Ownable, Constants {
players[player].n_runs += 1;
if (existing_player) {
// TODO: Emit new run
emit PleyerStrikesAgain(player, players[player].total_rewards, INITIAL_GELD);
emit PlayerStrikesAgain(player, players[player].total_rewards, INITIAL_GELD);
} else {
// Emit event
emit PlayerRegistered(msg.sender, INITIAL_GELD);
@ -152,6 +155,9 @@ contract RaidGeld is ERC20, Ownable, Constants {
profit_per_second: 0
});
bosses[_playerAddress] = Boss({level: 0, variants: RaidGeldUtils.generate_boss_variants(block.prevrandao)});
// dont change lastBossResult
// that only changes after boss battles and on init
}
// New player want to register with ETH
@ -223,6 +229,11 @@ contract RaidGeld is ERC20, Ownable, Constants {
return bosses[addr];
}
// Function to get the Boss struct
function getLastBossResult(address addr) public view returns (LastBossResult memory) {
return lastBossResults[addr];
}
// Quick fn to check if user is registered
function isRegistered(address addr) public view returns (bool) {
return players[addr].created_at != 0;
@ -300,16 +311,30 @@ contract RaidGeld is ERC20, Ownable, Constants {
? RaidGeldUtils.getBossPower(boss_to_attack.level)
: balanceOf(msg.sender);
bool hasWonBattle = RaidGeldUtils.calculateBossFight(boss_to_attack.level, geld_to_burn, block.prevrandao);
emit BossBattle(msg.sender, boss_to_attack.level, hasWonBattle);
lastBossResults[msg.sender] = LastBossResult({
battled_at: block.timestamp,
level: boss_to_attack.level,
variant: boss_to_attack.variants[boss_to_attack.level],
prestigeGained: false,
reward: 0
});
if (hasWonBattle) {
// Burn geld, send some sweet DAO Token and continue
_burn(msg.sender, geld_to_burn);
uint256 reward = RaidGeldUtils.calculateBossReward(boss_to_attack.level, BUY_IN_DAO_TOKEN_AMOUNT);
players[msg.sender].total_rewards += reward;
daoToken.transfer(msg.sender, reward);
emit BossDefeated(msg.sender, boss_to_attack.level, reward);
lastBossResults[msg.sender].reward = reward;
if (boss_to_attack.level == 6) {
// User ascends! Moloch is defeated, user can start a new run
players[msg.sender].prestige_level += 1;
emit PrestigeGained(msg.sender, players[msg.sender].prestige_level);
player_dies(msg.sender);
lastBossResults[msg.sender].prestigeGained = true;
return [hasWonBattle, true /* New prestige level! */ ];
} else {
// else go to next boss
@ -317,6 +342,7 @@ contract RaidGeld is ERC20, Ownable, Constants {
}
} else {
// Whoops u died, boss defeated you
lastBossResults[msg.sender].reward = 0;
player_dies(msg.sender);
}
return [hasWonBattle, false /* hasnt gotten prestige level */ ];

View File

@ -28,3 +28,11 @@ struct Boss {
uint8 level;
uint8[7] variants;
}
struct LastBossResult {
uint256 battled_at;
uint256 reward;
uint8 level;
uint8 variant;
bool prestigeGained;
}

View File

@ -26,13 +26,13 @@ library RaidGeldUtils {
function getBossPower(uint8 level) internal pure returns (uint256 power) {
require(level <= 7, "Bosses only go to level 7");
if (level == 0) return 9000000;
else if (level == 1) return 90000000;
else if (level == 2) return 900000000;
else if (level == 3) return 9000000000;
else if (level == 4) return 90000000000;
else if (level == 5) return 900000000000;
else if (level == 6) return 9000000000000;
if (level == 0) return 3000000;
else if (level == 1) return 30000000;
else if (level == 2) return 300000000;
else if (level == 3) return 3000000000;
else if (level == 4) return 30000000000;
else if (level == 5) return 300000000000;
else if (level == 6) return 3000000000000;
}
function getBossCumulativeChance(uint8 level) internal pure returns (uint256 chance) {
@ -98,8 +98,8 @@ library RaidGeldUtils {
function calculateBossReward(uint8 bossLevel, uint256 baseReward) internal pure returns (uint256) {
// TODO: This could as well just be pre-calculated
uint256 cumulativeChance = getBossCumulativeChance(bossLevel); // 0 - 1e18 range
uint256 rewardMultiplier = ((2 * (1e18 - cumulativeChance)) ** 2) / 1e18;
return (baseReward * rewardMultiplier);
uint256 rewardMultiplier = ((2 * (1e18 - cumulativeChance)) ** 2);
return (baseReward * rewardMultiplier) / 1e18 ** 2;
}
// Calculates whether user survives the fight

View File

@ -3,7 +3,7 @@ pragma solidity ^0.8.13;
import {Test, console} from "forge-std/Test.sol";
import {stdStorage, StdStorage} from "forge-std/Test.sol";
import {RaidGeld, Army, Player, Boss} from "../src/RaidGeld.sol";
import {RaidGeld, Army, Player, Boss, LastBossResult} from "../src/RaidGeld.sol";
import "../src/RaidGeldUtils.sol";
import {Constants} from "../src/Constants.sol";
@ -288,6 +288,13 @@ contract raid_geldTest is Test, Constants {
uint256 afterBossDaoBalance = raid_geld.daoToken().balanceOf(player1);
uint256 afterBossContractBalance = raid_geld.daoToken().balanceOf(address(raid_geld));
LastBossResult memory bossResult = raid_geld.getLastBossResult(player1);
assertEq(bossResult.battled_at, block.timestamp);
assertEq(bossResult.reward, afterBossDaoBalance - initialDaoBalance);
assertEq(bossResult.level, boss.level);
assertEq(bossResult.variant, boss.variants[boss.level]);
assertEq(bossResult.prestigeGained, false);
// User should receive funs, contract should lose them
assertLt(initialDaoBalance, afterBossDaoBalance);
@ -311,6 +318,7 @@ contract raid_geldTest is Test, Constants {
registerPlayerWithDaoToken();
raid_geld.addUnit(0, 1);
Boss memory boss = raid_geld.getBoss(player1);
bool[2] memory results = raid_geld.battle_with_boss();
// Should lose with just starting GELD
assertEq(results[0], false);
@ -326,6 +334,13 @@ contract raid_geldTest is Test, Constants {
assertEq(raid_geld.balanceOf(player1), 0);
// Units should reset
assertEq(army.moloch_denier.level, 0);
LastBossResult memory bossResult = raid_geld.getLastBossResult(player1);
assertEq(bossResult.battled_at, block.timestamp);
assertEq(bossResult.reward, 0);
assertEq(bossResult.level, boss.level);
assertEq(bossResult.variant, boss.variants[boss.level]);
assertEq(bossResult.prestigeGained, false);
}
function test_08_player_who_lost_can_restart() public {
@ -393,6 +408,8 @@ contract raid_geldTest is Test, Constants {
if (results[0] == true && results[1] == true) {
success = true;
Player memory player = raid_geld.getPlayer(player1);
LastBossResult memory bossResult = raid_geld.getLastBossResult(player1);
assertEq(bossResult.prestigeGained, true);
vm.assertEq(player.prestige_level, 1);
vm.assertEq(player.n_runs, tries);
break;

View File

@ -142,10 +142,15 @@ contract raid_geldTest is Test {
function test_4_print_boss_rewards() public {
uint256 total = 0;
for (uint8 i = 0; i < 7; i++) {
uint256 reward = RaidGeldUtils.calculateBossReward(i, 500);
uint256 reward = RaidGeldUtils.calculateBossReward(i, 500e18);
console.log("Reward", i,reward);
total += reward;
}
console.log("Total", total);
}
function test_5_last_boss_is_unique(uint256 _randomSeed) public {
uint8[7] memory bosses = RaidGeldUtils.generate_boss_variants(_randomSeed);
vm.assertEq(bosses[6], 6);
}
}