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