You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

228 lines
9.1 KiB
Solidity

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Owned} from "solmate/auth/Owned.sol";
import {Duck} from "./Duck.sol";
import {Zapper} from "./Zapper.sol";
/**
@title Punkhunt
@author lzamenace.eth
@notice This is the main contract interface for...
*/
contract Punkhunt is Owned {
/// Track the number of kills for each address
mapping(address => uint256) public killCount;
/// Index addresses to form a basic leaderboard
mapping(uint256 => address) public leaderboard;
/// Point to the latest leaderboard update
uint256 public leaderboardPointer;
/// Price of the Duck ERC-721 token
uint256 public duckPrice = 0.01 ether;
/// Price of the Zapper ERC-1155 token
uint256 public zapperPrice = 0.0025 ether;
/// If PvP can commence
bool public mayhem;
/// Duck contract
Duck public duck;
/// Zapper contract
Zapper public zapper;
/// ShotDuck event is for recording the results of sendZappers for real-time feedback to a frontend interface
/// @param from Sender of the zappers
/// @param tokenId Duck token which was targeted
/// @param hit Whether or not the zapper killed the token or not (was a dud / already killed)
/// @param owned Whether or not the sender was the owner of the DUCK token
event ShotDuck(address indexed from, uint256 indexed tokenId, bool hit, bool owned);
constructor() Owned(msg.sender) {}
// =========================================================================
// Admin
// =========================================================================
/// Withdraw funds to contract owner and game winners
function withdraw() external onlyOwner {
// kill leader gets 25%
// surviving duck owner gets 25%
// contract deployer gets remaining 50%
uint256 balance = address(this).balance;
(bool success, ) = payable(msg.sender).call{value: balance}("");
require(success, "failed to withdraw");
}
/// Set price per DUCK
/// @param _price Price in wei to mint DUCK token
function setDuckPrice(uint256 _price) external onlyOwner {
duckPrice = _price;
}
/// Set price per ZAPPER
/// @param _price Price in wei to mint ZAPPER token
function setZapperPrice(uint256 _price) external onlyOwner {
zapperPrice = _price;
}
/// Set contract address for Duck tokens
/// @param _address Address of the Duck / DUCK contract
function setDuckContract(address _address) external onlyOwner {
duck = Duck(_address);
}
/// Set contract address for Zapper tokens
/// @param _address Address of the Zapper / ZAPPER contract
function setZapperContract(address _address) external onlyOwner {
zapper = Zapper(_address);
}
/// Toggle mayhem switch to enable shooting
function toggleMayhem() external onlyOwner {
mayhem = !mayhem;
}
// =========================================================================
// Modifiers
// =========================================================================
/// This modifier prevents actions once the Duck survivor count is breached.
/// The game stops; no more zappering/killing. Survivors make it to the next round.
modifier gameNotCompleted {
require(
duck.totalSupply() > 1,
"game is completed"
);
_;
}
// =========================================================================
// Getters
// =========================================================================
/// Get DUCK token balance of wallet
/// @param _address Wallet address to query balance of DUCK token
/// @return balance Amount of DUCK tokens owned by _address
function duckBalanceOf(address _address) public view returns (uint256) {
return duck.balanceOf(_address);
}
/// Get DUCK amount minted (including ones that have been burned/killed)
/// @param _address Wallet address to query the amount of DUCK token minted
/// @return balance Amount of DUCK tokens that have been minted by _address
function ducksMintedBy(address _address) public view returns (uint256) {
return duck.tokensMintedByWallet(_address);
}
/// Get DUCK token total supply
/// @return supply Amount of DUCK tokens minted in total
function totalDucksMinted() public view returns (uint256) {
return duck.minted();
}
/// Get DUCK ducks killed
/// @return killCount Amount of DUCK tokens killed
function totalDucksKilled() public view returns (uint256) {
return duck.burned();
}
/// Get DUCK supply (minted - burned)
/// @return totalSupply Amount of DUCK tokens killed
function totalDuckSupply() public view returns (uint256) {
return duck.totalSupply();
}
/// Get DUCK token survivor count
/// @return survivorCount Maximum amount of DUCK survivor tokens that can ever exist
function duckMaxSurvivorCount() public view returns (uint256) {
return duck.MAX_SURVIVOR_COUNT();
}
/// Get DUCK token max mint amount per wallet
/// @return mintAmount Maximum amount of DUCK tokens that can be minted per wallet
function duckMaxMintPerWallet() public view returns (uint256) {
return duck.MAX_MINT_AMOUNT();
}
/// Get ZAPPER token balance of wallet
/// @param _address Wallet address to query balance of ZAPPER token
/// @return balance Amount of ZAPPER tokens owned by _address
function zapperBalanceOf(address _address) public view returns (uint256) {
return zapper.balanceOf(_address, 1);
}
/// Get ZAPPER token supply
/// @return supply Amount of ZAPPER tokens ever minted
function totalZappersMinted() public view returns (uint256) {
return zapper.zappersMinted();
}
/// Get ZAPPER burned amount
/// @return exploded Amount of ZAPPER tokens that have burned
function totalZappersBurned() public view returns (uint256) {
return zapper.zappersBurned();
}
// =========================================================================
// Tokens
// =========================================================================
/// Mint a duck and give 1 free zapper
/// @param _amount Amount of Ducks to mint
function mintDucks(uint256 _amount) external payable gameNotCompleted {
require(msg.value >= _amount * duckPrice, "not enough ether");
duck.mint(msg.sender, _amount);
zapper.mint(msg.sender, _amount);
}
/// Mint additional Zappers to kill ducks
/// @param _amount Amount of Zappers to mint
function mintZappers(uint256 _amount) external payable gameNotCompleted {
require(msg.value >= _amount * zapperPrice, "not enough ether");
zapper.mint(msg.sender, _amount);
}
/// Send N zappers to pseudo-random Duck tokenIds to kill them.
/// If the Duck is already dead, the zapper is considered a dud.
/// Update a leaderboard with updated kill counts.
/// @dev Pick a pseudo-random tokenID from Duck contract and toggle a mapping value
/// @dev The likelihood of killing a boomer decreases as time goes on - i.e. more duds
/// @param _amount Amount of zappers to send to kill Ducks (dead pfps)
function shootDucks(uint256 _amount) external gameNotCompleted {
// Require mayhem is set (allow time to mint and trade)
require(mayhem, "not ready for mayhem");
// Ensure _amount will not exceed wallet balance of zappers, Duck supply, and active Ducks
uint256 supply = totalDuckSupply();
uint256 bal = zapperBalanceOf(msg.sender);
require(_amount <= bal, "not enough zappers");
for (uint256 i; i < _amount; i++) {
// Pick a pseudo-random Duck token - imperfectly derives token IDs so that repeats are probable
uint256 randomDuck = (uint256(keccak256(abi.encodePacked(i, supply, bal, msg.sender))) % supply) + 1;
// Capture owner
address _owner = duck.ownerOf(randomDuck);
// Check if it was already killed
bool dud = _owner == address(0);
// Check if the sender owns it (misfired, killed own pfp)
bool senderOwned = msg.sender == _owner;
// Kill it (does nothing if already toggled as dead)
duck.die(randomDuck);
// Emit event for displaying in web app
emit ShotDuck(msg.sender, randomDuck, !dud, senderOwned);
// Increment kill count if successfully killed another player's Duck
if(!dud && !senderOwned) {
killCount[msg.sender]++;
}
}
// Update the leaderboard and pointer for tracking the highest amount of kills for wallets
uint256 kills = killCount[msg.sender];
address leader = leaderboard[leaderboardPointer];
if (kills > killCount[leader]) {
if (leader != msg.sender) {
leaderboardPointer++;
leaderboard[leaderboardPointer] = msg.sender;
}
}
// Burn ERC-1155 ZAPPER tokens
zapper.burn(msg.sender, _amount);
}
}