diff --git a/app/public/background/boss/2_lust.svg b/app/public/background/boss/2_lust.svg index 04def42..bb72eaf 100644 --- a/app/public/background/boss/2_lust.svg +++ b/app/public/background/boss/2_lust.svg @@ -1,938 +1,938 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + diff --git a/app/public/background/clouds_large.png b/app/public/background/clouds_large.png index 879f8ba..532b051 100644 Binary files a/app/public/background/clouds_large.png and b/app/public/background/clouds_large.png differ diff --git a/app/public/background/clouds_small.png b/app/public/background/clouds_small.png index 34ead34..def7276 100644 Binary files a/app/public/background/clouds_small.png and b/app/public/background/clouds_small.png differ diff --git a/app/public/background/tower/5_pride.svg b/app/public/background/tower/5_pride.svg index c95ee68..e5f4fd6 100644 --- a/app/public/background/tower/5_pride.svg +++ b/app/public/background/tower/5_pride.svg @@ -1,494 +1,347 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/components/Army.tsx b/app/src/components/Army.tsx index 1645a3c..9989226 100644 --- a/app/src/components/Army.tsx +++ b/app/src/components/Army.tsx @@ -80,7 +80,7 @@ const unitAvailableToDiscoverAt: Record = { }; 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 (
addUnit(unitType)} - className={`${styles.armyUnit} ${canPurchase ? "" : styles.isUnavailable - }`} + className={`${styles.armyUnit} ${ + canPurchase ? "" : styles.isUnavailable + }`} >
GELD )} - {n_units > 0 ? ( + {unitLevel > 0 ? ( - {n_units} + {`lvl ${unitLevel}`} ) : null} - {n_units > 0 ? ( + {unitLevel > 0 ? ( {unitProfit} per sec diff --git a/app/src/components/BossInfo.tsx b/app/src/components/BossInfo.tsx index eb36b23..a17e8be 100644 --- a/app/src/components/BossInfo.tsx +++ b/app/src/components/BossInfo.tsx @@ -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 = { 0: styles.boss0, @@ -14,7 +14,7 @@ export const bossLevelToClass: Record = { 6: styles.boss6, } -const bossToName: Record = { +export const bossToName: Record = { 0: "Gluttonous", 1: "Slothful", 2: "Lusty", @@ -24,7 +24,7 @@ const bossToName: Record = { 6: "Greedy", } -const bossToReward: Record = { +export const bossToReward: Record = { 0: BigInt("200000000000000000"), 1: BigInt("28274420000000000000"), 2: BigInt("174191628800000000000"), @@ -46,13 +46,13 @@ const bossToChance: Record = { } const bossToBossPower: Record = { - 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
-

{bossToName[variant]} Moloch (lvl {boss ? boss.level + 1 : 0})

-

{formatUnits(bossToReward[boss?.level || 0], 18)} RGCVII reward

+

{bossToName[variant]} Moloch (lvl {boss ? boss.level + 1 : 0})

+

{parseFloat(parseFloat(formatUnits(bossToReward[boss?.level || 0], 18).toString()).toFixed(4))} RGCVII reward

- {chanceToDefeat.current * 100} % to slay{" "} + {parseFloat((chanceToDefeat.current * 100).toFixed(2))} % to slay{" "} {chanceToDefeat.current == maxChance ? (MAXED) : (Max {maxChance * 100}%)}

+

{toReadable(bossToBossPower[boss?.level ?? 0])} GELD = max chance

} diff --git a/app/src/components/BossOutcomeModal.tsx b/app/src/components/BossOutcomeModal.tsx new file mode 100644 index 0000000..793b082 --- /dev/null +++ b/app/src/components/BossOutcomeModal.tsx @@ -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 ? and you won! 🤩 : and you lost 😔; + const rewardAmount = parseFloat(parseFloat(formatUnits(bossToReward[lastBossResult.level], 18).toString()).toFixed(4)); + const rewardText = + ascended ?

You won {rewardAmount} RGCVII and ASCENDED!!!. This means you beat the bosses and gained a Prestige level. Your GELD is now forfeit, but your legend lives on.

+ : outcome ?

You won {rewardAmount} RGCVII

+ :

Your GELD is now forfeit.
Try again 💪 we know you can do it!

+ + const bossName = bossToName[lastBossResult.variant]; + const bossClass = bossLevelToClass[lastBossResult.variant]; + + return
+

You battled {bossName} Moloch!

+
+

{text}

+ {rewardText} +
+ +
+
+} + +export default BossOutcomeModal diff --git a/app/src/components/Counter.tsx b/app/src/components/Counter.tsx index 843fa32..41e2646 100644 --- a/app/src/components/Counter.tsx +++ b/app/src/components/Counter.tsx @@ -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}`; } } } diff --git a/app/src/components/Header.tsx b/app/src/components/Header.tsx index 60eecdf..7e66825 100644 --- a/app/src/components/Header.tsx +++ b/app/src/components/Header.tsx @@ -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 + if (isRegistered && player?.has_active_session) { + return ; } else { - return

SLAY THE MOLOCH

+ return ( +

+ SLAY THE MOLOCH +

+ ); } - }, [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 ? (

per second: {perSecond}

- : 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
-

{title}

- {subtitle} - {perSecondParagraph} -
-} + return ( +
+

{title}

+ {subtitle} + {perSecondParagraph} +
+ ); +}; // export default Header diff --git a/app/src/components/Leaderboard.tsx b/app/src/components/Leaderboard.tsx new file mode 100644 index 0000000..47f7287 --- /dev/null +++ b/app/src/components/Leaderboard.tsx @@ -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() + const [topRaiders, setTopRaiders] = useState() + 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 ( +
+

Leaderboard

+ +
+ + +
+ + {activeTab === 'earners' && ( +
+ {topEarners?.armies.map((army, index) => ( +
+ #{index + 1} + {army.player.id.slice(0, 6)}...{army.player.id.slice(-4)} + {formatUnits(BigInt(army.profitPerSecond), 4)} GELD/s +
+ ))} +
+ )} + + {activeTab === 'raiders' && ( +
+ {topRaiders?.players.map((player, index) => ( +
+ #{index + 1} + {player.id.slice(0, 6)}...{player.id.slice(-4)} + {player.numberOfRaids} raids +
+ ))} +
+ )} +
+ ) +} + +export default Leaderboard \ No newline at end of file diff --git a/app/src/components/PixelatedQuote.tsx b/app/src/components/PixelatedQuote.tsx index 45cabf9..311477e 100644 --- a/app/src/components/PixelatedQuote.tsx +++ b/app/src/components/PixelatedQuote.tsx @@ -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(null); // Define the type for Node environment compatibility + const intervalIdRef = useRef(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 (
{currentQuote}
diff --git a/app/src/components/RegistrationModal.tsx b/app/src/components/RegistrationModal.tsx index 0161a9e..ed05ec6 100644 --- a/app/src/components/RegistrationModal.tsx +++ b/app/src/components/RegistrationModal.tsx @@ -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
-

Insert coins to continue

+ return
+ setIsOpen(false)}>x +

Insert coins to continue

- +
-
+
} export default RegistrationModal diff --git a/app/src/components/Scene.tsx b/app/src/components/Scene.tsx index 997d97b..e891639 100644 --- a/app/src/components/Scene.tsx +++ b/app/src/components/Scene.tsx @@ -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
@@ -35,12 +41,31 @@ const Scene = () => {
- + {isRegistered && player?.has_active_session && }
+ + {isLeaderboardOpen && ( +
+
+ + +
+ )}
} -export default Scene +export default Scene; diff --git a/app/src/components/WaitingForTxModal.tsx b/app/src/components/WaitingForTxModal.tsx index be8ccde..ce190c4 100644 --- a/app/src/components/WaitingForTxModal.tsx +++ b/app/src/components/WaitingForTxModal.tsx @@ -24,7 +24,7 @@ const WaitingForTxModal = ({
-

Writing contract ...

+

Spinning the chain ...

} diff --git a/app/src/pages/_app.tsx b/app/src/pages/_app.tsx index 533f4e8..23c9c58 100644 --- a/app/src/pages/_app.tsx +++ b/app/src/pages/_app.tsx @@ -36,6 +36,7 @@ function MyApp({ Component, pageProps }: AppProps) { h4, h5, h6, + button, .title { font-family: ${font.style.fontFamily}; } diff --git a/app/src/pages/index.tsx b/app/src/pages/index.tsx index 6af5fad..17a3b03 100644 --- a/app/src/pages/index.tsx +++ b/app/src/pages/index.tsx @@ -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 = () => {
- Made with ❤️ by your frens at 😈 Slay the Moloch team for Cohort VII of RaidGuild + Made with ❤️ by your frens at 😈 Slay the Moloch team for Cohort VII of{" "} + + RaidGuild +
); diff --git a/app/src/providers/PlayerProvider.tsx b/app/src/providers/PlayerProvider.tsx index 063bc35..199fe9f 100644 --- a/app/src/providers/PlayerProvider.tsx +++ b/app/src/providers/PlayerProvider.tsx @@ -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({ @@ -50,6 +61,7 @@ const PlayerContext = createContext({ 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 ( { army: army as Army, boss: boss as Boss, balance: balance as bigint, + lastBossResult: lastBossResult as LastBossResult, register, raid, addUnit, battleWithBoss }}> {children} - {txHash && } +
+ {txHash && } + {bossBattledModalOpen && } +
); } diff --git a/app/src/styles/Army.module.css b/app/src/styles/Army.module.css index e371096..bc8e406 100644 --- a/app/src/styles/Army.module.css +++ b/app/src/styles/Army.module.css @@ -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%; diff --git a/app/src/styles/Background.module.css b/app/src/styles/Background.module.css index e46e014..2d426d6 100644 --- a/app/src/styles/Background.module.css +++ b/app/src/styles/Background.module.css @@ -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); +} diff --git a/app/src/styles/Header.module.css b/app/src/styles/Header.module.css index ce97767..6cfb7fa 100644 --- a/app/src/styles/Header.module.css +++ b/app/src/styles/Header.module.css @@ -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; diff --git a/app/src/styles/Home.module.css b/app/src/styles/Home.module.css index 8223cef..4b20934 100644 --- a/app/src/styles/Home.module.css +++ b/app/src/styles/Home.module.css @@ -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; } diff --git a/app/src/styles/Info.module.css b/app/src/styles/Info.module.css index 39ab2b2..440b4de 100644 --- a/app/src/styles/Info.module.css +++ b/app/src/styles/Info.module.css @@ -1,5 +1,6 @@ .bossInfo { position: absolute; + z-index: 10; top: 350px; right: 28px; background: var(--bg-color); diff --git a/app/src/styles/Leaderboard.module.css b/app/src/styles/Leaderboard.module.css new file mode 100644 index 0000000..952f19f --- /dev/null +++ b/app/src/styles/Leaderboard.module.css @@ -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; + } \ No newline at end of file diff --git a/app/src/styles/Modal.module.css b/app/src/styles/Modal.module.css index a1ec842..08a220d 100644 --- a/app/src/styles/Modal.module.css +++ b/app/src/styles/Modal.module.css @@ -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% { diff --git a/app/src/types/leaderboard.ts b/app/src/types/leaderboard.ts new file mode 100644 index 0000000..ef2507d --- /dev/null +++ b/app/src/types/leaderboard.ts @@ -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[] + } \ No newline at end of file diff --git a/app/src/wagmi.ts b/app/src/wagmi.ts index 332811c..9cf52cd 100644 --- a/app/src/wagmi.ts +++ b/app/src/wagmi.ts @@ -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, }); diff --git a/src/RaidGeld.sol b/src/RaidGeld.sol index b480bb8..fb2a4d8 100644 --- a/src/RaidGeld.sol +++ b/src/RaidGeld.sol @@ -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 */ ]; diff --git a/src/RaidGeldStructs.sol b/src/RaidGeldStructs.sol index f30deed..e045916 100644 --- a/src/RaidGeldStructs.sol +++ b/src/RaidGeldStructs.sol @@ -28,3 +28,11 @@ struct Boss { uint8 level; uint8[7] variants; } + +struct LastBossResult { + uint256 battled_at; + uint256 reward; + uint8 level; + uint8 variant; + bool prestigeGained; +} diff --git a/src/RaidGeldUtils.sol b/src/RaidGeldUtils.sol index b680008..f167959 100644 --- a/src/RaidGeldUtils.sol +++ b/src/RaidGeldUtils.sol @@ -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 diff --git a/test/RaidGeld.t.sol b/test/RaidGeld.t.sol index e71a781..0ecbbdb 100644 --- a/test/RaidGeld.t.sol +++ b/test/RaidGeld.t.sol @@ -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; diff --git a/test/RaidGeldUtils.t.sol b/test/RaidGeldUtils.t.sol index 2859ab1..5bb42b3 100644 --- a/test/RaidGeldUtils.t.sol +++ b/test/RaidGeldUtils.t.sol @@ -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); + } }