1
0
forked from mico/idle_moloch

Merges main

This commit is contained in:
mic0 2024-10-31 14:00:51 +01:00
commit d83e5323d2
Signed by: mico
GPG Key ID: A3F8023524CF1C8D
6 changed files with 483 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,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<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.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 (
<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,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 = () => {
<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

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

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