Spaces:
Runtime error
Runtime error
/** | |
*Submitted for verification at Etherscan.io on 2018-05-14 | |
*/ | |
pragma solidity 0.4.23; | |
/** | |
* @title SafeMath | |
* @dev Math operations with safety checks that throw on error | |
*/ | |
library SafeMath { | |
/** | |
* @dev Subtracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend). | |
*/ | |
function sub(uint256 a, uint256 b) internal pure returns (uint256) { | |
assert(b <= a); | |
return a - b; | |
} | |
/** | |
* @dev Adds two numbers, throws on overflow. | |
*/ | |
function add(uint256 a, uint256 b) internal pure returns (uint256) { | |
uint256 c = a + b; | |
assert(c >= a); | |
return c; | |
} | |
} | |
/** | |
* @title Ownable | |
* @dev The Ownable contract has an owner address, and provides basic authorization control | |
* functions, this simplifies the implementation of "user permissions". | |
*/ | |
contract Ownable { | |
address internal contractOwner; | |
constructor () internal { | |
if(contractOwner == address(0)){ | |
contractOwner = msg.sender; | |
} | |
} | |
/** | |
* @dev Throws if called by any account other than the owner. | |
*/ | |
modifier onlyOwner() { | |
require(msg.sender == contractOwner); | |
_; | |
} | |
/** | |
* @dev Allows the current owner to transfer control of the contract to a newOwner. | |
* @param newOwner The address to transfer ownership to. | |
*/ | |
function transferOwnership(address newOwner) public onlyOwner { | |
require(newOwner != address(0)); | |
contractOwner = newOwner; | |
} | |
} | |
/** | |
* @title Pausable | |
* @dev Base contract which allows children to implement an emergency stop mechanism. | |
*/ | |
contract Pausable is Ownable { | |
bool private paused = false; | |
/** | |
* @dev Modifier to allow actions only when the contract IS paused | |
@dev If is paused msg.value is send back | |
*/ | |
modifier whenNotPaused() { | |
if(paused == true && msg.value > 0){ | |
msg.sender.transfer(msg.value); | |
} | |
require(!paused); | |
_; | |
} | |
/** | |
* @dev Called by the owner to pause, triggers stopped state | |
*/ | |
function triggerPause() onlyOwner external { | |
paused = !paused; | |
} | |
} | |
/// @title A contract for creating new champs and making withdrawals | |
contract ChampFactory is Pausable{ | |
event NewChamp(uint256 champID, address owner); | |
using SafeMath for uint; //SafeMath for overflow prevention | |
/* | |
* Variables | |
*/ | |
struct Champ { | |
uint256 id; //same as position in Champ[] | |
uint256 attackPower; | |
uint256 defencePower; | |
uint256 cooldownTime; //how long does it take to be ready attack again | |
uint256 readyTime; //if is smaller than block.timestamp champ is ready to fight | |
uint256 winCount; | |
uint256 lossCount; | |
uint256 position; //position in leaderboard. subtract 1 and you got position in leaderboard[] | |
uint256 price; //selling price | |
uint256 withdrawCooldown; //if you one of the 800 best champs and withdrawCooldown is less as block.timestamp then you get ETH reward | |
uint256 eq_sword; | |
uint256 eq_shield; | |
uint256 eq_helmet; | |
bool forSale; //is champ for sale? | |
} | |
struct AddressInfo { | |
uint256 withdrawal; | |
uint256 champsCount; | |
uint256 itemsCount; | |
string name; | |
} | |
//Item struct | |
struct Item { | |
uint8 itemType; // 1 - Sword | 2 - Shield | 3 - Helmet | |
uint8 itemRarity; // 1 - Common | 2 - Uncommon | 3 - Rare | 4 - Epic | 5 - Legendery | 6 - Forged | |
uint256 attackPower; | |
uint256 defencePower; | |
uint256 cooldownReduction; | |
uint256 price; | |
uint256 onChampId; //can not be used to decide if item is on champ, because champ's id can be 0, 'bool onChamp' solves it. | |
bool onChamp; | |
bool forSale; //is item for sale? | |
} | |
mapping (address => AddressInfo) public addressInfo; | |
mapping (uint256 => address) public champToOwner; | |
mapping (uint256 => address) public itemToOwner; | |
mapping (uint256 => string) public champToName; | |
Champ[] public champs; | |
Item[] public items; | |
uint256[] public leaderboard; | |
uint256 internal createChampFee = 5 finney; | |
uint256 internal lootboxFee = 5 finney; | |
uint256 internal pendingWithdrawal = 0; | |
uint256 private randNonce = 0; //being used in generating random numbers | |
uint256 public champsForSaleCount; | |
uint256 public itemsForSaleCount; | |
/* | |
* Modifiers | |
*/ | |
/// @dev Checks if msg.sender is owner of champ | |
modifier onlyOwnerOfChamp(uint256 _champId) { | |
require(msg.sender == champToOwner[_champId]); | |
_; | |
} | |
/// @dev Checks if msg.sender is NOT owner of champ | |
modifier onlyNotOwnerOfChamp(uint256 _champId) { | |
require(msg.sender != champToOwner[_champId]); | |
_; | |
} | |
/// @notice Checks if amount was sent | |
modifier isPaid(uint256 _price){ | |
require(msg.value >= _price); | |
_; | |
} | |
/// @notice People are allowed to withdraw only if min. balance (0.01 gwei) is reached | |
modifier contractMinBalanceReached(){ | |
require( (address(this).balance).sub(pendingWithdrawal) > 1000000 ); | |
_; | |
} | |
/// @notice Checks if withdraw cooldown passed | |
modifier isChampWithdrawReady(uint256 _id){ | |
require(champs[_id].withdrawCooldown < block.timestamp); | |
_; | |
} | |
/// @notice Distribute input funds between contract owner and players | |
modifier distributeInput(address _affiliateAddress){ | |
//contract owner | |
uint256 contractOwnerWithdrawal = (msg.value / 100) * 50; // 50% | |
addressInfo[contractOwner].withdrawal += contractOwnerWithdrawal; | |
pendingWithdrawal += contractOwnerWithdrawal; | |
//affiliate | |
//checks if _affiliateAddress is set & if affiliate address is not buying player | |
if(_affiliateAddress != address(0) && _affiliateAddress != msg.sender){ | |
uint256 affiliateBonus = (msg.value / 100) * 25; //provision is 25% | |
addressInfo[_affiliateAddress].withdrawal += affiliateBonus; | |
pendingWithdrawal += affiliateBonus; | |
} | |
_; | |
} | |
/* | |
* View | |
*/ | |
/// @notice Gets champs by address | |
/// @param _owner Owner address | |
function getChampsByOwner(address _owner) external view returns(uint256[]) { | |
uint256[] memory result = new uint256[](addressInfo[_owner].champsCount); | |
uint256 counter = 0; | |
for (uint256 i = 0; i < champs.length; i++) { | |
if (champToOwner[i] == _owner) { | |
result[counter] = i; | |
counter++; | |
} | |
} | |
return result; | |
} | |
/// @notice Gets total champs count | |
function getChampsCount() external view returns(uint256){ | |
return champs.length; | |
} | |
/// @notice Gets champ's reward in wei | |
function getChampReward(uint256 _position) public view returns(uint256) { | |
if(_position <= 800){ | |
//percentageMultipier = 10,000 | |
//maxReward = 2000 = .2% * percentageMultipier | |
//subtractPerPosition = 2 = .0002% * percentageMultipier | |
//2000 - (2 * (_position - 1)) | |
uint256 rewardPercentage = uint256(2000).sub(2 * (_position - 1)); | |
//available funds are all funds - already pending | |
uint256 availableWithdrawal = address(this).balance.sub(pendingWithdrawal); | |
//calculate reward for champ's position | |
//1000000 = percentageMultipier * 100 | |
return availableWithdrawal / 1000000 * rewardPercentage; | |
}else{ | |
return uint256(0); | |
} | |
} | |
/* | |
* Internal | |
*/ | |
/// @notice Generates random modulus | |
/// @param _modulus Max random modulus | |
function randMod(uint256 _modulus) internal returns(uint256) { | |
randNonce++; | |
return uint256(keccak256(randNonce, blockhash(block.number - 1))) % _modulus; | |
} | |
/* | |
* External | |
*/ | |
/// @notice Creates new champ | |
/// @param _affiliateAddress Affiliate address (optional) | |
function createChamp(address _affiliateAddress) external payable | |
whenNotPaused | |
isPaid(createChampFee) | |
distributeInput(_affiliateAddress) | |
{ | |
/* | |
Champ memory champ = Champ({ | |
id: 0, | |
attackPower: 2 + randMod(4), | |
defencePower: 1 + randMod(4), | |
cooldownTime: uint256(1 days) - uint256(randMod(9) * 1 hours), | |
readyTime: 0, | |
winCount: 0, | |
lossCount: 0, | |
position: leaderboard.length + 1, //Last place in leaderboard is new champ's position. Used in new champ struct bellow. +1 to avoid zero position. | |
price: 0, | |
withdrawCooldown: uint256(block.timestamp), | |
eq_sword: 0, | |
eq_shield: 0, | |
eq_helmet: 0, | |
forSale: false | |
}); | |
*/ | |
// This line bellow is about 30k gas cheaper than lines above. They are the same. Lines above are just more readable. | |
uint256 id = champs.push(Champ(0, 2 + randMod(4), 1 + randMod(4), uint256(1 days) - uint256(randMod(9) * 1 hours), 0, 0, 0, leaderboard.length + 1, 0, uint256(block.timestamp), 0,0,0, false)) - 1; | |
champs[id].id = id; //sets id in Champ struct | |
leaderboard.push(id); //push champ on the last place in leaderboard | |
champToOwner[id] = msg.sender; //sets owner of this champ - msg.sender | |
addressInfo[msg.sender].champsCount++; | |
emit NewChamp(id, msg.sender); | |
} | |
/// @notice Change "CreateChampFee". If ETH price will grow up it can expensive to create new champ. | |
/// @param _fee New "CreateChampFee" | |
/// @dev Only owner of contract can change "CreateChampFee" | |
function setCreateChampFee(uint256 _fee) external onlyOwner { | |
createChampFee = _fee; | |
} | |
/// @notice Change champ's name | |
function changeChampsName(uint _champId, string _name) external | |
onlyOwnerOfChamp(_champId){ | |
champToName[_champId] = _name; | |
} | |
/// @notice Change players's name | |
function changePlayersName(string _name) external { | |
addressInfo[msg.sender].name = _name; | |
} | |
/// @notice Withdraw champ's reward | |
/// @param _id Champ id | |
/// @dev Move champ reward to pending withdrawal to his wallet. | |
function withdrawChamp(uint _id) external | |
onlyOwnerOfChamp(_id) | |
contractMinBalanceReached | |
isChampWithdrawReady(_id) | |
whenNotPaused { | |
Champ storage champ = champs[_id]; | |
require(champ.position <= 800); | |
champ.withdrawCooldown = block.timestamp + 1 days; //one withdrawal 1 per day | |
uint256 withdrawal = getChampReward(champ.position); | |
addressInfo[msg.sender].withdrawal += withdrawal; | |
pendingWithdrawal += withdrawal; | |
} | |
/// @dev Send all pending funds of caller's address | |
function withdrawToAddress(address _address) external | |
whenNotPaused { | |
address playerAddress = _address; | |
if(playerAddress == address(0)){ playerAddress = msg.sender; } | |
uint256 share = addressInfo[playerAddress].withdrawal; //gets pending funds | |
require(share > 0); //is it more than 0? | |
//first sets players withdrawal pending to 0 and subtract amount from playerWithdrawals then transfer funds to avoid reentrancy | |
addressInfo[playerAddress].withdrawal = 0; //set player's withdrawal pendings to 0 | |
pendingWithdrawal = pendingWithdrawal.sub(share); //subtract share from total pendings | |
playerAddress.transfer(share); //transfer | |
} | |
} | |
/// @title Moderates items and creates new ones | |
contract Items is ChampFactory { | |
event NewItem(uint256 itemID, address owner); | |
constructor () internal { | |
//item -> nothing | |
items.push(Item(0, 0, 0, 0, 0, 0, 0, false, false)); | |
} | |
/* | |
* Modifiers | |
*/ | |
/// @notice Checks if sender is owner of item | |
modifier onlyOwnerOfItem(uint256 _itemId) { | |
require(_itemId != 0); | |
require(msg.sender == itemToOwner[_itemId]); | |
_; | |
} | |
/// @notice Checks if sender is NOT owner of item | |
modifier onlyNotOwnerOfItem(uint256 _itemId) { | |
require(msg.sender != itemToOwner[_itemId]); | |
_; | |
} | |
/* | |
* View | |
*/ | |
///@notice Check if champ has something on | |
///@param _type Sword, shield or helmet | |
function hasChampSomethingOn(uint _champId, uint8 _type) internal view returns(bool){ | |
Champ storage champ = champs[_champId]; | |
if(_type == 1){ | |
return (champ.eq_sword == 0) ? false : true; | |
} | |
if(_type == 2){ | |
return (champ.eq_shield == 0) ? false : true; | |
} | |
if(_type == 3){ | |
return (champ.eq_helmet == 0) ? false : true; | |
} | |
} | |
/// @notice Gets items by address | |
/// @param _owner Owner address | |
function getItemsByOwner(address _owner) external view returns(uint256[]) { | |
uint256[] memory result = new uint256[](addressInfo[_owner].itemsCount); | |
uint256 counter = 0; | |
for (uint256 i = 0; i < items.length; i++) { | |
if (itemToOwner[i] == _owner) { | |
result[counter] = i; | |
counter++; | |
} | |
} | |
return result; | |
} | |
/* | |
* Public | |
*/ | |
///@notice Takes item off champ | |
function takeOffItem(uint _champId, uint8 _type) public | |
onlyOwnerOfChamp(_champId) { | |
uint256 itemId; | |
Champ storage champ = champs[_champId]; | |
if(_type == 1){ | |
itemId = champ.eq_sword; //Get item ID | |
if (itemId > 0) { //0 = nothing | |
champ.eq_sword = 0; //take off sword | |
} | |
} | |
if(_type == 2){ | |
itemId = champ.eq_shield; //Get item ID | |
if(itemId > 0) {//0 = nothing | |
champ.eq_shield = 0; //take off shield | |
} | |
} | |
if(_type == 3){ | |
itemId = champ.eq_helmet; //Get item ID | |
if(itemId > 0) { //0 = nothing | |
champ.eq_helmet = 0; //take off | |
} | |
} | |
if(itemId > 0){ | |
items[itemId].onChamp = false; //item is free to use, is not on champ | |
} | |
} | |
/* | |
* External | |
*/ | |
///@notice Puts item on champ | |
function putOn(uint256 _champId, uint256 _itemId) external | |
onlyOwnerOfChamp(_champId) | |
onlyOwnerOfItem(_itemId) { | |
Champ storage champ = champs[_champId]; | |
Item storage item = items[_itemId]; | |
//checks if items is on some other champ | |
if(item.onChamp){ | |
takeOffItem(item.onChampId, item.itemType); //take off from champ | |
} | |
item.onChamp = true; //item is on champ | |
item.onChampId = _champId; //champ's id | |
//put on | |
if(item.itemType == 1){ | |
//take off actual sword | |
if(champ.eq_sword > 0){ | |
takeOffItem(champ.id, 1); | |
} | |
champ.eq_sword = _itemId; //put on sword | |
} | |
if(item.itemType == 2){ | |
//take off actual shield | |
if(champ.eq_shield > 0){ | |
takeOffItem(champ.id, 2); | |
} | |
champ.eq_shield = _itemId; //put on shield | |
} | |
if(item.itemType == 3){ | |
//take off actual helmet | |
if(champ.eq_helmet > 0){ | |
takeOffItem(champ.id, 3); | |
} | |
champ.eq_helmet = _itemId; //put on helmet | |
} | |
} | |
/// @notice Opens loot box and generates new item | |
function openLootbox(address _affiliateAddress) external payable | |
whenNotPaused | |
isPaid(lootboxFee) | |
distributeInput(_affiliateAddress) { | |
uint256 pointToCooldownReduction; | |
uint256 randNum = randMod(1001); //random number <= 1000 | |
uint256 pointsToShare; //total points given | |
uint256 itemID; | |
//sets up item | |
Item memory item = Item({ | |
itemType: uint8(uint256(randMod(3) + 1)), //generates item type - max num is 2 -> 0 + 1 SWORD | 1 + 1 SHIELD | 2 + 1 HELMET; | |
itemRarity: uint8(0), | |
attackPower: 0, | |
defencePower: 0, | |
cooldownReduction: 0, | |
price: 0, | |
onChampId: 0, | |
onChamp: false, | |
forSale: false | |
}); | |
// Gets Rarity of item | |
// 45% common | |
// 27% uncommon | |
// 19% rare | |
// 7% epic | |
// 2% legendary | |
if(450 > randNum){ | |
pointsToShare = 25 + randMod(9); //25 basic + random number max to 8 | |
item.itemRarity = uint8(1); | |
}else if(720 > randNum){ | |
pointsToShare = 42 + randMod(17); //42 basic + random number max to 16 | |
item.itemRarity = uint8(2); | |
}else if(910 > randNum){ | |
pointsToShare = 71 + randMod(25); //71 basic + random number max to 24 | |
item.itemRarity = uint8(3); | |
}else if(980 > randNum){ | |
pointsToShare = 119 + randMod(33); //119 basic + random number max to 32 | |
item.itemRarity = uint8(4); | |
}else{ | |
pointsToShare = 235 + randMod(41); //235 basic + random number max to 40 | |
item.itemRarity = uint8(5); | |
} | |
//Gets type of item | |
if(item.itemType == uint8(1)){ //ITEM IS SWORDS | |
item.attackPower = pointsToShare / 10 * 7; //70% attackPower | |
pointsToShare -= item.attackPower; //points left; | |
item.defencePower = pointsToShare / 10 * randMod(6); //up to 15% defencePower | |
pointsToShare -= item.defencePower; //points left; | |
item.cooldownReduction = pointsToShare * uint256(1 minutes); //rest of points is cooldown reduction | |
item.itemType = uint8(1); | |
} | |
if(item.itemType == uint8(2)){ //ITEM IS SHIELD | |
item.defencePower = pointsToShare / 10 * 7; //70% defencePower | |
pointsToShare -= item.defencePower; //points left; | |
item.attackPower = pointsToShare / 10 * randMod(6); //up to 15% attackPowerPower | |
pointsToShare -= item.attackPower; //points left; | |
item.cooldownReduction = pointsToShare * uint256(1 minutes); //rest of points is cooldown reduction | |
item.itemType = uint8(2); | |
} | |
if(item.itemType == uint8(3)){ //ITEM IS HELMET | |
pointToCooldownReduction = pointsToShare / 10 * 7; //70% cooldown reduction | |
item.cooldownReduction = pointToCooldownReduction * uint256(1 minutes); //points to time | |
pointsToShare -= pointToCooldownReduction; //points left; | |
item.attackPower = pointsToShare / 10 * randMod(6); //up to 15% attackPower | |
pointsToShare -= item.attackPower; //points left; | |
item.defencePower = pointsToShare; //rest of points is defencePower | |
item.itemType = uint8(3); | |
} | |
itemID = items.push(item) - 1; | |
itemToOwner[itemID] = msg.sender; //sets owner of this item - msg.sender | |
addressInfo[msg.sender].itemsCount++; //every address has count of items | |
emit NewItem(itemID, msg.sender); | |
} | |
/// @notice Change "lootboxFee". | |
/// @param _fee New "lootboxFee" | |
/// @dev Only owner of contract can change "lootboxFee" | |
function setLootboxFee(uint _fee) external onlyOwner { | |
lootboxFee = _fee; | |
} | |
} | |
/// @title Moderates buying and selling items | |
contract ItemMarket is Items { | |
event TransferItem(address from, address to, uint256 itemID); | |
/* | |
* Modifiers | |
*/ | |
///@notice Checks if item is for sale | |
modifier itemIsForSale(uint256 _id){ | |
require(items[_id].forSale); | |
_; | |
} | |
///@notice Checks if item is NOT for sale | |
modifier itemIsNotForSale(uint256 _id){ | |
require(items[_id].forSale == false); | |
_; | |
} | |
///@notice If item is for sale then cancel sale | |
modifier ifItemForSaleThenCancelSale(uint256 _itemID){ | |
Item storage item = items[_itemID]; | |
if(item.forSale){ | |
_cancelItemSale(item); | |
} | |
_; | |
} | |
///@notice Distribute sale eth input | |
modifier distributeSaleInput(address _owner) { | |
uint256 contractOwnerCommision; //1% | |
uint256 playerShare; //99% | |
if(msg.value > 100){ | |
contractOwnerCommision = (msg.value / 100); | |
playerShare = msg.value - contractOwnerCommision; | |
}else{ | |
contractOwnerCommision = 0; | |
playerShare = msg.value; | |
} | |
addressInfo[_owner].withdrawal += playerShare; | |
addressInfo[contractOwner].withdrawal += contractOwnerCommision; | |
pendingWithdrawal += playerShare + contractOwnerCommision; | |
_; | |
} | |
/* | |
* View | |
*/ | |
function getItemsForSale() view external returns(uint256[]){ | |
uint256[] memory result = new uint256[](itemsForSaleCount); | |
if(itemsForSaleCount > 0){ | |
uint256 counter = 0; | |
for (uint256 i = 0; i < items.length; i++) { | |
if (items[i].forSale == true) { | |
result[counter]=i; | |
counter++; | |
} | |
} | |
} | |
return result; | |
} | |
/* | |
* Private | |
*/ | |
///@notice Cancel sale. Should not be called without checking if item is really for sale. | |
function _cancelItemSale(Item storage item) private { | |
//No need to overwrite item's price | |
item.forSale = false; | |
itemsForSaleCount--; | |
} | |
/* | |
* Internal | |
*/ | |
/// @notice Transfer item | |
function transferItem(address _from, address _to, uint256 _itemID) internal | |
ifItemForSaleThenCancelSale(_itemID) { | |
Item storage item = items[_itemID]; | |
//take off | |
if(item.onChamp && _to != champToOwner[item.onChampId]){ | |
takeOffItem(item.onChampId, item.itemType); | |
} | |
addressInfo[_to].itemsCount++; | |
addressInfo[_from].itemsCount--; | |
itemToOwner[_itemID] = _to; | |
emit TransferItem(_from, _to, _itemID); | |
} | |
/* | |
* Public | |
*/ | |
/// @notice Calls transfer item | |
/// @notice Address _from is msg.sender. Cannot be used is market, bc msg.sender is buyer | |
function giveItem(address _to, uint256 _itemID) public | |
onlyOwnerOfItem(_itemID) { | |
transferItem(msg.sender, _to, _itemID); | |
} | |
/// @notice Calcels item's sale | |
function cancelItemSale(uint256 _id) public | |
itemIsForSale(_id) | |
onlyOwnerOfItem(_id){ | |
Item storage item = items[_id]; | |
_cancelItemSale(item); | |
} | |
/* | |
* External | |
*/ | |
/// @notice Sets item for sale | |
function setItemForSale(uint256 _id, uint256 _price) external | |
onlyOwnerOfItem(_id) | |
itemIsNotForSale(_id) { | |
Item storage item = items[_id]; | |
item.forSale = true; | |
item.price = _price; | |
itemsForSaleCount++; | |
} | |
/// @notice Buys item | |
function buyItem(uint256 _id) external payable | |
whenNotPaused | |
onlyNotOwnerOfItem(_id) | |
itemIsForSale(_id) | |
isPaid(items[_id].price) | |
distributeSaleInput(itemToOwner[_id]) | |
{ | |
transferItem(itemToOwner[_id], msg.sender, _id); | |
} | |
} | |
/// @title Manages forging | |
contract ItemForge is ItemMarket { | |
event Forge(uint256 forgedItemID); | |
///@notice Forge items together | |
function forgeItems(uint256 _parentItemID, uint256 _childItemID) external | |
onlyOwnerOfItem(_parentItemID) | |
onlyOwnerOfItem(_childItemID) | |
ifItemForSaleThenCancelSale(_parentItemID) | |
ifItemForSaleThenCancelSale(_childItemID) { | |
//checks if items are not the same | |
require(_parentItemID != _childItemID); | |
Item storage parentItem = items[_parentItemID]; | |
Item storage childItem = items[_childItemID]; | |
//take child item off, because child item will be burned | |
if(childItem.onChamp){ | |
takeOffItem(childItem.onChampId, childItem.itemType); | |
} | |
//update parent item | |
parentItem.attackPower = (parentItem.attackPower > childItem.attackPower) ? parentItem.attackPower : childItem.attackPower; | |
parentItem.defencePower = (parentItem.defencePower > childItem.defencePower) ? parentItem.defencePower : childItem.defencePower; | |
parentItem.cooldownReduction = (parentItem.cooldownReduction > childItem.cooldownReduction) ? parentItem.cooldownReduction : childItem.cooldownReduction; | |
parentItem.itemRarity = uint8(6); | |
//burn child item | |
transferItem(msg.sender, address(0), _childItemID); | |
emit Forge(_parentItemID); | |
} | |
} | |
/// @title Manages attacks in game | |
contract ChampAttack is ItemForge { | |
event Attack(uint256 winnerChampID, uint256 defeatedChampID, bool didAttackerWin); | |
/* | |
* Modifiers | |
*/ | |
/// @notice Is champ ready to fight again? | |
modifier isChampReady(uint256 _champId) { | |
require (champs[_champId].readyTime <= block.timestamp); | |
_; | |
} | |
/// @notice Prevents from self-attack | |
modifier notSelfAttack(uint256 _champId, uint256 _targetId) { | |
require(_champId != _targetId); | |
_; | |
} | |
/// @notice Checks if champ does exist | |
modifier targetExists(uint256 _targetId){ | |
require(champToOwner[_targetId] != address(0)); | |
_; | |
} | |
/* | |
* View | |
*/ | |
/// @notice Gets champ's attack power, defence power and cooldown reduction with items on | |
function getChampStats(uint256 _champId) public view returns(uint256,uint256,uint256){ | |
Champ storage champ = champs[_champId]; | |
Item storage sword = items[champ.eq_sword]; | |
Item storage shield = items[champ.eq_shield]; | |
Item storage helmet = items[champ.eq_helmet]; | |
//AP | |
uint256 totalAttackPower = champ.attackPower + sword.attackPower + shield.attackPower + helmet.attackPower; //Gets champs AP | |
//DP | |
uint256 totalDefencePower = champ.defencePower + sword.defencePower + shield.defencePower + helmet.defencePower; //Gets champs DP | |
//CR | |
uint256 totalCooldownReduction = sword.cooldownReduction + shield.cooldownReduction + helmet.cooldownReduction; //Gets CR | |
return (totalAttackPower, totalDefencePower, totalCooldownReduction); | |
} | |
/* | |
* Pure | |
*/ | |
/// @notice Subtracts ability points. Helps to not cross minimal attack ability points -> 2 | |
/// @param _playerAttackPoints Actual player's attack points | |
/// @param _x Amount to subtract | |
function subAttack(uint256 _playerAttackPoints, uint256 _x) internal pure returns (uint256) { | |
return (_playerAttackPoints <= _x + 2) ? 2 : _playerAttackPoints - _x; | |
} | |
/// @notice Subtracts ability points. Helps to not cross minimal defence ability points -> 1 | |
/// @param _playerDefencePoints Actual player's defence points | |
/// @param _x Amount to subtract | |
function subDefence(uint256 _playerDefencePoints, uint256 _x) internal pure returns (uint256) { | |
return (_playerDefencePoints <= _x) ? 1 : _playerDefencePoints - _x; | |
} | |
/* | |
* Private | |
*/ | |
/// @dev Is called from from Attack function after the winner is already chosen | |
/// @dev Updates abilities, champ's stats and swaps positions | |
function _attackCompleted(Champ storage _winnerChamp, Champ storage _defeatedChamp, uint256 _pointsGiven, uint256 _pointsToAttackPower) private { | |
/* | |
* Updates abilities after fight | |
*/ | |
//winner abilities update | |
_winnerChamp.attackPower += _pointsToAttackPower; //increase attack power | |
_winnerChamp.defencePower += _pointsGiven - _pointsToAttackPower; //max point that was given - already given to AP | |
//defeated champ's abilities update | |
//checks for not cross minimal AP & DP points | |
_defeatedChamp.attackPower = subAttack(_defeatedChamp.attackPower, _pointsToAttackPower); //decrease attack power | |
_defeatedChamp.defencePower = subDefence(_defeatedChamp.defencePower, _pointsGiven - _pointsToAttackPower); // decrease defence power | |
/* | |
* Update champs' wins and losses | |
*/ | |
_winnerChamp.winCount++; | |
_defeatedChamp.lossCount++; | |
/* | |
* Swap positions | |
*/ | |
if(_winnerChamp.position > _defeatedChamp.position) { //require loser to has better (lower) postion than attacker | |
uint256 winnerPosition = _winnerChamp.position; | |
uint256 loserPosition = _defeatedChamp.position; | |
_defeatedChamp.position = winnerPosition; | |
_winnerChamp.position = loserPosition; | |
//position in champ struct is always one point bigger than in leaderboard array | |
leaderboard[winnerPosition - 1] = _defeatedChamp.id; | |
leaderboard[loserPosition - 1] = _winnerChamp.id; | |
} | |
} | |
/// @dev Gets pointsGiven and pointsToAttackPower | |
function _getPoints(uint256 _pointsGiven) private returns (uint256 pointsGiven, uint256 pointsToAttackPower){ | |
return (_pointsGiven, randMod(_pointsGiven+1)); | |
} | |
/* | |
* External | |
*/ | |
/// @notice Attack function | |
/// @param _champId Attacker champ | |
/// @param _targetId Target champ | |
function attack(uint256 _champId, uint256 _targetId) external | |
onlyOwnerOfChamp(_champId) | |
isChampReady(_champId) | |
notSelfAttack(_champId, _targetId) | |
targetExists(_targetId) { | |
Champ storage myChamp = champs[_champId]; | |
Champ storage enemyChamp = champs[_targetId]; | |
uint256 pointsGiven; //total points that will be divided between AP and DP | |
uint256 pointsToAttackPower; //part of points that will be added to attack power, the rest of points go to defence power | |
uint256 myChampAttackPower; | |
uint256 enemyChampDefencePower; | |
uint256 myChampCooldownReduction; | |
(myChampAttackPower,,myChampCooldownReduction) = getChampStats(_champId); | |
(,enemyChampDefencePower,) = getChampStats(_targetId); | |
//if attacker's AP is more than target's DP then attacker wins | |
if (myChampAttackPower > enemyChampDefencePower) { | |
//this should demotivate players from farming on way weaker champs than they are | |
//the bigger difference between AP & DP is, the reward is smaller | |
if(myChampAttackPower - enemyChampDefencePower < 5){ | |
//big experience - 3 ability points | |
(pointsGiven, pointsToAttackPower) = _getPoints(3); | |
}else if(myChampAttackPower - enemyChampDefencePower < 10){ | |
//medium experience - 2 ability points | |
(pointsGiven, pointsToAttackPower) = _getPoints(2); | |
}else{ | |
//small experience - 1 ability point to random ability (attack power or defence power) | |
(pointsGiven, pointsToAttackPower) = _getPoints(1); | |
} | |
_attackCompleted(myChamp, enemyChamp, pointsGiven, pointsToAttackPower); | |
emit Attack(myChamp.id, enemyChamp.id, true); | |
} else { | |
//1 ability point to random ability (attack power or defence power) | |
(pointsGiven, pointsToAttackPower) = _getPoints(1); | |
_attackCompleted(enemyChamp, myChamp, pointsGiven, pointsToAttackPower); | |
emit Attack(enemyChamp.id, myChamp.id, false); | |
} | |
//Trigger cooldown for attacker | |
myChamp.readyTime = uint256(block.timestamp + myChamp.cooldownTime - myChampCooldownReduction); | |
} | |
} | |
/// @title Moderates buying and selling champs | |
contract ChampMarket is ChampAttack { | |
event TransferChamp(address from, address to, uint256 champID); | |
/* | |
* Modifiers | |
*/ | |
///@notice Require champ to be sale | |
modifier champIsForSale(uint256 _id){ | |
require(champs[_id].forSale); | |
_; | |
} | |
///@notice Require champ NOT to be for sale | |
modifier champIsNotForSale(uint256 _id){ | |
require(champs[_id].forSale == false); | |
_; | |
} | |
///@notice If champ is for sale then cancel sale | |
modifier ifChampForSaleThenCancelSale(uint256 _champID){ | |
Champ storage champ = champs[_champID]; | |
if(champ.forSale){ | |
_cancelChampSale(champ); | |
} | |
_; | |
} | |
/* | |
* View | |
*/ | |
///@notice Gets all champs for sale | |
function getChampsForSale() view external returns(uint256[]){ | |
uint256[] memory result = new uint256[](champsForSaleCount); | |
if(champsForSaleCount > 0){ | |
uint256 counter = 0; | |
for (uint256 i = 0; i < champs.length; i++) { | |
if (champs[i].forSale == true) { | |
result[counter]=i; | |
counter++; | |
} | |
} | |
} | |
return result; | |
} | |
/* | |
* Private | |
*/ | |
///@dev Cancel sale. Should not be called without checking if champ is really for sale. | |
function _cancelChampSale(Champ storage champ) private { | |
//cancel champ's sale | |
//no need waste gas to overwrite his price. | |
champ.forSale = false; | |
champsForSaleCount--; | |
} | |
/* | |
* Internal | |
*/ | |
/// @notice Transfer champ | |
function transferChamp(address _from, address _to, uint256 _champId) internal ifChampForSaleThenCancelSale(_champId){ | |
Champ storage champ = champs[_champId]; | |
//transfer champ | |
addressInfo[_to].champsCount++; | |
addressInfo[_from].champsCount--; | |
champToOwner[_champId] = _to; | |
//transfer items | |
if(champ.eq_sword != 0) { transferItem(_from, _to, champ.eq_sword); } | |
if(champ.eq_shield != 0) { transferItem(_from, _to, champ.eq_shield); } | |
if(champ.eq_helmet != 0) { transferItem(_from, _to, champ.eq_helmet); } | |
emit TransferChamp(_from, _to, _champId); | |
} | |
/* | |
* Public | |
*/ | |
/// @notice Champ is no more for sale | |
function cancelChampSale(uint256 _id) public | |
champIsForSale(_id) | |
onlyOwnerOfChamp(_id) { | |
Champ storage champ = champs[_id]; | |
_cancelChampSale(champ); | |
} | |
/* | |
* External | |
*/ | |
/// @notice Gift champ | |
/// @dev Address _from is msg.sender | |
function giveChamp(address _to, uint256 _champId) external | |
onlyOwnerOfChamp(_champId) { | |
transferChamp(msg.sender, _to, _champId); | |
} | |
/// @notice Sets champ for sale | |
function setChampForSale(uint256 _id, uint256 _price) external | |
onlyOwnerOfChamp(_id) | |
champIsNotForSale(_id) { | |
Champ storage champ = champs[_id]; | |
champ.forSale = true; | |
champ.price = _price; | |
champsForSaleCount++; | |
} | |
/// @notice Buys champ | |
function buyChamp(uint256 _id) external payable | |
whenNotPaused | |
onlyNotOwnerOfChamp(_id) | |
champIsForSale(_id) | |
isPaid(champs[_id].price) | |
distributeSaleInput(champToOwner[_id]) { | |
transferChamp(champToOwner[_id], msg.sender, _id); | |
} | |
} | |
/// @title Only used for deploying all contracts | |
contract MyCryptoChampCore is ChampMarket { | |
/* | |
© Copyright 2018 - Patrik Mojzis | |
Redistributing and modifying is prohibited. | |
https://mycryptochamp.io/ | |
What is MyCryptoChamp? | |
- Blockchain game about upgrading champs by fighting, getting better items, | |
trading them and the best 800 champs are daily rewarded by real Ether. | |
Feel free to ask any questions | |
hello@mycryptochamp.io | |
*/ | |
} |