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..96ffd66 100644 --- a/app/src/components/PixelatedQuote.tsx +++ b/app/src/components/PixelatedQuote.tsx @@ -1,51 +1,219 @@ 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 [currentQuote, setCurrentQuote] = useState("Welcome to the Dark Forest!"); + 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); - // 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/Scene.tsx b/app/src/components/Scene.tsx index f7b8813..ca19d1e 100644 --- a/app/src/components/Scene.tsx +++ b/app/src/components/Scene.tsx @@ -1,9 +1,10 @@ -import React, { useCallback } from "react" +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"; @@ -20,6 +21,8 @@ const bossToMountainsClass = { const Scene = () => { const { isRegistered, boss } = usePlayer(); + const [isLeaderboardOpen, setIsLeaderboardOpen] = useState(false); + const handleMusicReady = useCallback((unmute: () => void) => { if (isRegistered) { unmute(); @@ -40,6 +43,26 @@ const Scene = () => {
+ + {isLeaderboardOpen && ( +
+
+ + +
+
+ )}
} diff --git a/app/src/styles/Background.module.css b/app/src/styles/Background.module.css index e46e014..ba3de6b 100644 --- a/app/src/styles/Background.module.css +++ b/app/src/styles/Background.module.css @@ -364,3 +364,69 @@ transform: scale(1.02, 1.03) skew(0deg, -1deg); } } + +.leaderboardButton { + position: absolute; + top: 80px; + right: 30px; + 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); +} + +.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/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/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