1
0
forked from mico/idle_moloch

WIP adding bosses

This commit is contained in:
mic0 2024-10-30 00:28:48 +01:00
parent 52aec1ae9d
commit 1afd975e0f
Signed by: mico
GPG Key ID: A3F8023524CF1C8D
4 changed files with 148 additions and 93 deletions

View File

@ -5,7 +5,7 @@ import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol"; import "@openzeppelin/contracts/access/Ownable.sol";
import {RaidGeldUtils} from "../src/RaidGeldUtils.sol"; import {RaidGeldUtils} from "../src/RaidGeldUtils.sol";
import {Army, Player, Raider} from "../src/RaidGeldStructs.sol"; import {Army, Player, Raider, Boss} from "../src/RaidGeldStructs.sol";
import "../src/Constants.sol"; import "../src/Constants.sol";
contract RaidGeld is ERC20, Ownable, Constants { contract RaidGeld is ERC20, Ownable, Constants {
@ -15,6 +15,7 @@ contract RaidGeld is ERC20, Ownable, Constants {
uint256 public constant INITIAL_GELD = 500 * MANTISSA; uint256 public constant INITIAL_GELD = 500 * MANTISSA;
mapping(address => Player) private players; mapping(address => Player) private players;
mapping(address => Army) private armies; mapping(address => Army) private armies;
mapping(address => Boss) private bosses;
// WETH // WETH
IWETH public immutable weth = IWETH(WETH); IWETH public immutable weth = IWETH(WETH);
@ -25,12 +26,9 @@ contract RaidGeld is ERC20, Ownable, Constants {
// Uniswap // Uniswap
ISwapRouter02 private constant router = ISwapRouter02(SWAP_ROUTER); ISwapRouter02 private constant router = ISwapRouter02(SWAP_ROUTER);
// Events // Events
event PlayerRegistered(address indexed player, uint256 initialGeld); event PlayerRegistered(address indexed player, uint256 initialGeld);
event RaidPerformed( event RaidPerformed(address indexed player, uint256 totalMinted, uint256 geldBalance);
address indexed player,
uint256 totalMinted,
uint256 geldBalance
);
event UnitAdded( event UnitAdded(
address indexed player, address indexed player,
uint8 unitType, uint8 unitType,
@ -49,25 +47,54 @@ contract RaidGeld is ERC20, Ownable, Constants {
_; _;
} }
modifier newPlayer() { modifier onlyActiveSession() {
require(players[msg.sender].created_at == 0, "Whoops, player already exists :)"); require(players[msg.sender].active_session, "Session is not active, you need to buy into the game first");
_;
}
modifier newPlay() {
Player memory player = players[msg.sender];
bool notRegistered = player.created_at == 0;
bool returningPlayer = player.has_registered && !player.has_active_session;
require(notRegistered || returningPlayer, "Active session already in progress");
_; _;
} }
constructor(address _daoToken, address _pool) ERC20("Raid Geld", "GELD") Ownable(msg.sender) { constructor(address _daoToken, address _pool) ERC20("Raid Geld", "GELD") Ownable(msg.sender) {
daoToken = ERC20(_daoToken); daoToken = ERC20(_daoToken);
pool = _pool; pool = _pool;
BUY_IN_DAO_TOKEN_AMOUNT = 50 * 10 ** daoToken.decimals(); BUY_IN_DAO_TOKEN_AMOUNT = 500 * 10 ** daoToken.decimals();
} }
function init_player(address player) private { function init_player(address player) private {
bool existing_player = players[player].is_registered;
// Mint some starting tokens to the player // Mint some starting tokens to the player
_mint(player, INITIAL_GELD); _mint(player, INITIAL_GELD);
// Reset or set player
reset_player(player);
if (existing_player) {
// TODO: Emit new run
} else {
// Emit event
emit PlayerRegistered(msg.sender, INITIAL_GELD);
}
}
function reset_player(address addr) private {
Player memory player = players[addr];
uint32 current_n_runs = player.n_runs + 1;
uint256 current_total_rewards = player.total_rewards;
bool has_registered = player.is_registered;
// Set initial states // Set initial states
players[msg.sender] = Player({ players[msg.sender] = Player({
total_minted: INITIAL_GELD, total_minted: INITIAL_GELD,
created_at: block.timestamp, created_at: block.timestamp,
last_raided_at: block.timestamp last_raided_at: block.timestamp,
n_runs: current_n_runs,
total_rewards: current_total_rewards,
active_session: false
}); });
armies[msg.sender] = Army({ armies[msg.sender] = Army({
moloch_denier: Raider({level: 0}), moloch_denier: Raider({level: 0}),
@ -76,13 +103,11 @@ contract RaidGeld is ERC20, Ownable, Constants {
champion: Raider({level: 0}), champion: Raider({level: 0}),
profit_per_second: 0 profit_per_second: 0
}); });
bosses[msg.sender] = Boss({level: 0, variant: 0});
// Emit event
emit PlayerRegistered(msg.sender, INITIAL_GELD);
} }
// New player want to register with ETH // New player want to register with ETH
function register_eth() external payable newPlayer { function register_eth() external payable newPlay {
require(msg.value == BUY_IN_AMOUNT, "Incorrect buy in amount"); require(msg.value == BUY_IN_AMOUNT, "Incorrect buy in amount");
weth.deposit{value: BUY_IN_AMOUNT}(); weth.deposit{value: BUY_IN_AMOUNT}();
weth.approve(address(router), BUY_IN_AMOUNT); weth.approve(address(router), BUY_IN_AMOUNT);
@ -100,7 +125,7 @@ contract RaidGeld is ERC20, Ownable, Constants {
} }
// New player wants to register with dao // New player wants to register with dao
function register_dao() external payable newPlayer { function register_dao() external payable newPlay {
//@notice this is not safe for arbitrary tokens, which may not follow the interface eg. USDT //@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 //@notice but should be fine for the DAO token
require( require(
@ -123,25 +148,18 @@ contract RaidGeld is ERC20, Ownable, Constants {
} }
// Manual minting for itchy fingers // Manual minting for itchy fingers
function raid() external onlyPlayer { function raid() external onlyActiveSession {
uint256 totalMinted = performRaid(msg.sender); performRaid();
// Emit event
emit RaidPerformed(msg.sender, totalMinted, balanceOf(msg.sender));
} }
// Helper so we can use it when buying units too // Helper so we can use it when buying units too
function performRaid(address player) private returns (uint256) { function performRaid(address player) private {
uint256 time_past = block.timestamp - players[player].last_raided_at; uint256 time_past = block.timestamp - players[player].last_raided_at;
uint256 new_geld = armies[player].profit_per_second * time_past; 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); _mint(player, new_geld);
players[player].last_raided_at = block.timestamp; players[player].last_raided_at = block.timestamp;
players[player].total_minted += new_geld; players[player].total_minted += new_geld;
emit RaidPerformed(player, players[player].total_minted, balanceOf(player));
return players[player].total_minted;
} }
// Function to get Player struct // Function to get Player struct
@ -160,7 +178,7 @@ contract RaidGeld is ERC20, Ownable, Constants {
} }
// Add a unit to your army // Add a unit to your army
function addUnit(uint8 unit, uint16 n_units) external onlyPlayer { function addUnit(uint8 unit, uint16 n_units) external onlyActiveSession {
require(unit <= 3, "Unknown unit"); require(unit <= 3, "Unknown unit");
Army storage army = armies[msg.sender]; Army storage army = armies[msg.sender];
@ -179,24 +197,15 @@ contract RaidGeld is ERC20, Ownable, Constants {
currentLevel = army.champion.level; currentLevel = army.champion.level;
} }
uint256 cost = RaidGeldUtils.calculateUnitPrice( uint256 cost = RaidGeldUtils.calculateUnitPrice(unit, currentLevel, n_units);
unit,
currentLevel,
n_units
);
// First trigger a raid so player receives what he is due at to this moment
uint256 time_past = block.timestamp - // First trigger a raid so player receives what he is due at to this moment
players[msg.sender].last_raided_at; uint256 time_past = block.timestamp - players[msg.sender].last_raided_at;
uint256 new_geld = armies[msg.sender].profit_per_second * time_past; uint256 new_geld = armies[msg.sender].profit_per_second * time_past;
require( require(balanceOf(msg.sender) + new_geld >= cost, "Not enough GELD to add this unit");
balanceOf(msg.sender) + new_geld >= cost,
"Not enough GELD to add this unit"
);
uint256 totalMinted = performRaid(msg.sender); uint256 totalMinted = performRaid(msg.sender);
// Emit event // Emit event
emit RaidPerformed(msg.sender, totalMinted, balanceOf(msg.sender));
// TODO: Since we are first minting then burning the token, this could be simplified // 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 // by first calculating the difference and then minting / burning in just one operation
@ -236,10 +245,37 @@ contract RaidGeld is ERC20, Ownable, Constants {
); );
} }
receive() external payable { function battle_with_boss() external onlyActiveSession {
revert( // first perform raid
"No plain Ether accepted, use register() function to check in :)" performRaid();
Boss memory boss_to_attack = bosses[msg.sender];
// calculate how much the player will put into battle
uint256 geld_to_burn = balanceOf(msg.sender) >= RaidGeldUtils.BOSS_POWERS[boss_to_attack.level]
? RaidGeldUtils.BOSS_POWERS[boss_to_attack.level]
: balanceOf(msg.sender);
bool hasWonBattle = RaidGeldUtils.calculateBossFight(
boss_to_attack.level, geld_to_burn, block.timestamp, block.difficulty
); );
if (hasWonBattle) {
// Burn geld, get some sweet DAO Token and continue
_burn(msg.sender, geld_to_burn);
uint256 reward = RaidGeldUtils.calculateBossReward(boss_to_attack.level);
players[msg.sender].total_rewards += reward;
daoToken.transferFrom(address(this), msg.sender, reward);
} else {
// Whoops u die
player_dies(msg.sender);
}
}
function player_dies(address player) private {
reset_player();
players[player].has_active_session = false;
_burn(msg.sender, balanceOf(player));
}
receive() external payable {
revert("No plain Ether accepted, use register() function to check in :)");
} }
// Revert any non-function-call Ether transfers or calls to non-existent functions // Revert any non-function-call Ether transfers or calls to non-existent functions
@ -259,10 +295,7 @@ interface ISwapRouter02 {
uint160 sqrtPriceLimitX96; uint160 sqrtPriceLimitX96;
} }
function exactInputSingle(ExactInputSingleParams calldata params) function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);
external
payable
returns (uint256 amountOut);
struct ExactOutputSingleParams { struct ExactOutputSingleParams {
address tokenIn; address tokenIn;
@ -274,10 +307,7 @@ interface ISwapRouter02 {
uint160 sqrtPriceLimitX96; uint160 sqrtPriceLimitX96;
} }
function exactOutputSingle(ExactOutputSingleParams calldata params) function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn);
external
payable
returns (uint256 amountIn);
} }
interface IWETH is IERC20 { interface IWETH is IERC20 {

View File

@ -17,4 +17,13 @@ struct Player {
uint256 total_minted; uint256 total_minted;
uint256 created_at; uint256 created_at;
uint256 last_raided_at; uint256 last_raided_at;
uint256 total_rewards;
uint32 n_runs;
bool is_registered;
bool has_active_session;
}
struct Boss {
uint8 variant;
uint8 level;
} }

View File

@ -19,6 +19,26 @@ library RaidGeldUtils {
uint256 constant ANOINTED_BASE_COST = 30096000; uint256 constant ANOINTED_BASE_COST = 30096000;
uint256 constant CHAMPION_BASE_COST = 255816000; uint256 constant CHAMPION_BASE_COST = 255816000;
// Boss lvels
uint256[7] constant BOSS_POWERS = [
9000000, // 900e4
90000000, // 900e5
900000000, // 900e6
9000000000, // 900e7
90000000000, // 900e8
900000000000, // 900e9
9000000000000 // 900e10
];
// First boss is almost guaranteed, others arent
uint256[7] constant BOSS_CHANCES = [99, 89, 80, 70, 62, 51, 40];
// These are cumulative chances from above with precision 18
uint256[7] constant CUMULATIVE_BOSS_CHANCES =
[99e16, 8811e14, 7048e14, 493416e12, 30591792e10, 1560181392e8, 6240725568e7];
uint256 constant RISK_BETA = 15e17;
function calculateUnitPrice(uint8 unit, uint16 currentLevel, uint16 units) internal pure returns (uint256) { function calculateUnitPrice(uint8 unit, uint16 currentLevel, uint16 units) internal pure returns (uint256) {
require(unit <= 3, "No matching unit found"); require(unit <= 3, "No matching unit found");
uint256 rollingPriceCalculation = MOLOCH_DENIER_BASE_COST; uint256 rollingPriceCalculation = MOLOCH_DENIER_BASE_COST;
@ -43,7 +63,6 @@ library RaidGeldUtils {
function calculateProfitsPerSecond(Army memory army) internal pure returns (uint256) { function calculateProfitsPerSecond(Army memory army) internal pure returns (uint256) {
// Each next unit scales progressivelly better // 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 * MOLOCH_DENIER_PROFIT;
uint256 apprentice_profit = army.apprentice.level * APPRENTICE_PROFIT; uint256 apprentice_profit = army.apprentice.level * APPRENTICE_PROFIT;
uint256 anointed_profit = army.anointed.level * ANOINTED_PROFIT; uint256 anointed_profit = army.anointed.level * ANOINTED_PROFIT;
@ -51,4 +70,33 @@ library RaidGeldUtils {
return moloch_denier_profit + apprentice_profit + anointed_profit + champion_profit; return moloch_denier_profit + apprentice_profit + anointed_profit + champion_profit;
} }
// Returns how much Dao Token player is due for winning
function calculateBossReward(uint256 bossLevel) public view returns (uint256) {
return BOSS_POWERS[bossLevel] * (CUMULATIVE_BOSS_CHANCES[bossLevel] * 10 / 5) ** 2;
}
// Calculates whether user survives the fight
function calculateBossFight(uint8 bossLevel, uint256 geldBurnt, uint256 timestamp, uint256 difficulty)
internal
pure
returns (uint256)
{
uint256 bossPower = BOSS_POWERS[bossLevel];
require(geldBurnt <= bossPower, "Cant try to defeat boss with more than what boss power is");
uint256 random_n = random(timestamp, difficulty, 1, 100);
// Relative power as in, you can only put in 800 geld to defeat 900 geld boss,
// but you will get exponentially worse chances
uint256 relativePower = ((geldBurnt ** 2) * 100) / bossPower ** 2;
uint256 roll = (random_n * relativePower * PRECISION) / (100 * PRECISION);
return roll >= bossPower;
}
// TODO: Implement actual randomness
function random(uint256 timestamp, uint256 difficulty, uint256 min, uint256 max) internal pure returns (uint256) {
// returns 0 - 100
require(max >= min, "Max must be greater than or equal to min");
uint256 range = max - min + 1;
return min + (uint256(keccak256(abi.encodePacked(timestamp, difficulty))) % range);
}
} }

View File

@ -17,11 +17,7 @@ contract raid_geldTest is Test, Constants {
event Approval(address indexed owner, address indexed spender, uint256 value); event Approval(address indexed owner, address indexed spender, uint256 value);
event PlayerRegistered(address indexed player, uint256 initialGeld); event PlayerRegistered(address indexed player, uint256 initialGeld);
event RaidPerformed( event RaidPerformed(address indexed player, uint256 totalMinted, uint256 geldBalance);
address indexed player,
uint256 totalMinted,
uint256 geldBalance
);
event UnitAdded( event UnitAdded(
address indexed player, address indexed player,
uint8 unitType, uint8 unitType,
@ -61,7 +57,7 @@ contract raid_geldTest is Test, Constants {
function test_00_no_fallback() public { function test_00_no_fallback() public {
vm.expectRevert(); vm.expectRevert();
// Send Ether with some data to trigger fallback // Send Ether with some data to trigger fallback
(bool success, ) = address(raid_geld).call{value: 0.1 ether}("0x1234"); (bool success,) = address(raid_geld).call{value: 0.1 ether}("0x1234");
} }
function test_01_no_receive() public { function test_01_no_receive() public {
@ -113,7 +109,7 @@ contract raid_geldTest is Test, Constants {
uint256 initialBalance = raid_geld.daoToken().balanceOf(address(raid_geld)); uint256 initialBalance = raid_geld.daoToken().balanceOf(address(raid_geld));
// Making sure event is emitted when player is registered // Making sure event is emitted when player is registered
// doesnt test player emitted event because other events get emitted before it // doesnt test player emitted event because other events get emitted before it
registerPlayerWithDaoToken(); registerPlayerWithDaoToken();
// Check that initial raid_geld is received by the player // Check that initial raid_geld is received by the player
@ -136,7 +132,7 @@ contract raid_geldTest is Test, Constants {
assertEq(army.apprentice.level, 0); assertEq(army.apprentice.level, 0);
assertEq(army.anointed.level, 0); assertEq(army.anointed.level, 0);
assertEq(army.champion.level, 0); assertEq(army.champion.level, 0);
} }
function test_03_dao_token_can_be_withdrawn() public { function test_03_dao_token_can_be_withdrawn() public {
uint256 initialBalance = raid_geld.daoToken().balanceOf(address(raid_geld)); uint256 initialBalance = raid_geld.daoToken().balanceOf(address(raid_geld));
@ -144,7 +140,7 @@ contract raid_geldTest is Test, Constants {
// Switch to Player 1 and register it // Switch to Player 1 and register it
vm.startPrank(player1); vm.startPrank(player1);
// doesnt test player emitted event because other events get emitted before it // doesnt test player emitted event because other events get emitted before it
registerPlayerWithDaoToken(); registerPlayerWithDaoToken();
// Switch back to owner and withdraw funds // Switch back to owner and withdraw funds
@ -211,17 +207,7 @@ contract raid_geldTest is Test, Constants {
// Making sure event is emitted when player adds a unit // Making sure event is emitted when player adds a unit
vm.expectEmit(address(raid_geld)); vm.expectEmit(address(raid_geld));
emit UnitAdded( emit UnitAdded(address(player1), 0, 1, cost, playerBalance - cost, 1, 0, 0, 0);
address(player1),
0,
1,
cost,
playerBalance - cost,
1,
0,
0,
0
);
// Add 1 unit // Add 1 unit
raid_geld.addUnit(0, 1); raid_geld.addUnit(0, 1);
@ -257,17 +243,7 @@ contract raid_geldTest is Test, Constants {
// Making sure event is emitted when player adds a unit // Making sure event is emitted when player adds a unit
vm.expectEmit(address(raid_geld)); vm.expectEmit(address(raid_geld));
emit UnitAdded( emit UnitAdded(address(player1), 0, 1, cost, playerBalance - cost, 1, 0, 0, 0);
address(player1),
0,
1,
cost,
playerBalance - cost,
1,
0,
0,
0
);
// bought 1 moloch_denier // bought 1 moloch_denier
raid_geld.addUnit(0, 1); raid_geld.addUnit(0, 1);
@ -282,11 +258,7 @@ contract raid_geldTest is Test, Constants {
// Making sure event is emitted when player performs a raid // Making sure event is emitted when player performs a raid
vm.expectEmit(address(raid_geld)); vm.expectEmit(address(raid_geld));
emit RaidPerformed( emit RaidPerformed(address(player1), player.total_minted + amountMinted, balance + amountMinted);
address(player1),
player.total_minted + amountMinted,
balance + amountMinted
);
// Trigger raid funds minting // Trigger raid funds minting
raid_geld.raid(); raid_geld.raid();
@ -302,11 +274,7 @@ contract raid_geldTest is Test, Constants {
amountMinted = army.profit_per_second * 15; amountMinted = army.profit_per_second * 15;
emit RaidPerformed( emit RaidPerformed(address(player1), player.total_minted + amountMinted, balance + amountMinted);
address(player1),
player.total_minted + amountMinted,
balance + amountMinted
);
raid_geld.raid(); raid_geld.raid();