forked from mico/idle_moloch
Compare commits
2 Commits
main
...
dao-inegra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b0d24ccba | ||
|
|
9873dfc76b |
6
.gitmodules
vendored
6
.gitmodules
vendored
@ -4,9 +4,3 @@
|
||||
[submodule "lib/openzeppelin-contracts"]
|
||||
path = lib/openzeppelin-contracts
|
||||
url = https://github.com/OpenZeppelin/openzeppelin-contracts
|
||||
[submodule "lib\\openzeppelin-foundry-upgrades"]
|
||||
path = lib\\openzeppelin-foundry-upgrades
|
||||
url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades
|
||||
[submodule "lib\\openzeppelin-contracts-upgradeable"]
|
||||
path = lib\\openzeppelin-contracts-upgradeable
|
||||
url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable
|
||||
|
||||
17
README.md
17
README.md
@ -4,7 +4,10 @@ Idle game & shitcoin advanture dedicated to cohort VII of Raid Guild.
|
||||
|
||||
## Set up for local DEV
|
||||
|
||||
### 1. Run `anvil` to setup local RPC
|
||||
### 1. Run `anvil` to setup local RPC as a fork of base mainnet
|
||||
`anvil --rpc-url <you'r base mainnet rpc url>`
|
||||
|
||||
you can get a free rpc url by registering with https://alchemy.com and creating and app
|
||||
|
||||
### 2. Deploy contract
|
||||
|
||||
@ -14,14 +17,14 @@ Either use `./deploy_contract.sh` script (!! change contract values and set priv
|
||||
|
||||
Move to `app` dir, install deps via `npm install` and run `npm run dev` to start the dev server.
|
||||
|
||||
#### 3. 1. Run `cast rpc anvil_mine`
|
||||
#### 3. 1. Point Metamask to Anvil network for local dev
|
||||
|
||||
This is so time gets set on the local chain, otherwise you will start at 0 time and first mint will give you bajillion GELD.
|
||||
|
||||
#### 3. 2. Point Metamask to Anvil network for local dev
|
||||
|
||||
#### 3. 3. Change `app/contract_address.ts` to match your program address if needed
|
||||
#### 3. 2. Change `app/contract_address.ts` to match your program address if needed
|
||||
|
||||
### 4. Local development requires mining blocks by hand
|
||||
|
||||
Call `cast rpc anvil_mine` to mine next block, otherwise it wont ever progress and time "stands still" as far as the game is concerned
|
||||
|
||||
|
||||
### 5. Fork tests
|
||||
forge test --rpc-url <you'r base mainnet rpc url>
|
||||
|
||||
@ -1,177 +1,26 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { UnitType, usePlayer } from '../providers/PlayerProvider';
|
||||
import { usePlayer } from '../providers/PlayerProvider';
|
||||
import styles from '../styles/Army.module.css';
|
||||
import { calculateBalance, toReadable } from './Counter';
|
||||
|
||||
const PRECISION = BigInt(10000);
|
||||
const PRICE_FACTOR = BigInt(11500);
|
||||
|
||||
const base_cost: Record<UnitType, bigint> = {
|
||||
0: BigInt(380000),
|
||||
1: BigInt(3420000),
|
||||
2: BigInt(30096000),
|
||||
3: BigInt(255816000),
|
||||
}
|
||||
|
||||
const profits: Record<UnitType, bigint> = {
|
||||
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<UnitType, string> = {
|
||||
0: styles.moloch_denier,
|
||||
1: styles.apprentice,
|
||||
2: styles.anointed,
|
||||
3: styles.champion
|
||||
}
|
||||
|
||||
const unitTypeToName: Record<UnitType, string> = {
|
||||
0: "Moloch denier",
|
||||
1: "Apprentice",
|
||||
2: "Anointed",
|
||||
3: "Champion"
|
||||
}
|
||||
|
||||
const defaultAvailabilityMap: Record<UnitType, boolean> = {
|
||||
0: false,
|
||||
1: false,
|
||||
2: false,
|
||||
3: false
|
||||
}
|
||||
|
||||
const unitDiscoveredAt: Record<UnitType, bigint> = {
|
||||
0: BigInt(0),
|
||||
1: BigInt(300_0000),
|
||||
2: BigInt(2800_0000),
|
||||
3: BigInt(24000_0000)
|
||||
}
|
||||
const unitAvailableToDiscoverAt: Record<UnitType, bigint> = {
|
||||
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 <div onClick={() => addUnit(unitType)} className={`${styles.armyUnit} ${canPurchase ? "" : styles.isUnavailable}`}>
|
||||
<div className={`
|
||||
${unitTypeToCss[unitType]}
|
||||
${styles.person}
|
||||
${styles.static}
|
||||
${isShrouded ? styles.isShrouded : ""}
|
||||
`} />
|
||||
<span className={`${styles.unitName} ${styles.uiElement}`}>{isShrouded ? "???????" : unitTypeToName[unitType]}</span>
|
||||
{isShrouded ? null : <span className={`${styles.unitPrice} ${styles.uiElement}`}>{unitPrice} <small>GELD</small></span>}
|
||||
{n_units > 0 ? <span className={`${styles.unitSupply} ${styles.uiElement}`}>{n_units}</span> : null}
|
||||
{n_units > 0 ? <span className={`${styles.unitProfit} ${styles.uiElement}`}>{unitProfit} <small>per sec</small></span> : null}
|
||||
</div>
|
||||
}
|
||||
|
||||
const Army = () => {
|
||||
const { army, addUnit, isRegistered, player, balance } = usePlayer()
|
||||
const [canPurchase, setCanPurchase] = useState<Record<UnitType, boolean>>(defaultAvailabilityMap);
|
||||
const [isShrouded, setIsShrouded] = useState<Record<UnitType, boolean>>(defaultAvailabilityMap);
|
||||
const [canKnowAbout, setCanKnowAbout] = useState<Record<UnitType, boolean>>(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<UnitType, number> = {
|
||||
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 <>
|
||||
const { army, addUnit, isRegistered } = usePlayer()
|
||||
return <div className="styles.armyGathering">
|
||||
<div className={`${styles.tavern_keeper} ${styles.person} ${styles.static}`} />
|
||||
<div className={styles.armyUnits}>
|
||||
{canKnowAbout[0] && <Unit n_units={army?.moloch_denier.level ?? 0} addUnit={addUnit} unitType={0} canPurchase={canPurchase[0]} isShrouded={isShrouded[0]} />}
|
||||
{canKnowAbout[1] && <Unit n_units={army?.apprentice.level ?? 0} addUnit={addUnit} unitType={1} canPurchase={canPurchase[1]} isShrouded={isShrouded[1]} />}
|
||||
{canKnowAbout[2] && <Unit n_units={army?.anointed.level ?? 0} addUnit={addUnit} unitType={2} canPurchase={canPurchase[2]} isShrouded={isShrouded[2]} />}
|
||||
{canKnowAbout[3] && <Unit n_units={army?.champion.level ?? 0} addUnit={addUnit} unitType={3} canPurchase={canPurchase[3]} isShrouded={isShrouded[3]} />}
|
||||
{isRegistered && <>
|
||||
<div onClick={() => addUnit(0)} className={`${styles.scribe} ${styles.person} ${styles.moloch_denier} ${styles.static}`}>
|
||||
<div className={styles.supply}>Moloch denier: {army?.moloch_denier.level}</div>
|
||||
</div>
|
||||
<div onClick={() => addUnit(1)} className={`${styles.druid} ${styles.person} ${styles.apprentice} ${styles.static}`} >
|
||||
<div className={styles.supply}>Apprentice: {army?.apprentice.level}</div>
|
||||
</div>
|
||||
<div onClick={() => addUnit(2)} className={`${styles.ranger} ${styles.person} ${styles.anointed} ${styles.static}`} >
|
||||
<div className={styles.supply}>Anointed: {army?.anointed.level}</div>
|
||||
</div>
|
||||
<div onClick={() => addUnit(3)} className={`${styles.warrior} ${styles.person} ${styles.champion} ${styles.static}`} >
|
||||
<div className={styles.supply}>Champion: {army?.champion.level}</div>
|
||||
</div>
|
||||
|
||||
</>}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
export default Army
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
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) => {
|
||||
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;
|
||||
@ -13,8 +12,8 @@ export const calculateBalance = (balance: bigint, perSecond: bigint, lastRaidedA
|
||||
/ BigInt(1000) /* deduct milliseconds*/))
|
||||
}
|
||||
|
||||
export const toReadable = (rawValue: bigint) => {
|
||||
const value = rawValue / BigInt(10000);
|
||||
export const toReadable = (value: bigint) => {
|
||||
value = value / BigInt(10000);
|
||||
const suffixes = [
|
||||
{ value: BigInt('1000'), suffix: 'thousand' },
|
||||
{ value: BigInt('1000000'), suffix: 'million' },
|
||||
@ -39,10 +38,10 @@ export const toReadable = (rawValue: bigint) => {
|
||||
for (let i = 0; i < suffixes.length; i++) {
|
||||
if (value < suffixes[i].value) {
|
||||
if (i == 0) {
|
||||
return formatUnits(rawValue, 4);
|
||||
return value.toString();
|
||||
} else {
|
||||
const divided = value / suffixes[i - 1].value;
|
||||
const numStr = (value % suffixes[i - 1].value).toString().slice(0, 3);
|
||||
const numStr = (value % suffixes[i - 0].value).toString().slice(0, 3);
|
||||
const remainder = parseInt(numStr.replace(/0+$/, ''), 10);
|
||||
return `${divided.toString()}.${remainder.toString()} ${suffixes[i - 1].suffix}`;
|
||||
}
|
||||
@ -74,7 +73,7 @@ const Counter = () => {
|
||||
<p className={styles.counter}>
|
||||
{balanceCount.current} GELD
|
||||
</p>
|
||||
<p className={styles.counter_available}>in wallet: {availableBalance.current} GELD</p>
|
||||
<p className={styles.counter_available}>available on chain: {availableBalance.current} GELD</p>
|
||||
</>
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
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";
|
||||
@ -37,7 +36,7 @@ const Header = () => {
|
||||
}, [isRegistered, register])
|
||||
|
||||
return <header onClick={onRegister} className={styles.header}>
|
||||
<h1 className={`${styles.title} ${isConnected && !isRegistered ? bgStyles.excited : ""}`}>{title}</h1>
|
||||
<h1 className={styles.title}>{title}</h1>
|
||||
{subtitle}
|
||||
{perSecondParagraph}
|
||||
</header>
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
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<HTMLAudioElement | null>(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 (
|
||||
<>
|
||||
<audio ref={audioRef} src="https://olive-fashionable-swallow-983.mypinata.cloud/ipfs/QmT74A6AVYTXywz7SGGuvqSqgoidV2SDCn9jGW5KEiRYtR" />
|
||||
<button onClick={toggleMute} className={styles.musicButton}>
|
||||
{isMuted ? '🔇' : '🔊'}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MusicPlayer;
|
||||
@ -1,19 +1,10 @@
|
||||
import React, { useCallback } from "react"
|
||||
import React from "react"
|
||||
import styles from '../styles/Background.module.css';
|
||||
import Tower from "./Tower";
|
||||
import Army from "./Army";
|
||||
import MarchingBand from "./MarchingBand";
|
||||
import MusicPlayer from "./MusicPlayer";
|
||||
import { usePlayer } from "../providers/PlayerProvider";
|
||||
|
||||
const Scene = () => {
|
||||
const { isRegistered } = usePlayer();
|
||||
const handleMusicReady = useCallback((unmute: () => void) => {
|
||||
if (isRegistered) {
|
||||
unmute();
|
||||
}
|
||||
}, [isRegistered]);
|
||||
|
||||
return <div className={styles.frame}>
|
||||
<div className={`${styles.air} ${styles.background_asset}`} />
|
||||
<div className={`${styles.clouds_small} ${styles.background_asset}`} />
|
||||
@ -24,7 +15,6 @@ const Scene = () => {
|
||||
<MarchingBand />
|
||||
<div className={`${styles.bonfire} ${styles.background_asset}`} />
|
||||
<Army />
|
||||
<MusicPlayer onReady={handleMusicReady} />
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
@ -6,8 +6,6 @@ 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,
|
||||
@ -28,7 +26,7 @@ export interface PlayerContextType {
|
||||
balance: bigint,
|
||||
register: () => void,
|
||||
raid: () => void,
|
||||
addUnit: (unit: UnitType) => void
|
||||
addUnit: (unit: number) => void
|
||||
}
|
||||
|
||||
const PlayerContext = createContext<PlayerContextType>({
|
||||
@ -93,7 +91,7 @@ const PlayerProvider = ({ children }: { children: ReactNode }) => {
|
||||
}
|
||||
});
|
||||
|
||||
console.log(balance, player, army)
|
||||
console.log(player, army)
|
||||
|
||||
const register = useCallback(() => {
|
||||
writeContract({
|
||||
@ -112,7 +110,7 @@ const PlayerProvider = ({ children }: { children: ReactNode }) => {
|
||||
})
|
||||
}, [writeContract])
|
||||
|
||||
const addUnit = useCallback((unit: UnitType) => {
|
||||
const addUnit = useCallback((unit: number) => {
|
||||
writeContract({
|
||||
abi,
|
||||
address: contractAddress,
|
||||
|
||||
@ -108,78 +108,6 @@
|
||||
.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;
|
||||
@ -201,18 +129,47 @@
|
||||
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);
|
||||
|
||||
@ -48,7 +48,7 @@
|
||||
background-image: url("/background/tower.png");
|
||||
width: 218px;
|
||||
height: 240px;
|
||||
top: 150px;
|
||||
top: 200px;
|
||||
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,10 +82,7 @@
|
||||
}
|
||||
.tower::after {
|
||||
position: absolute;
|
||||
content: "RAID\A(collect to wallet)";
|
||||
text-align: center;
|
||||
white-space: pre;
|
||||
word-wrap: break-word;
|
||||
content: "RAID";
|
||||
left: 50px;
|
||||
top: 22px;
|
||||
}
|
||||
@ -113,18 +110,6 @@
|
||||
bonfire 12s linear infinite,
|
||||
bonfire_skew 5s infinite linear;
|
||||
}
|
||||
.musicButton {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
left: 30px;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
z-index: 1000;
|
||||
}
|
||||
@keyframes scrollBackground {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
@ -240,10 +225,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.excited {
|
||||
animation: excited 0.5s infinite linear;
|
||||
}
|
||||
|
||||
@keyframes excited {
|
||||
0%,
|
||||
100% {
|
||||
|
||||
@ -2,9 +2,5 @@
|
||||
src = "src"
|
||||
out = "out"
|
||||
libs = ["lib"]
|
||||
ffi = true
|
||||
ast = true
|
||||
build_info = true
|
||||
extra_output = ["storageLayout"]
|
||||
|
||||
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
|
||||
|
||||
@ -1 +0,0 @@
|
||||
Subproject commit fa525310e45f91eb20a6d3baa2644be8e0adba31
|
||||
@ -1 +0,0 @@
|
||||
Subproject commit 16e0ae21e0e39049f619f2396fa28c57fad07368
|
||||
@ -1,2 +1 @@
|
||||
@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/
|
||||
@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/
|
||||
@openzeppelin/contracts/=lib/openzeppelin-contracts/contracts/
|
||||
|
||||
@ -3,26 +3,17 @@ pragma solidity ^0.8.13;
|
||||
|
||||
import {Script, console} from "forge-std/Script.sol";
|
||||
import {RaidGeld} from "../src/RaidGeld.sol";
|
||||
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";
|
||||
|
||||
contract RaidGeldScript is Script {
|
||||
import {Constants} from "../src/Constants.sol";
|
||||
|
||||
contract RaidGeldScript is Script, Constants {
|
||||
RaidGeld public raidgeld;
|
||||
|
||||
function setUp() public {}
|
||||
|
||||
function run() public {
|
||||
vm.startBroadcast();
|
||||
|
||||
// Deploy the upgradeable contract
|
||||
address _proxyAddress = Upgrades.deployTransparentProxy(
|
||||
"RaidGeld.sol",
|
||||
msg.sender,
|
||||
abi.encodeCall(RaidGeld.initialize, ())
|
||||
);
|
||||
|
||||
// Get the implementation address
|
||||
address implementationAddress = Upgrades.getImplementationAddress(
|
||||
_proxyAddress
|
||||
);
|
||||
|
||||
raidgeld = new RaidGeld(DAO_TOKEN, POOL);
|
||||
vm.stopBroadcast();
|
||||
}
|
||||
}
|
||||
|
||||
8
src/Constants.sol
Normal file
8
src/Constants.sol
Normal file
@ -0,0 +1,8 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
contract Constants {
|
||||
//base addresses
|
||||
address public constant DAO_TOKEN = 0x11dC980faf34A1D082Ae8A6a883db3A950a3c6E8;
|
||||
address public constant POOL = 0x27004f6d0c1bB7979367D32Ba9d6DF6d61A18926;
|
||||
}
|
||||
@ -1,36 +1,49 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
import "@openzeppelin/contracts-upgradeable/token/ERC20/ERC20Upgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
|
||||
import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
|
||||
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
|
||||
import "@openzeppelin/contracts/access/Ownable.sol";
|
||||
import {RaidGeldUtils} from "../src/RaidGeldUtils.sol";
|
||||
import {Army, Player, Raider} from "../src/RaidGeldStructs.sol";
|
||||
|
||||
contract RaidGeld is Initializable, ERC20Upgradeable, OwnableUpgradeable {
|
||||
|
||||
contract RaidGeld is ERC20, Ownable {
|
||||
uint256 public constant MANTISSA = 1e4;
|
||||
|
||||
uint256 public constant BUY_IN_AMOUNT = 0.00005 ether;
|
||||
uint256 public immutable BUY_IN_DAO_TOKEN_AMOUNT;
|
||||
|
||||
uint256 public constant INITIAL_GELD = 50 * MANTISSA;
|
||||
uint256 public constant RAID_WAIT = 15 seconds;
|
||||
|
||||
mapping(address => Player) private players;
|
||||
mapping(address => Army) private armies;
|
||||
|
||||
ERC20 public daoToken;
|
||||
address public pool;
|
||||
|
||||
// Modifier for functions that should only be available to registered players
|
||||
modifier onlyPlayer() {
|
||||
require(players[msg.sender].created_at != 0, "Not an initiated player");
|
||||
_;
|
||||
}
|
||||
|
||||
function initialize() public initializer {
|
||||
__ERC20_init("Raid Geld", "GELD");
|
||||
__Ownable_init(msg.sender);
|
||||
constructor(address _daoToken, address _pool) ERC20("Raid Geld", "GELD") Ownable(msg.sender) {
|
||||
daoToken = ERC20(_daoToken);
|
||||
pool = _pool;
|
||||
BUY_IN_DAO_TOKEN_AMOUNT = 50 * 10 ** daoToken.decimals();
|
||||
}
|
||||
|
||||
// This effectively registers the user
|
||||
function register() external payable {
|
||||
require(players[msg.sender].created_at == 0, "Whoops, player already exists :)");
|
||||
if (msg.value != 0) {
|
||||
require(msg.value == BUY_IN_AMOUNT, "Incorrect buy in amount");
|
||||
} else {
|
||||
//@notice this is not safe for arbitrary tokens, which may not follow the interface eg. USDT
|
||||
//@notice but should be fine for the DAO token
|
||||
require(daoToken.transferFrom(msg.sender, address(this), BUY_IN_DAO_TOKEN_AMOUNT), "Failed to transfer DAO tokens");
|
||||
}
|
||||
|
||||
// Mint some starting tokens to the player
|
||||
_mint(msg.sender, INITIAL_GELD);
|
||||
@ -59,19 +72,21 @@ contract RaidGeld is Initializable, ERC20Upgradeable, OwnableUpgradeable {
|
||||
|
||||
// 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
|
||||
@ -111,11 +126,8 @@ contract RaidGeld is Initializable, ERC20Upgradeable, OwnableUpgradeable {
|
||||
|
||||
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
|
||||
|
||||
@ -5,38 +5,24 @@ import {Army} from "../src/RaidGeldStructs.sol";
|
||||
|
||||
library RaidGeldUtils {
|
||||
uint256 public constant PRECISION = 10000;
|
||||
uint256 constant BASE_PRICE = 380000;
|
||||
uint256 constant PRICE_FACTOR = 11500;
|
||||
|
||||
// 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;
|
||||
uint256 constant APPRENTICE_PROFIT = 61000;
|
||||
uint256 constant ANOINTED_PROFIT = 6 * 64000;
|
||||
uint256 constant CHAMPION_PROFIT = 67000 * 61000 * 64000 / PRECISION / PRECISION;
|
||||
|
||||
function calculateUnitPrice(uint8 unit, uint16 currentLevel, uint16 units) internal pure returns (uint256) {
|
||||
require(unit <= 3, "No matching unit found");
|
||||
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;
|
||||
}
|
||||
|
||||
uint256 rollingPriceCalculation = uint256(unit + 1) * BASE_PRICE;
|
||||
uint256 price = rollingPriceCalculation;
|
||||
|
||||
// Each level costs 15% more than previous
|
||||
for (uint256 i = 0; i < currentLevel + units; i++) {
|
||||
for (uint256 i = 1; i < currentLevel + units; i++) {
|
||||
rollingPriceCalculation = rollingPriceCalculation * PRICE_FACTOR / PRECISION;
|
||||
if (i >= currentLevel) {
|
||||
price += rollingPriceCalculation;
|
||||
}
|
||||
rollingPriceCalculation = rollingPriceCalculation * PRICE_FACTOR / PRECISION;
|
||||
}
|
||||
return price;
|
||||
}
|
||||
@ -44,9 +30,12 @@ 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 * MOLOCH_DENIER_PROFIT;
|
||||
uint256 moloch_denier_profit = army.moloch_denier.level * PRECISION;
|
||||
|
||||
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;
|
||||
|
||||
@ -2,14 +2,17 @@
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
import {Test, console} from "forge-std/Test.sol";
|
||||
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";
|
||||
import {stdStorage, StdStorage} from "forge-std/Test.sol";
|
||||
|
||||
import {RaidGeld, Army, Player} from "../src/RaidGeld.sol";
|
||||
import "../src/RaidGeldUtils.sol";
|
||||
import {Constants} from "../src/Constants.sol";
|
||||
|
||||
contract raid_geldTest is Test, Constants {
|
||||
|
||||
using stdStorage for StdStorage;
|
||||
|
||||
contract raid_geldTest is Test {
|
||||
RaidGeld public raid_geld;
|
||||
address implementationAddress;
|
||||
address payable proxyAddress;
|
||||
address public player1;
|
||||
address public player2;
|
||||
address public owner;
|
||||
@ -18,23 +21,28 @@ contract raid_geldTest is Test {
|
||||
owner = address(0x126);
|
||||
player1 = address(0x123);
|
||||
vm.deal(owner, 10 ether);
|
||||
vm.deal(player1, 10 ether);
|
||||
fundAccount(player1);
|
||||
vm.prank(owner);
|
||||
raid_geld = new RaidGeld(DAO_TOKEN, POOL);
|
||||
}
|
||||
function fundAccount(address _acc) private {
|
||||
vm.deal(_acc, 10 ether);
|
||||
stdstore
|
||||
.target(DAO_TOKEN)
|
||||
.sig("balanceOf(address)")
|
||||
.with_key(_acc)
|
||||
.checked_write(100 ether);
|
||||
|
||||
// Deploy the upgradeable contract
|
||||
address _proxyAddress = Upgrades.deployTransparentProxy(
|
||||
"RaidGeld.sol",
|
||||
msg.sender,
|
||||
abi.encodeCall(RaidGeld.initialize, ())
|
||||
);
|
||||
|
||||
proxyAddress = payable(_proxyAddress);
|
||||
raid_geld = RaidGeld(proxyAddress);
|
||||
}
|
||||
|
||||
function registerPlayer() private {
|
||||
raid_geld.register{value: raid_geld.BUY_IN_AMOUNT()}();
|
||||
}
|
||||
function registerPlayerWithDaoToken() private {
|
||||
raid_geld.daoToken().approve(address(raid_geld), raid_geld.BUY_IN_DAO_TOKEN_AMOUNT());
|
||||
raid_geld.register();
|
||||
}
|
||||
|
||||
|
||||
function test_00_no_fallback() public {
|
||||
vm.expectRevert();
|
||||
@ -48,7 +56,7 @@ contract raid_geldTest is Test {
|
||||
payable(address(raid_geld)).transfer(0.1 ether);
|
||||
}
|
||||
|
||||
function test_02_registration() public {
|
||||
function test_02_1_registrationWithEth() public {
|
||||
vm.startPrank(player1);
|
||||
|
||||
uint256 initialBalance = address(raid_geld).balance;
|
||||
@ -76,6 +84,35 @@ contract raid_geldTest is Test {
|
||||
assertEq(army.champion.level, 0);
|
||||
}
|
||||
|
||||
function test_02_2_registrationWithDaoToken() public {
|
||||
vm.startPrank(player1);
|
||||
|
||||
uint256 initialBalance = raid_geld.daoToken().balanceOf(address(raid_geld));
|
||||
|
||||
// Send registration fee ETH to the contract
|
||||
registerPlayerWithDaoToken();
|
||||
|
||||
// Check that initialraid_geld.is received by the player
|
||||
assertEq(raid_geld.balanceOf(player1), raid_geld.INITIAL_GELD());
|
||||
|
||||
// Verify the contract dao token balance is updated
|
||||
assertEq(raid_geld.daoToken().balanceOf(address(raid_geld)), initialBalance + raid_geld.BUY_IN_DAO_TOKEN_AMOUNT());
|
||||
|
||||
// Verify player is set initially
|
||||
Player memory player = raid_geld.getPlayer(player1);
|
||||
assertEq(player.total_minted, raid_geld.INITIAL_GELD());
|
||||
assertEq(player.created_at, block.timestamp);
|
||||
assertEq(player.last_raided_at, block.timestamp);
|
||||
|
||||
Army memory army = raid_geld.getArmy(player1);
|
||||
|
||||
assertEq(army.moloch_denier.level, 0);
|
||||
assertEq(army.apprentice.level, 0);
|
||||
assertEq(army.anointed.level, 0);
|
||||
assertEq(army.champion.level, 0);
|
||||
}
|
||||
|
||||
|
||||
function test_03_funds_can_be_withdrawn() public {
|
||||
uint256 initialBalance = owner.balance;
|
||||
|
||||
@ -160,10 +197,11 @@ 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();
|
||||
|
||||
@ -173,8 +211,12 @@ 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
|
||||
vm.warp(block.timestamp + 15);
|
||||
// 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());
|
||||
raid_geld.raid();
|
||||
|
||||
// Balance should reflect that
|
||||
|
||||
@ -2,14 +2,14 @@
|
||||
pragma solidity ^0.8.13;
|
||||
|
||||
import {Test, console} from "forge-std/Test.sol";
|
||||
import {Army, Raider} from "../src/RaidGeld.sol";
|
||||
import {Army, Raider} from "../src/RaidGeldStructs.sol";
|
||||
import "../src/RaidGeldUtils.sol";
|
||||
|
||||
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.MOLOCH_DENIER_BASE_COST);
|
||||
assertEq(basePriceMolochDenier, RaidGeldUtils.BASE_PRICE);
|
||||
|
||||
// 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.MOLOCH_DENIER_PROFIT);
|
||||
assertEq(profits_per_second, RaidGeldUtils.PRECISION);
|
||||
|
||||
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.MOLOCH_DENIER_PROFIT + _apLvl * RaidGeldUtils.APPRENTICE_PROFIT
|
||||
uint256 expected = _dLvl * RaidGeldUtils.PRECISION + _apLvl * RaidGeldUtils.APPRENTICE_PROFIT
|
||||
+ _anLvl * RaidGeldUtils.ANOINTED_PROFIT + _cLvl * RaidGeldUtils.CHAMPION_PROFIT;
|
||||
assertEq(profits_per_second, expected);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user