Adding bosses to the game #6

Merged
mico merged 21 commits from feature/boss-mechanic into main 2024-10-31 13:56:05 +00:00
4 changed files with 148 additions and 93 deletions
Showing only changes of commit 1afd975e0f - Show all commits

View File

@ -5,7 +5,7 @@ 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";
import {Army, Player, Raider, Boss} from "../src/RaidGeldStructs.sol";
import "../src/Constants.sol";
contract RaidGeld is ERC20, Ownable, Constants {
@ -15,6 +15,7 @@ contract RaidGeld is ERC20, Ownable, Constants {
uint256 public constant INITIAL_GELD = 500 * MANTISSA;
mapping(address => Player) private players;
mapping(address => Army) private armies;
mapping(address => Boss) private bosses;
// WETH
IWETH public immutable weth = IWETH(WETH);
@ -25,12 +26,9 @@ contract RaidGeld is ERC20, Ownable, Constants {
// Uniswap
ISwapRouter02 private constant router = ISwapRouter02(SWAP_ROUTER);
// Events
event PlayerRegistered(address indexed player, uint256 initialGeld);
event RaidPerformed(
address indexed player,
uint256 totalMinted,
uint256 geldBalance
);
event RaidPerformed(address indexed player, uint256 totalMinted, uint256 geldBalance);
event UnitAdded(
address indexed player,
uint8 unitType,
@ -49,25 +47,54 @@ contract RaidGeld is ERC20, Ownable, Constants {
_;
}
modifier newPlayer() {
require(players[msg.sender].created_at == 0, "Whoops, player already exists :)");
modifier onlyActiveSession() {
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) {
daoToken = ERC20(_daoToken);
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 {
bool existing_player = players[player].is_registered;
// Mint some starting tokens to the player
_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
players[msg.sender] = Player({
total_minted: INITIAL_GELD,
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({
moloch_denier: Raider({level: 0}),
@ -76,13 +103,11 @@ contract RaidGeld is ERC20, Ownable, Constants {
champion: Raider({level: 0}),
profit_per_second: 0
});
// Emit event
emit PlayerRegistered(msg.sender, INITIAL_GELD);
bosses[msg.sender] = Boss({level: 0, variant: 0});
}
// 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");
weth.deposit{value: 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
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 but should be fine for the DAO token
require(
@ -123,25 +148,18 @@ contract RaidGeld is ERC20, Ownable, Constants {
}
// Manual minting for itchy fingers
function raid() external onlyPlayer {
uint256 totalMinted = performRaid(msg.sender);
// Emit event
emit RaidPerformed(msg.sender, totalMinted, balanceOf(msg.sender));
function raid() external onlyActiveSession {
performRaid();
}
// 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 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;
return players[player].total_minted;
emit RaidPerformed(player, players[player].total_minted, balanceOf(player));
}
// Function to get Player struct
@ -160,7 +178,7 @@ contract RaidGeld is ERC20, Ownable, Constants {
}
// 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");
Army storage army = armies[msg.sender];
@ -179,24 +197,15 @@ contract RaidGeld is ERC20, Ownable, Constants {
currentLevel = army.champion.level;
}
uint256 cost = RaidGeldUtils.calculateUnitPrice(
unit,
currentLevel,
n_units
);
// First trigger a raid so player receives what he is due at to this moment
uint256 cost = RaidGeldUtils.calculateUnitPrice(unit, currentLevel, n_units);
uint256 time_past = block.timestamp -
players[msg.sender].last_raided_at;
// 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"
);
require(balanceOf(msg.sender) + new_geld >= cost, "Not enough GELD to add this unit");
uint256 totalMinted = performRaid(msg.sender);
// Emit event
emit RaidPerformed(msg.sender, totalMinted, balanceOf(msg.sender));
// 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
@ -236,10 +245,37 @@ contract RaidGeld is ERC20, Ownable, Constants {
);
}
receive() external payable {
revert(
"No plain Ether accepted, use register() function to check in :)"
function battle_with_boss() external onlyActiveSession {
// first perform raid
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
@ -259,10 +295,7 @@ interface ISwapRouter02 {
uint160 sqrtPriceLimitX96;
}
function exactInputSingle(ExactInputSingleParams calldata params)
external
payable
returns (uint256 amountOut);
function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);
struct ExactOutputSingleParams {
address tokenIn;
@ -274,10 +307,7 @@ interface ISwapRouter02 {
uint160 sqrtPriceLimitX96;
}
function exactOutputSingle(ExactOutputSingleParams calldata params)
external
payable
returns (uint256 amountIn);
function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn);
}
interface IWETH is IERC20 {

View File

@ -17,4 +17,13 @@ struct Player {
uint256 total_minted;
uint256 created_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 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) {
require(unit <= 3, "No matching unit found");
uint256 rollingPriceCalculation = MOLOCH_DENIER_BASE_COST;
@ -43,7 +63,6 @@ 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 apprentice_profit = army.apprentice.level * APPRENTICE_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;
}
// 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 PlayerRegistered(address indexed player, uint256 initialGeld);
event RaidPerformed(
address indexed player,
uint256 totalMinted,
uint256 geldBalance
);
event RaidPerformed(address indexed player, uint256 totalMinted, uint256 geldBalance);
event UnitAdded(
address indexed player,
uint8 unitType,
@ -211,17 +207,7 @@ contract raid_geldTest is Test, Constants {
// Making sure event is emitted when player adds a unit
vm.expectEmit(address(raid_geld));
emit UnitAdded(
address(player1),
0,
1,
cost,
playerBalance - cost,
1,
0,
0,
0
);
emit UnitAdded(address(player1), 0, 1, cost, playerBalance - cost, 1, 0, 0, 0);
// Add 1 unit
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
vm.expectEmit(address(raid_geld));
emit UnitAdded(
address(player1),
0,
1,
cost,
playerBalance - cost,
1,
0,
0,
0
);
emit UnitAdded(address(player1), 0, 1, cost, playerBalance - cost, 1, 0, 0, 0);
// bought 1 moloch_denier
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
vm.expectEmit(address(raid_geld));
emit RaidPerformed(
address(player1),
player.total_minted + amountMinted,
balance + amountMinted
);
emit RaidPerformed(address(player1), player.total_minted + amountMinted, balance + amountMinted);
// Trigger raid funds minting
raid_geld.raid();
@ -302,11 +274,7 @@ contract raid_geldTest is Test, Constants {
amountMinted = army.profit_per_second * 15;
emit RaidPerformed(
address(player1),
player.total_minted + amountMinted,
balance + amountMinted
);
emit RaidPerformed(address(player1), player.total_minted + amountMinted, balance + amountMinted);
raid_geld.raid();