diff --git a/app/src/components/Army.tsx b/app/src/components/Army.tsx index 888368d..5d3106f 100644 --- a/app/src/components/Army.tsx +++ b/app/src/components/Army.tsx @@ -1,26 +1,177 @@ -import { usePlayer } from '../providers/PlayerProvider'; +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'; -const Army = () => { - const { army, addUnit, isRegistered } = usePlayer() - return
-
- {isRegistered && <> -
addUnit(0)} className={`${styles.scribe} ${styles.person} ${styles.moloch_denier} ${styles.static}`}> -
Moloch denier: {army?.moloch_denier.level}
-
-
addUnit(1)} className={`${styles.druid} ${styles.person} ${styles.apprentice} ${styles.static}`} > -
Apprentice: {army?.apprentice.level}
-
-
addUnit(2)} className={`${styles.ranger} ${styles.person} ${styles.anointed} ${styles.static}`} > -
Anointed: {army?.anointed.level}
-
-
addUnit(3)} className={`${styles.warrior} ${styles.person} ${styles.champion} ${styles.static}`} > -
Champion: {army?.champion.level}
-
+const PRECISION = BigInt(10000); +const PRICE_FACTOR = BigInt(11500); - } +const base_cost: Record = { + 0: BigInt(380000), + 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) { + let rollingPriceCalculation = base_cost[unit]; + let price = BigInt(0); + + // Each level costs 15% more than previous + for (let i = 0; i < currentLevel + units; i++) { + if (i >= currentLevel) { + price += rollingPriceCalculation; + } + rollingPriceCalculation = rollingPriceCalculation * PRICE_FACTOR / PRECISION; + } + return price; +} + +function calculateProfitPerSecond(unit: UnitType, level: number) { + // Each next unit scales progressivelly better + return BigInt(level) * profits[unit] +} + +const unitTypeToCss: Record = { + 0: styles.moloch_denier, + 1: styles.apprentice, + 2: styles.anointed, + 3: styles.champion +} + +const unitTypeToName: Record = { + 0: "Moloch denier", + 1: "Apprentice", + 2: "Anointed", + 3: "Champion" +} + +const defaultAvailabilityMap: Record = { + 0: false, + 1: false, + 2: false, + 3: false +} + +const unitDiscoveredAt: Record = { + 0: BigInt(0), + 1: BigInt(300_0000), + 2: BigInt(2800_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 +} + +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)) + ] + }, [n_units, unitType]); + return
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}
} +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 setAvailabilities = useCallback(() => { + if (isRegistered) { + 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) + } else { + setCanPurchase(defaultAvailabilityMap) + setIsShrouded(defaultAvailabilityMap) + setCanKnowAbout(defaultAvailabilityMap) + } + }, [ + army, + balance, + isRegistered, + player?.total_minted + ]) + + useEffect(() => { + const tickInterval = setInterval(() => { + balanceCount.current = calculateBalance( + balance ?? BigInt(0), + army?.profit_per_second ?? BigInt(0), + player?.last_raided_at ?? BigInt(0) + ); + setAvailabilities() + }, 100); + return () => clearInterval(tickInterval) + }, [balance, army?.profit_per_second, player?.last_raided_at, setAvailabilities]) + + return <> +
+
+ {canKnowAbout[0] && } + {canKnowAbout[1] && } + {canKnowAbout[2] && } + {canKnowAbout[3] && } +
+ +} + export default Army diff --git a/app/src/components/Counter.tsx b/app/src/components/Counter.tsx index 5d924b3..b1c0a32 100644 --- a/app/src/components/Counter.tsx +++ b/app/src/components/Counter.tsx @@ -1,8 +1,9 @@ import { useEffect, useReducer, useRef } from "react"; import { usePlayer } from "../providers/PlayerProvider" import styles from "../styles/Header.module.css" +import { formatUnits } from "viem"; -const calculateBalance = (balance: bigint, perSecond: bigint, lastRaidedAt: bigint) => { +export const calculateBalance = (balance: bigint, perSecond: bigint, lastRaidedAt: bigint) => { // convert to milliseconds trick so we get a more smooth counter const millisecondsSinceLastRaid = (new Date()).getTime() - parseInt(lastRaidedAt.toString()) * 1000; @@ -12,8 +13,8 @@ const calculateBalance = (balance: bigint, perSecond: bigint, lastRaidedAt: bigi / BigInt(1000) /* deduct milliseconds*/)) } -export const toReadable = (value: bigint) => { - value = value / BigInt(10000); +export const toReadable = (rawValue: bigint) => { + const value = rawValue / BigInt(10000); const suffixes = [ { value: BigInt('1000'), suffix: 'thousand' }, { value: BigInt('1000000'), suffix: 'million' }, @@ -38,10 +39,10 @@ export const toReadable = (value: bigint) => { for (let i = 0; i < suffixes.length; i++) { if (value < suffixes[i].value) { if (i == 0) { - return value.toString(); + return formatUnits(rawValue, 4); } else { const divided = value / suffixes[i - 1].value; - const numStr = (value % suffixes[i - 0].value).toString().slice(0, 3); + const numStr = (value % suffixes[i - 1].value).toString().slice(0, 3); const remainder = parseInt(numStr.replace(/0+$/, ''), 10); return `${divided.toString()}.${remainder.toString()} ${suffixes[i - 1].suffix}`; } @@ -73,7 +74,7 @@ const Counter = () => {

{balanceCount.current} GELD

-

available on chain: {availableBalance.current} GELD

+

in wallet: {availableBalance.current} GELD

} diff --git a/app/src/components/Header.tsx b/app/src/components/Header.tsx index c283eaf..cf88274 100644 --- a/app/src/components/Header.tsx +++ b/app/src/components/Header.tsx @@ -1,5 +1,6 @@ import React, { useCallback, useMemo } from "react" import styles from "../styles/Header.module.css" +import bgStyles from "../styles/Background.module.css" import { usePlayer } from "../providers/PlayerProvider"; import { useAccount } from 'wagmi'; import dynamic from "next/dynamic"; @@ -36,7 +37,7 @@ const Header = () => { }, [isRegistered, register]) return
-

{title}

+

{title}

{subtitle} {perSecondParagraph}
diff --git a/app/src/components/MusicPlayer.tsx b/app/src/components/MusicPlayer.tsx new file mode 100644 index 0000000..b54ed6e --- /dev/null +++ b/app/src/components/MusicPlayer.tsx @@ -0,0 +1,52 @@ +import { useState, useEffect, useRef, useCallback } from 'react'; +import styles from '../styles/Background.module.css'; + +const MusicPlayer = ({ onReady }: { onReady: (unmute: () => void) => void }) => { + const [isMuted, setIsMuted] = useState(false); + const audioRef = useRef(null); + + const play = useCallback(() => { + if (audioRef.current) { + audioRef.current.play().catch(error => console.log("Audio play failed:", error)); + } + }, []); + + const unmute = useCallback(() => { + if (audioRef.current) { + audioRef.current.muted = false; + setIsMuted(false); + play(); + } + }, [play]); + + useEffect(() => { + if (audioRef.current) { + audioRef.current.volume = 0.5; + audioRef.current.loop = true; + audioRef.current.muted = true; + play(); + onReady(unmute); + } + }, [play, onReady, unmute]); + + const toggleMute = () => { + if (audioRef.current) { + audioRef.current.muted = !isMuted; + setIsMuted(!isMuted); + if (!isMuted) { + play(); + } + } + }; + + return ( + <> +