From eb318972b884dfb0d41cbf69bb6a2bc6ba5f7c91 Mon Sep 17 00:00:00 2001 From: Mitja Belak Date: Fri, 25 Oct 2024 15:58:09 +0200 Subject: [PATCH 01/11] Fixed a formatting bug --- app/src/components/Counter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/components/Counter.tsx b/app/src/components/Counter.tsx index 5d924b3..c18414c 100644 --- a/app/src/components/Counter.tsx +++ b/app/src/components/Counter.tsx @@ -41,7 +41,7 @@ export const toReadable = (value: bigint) => { return value.toString(); } 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}`; } From c1e6494937270b12978abcab0bdd30a7ad41d0c9 Mon Sep 17 00:00:00 2001 From: Mitja Belak Date: Sat, 26 Oct 2024 13:12:30 +0200 Subject: [PATCH 02/11] Added shrouding of units & display of per second profits on units plus fixed calc bug on unit costs --- app/src/components/Army.tsx | 173 ++++++++++++++++++++++++--- app/src/components/Counter.tsx | 2 +- app/src/components/Header.tsx | 3 +- app/src/providers/PlayerProvider.tsx | 8 +- app/src/styles/Army.module.css | 95 ++++++++++----- app/src/styles/Background.module.css | 11 +- src/RaidGeldUtils.sol | 14 +-- 7 files changed, 243 insertions(+), 63 deletions(-) diff --git a/app/src/components/Army.tsx b/app/src/components/Army.tsx index 888368d..4d7b0d0 100644 --- a/app/src/components/Army.tsx +++ b/app/src/components/Army.tsx @@ -1,26 +1,161 @@ -import { usePlayer } from '../providers/PlayerProvider'; +import { useEffect, useMemo, useState } from 'react'; +import { UnitType, usePlayer } from '../providers/PlayerProvider'; import styles from '../styles/Army.module.css'; +import { 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 BASE_PRICE = BigInt(380000); +const PRICE_FACTOR = BigInt(11500); +const APPRENTICE_PROFIT = BigInt(61000); +const ANOINTED_PROFIT = BigInt(6 * 64000); +const CHAMPION_PROFIT = BigInt(BigInt(67000 * 61000 * 64000) / PRECISION / PRECISION); - } +function calculateUnitPrice(unit: UnitType, currentLevel: number, units: number) { + let rollingPriceCalculation = BigInt(unit + 1) * BASE_PRICE; + 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 + switch (unit) { + case 0: return BigInt(level) * PRECISION + case 1: return BigInt(level) * APPRENTICE_PROFIT + case 2: return BigInt(level) * ANOINTED_PROFIT + case 3: return BigInt(level) * CHAMPION_PROFIT + } +} + +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(2_000_0000), + 2: BigInt(2_000_000_0000), + 3: BigInt(200_000_000_0000) +} +const unitAvailableToDiscoverAt: Record = { + 0: BigInt(0), + 1: BigInt(1_000_0000), + 2: BigInt(1_000_000_0000), + 3: BigInt(100_000_000_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)) + " geld", + toReadable(calculateProfitPerSecond(unitType, n_units)) + " geld / sec" + ] + }, [n_units, unitType]); + return
addUnit(unitType)} className={`${styles.armyUnit} ${canPurchase ? "" : styles.isUnavailable}`}> +
+ {isShrouded ? "???????" : unitTypeToName[unitType]} + {isShrouded ? null : {unitPrice}} + {n_units > 0 ? {n_units} : null} + {n_units > 0 ? {unitProfit} : 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); + + console.log(canPurchase, "\nshroud", isShrouded, "\ncanknowabout", canKnowAbout) + + useEffect(() => { + if (isRegistered) { + const totalMinted = player?.total_minted ?? 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: balance >= calculateUnitPrice(0, n_units[0], 1) && isKnown[0] && !inShroud[0], + 1: balance >= calculateUnitPrice(1, n_units[1], 1) && isKnown[1] && !inShroud[1], + 2: balance >= calculateUnitPrice(2, n_units[2], 1) && isKnown[2] && !inShroud[2], + 3: balance >= calculateUnitPrice(3, n_units[3], 1) && isKnown[3] && !inShroud[3], + }; + setCanPurchase(canActuallyBuy) + setIsShrouded(inShroud) + setCanKnowAbout(isKnown) + } else { + setCanPurchase(defaultAvailabilityMap) + setIsShrouded(defaultAvailabilityMap) + setCanKnowAbout(defaultAvailabilityMap) + } + }, [ + balance, + army, + isRegistered, + player?.total_minted + ]) + + 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..82850dc 100644 --- a/app/src/components/Counter.tsx +++ b/app/src/components/Counter.tsx @@ -73,7 +73,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..8e97598 100644 --- a/app/src/styles/Army.module.css +++ b/app/src/styles/Army.module.css @@ -108,6 +108,72 @@ .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; + } +} +.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; +} .static { width: 110px; height: 110px; @@ -129,47 +195,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/RaidGeldUtils.sol b/src/RaidGeldUtils.sol index 9b6581e..6d7d276 100644 --- a/src/RaidGeldUtils.sol +++ b/src/RaidGeldUtils.sol @@ -7,22 +7,23 @@ library RaidGeldUtils { uint256 public constant PRECISION = 10000; uint256 constant BASE_PRICE = 380000; uint256 constant PRICE_FACTOR = 11500; + uint256 constant MOLOCH_DENIER_PROFIT = 10000; uint256 constant APPRENTICE_PROFIT = 61000; uint256 constant ANOINTED_PROFIT = 6 * 64000; - uint256 constant CHAMPION_PROFIT = 67000 * 61000 * 64000 / PRECISION / PRECISION; + uint256 constant CHAMPION_PROFIT = 36 * 67000; 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 price = 0; // 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 +31,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; From 203ebd3e97f3c9a332108c1fb90521558fb96d4d Mon Sep 17 00:00:00 2001 From: Mitja Belak Date: Sat, 26 Oct 2024 13:14:43 +0200 Subject: [PATCH 03/11] Removed RAID_WAIT because it was just getting in way of UX --- src/RaidGeld.sol | 2 -- test/RaidGeld.t.sol | 8 +------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/RaidGeld.sol b/src/RaidGeld.sol index 52c96a4..f9f707f 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,7 +55,6 @@ 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); } diff --git a/test/RaidGeld.t.sol b/test/RaidGeld.t.sol index 375fb31..d1f5d48 100644 --- a/test/RaidGeld.t.sol +++ b/test/RaidGeld.t.sol @@ -151,8 +151,6 @@ contract raid_geldTest is Test { 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,11 +160,7 @@ 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 + // After wait time passes raid should bring in profits again vm.warp(block.timestamp + raid_geld.RAID_WAIT()); raid_geld.raid(); From fefa2641e4cca44c303da082f90be3175e546465 Mon Sep 17 00:00:00 2001 From: Mitja Belak Date: Sat, 26 Oct 2024 13:16:34 +0200 Subject: [PATCH 04/11] Fixes broken test --- test/RaidGeld.t.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/RaidGeld.t.sol b/test/RaidGeld.t.sol index d1f5d48..718ec9e 100644 --- a/test/RaidGeld.t.sol +++ b/test/RaidGeld.t.sol @@ -148,6 +148,7 @@ 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); @@ -161,7 +162,7 @@ contract raid_geldTest is Test { assertLt(balance, newBalance); // After wait time passes raid should bring in profits again - vm.warp(block.timestamp + raid_geld.RAID_WAIT()); + vm.warp(block.timestamp + 15); raid_geld.raid(); // Balance should reflect that From 84c0aeff033b459ce804d39bc90e391aa4307e72 Mon Sep 17 00:00:00 2001 From: Mitja Belak Date: Sat, 26 Oct 2024 13:49:29 +0200 Subject: [PATCH 05/11] Better UX: player now can add units wven if funds are not yet minted --- app/src/components/Army.tsx | 30 ++++++++++++++++++++---------- app/src/components/Counter.tsx | 2 +- src/RaidGeld.sol | 8 +++++--- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/app/src/components/Army.tsx b/app/src/components/Army.tsx index 4d7b0d0..dd0e572 100644 --- a/app/src/components/Army.tsx +++ b/app/src/components/Army.tsx @@ -1,7 +1,7 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { UnitType, usePlayer } from '../providers/PlayerProvider'; import styles from '../styles/Army.module.css'; -import { toReadable } from './Counter'; +import { calculateBalance, toReadable } from './Counter'; const PRECISION = BigInt(10000); const BASE_PRICE = BigInt(380000); @@ -102,10 +102,9 @@ const Army = () => { const [canPurchase, setCanPurchase] = useState>(defaultAvailabilityMap); const [isShrouded, setIsShrouded] = useState>(defaultAvailabilityMap); const [canKnowAbout, setCanKnowAbout] = useState>(defaultAvailabilityMap); + const balanceCount = useRef(BigInt(balance ?? 0)) - console.log(canPurchase, "\nshroud", isShrouded, "\ncanknowabout", canKnowAbout) - - useEffect(() => { + const setAvailabilities = useCallback(() => { if (isRegistered) { const totalMinted = player?.total_minted ?? BigInt(0); const n_units: Record = { @@ -127,10 +126,10 @@ const Army = () => { 3: totalMinted >= unitAvailableToDiscoverAt[3], } const canActuallyBuy = { - 0: balance >= calculateUnitPrice(0, n_units[0], 1) && isKnown[0] && !inShroud[0], - 1: balance >= calculateUnitPrice(1, n_units[1], 1) && isKnown[1] && !inShroud[1], - 2: balance >= calculateUnitPrice(2, n_units[2], 1) && isKnown[2] && !inShroud[2], - 3: balance >= calculateUnitPrice(3, n_units[3], 1) && isKnown[3] && !inShroud[3], + 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) @@ -141,12 +140,23 @@ const Army = () => { setCanKnowAbout(defaultAvailabilityMap) } }, [ - balance, army, 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 <>
diff --git a/app/src/components/Counter.tsx b/app/src/components/Counter.tsx index 82850dc..d188dcf 100644 --- a/app/src/components/Counter.tsx +++ b/app/src/components/Counter.tsx @@ -2,7 +2,7 @@ import { useEffect, useReducer, useRef } from "react"; import { usePlayer } from "../providers/PlayerProvider" import styles from "../styles/Header.module.css" -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; diff --git a/src/RaidGeld.sol b/src/RaidGeld.sol index f9f707f..047f185 100644 --- a/src/RaidGeld.sol +++ b/src/RaidGeld.sol @@ -61,14 +61,13 @@ contract RaidGeld is ERC20, Ownable { // 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 @@ -108,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 From 54b370f41ba488ea6331251654a5918140f65c97 Mon Sep 17 00:00:00 2001 From: Mitja Belak Date: Sat, 26 Oct 2024 13:53:23 +0200 Subject: [PATCH 06/11] Checking for known / shrouded units now calculated with actual total_minted at that moment --- app/src/components/Army.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/components/Army.tsx b/app/src/components/Army.tsx index dd0e572..7cf9c74 100644 --- a/app/src/components/Army.tsx +++ b/app/src/components/Army.tsx @@ -106,7 +106,7 @@ const Army = () => { const setAvailabilities = useCallback(() => { if (isRegistered) { - const totalMinted = player?.total_minted ?? 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, From 26b2480b52dd9ddbcc2d13fcb5363bda62aa5649 Mon Sep 17 00:00:00 2001 From: Mitja Belak Date: Sat, 26 Oct 2024 13:54:08 +0200 Subject: [PATCH 07/11] Checking for known / shrouded units now calculated with actual total_minted at that moment 2 --- app/src/components/Army.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/components/Army.tsx b/app/src/components/Army.tsx index 7cf9c74..428931d 100644 --- a/app/src/components/Army.tsx +++ b/app/src/components/Army.tsx @@ -141,6 +141,7 @@ const Army = () => { } }, [ army, + balance, isRegistered, player?.total_minted ]) From 0ea4ba320c18aff25bbc753b5d505aaefa020272 Mon Sep 17 00:00:00 2001 From: Mitja Belak Date: Sat, 26 Oct 2024 14:45:25 +0200 Subject: [PATCH 08/11] Balances how much units cost and profit --- src/RaidGeld.sol | 2 +- src/RaidGeldUtils.sol | 27 ++++++++++++++++++++------- test/RaidGeld.t.sol | 2 +- test/RaidGeldUtils.t.sol | 6 +++--- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/RaidGeld.sol b/src/RaidGeld.sol index 047f185..361d645 100644 --- a/src/RaidGeld.sol +++ b/src/RaidGeld.sol @@ -107,7 +107,7 @@ 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"); diff --git a/src/RaidGeldUtils.sol b/src/RaidGeldUtils.sol index 6d7d276..42d4c98 100644 --- a/src/RaidGeldUtils.sol +++ b/src/RaidGeldUtils.sol @@ -5,18 +5,31 @@ 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 MOLOCH_DENIER_PROFIT = 10000; - uint256 constant APPRENTICE_PROFIT = 61000; - uint256 constant ANOINTED_PROFIT = 6 * 64000; - uint256 constant CHAMPION_PROFIT = 36 * 67000; + + // 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 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 = 0; i < currentLevel + units; i++) { diff --git a/test/RaidGeld.t.sol b/test/RaidGeld.t.sol index 718ec9e..378e289 100644 --- a/test/RaidGeld.t.sol +++ b/test/RaidGeld.t.sol @@ -161,7 +161,7 @@ contract raid_geldTest is Test { uint256 last_raided_at = player.last_raided_at; assertLt(balance, newBalance); - // After wait time passes raid should bring in profits again + // After wait time passes raid should bring in profits again vm.warp(block.timestamp + 15); raid_geld.raid(); 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); } From 5bb2b5f97b1909eaa326832630e9cc14e03455b9 Mon Sep 17 00:00:00 2001 From: Mitja Belak Date: Sat, 26 Oct 2024 15:03:27 +0200 Subject: [PATCH 09/11] Enables the counter to display sub zero amounts per second + prices and profits adjusted to match the contract --- app/src/components/Army.tsx | 35 +++++++++++++++++++--------------- app/src/components/Counter.tsx | 7 ++++--- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/app/src/components/Army.tsx b/app/src/components/Army.tsx index 428931d..cc12d55 100644 --- a/app/src/components/Army.tsx +++ b/app/src/components/Army.tsx @@ -4,14 +4,24 @@ import styles from '../styles/Army.module.css'; import { calculateBalance, toReadable } from './Counter'; const PRECISION = BigInt(10000); -const BASE_PRICE = BigInt(380000); const PRICE_FACTOR = BigInt(11500); -const APPRENTICE_PROFIT = BigInt(61000); -const ANOINTED_PROFIT = BigInt(6 * 64000); -const CHAMPION_PROFIT = BigInt(BigInt(67000 * 61000 * 64000) / PRECISION / PRECISION); + +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 = BigInt(unit + 1) * BASE_PRICE; + let rollingPriceCalculation = base_cost[unit]; let price = BigInt(0); // Each level costs 15% more than previous @@ -26,12 +36,7 @@ function calculateUnitPrice(unit: UnitType, currentLevel: number, units: number) function calculateProfitPerSecond(unit: UnitType, level: number) { // Each next unit scales progressivelly better - switch (unit) { - case 0: return BigInt(level) * PRECISION - case 1: return BigInt(level) * APPRENTICE_PROFIT - case 2: return BigInt(level) * ANOINTED_PROFIT - case 3: return BigInt(level) * CHAMPION_PROFIT - } + return BigInt(level) * profits[unit] } const unitTypeToCss: Record = { @@ -79,8 +84,8 @@ interface UnitProps { const Unit = ({ addUnit, unitType, canPurchase, isShrouded, n_units }: UnitProps) => { const [unitPrice, unitProfit] = useMemo(() => { return [ - toReadable(calculateUnitPrice(unitType, n_units, 1)) + " geld", - toReadable(calculateProfitPerSecond(unitType, n_units)) + " geld / sec" + toReadable(calculateUnitPrice(unitType, n_units, 1)), + toReadable(calculateProfitPerSecond(unitType, n_units)) ] }, [n_units, unitType]); return
addUnit(unitType)} className={`${styles.armyUnit} ${canPurchase ? "" : styles.isUnavailable}`}> @@ -91,9 +96,9 @@ const Unit = ({ addUnit, unitType, canPurchase, isShrouded, n_units }: UnitProps ${isShrouded ? styles.isShrouded : ""} `} /> {isShrouded ? "???????" : unitTypeToName[unitType]} - {isShrouded ? null : {unitPrice}} + {isShrouded ? null : {unitPrice} GELD} {n_units > 0 ? {n_units} : null} - {n_units > 0 ? {unitProfit} : null} + {n_units > 0 ? {unitProfit} per sec : null}
} diff --git a/app/src/components/Counter.tsx b/app/src/components/Counter.tsx index d188dcf..b1c0a32 100644 --- a/app/src/components/Counter.tsx +++ b/app/src/components/Counter.tsx @@ -1,6 +1,7 @@ import { useEffect, useReducer, useRef } from "react"; import { usePlayer } from "../providers/PlayerProvider" import styles from "../styles/Header.module.css" +import { formatUnits } from "viem"; export const calculateBalance = (balance: bigint, perSecond: bigint, lastRaidedAt: bigint) => { // convert to milliseconds trick so we get a more smooth counter @@ -12,8 +13,8 @@ export const calculateBalance = (balance: bigint, perSecond: bigint, lastRaidedA / 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); From 5491598dfc6a653144b766f4a826ef5add013e5b Mon Sep 17 00:00:00 2001 From: Mitja Belak Date: Sat, 26 Oct 2024 15:21:27 +0200 Subject: [PATCH 10/11] Prices in client now reflect those in contract, per second only shows on hover --- app/src/components/Army.tsx | 12 ++++++------ app/src/styles/Army.module.css | 6 ++++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/src/components/Army.tsx b/app/src/components/Army.tsx index cc12d55..5d3106f 100644 --- a/app/src/components/Army.tsx +++ b/app/src/components/Army.tsx @@ -62,15 +62,15 @@ const defaultAvailabilityMap: Record = { const unitDiscoveredAt: Record = { 0: BigInt(0), - 1: BigInt(2_000_0000), - 2: BigInt(2_000_000_0000), - 3: BigInt(200_000_000_0000) + 1: BigInt(300_0000), + 2: BigInt(2800_0000), + 3: BigInt(24000_0000) } const unitAvailableToDiscoverAt: Record = { 0: BigInt(0), - 1: BigInt(1_000_0000), - 2: BigInt(1_000_000_0000), - 3: BigInt(100_000_000_0000), + 1: BigInt(200_0000), + 2: BigInt(2000_0000), + 3: BigInt(25000_0000), } interface UnitProps { diff --git a/app/src/styles/Army.module.css b/app/src/styles/Army.module.css index 8e97598..0bfddc5 100644 --- a/app/src/styles/Army.module.css +++ b/app/src/styles/Army.module.css @@ -145,6 +145,11 @@ filter: sepia(1); pointer-events: none; } + &:hover { + .unitProfit { + display: block; + } + } } .uiElement { position: absolute; @@ -173,6 +178,7 @@ left: 0; right: 0; bottom: 5px; + display: none; } .static { width: 110px; From 956ad63e7fcf559e4d5b3cda08291c9ac8da0e6a Mon Sep 17 00:00:00 2001 From: san Date: Sat, 26 Oct 2024 22:32:08 +0530 Subject: [PATCH 11/11] add background music --- app/src/components/MusicPlayer.tsx | 52 ++++++++++++++++++++++++++++ app/src/components/Scene.tsx | 14 ++++++-- app/src/styles/Background.module.css | 12 +++++++ 3 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 app/src/components/MusicPlayer.tsx 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 ( + <> +