Merge branch 'main' into burn
|
Before Width: | Height: | Size: 433 KiB After Width: | Height: | Size: 433 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 178 KiB |
@ -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>
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
|
||||
41
app/src/components/BossOutcomeModal.tsx
Normal 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
|
||||
@ -16,26 +16,27 @@ 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) {
|
||||
if (i == 0) {
|
||||
@ -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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
114
app/src/components/Leaderboard.tsx
Normal 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
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
}
|
||||
|
||||
|
||||
@ -36,6 +36,7 @@ function MyApp({ Component, pageProps }: AppProps) {
|
||||
h4,
|
||||
h5,
|
||||
h6,
|
||||
button,
|
||||
.title {
|
||||
font-family: ${font.style.fontFamily};
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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%;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
.bossInfo {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 350px;
|
||||
right: 28px;
|
||||
background: var(--bg-color);
|
||||
|
||||
70
app/src/styles/Leaderboard.module.css
Normal 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;
|
||||
}
|
||||
@ -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% {
|
||||
|
||||
25
app/src/types/leaderboard.ts
Normal 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[]
|
||||
}
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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,6 +58,9 @@ 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
|
||||
@ -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 */ ];
|
||||
|
||||
@ -28,3 +28,11 @@ struct Boss {
|
||||
uint8 level;
|
||||
uint8[7] variants;
|
||||
}
|
||||
|
||||
struct LastBossResult {
|
||||
uint256 battled_at;
|
||||
uint256 reward;
|
||||
uint8 level;
|
||||
uint8 variant;
|
||||
bool prestigeGained;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||