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 c18414c..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,7 +39,7 @@ 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 - 1].value).toString().slice(0, 3); @@ -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/providers/PlayerProvider.tsx b/app/src/providers/PlayerProvider.tsx index 8221e9b..7f44202 100644 --- a/app/src/providers/PlayerProvider.tsx +++ b/app/src/providers/PlayerProvider.tsx @@ -6,6 +6,8 @@ import contractAddress from '../../contract_address' const abi = contractAbi.abi +export type UnitType = 0 | 1 | 2 | 3 + export interface Player { created_at: bigint, last_raided_at: bigint, @@ -26,7 +28,7 @@ export interface PlayerContextType { balance: bigint, register: () => void, raid: () => void, - addUnit: (unit: number) => void + addUnit: (unit: UnitType) => void } const PlayerContext = createContext({ @@ -91,7 +93,7 @@ const PlayerProvider = ({ children }: { children: ReactNode }) => { } }); - console.log(player, army) + console.log(balance, player, army) const register = useCallback(() => { writeContract({ @@ -110,7 +112,7 @@ const PlayerProvider = ({ children }: { children: ReactNode }) => { }) }, [writeContract]) - const addUnit = useCallback((unit: number) => { + const addUnit = useCallback((unit: UnitType) => { writeContract({ abi, address: contractAddress, diff --git a/app/src/styles/Army.module.css b/app/src/styles/Army.module.css index 4412592..0bfddc5 100644 --- a/app/src/styles/Army.module.css +++ b/app/src/styles/Army.module.css @@ -108,6 +108,78 @@ .hunter { background-image: url("/roles/hunter.svg"); } + +.moloch_denier { + filter: sepia(0.1); +} +.apprentice { +} +.anointed { + filter: saturate(1.1); +} +.champion { + filter: saturate(2); +} + +.armyUnits { + position: absolute; + bottom: 22px; + left: 22px; + right: 22px; + height: 180px; + display: flex; + justify-content: center; + align-items: center; +} +.armyUnit { + position: relative; + height: 100%; + width: 120px; + margin-right: 20px; + .person { + &.isShrouded { + filter: brightness(0); + } + } + &.isUnavailable { + filter: sepia(1); + pointer-events: none; + } + &:hover { + .unitProfit { + display: block; + } + } +} +.uiElement { + position: absolute; + border-radius: 10px; + background: rgba(0, 0, 0, 0.89); + padding: 0.1rem 1rem; + font-size: 0.8rem; + user-select: none; + text-align: center; +} +.unitSupply { + top: 0; + right: 0; +} +.unitName { + left: 0; + right: 0; + bottom: 45px; +} +.unitPrice { + left: 0; + right: 0; + bottom: 25px; +} +.unitProfit { + left: 0; + right: 0; + bottom: 5px; + display: none; +} .static { width: 110px; height: 110px; @@ -129,47 +201,18 @@ height: 90px; } .static.moloch_denier { - left: calc(20% - 60px); - bottom: 70px; background-image: url("/roles/scribe2.png"); } .static.apprentice { - left: calc(36% - 60px); background-image: url("/roles/druid2.png"); - bottom: 72px; } .static.anointed { - left: calc(50% - 60px); - bottom: 64px; background-image: url("/roles/ranger2.png"); } .static.champion { - left: calc(64% - 60px); - bottom: 66px; background-image: url("/roles/warrior2.png"); } -.moloch_denier { - filter: sepia(0.1); -} -.apprentice { -} -.anointed { - filter: saturate(1.1); -} -.champion { - filter: saturate(2); -} - -.supply { - position: absolute; - bottom: -40px; - background: rgba(0, 0, 0, 0.89); - padding: 0.5rem 1rem; - font-size: 0.8rem; - user-select: none; -} - @keyframes marching { 0% { transform: translate(-100px, -84px); diff --git a/app/src/styles/Background.module.css b/app/src/styles/Background.module.css index d86f961..90df215 100644 --- a/app/src/styles/Background.module.css +++ b/app/src/styles/Background.module.css @@ -48,7 +48,7 @@ background-image: url("/background/tower.png"); width: 218px; height: 240px; - top: 200px; + top: 150px; animation: thunder_hue_hard 12s linear infinite; transition: all 0.1s cubic-bezier(0.265, 1.4, 0.68, 1.65); transform-origin: bottom center; @@ -82,7 +82,10 @@ } .tower::after { position: absolute; - content: "RAID"; + content: "RAID\A(collect to wallet)"; + text-align: center; + white-space: pre; + word-wrap: break-word; left: 50px; top: 22px; } @@ -225,6 +228,10 @@ } } +.excited { + animation: excited 0.5s infinite linear; +} + @keyframes excited { 0%, 100% { diff --git a/src/RaidGeld.sol b/src/RaidGeld.sol index 52c96a4..361d645 100644 --- a/src/RaidGeld.sol +++ b/src/RaidGeld.sol @@ -11,7 +11,6 @@ contract RaidGeld is ERC20, Ownable { uint256 public constant BUY_IN_AMOUNT = 0.00005 ether; uint256 public constant INITIAL_GELD = 50 * MANTISSA; - uint256 public constant RAID_WAIT = 15 seconds; mapping(address => Player) private players; mapping(address => Army) private armies; @@ -56,21 +55,19 @@ contract RaidGeld is ERC20, Ownable { // Manual minting for itchy fingers function raid() external onlyPlayer { - require(block.timestamp >= players[msg.sender].last_raided_at + RAID_WAIT, "Tried minting too soon"); performRaid(msg.sender); } // Helper so we can use it when buying units too function performRaid(address player) private { uint256 time_past = block.timestamp - players[player].last_raided_at; - uint256 new_geld = armies[player].profit_per_second * time_past; // TODO: Pink noise, make it so sometimes its better than expected _mint(player, new_geld); players[player].last_raided_at = block.timestamp; - players[player].total_minted = new_geld; + players[player].total_minted += new_geld; } // Function to get Player struct @@ -110,8 +107,11 @@ contract RaidGeld is ERC20, Ownable { uint256 cost = RaidGeldUtils.calculateUnitPrice(unit, currentLevel, n_units); // First trigger a raid so player receives what he is due at to this moment + + uint256 time_past = block.timestamp - players[msg.sender].last_raided_at; + uint256 new_geld = armies[msg.sender].profit_per_second * time_past; + require(balanceOf(msg.sender) + new_geld > cost, "Not enough GELD to add this unit"); performRaid(msg.sender); - require(balanceOf(msg.sender) > cost, "Not enough GELD to add this much"); // TODO: Since we are first minting then burning the token, this could be simplified // by first calculating the difference and then minting / burning in just one operation diff --git a/src/RaidGeldUtils.sol b/src/RaidGeldUtils.sol index 9b6581e..42d4c98 100644 --- a/src/RaidGeldUtils.sol +++ b/src/RaidGeldUtils.sol @@ -5,24 +5,38 @@ import {Army} from "../src/RaidGeldStructs.sol"; library RaidGeldUtils { uint256 public constant PRECISION = 10000; - uint256 constant BASE_PRICE = 380000; uint256 constant PRICE_FACTOR = 11500; - uint256 constant APPRENTICE_PROFIT = 61000; - uint256 constant ANOINTED_PROFIT = 6 * 64000; - uint256 constant CHAMPION_PROFIT = 67000 * 61000 * 64000 / PRECISION / PRECISION; + + // base price * (0.00666) * 11 per each next unit + uint256 constant MOLOCH_DENIER_PROFIT = 2533; + uint256 constant APPRENTICE_PROFIT = 27863; + uint256 constant ANOINTED_PROFIT = 306493; + uint256 constant CHAMPION_PROFIT = 3371423; + + // each costs 10 times plus a bit more + uint256 constant MOLOCH_DENIER_BASE_COST = 380000; + uint256 constant APPRENTICE_BASE_COST = 3420000; + uint256 constant ANOINTED_BASE_COST = 30096000; + uint256 constant CHAMPION_BASE_COST = 255816000; function calculateUnitPrice(uint8 unit, uint16 currentLevel, uint16 units) internal pure returns (uint256) { require(unit <= 3, "No matching unit found"); - - uint256 rollingPriceCalculation = uint256(unit + 1) * BASE_PRICE; - uint256 price = rollingPriceCalculation; + uint256 rollingPriceCalculation = MOLOCH_DENIER_BASE_COST; + uint256 price = 0; + if (unit == 1) { + rollingPriceCalculation = APPRENTICE_BASE_COST; + } else if (unit == 2) { + rollingPriceCalculation = ANOINTED_BASE_COST; + } else if (unit == 3) { + rollingPriceCalculation = CHAMPION_BASE_COST; + } // Each level costs 15% more than previous - for (uint256 i = 1; i < currentLevel + units; i++) { - rollingPriceCalculation = rollingPriceCalculation * PRICE_FACTOR / PRECISION; + for (uint256 i = 0; i < currentLevel + units; i++) { if (i >= currentLevel) { price += rollingPriceCalculation; } + rollingPriceCalculation = rollingPriceCalculation * PRICE_FACTOR / PRECISION; } return price; } @@ -30,12 +44,9 @@ library RaidGeldUtils { function calculateProfitsPerSecond(Army memory army) internal pure returns (uint256) { // Each next unit scales progressivelly better - uint256 moloch_denier_profit = army.moloch_denier.level * PRECISION; - + uint256 moloch_denier_profit = army.moloch_denier.level * MOLOCH_DENIER_PROFIT; uint256 apprentice_profit = army.apprentice.level * APPRENTICE_PROFIT; - uint256 anointed_profit = army.anointed.level * ANOINTED_PROFIT; - uint256 champion_profit = army.champion.level * CHAMPION_PROFIT; return moloch_denier_profit + apprentice_profit + anointed_profit + champion_profit; diff --git a/test/RaidGeld.t.sol b/test/RaidGeld.t.sol index 375fb31..378e289 100644 --- a/test/RaidGeld.t.sol +++ b/test/RaidGeld.t.sol @@ -148,11 +148,10 @@ contract raid_geldTest is Test { // bought 1 moloch_denier raid_geld.addUnit(0, 1); + vm.warp(block.timestamp + 15); uint256 balance = raid_geld.balanceOf(player1); - // Warp time a bit so first raid doesnt fail - vm.warp(block.timestamp + raid_geld.RAID_WAIT()); // Trigger raid funds minting raid_geld.raid(); @@ -162,12 +161,8 @@ contract raid_geldTest is Test { uint256 last_raided_at = player.last_raided_at; assertLt(balance, newBalance); - // Expect fail if we raid again, we need to wait a bit - vm.expectRevert(); - raid_geld.raid(); - - // After wait time passes raid should work again - vm.warp(block.timestamp + raid_geld.RAID_WAIT()); + // After wait time passes raid should bring in profits again + vm.warp(block.timestamp + 15); raid_geld.raid(); // Balance should reflect that diff --git a/test/RaidGeldUtils.t.sol b/test/RaidGeldUtils.t.sol index 8de1385..58ee5a5 100644 --- a/test/RaidGeldUtils.t.sol +++ b/test/RaidGeldUtils.t.sol @@ -9,7 +9,7 @@ contract raid_geldTest is Test { function test_0_unit_price() public pure { // buying 1 unit of moloch_denier uint256 basePriceMolochDenier = RaidGeldUtils.calculateUnitPrice(0, 0, 1); - assertEq(basePriceMolochDenier, RaidGeldUtils.BASE_PRICE); + assertEq(basePriceMolochDenier, RaidGeldUtils.MOLOCH_DENIER_BASE_COST); // buying 3 units // has to be a bit more than 3 * 38 = 114 @@ -37,7 +37,7 @@ contract raid_geldTest is Test { profit_per_second: 0 // irrelevant for this test }); uint256 profits_per_second = RaidGeldUtils.calculateProfitsPerSecond(army); - assertEq(profits_per_second, RaidGeldUtils.PRECISION); + assertEq(profits_per_second, RaidGeldUtils.MOLOCH_DENIER_PROFIT); army = Army({ moloch_denier: Raider({level: _dLvl}), @@ -47,7 +47,7 @@ contract raid_geldTest is Test { profit_per_second: 0 // irrelevant for this test }); profits_per_second = RaidGeldUtils.calculateProfitsPerSecond(army); - uint256 expected = _dLvl * RaidGeldUtils.PRECISION + _apLvl * RaidGeldUtils.APPRENTICE_PROFIT + uint256 expected = _dLvl * RaidGeldUtils.MOLOCH_DENIER_PROFIT + _apLvl * RaidGeldUtils.APPRENTICE_PROFIT + _anLvl * RaidGeldUtils.ANOINTED_PROFIT + _cLvl * RaidGeldUtils.CHAMPION_PROFIT; assertEq(profits_per_second, expected); }