Compare commits

..

2 Commits

Author SHA1 Message Date
san
6d8caeae7c add tavern keeper quotes
Some checks failed
CI / Foundry project (push) Waiting to run
CI / Foundry project (pull_request) Has been cancelled
2024-10-31 12:59:56 +05:30
san
8abb460815 implement leaderboard ui
Some checks failed
CI / Foundry project (push) Has been cancelled
CI / Foundry project (pull_request) Has been cancelled
2024-10-30 19:55:28 +05:30
6 changed files with 402 additions and 17 deletions

View File

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

View File

@ -1,51 +1,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<NodeJS.Timeout | null>(null); // Define the type for Node environment compatibility
const [currentQuote, setCurrentQuote] = useState("Welcome to the Dark Forest!");
const intervalIdRef = useRef<NodeJS.Timeout | null>(null);
const hasShownWelcome = useRef(false);
// Determine which tier of quotes to use based on total minted
const getQuoteTier = (totalMinted: bigint) => {
if (totalMinted >= PROGRESSION_TIERS.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(() => {
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(
tavernerQuotes[Math.floor(Math.random() * tavernerQuotes.length)]
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 (
<div>
<div
className={`pixel-borders pixel-borders--2 pixelFont ${styles.pixelQuote}`}
style={{ opacity: isShown ? 1 : 0 /* Control visibility with opacity */ }}
style={{ opacity: isShown ? 1 : 0 }}
>
{currentQuote}
</div>

View File

@ -1,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 = () => {
<div className={`${styles.bonfire} ${styles.background_asset}`} />
<Army />
<MusicPlayer onReady={handleMusicReady} />
<button
onClick={() => setIsLeaderboardOpen(true)}
className={styles.leaderboardButton}
title="Leaderboard"
>
📜
</button>
{isLeaderboardOpen && (
<div className={styles.leaderboardOverlay}>
<div className={styles.leaderboardContent}>
<button
className={styles.closeButton}
onClick={() => setIsLeaderboardOpen(false)}
>
×
</button>
<Leaderboard />
</div>
</div>
)}
</div>
}

View File

@ -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);
}

View File

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

View File

@ -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[]
}