diff --git a/app/package-lock.json b/app/package-lock.json index ea5c5d7..f91ba15 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -14,6 +14,7 @@ "next": "^14.2.10", "react": "^18.3.1", "react-dom": "^18.3.1", + "sass": "^1.80.4", "viem": "^2.17.0", "wagmi": "^2.12.17" }, @@ -22,6 +23,7 @@ "@types/react": "^18.3.5", "eslint": "8.57.0", "eslint-config-next": "14.2.15", + "pixel-borders": "^1.1.4", "typescript": "5.5.2" } }, @@ -9103,6 +9105,11 @@ "node": ">=16.x" } }, + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==" + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -12110,6 +12117,15 @@ "node": ">= 6" } }, + "node_modules/pixel-borders": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/pixel-borders/-/pixel-borders-1.1.4.tgz", + "integrity": "sha512-jBX9MMRsCeYNl1sHnXnAgtBD5hLAMa59/MDvVBZUoeHtsF6t97goigzIPQHvssh8xsQ5w+vF7hqabutbpb0zxA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/pkg-dir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", @@ -13343,6 +13359,49 @@ "node": ">=10" } }, + "node_modules/sass": { + "version": "1.80.4", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.80.4.tgz", + "integrity": "sha512-rhMQ2tSF5CsuuspvC94nPM9rToiAFw2h3JTrLlgmNw1MH79v8Cr3DH6KF6o6r+8oofY3iYVPUf66KzC8yuVN1w==", + "dependencies": { + "@parcel/watcher": "^2.4.1", + "chokidar": "^4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", diff --git a/app/package.json b/app/package.json index 53665ae..3310ec1 100644 --- a/app/package.json +++ b/app/package.json @@ -15,6 +15,7 @@ "next": "^14.2.10", "react": "^18.3.1", "react-dom": "^18.3.1", + "sass": "^1.80.4", "viem": "^2.17.0", "wagmi": "^2.12.17" }, @@ -23,6 +24,7 @@ "@types/react": "^18.3.5", "eslint": "8.57.0", "eslint-config-next": "14.2.15", + "pixel-borders": "^1.1.4", "typescript": "5.5.2" } } diff --git a/app/src/components/Army.tsx b/app/src/components/Army.tsx index 5d3106f..fce26e6 100644 --- a/app/src/components/Army.tsx +++ b/app/src/components/Army.tsx @@ -1,7 +1,8 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { UnitType, usePlayer } from '../providers/PlayerProvider'; -import styles from '../styles/Army.module.css'; -import { calculateBalance, toReadable } from './Counter'; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { UnitType, usePlayer } from "../providers/PlayerProvider"; +import styles from "../styles/Army.module.css"; +import { calculateBalance, toReadable } from "./Counter"; +import PixelatedQuote from "./PixelatedQuote"; const PRECISION = BigInt(10000); const PRICE_FACTOR = BigInt(11500); @@ -11,16 +12,20 @@ const base_cost: Record = { 1: BigInt(3420000), 2: BigInt(30096000), 3: BigInt(255816000), -} +}; const profits: Record = { 0: BigInt(2533), 1: BigInt(27863), 2: BigInt(306493), 3: BigInt(3371423), -} +}; -function calculateUnitPrice(unit: UnitType, currentLevel: number, units: number) { +function calculateUnitPrice( + unit: UnitType, + currentLevel: number, + units: number +) { let rollingPriceCalculation = base_cost[unit]; let price = BigInt(0); @@ -29,127 +34,172 @@ function calculateUnitPrice(unit: UnitType, currentLevel: number, units: number) if (i >= currentLevel) { price += rollingPriceCalculation; } - rollingPriceCalculation = rollingPriceCalculation * PRICE_FACTOR / PRECISION; + rollingPriceCalculation = + (rollingPriceCalculation * PRICE_FACTOR) / PRECISION; } return price; } function calculateProfitPerSecond(unit: UnitType, level: number) { // Each next unit scales progressivelly better - return BigInt(level) * profits[unit] + return BigInt(level) * profits[unit]; } const unitTypeToCss: Record = { 0: styles.moloch_denier, 1: styles.apprentice, 2: styles.anointed, - 3: styles.champion -} + 3: styles.champion, +}; const unitTypeToName: Record = { 0: "Moloch denier", 1: "Apprentice", 2: "Anointed", - 3: "Champion" -} + 3: "Champion", +}; const defaultAvailabilityMap: Record = { 0: false, 1: false, 2: false, - 3: false -} + 3: false, +}; const unitDiscoveredAt: Record = { 0: BigInt(0), 1: BigInt(300_0000), 2: BigInt(2800_0000), - 3: BigInt(24000_0000) -} + 3: BigInt(24000_0000), +}; const unitAvailableToDiscoverAt: Record = { 0: BigInt(0), 1: BigInt(200_0000), 2: BigInt(2000_0000), 3: BigInt(25000_0000), -} +}; interface UnitProps { addUnit: (unitType: UnitType) => void; unitType: UnitType; canPurchase: boolean; isShrouded: boolean; - n_units: number + n_units: number; } -const Unit = ({ addUnit, unitType, canPurchase, isShrouded, n_units }: UnitProps) => { +const Unit = ({ + addUnit, + unitType, + canPurchase, + isShrouded, + n_units, +}: UnitProps) => { const [unitPrice, unitProfit] = useMemo(() => { return [ toReadable(calculateUnitPrice(unitType, n_units, 1)), - toReadable(calculateProfitPerSecond(unitType, n_units)) - ] + toReadable(calculateProfitPerSecond(unitType, n_units)), + ]; }, [n_units, unitType]); - return
addUnit(unitType)} className={`${styles.armyUnit} ${canPurchase ? "" : styles.isUnavailable}`}> -
addUnit(unitType)} + className={`${styles.armyUnit} ${ + canPurchase ? "" : styles.isUnavailable + }`} + > +
- {isShrouded ? "???????" : unitTypeToName[unitType]} - {isShrouded ? null : {unitPrice} GELD} - {n_units > 0 ? {n_units} : null} - {n_units > 0 ? {unitProfit} per sec : null} -
-} + `} + /> + + {isShrouded ? "???????" : unitTypeToName[unitType]} + + {isShrouded ? null : ( + + {unitPrice} GELD + + )} + {n_units > 0 ? ( + + {n_units} + + ) : null} + {n_units > 0 ? ( + + {unitProfit} per sec + + ) : null} +
+ ); +}; const Army = () => { - const { army, addUnit, isRegistered, player, balance } = usePlayer() - const [canPurchase, setCanPurchase] = useState>(defaultAvailabilityMap); - const [isShrouded, setIsShrouded] = useState>(defaultAvailabilityMap); - const [canKnowAbout, setCanKnowAbout] = useState>(defaultAvailabilityMap); - const balanceCount = useRef(BigInt(balance ?? 0)) + const { army, addUnit, isRegistered, player, balance } = usePlayer(); + const [canPurchase, setCanPurchase] = useState>( + defaultAvailabilityMap + ); + const [isShrouded, setIsShrouded] = useState>( + defaultAvailabilityMap + ); + const [canKnowAbout, setCanKnowAbout] = useState>( + defaultAvailabilityMap + ); + const balanceCount = useRef(BigInt(balance ?? 0)); const setAvailabilities = useCallback(() => { if (isRegistered) { - const totalMinted = (player?.total_minted ?? BigInt(0)) + (balanceCount.current - (balance ?? BigInt(0))); + const totalMinted = + (player?.total_minted ?? BigInt(0)) + + (balanceCount.current - (balance ?? BigInt(0))); const n_units: Record = { 0: army?.moloch_denier.level ?? 0, 1: army?.apprentice.level ?? 0, 2: army?.anointed.level ?? 0, 3: army?.champion.level ?? 0, - } + }; const inShroud = { 0: totalMinted < unitDiscoveredAt[0], 1: totalMinted < unitDiscoveredAt[1], 2: totalMinted < unitDiscoveredAt[2], 3: totalMinted < unitDiscoveredAt[3], - } + }; const isKnown = { 0: totalMinted >= unitAvailableToDiscoverAt[0], 1: totalMinted >= unitAvailableToDiscoverAt[1], 2: totalMinted >= unitAvailableToDiscoverAt[2], 3: totalMinted >= unitAvailableToDiscoverAt[3], - } - const canActuallyBuy = { - 0: balanceCount.current >= calculateUnitPrice(0, n_units[0], 1) && isKnown[0] && !inShroud[0], - 1: balanceCount.current >= calculateUnitPrice(1, n_units[1], 1) && isKnown[1] && !inShroud[1], - 2: balanceCount.current >= calculateUnitPrice(2, n_units[2], 1) && isKnown[2] && !inShroud[2], - 3: balanceCount.current >= calculateUnitPrice(3, n_units[3], 1) && isKnown[3] && !inShroud[3], }; - setCanPurchase(canActuallyBuy) - setIsShrouded(inShroud) - setCanKnowAbout(isKnown) + const canActuallyBuy = { + 0: + balanceCount.current >= calculateUnitPrice(0, n_units[0], 1) && + isKnown[0] && + !inShroud[0], + 1: + balanceCount.current >= calculateUnitPrice(1, n_units[1], 1) && + isKnown[1] && + !inShroud[1], + 2: + balanceCount.current >= calculateUnitPrice(2, n_units[2], 1) && + isKnown[2] && + !inShroud[2], + 3: + balanceCount.current >= calculateUnitPrice(3, n_units[3], 1) && + isKnown[3] && + !inShroud[3], + }; + setCanPurchase(canActuallyBuy); + setIsShrouded(inShroud); + setCanKnowAbout(isKnown); } else { - setCanPurchase(defaultAvailabilityMap) - setIsShrouded(defaultAvailabilityMap) - setCanKnowAbout(defaultAvailabilityMap) + setCanPurchase(defaultAvailabilityMap); + setIsShrouded(defaultAvailabilityMap); + setCanKnowAbout(defaultAvailabilityMap); } - }, [ - army, - balance, - isRegistered, - player?.total_minted - ]) + }, [army, balance, isRegistered, player?.total_minted]); useEffect(() => { const tickInterval = setInterval(() => { @@ -158,20 +208,63 @@ const Army = () => { army?.profit_per_second ?? BigInt(0), player?.last_raided_at ?? BigInt(0) ); - setAvailabilities() + setAvailabilities(); }, 100); - return () => clearInterval(tickInterval) - }, [balance, army?.profit_per_second, player?.last_raided_at, setAvailabilities]) + return () => clearInterval(tickInterval); + }, [ + balance, + army?.profit_per_second, + player?.last_raided_at, + setAvailabilities, + ]); - return <> -
-
- {canKnowAbout[0] && } - {canKnowAbout[1] && } - {canKnowAbout[2] && } - {canKnowAbout[3] && } -
- -} + return ( + <> +
+ +
+
+ {canKnowAbout[0] && ( + + )} + {canKnowAbout[1] && ( + + )} + {canKnowAbout[2] && ( + + )} + {canKnowAbout[3] && ( + + )} +
+ + ); +}; -export default Army +export default Army; diff --git a/app/src/components/PixelatedQuote.tsx b/app/src/components/PixelatedQuote.tsx new file mode 100644 index 0000000..98aba2c --- /dev/null +++ b/app/src/components/PixelatedQuote.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; + +const tavernerQuotes = [ + "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 [isShown, setIsShown] = useState(true); + const [currentQuote, setCurrentQuote] = useState( + "Welcome to the Dark Forest!" + ); + const intervalIdRef = useRef(null); // Define the type for Node environment compatibility + + 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)] + ); + setIsShown(true); + + // Hide the toast after 4 seconds + setTimeout(() => { + setIsShown(false); + }, 4000); + }, 15000); + + // Clean up the interval on component unmount + return () => { + if (intervalIdRef.current !== null) { + clearInterval(intervalIdRef.current); // Clear interval using correct reference + intervalIdRef.current = null; + } + }; + }, []); + + // const currentQuote = useMemo( + // () => tavernerQuotes[Math.floor(Math.random() * tavernerQuotes.length)], + // [] + // ); + + return ( +
+
+ {currentQuote} +
+
+ ); +} + +export default PixelatedQuote; diff --git a/app/src/pages/_app.tsx b/app/src/pages/_app.tsx index 1f69bf4..bbba428 100644 --- a/app/src/pages/_app.tsx +++ b/app/src/pages/_app.tsx @@ -1,15 +1,16 @@ -import '../styles/globals.css'; -import '@rainbow-me/rainbowkit/styles.css'; -import type { AppProps } from 'next/app'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { WagmiProvider } from 'wagmi'; -import { RainbowKitProvider, midnightTheme } from '@rainbow-me/rainbowkit'; -import { config } from '../wagmi'; -import { Texturina } from 'next/font/google' -import PlayerProvider from '../providers/PlayerProvider'; +import "../styles/globals.css"; +import "../styles/pixelatedBorders.scss"; +import "@rainbow-me/rainbowkit/styles.css"; +import type { AppProps } from "next/app"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { WagmiProvider } from "wagmi"; +import { RainbowKitProvider, midnightTheme } from "@rainbow-me/rainbowkit"; +import { config } from "../wagmi"; +import { Texturina } from "next/font/google"; +import PlayerProvider from "../providers/PlayerProvider"; const client = new QueryClient(); -const font = Texturina({ weight: ['400'], subsets: ["latin"] }) +const font = Texturina({ weight: ["400"], subsets: ["latin"] }); function MyApp({ Component, pageProps }: AppProps) { return ( @@ -17,11 +18,21 @@ function MyApp({ Component, pageProps }: AppProps) { diff --git a/app/src/styles/globals.css b/app/src/styles/globals.css index 8da1309..dd62d0f 100644 --- a/app/src/styles/globals.css +++ b/app/src/styles/globals.css @@ -1,3 +1,5 @@ +@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap'); + :root { --bg-color: #1a1a1a; --text-color: #ffffff; diff --git a/app/src/styles/pixelatedBorders.scss b/app/src/styles/pixelatedBorders.scss new file mode 100644 index 0000000..f5cdeca --- /dev/null +++ b/app/src/styles/pixelatedBorders.scss @@ -0,0 +1,3 @@ +@import "../../node_modules/pixel-borders/src/styles/pixel-borders.scss"; +@import "../../node_modules/pixel-borders/src/styles/pixel-borders/pixel-borders-mixins"; +