From 8abb4608159501ab4a0cdf601ed37b7010250786 Mon Sep 17 00:00:00 2001 From: san Date: Wed, 30 Oct 2024 19:55:28 +0530 Subject: [PATCH 1/3] implement leaderboard ui --- app/src/components/Leaderboard.tsx | 114 ++++++++++++++++++++++++++ app/src/components/Scene.tsx | 25 +++++- app/src/styles/Background.module.css | 66 +++++++++++++++ app/src/styles/Leaderboard.module.css | 70 ++++++++++++++++ app/src/types/leaderboard.ts | 25 ++++++ 5 files changed, 299 insertions(+), 1 deletion(-) create mode 100644 app/src/components/Leaderboard.tsx create mode 100644 app/src/styles/Leaderboard.module.css create mode 100644 app/src/types/leaderboard.ts 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/Scene.tsx b/app/src/components/Scene.tsx index c80fd96..cbd9b9a 100644 --- a/app/src/components/Scene.tsx +++ b/app/src/components/Scene.tsx @@ -1,13 +1,16 @@ -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"; const Scene = () => { const { isRegistered } = usePlayer(); + const [isLeaderboardOpen, setIsLeaderboardOpen] = useState(false); + const handleMusicReady = useCallback((unmute: () => void) => { if (isRegistered) { unmute(); @@ -25,6 +28,26 @@ const Scene = () => {
+ + {isLeaderboardOpen && ( +
+
+ + +
+
+ )}
} diff --git a/app/src/styles/Background.module.css b/app/src/styles/Background.module.css index 83d7bb1..8bc6159 100644 --- a/app/src/styles/Background.module.css +++ b/app/src/styles/Background.module.css @@ -256,3 +256,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 From 6d8caeae7c3317e93948f0b2ad3ed2a1d47cec7b Mon Sep 17 00:00:00 2001 From: san Date: Thu, 31 Oct 2024 12:59:56 +0530 Subject: [PATCH 2/3] add tavern keeper quotes --- app/src/components/PixelatedQuote.tsx | 119 ++++++++++++++++++++++---- 1 file changed, 103 insertions(+), 16 deletions(-) diff --git a/app/src/components/PixelatedQuote.tsx b/app/src/components/PixelatedQuote.tsx index 45cabf9..2579399 100644 --- a/app/src/components/PixelatedQuote.tsx +++ b/app/src/components/PixelatedQuote.tsx @@ -1,51 +1,138 @@ 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), + LEGENDARY: BigInt(1000000000000), +}; + +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.", + "Don't worry, we keep the bandages cheap for newcomers like yourself.", + "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.", + "Starting to walk with a bit of confidence, eh? Don't let it go to your head.", + "The local wolves don't laugh quite as hard when they see you coming now.", + "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.", + "Those monsters in the forest? They're starting to learn your name.", + ], + 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.", + "Your presence honors us. What tales will you bring today?", + "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!", + "The stuff of legends walks among us, friends!", + "They say you've slain dragons now. Dragons! In my tavern!", + "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.", + "To think, I've watched you grow from newcomer to living legend!", + "The ancient prophecies speak of one such as you...", + ], + LEGENDARY: [ + "The greatest raider of our age graces us! All hail!", + "Let it be known - a living legend walks among us!", + "Champion of champions! Slayer of the Moloch!", + "The Moloch speaks your name in terror!", + "Even the gods watch your journey with interest!", + ], +}; + +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.LEGENDARY) return "LEGENDARY"; + 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}
From b6f656c0dd0b59e77267263f760454eefac535a4 Mon Sep 17 00:00:00 2001 From: san Date: Thu, 31 Oct 2024 14:56:02 +0530 Subject: [PATCH 3/3] add all available quotes --- app/src/components/PixelatedQuote.tsx | 85 ++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 2 deletions(-) diff --git a/app/src/components/PixelatedQuote.tsx b/app/src/components/PixelatedQuote.tsx index 2579399..96ffd66 100644 --- a/app/src/components/PixelatedQuote.tsx +++ b/app/src/components/PixelatedQuote.tsx @@ -10,7 +10,11 @@ const PROGRESSION_TIERS = { EXPERIENCED: BigInt(1000000000), EXPERT: BigInt(10000000000), MASTER: BigInt(100000000000), - LEGENDARY: BigInt(1000000000000), + EPIC: BigInt(1000000000000), + LEGENDARY: BigInt(10000000000000), + GUILD_LEADER: BigInt(100000000000000), + DIVINE: BigInt(1000000000000000), + MAXIMUM: BigInt(10000000000000000), }; const quotes = { @@ -18,14 +22,24 @@ const quotes = { "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: [ @@ -33,35 +47,98 @@ const quotes = { "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!", - "The Moloch speaks your name in terror!", + "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!", ], }; @@ -80,7 +157,11 @@ function PixelatedQuote() { // 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";