contract DungeonChallenge is DungeonTransportation {

    /**
     * @dev The DungeonChallenged event is fired when user finished a dungeon challenge.
     */
    event DungeonChallenged(uint timestamp, address indexed playerAddress, uint indexed dungeonId, uint indexed heroId, uint heroGenes, uint floorNumber, uint floorGenes, bool success, uint newFloorGenes, uint successRewards, uint masterRewards);


    /* ======== GAME SETTINGS ======== */

    /**
     * @notice The actual fee contribution required to call challenge() is calculated by this feeMultiplier,
     *  times the dungeon difficulty. The payment is accumulated to the dungeon rewards,
     *  and a large proportion will be claimed by whoever successfully challenged the floor.
     *  1 finney = 0.001 ether
     */
    uint public challengeFeeMultiplier = 1 finney;

    /**
     * @dev The percentage for which successful challenger be rewarded of the dungeons' accumulated rewards.
     *  The remaining rewards subtract dungeon master rewards will be used as the base rewards for new floor.
     */
    uint public challengeRewardsPercent = 64;

    /**
     * @dev The developer fee for owner
     *  Note that when Ether Dungeon becomes truly decentralised, contract ownership will be ejected,
     *  and the master rewards will be rewarded to the dungeon owner (Dungeon Masters).
     */
    uint public masterRewardsPercent = 8;

    /**
     * @dev The cooldown time period where a hero can engage in challenge again.
     *  This settings will likely be changed to 20 minutes when multiple heroes system is launched in Version 1.
     */
    uint public challengeCooldownTime = 3 minutes;

    /**
     * @dev The preparation time period where a new dungeon is created, before it can be challenged.
     *  This settings will likely be changed to a smaller period (e.g. 20-30 minutes) .
     */
    uint public dungeonPreparationTime = 60 minutes;

    /**
     * @dev The challenge rewards percentage used right after the preparation period.
     */
    uint public rushTimeChallengeRewardsPercent = 30;

    /**
     * @dev The number of floor in which the rushTimeChallengeRewardsPercent be applied.
     */
    uint public rushTimeFloorCount = 30;

    /**
     * @dev The main external function to call when a player challenge a dungeon,
     *  it determines whether if the player successfully challenged the current floor.
     *  Will generate a DungeonChallenged event.
     */
    function challenge(uint _dungeonId, uint _heroId) whenNotPaused dungeonCanChallenge(_dungeonId) heroAllowedToChallenge(_heroId) external payable {
        // Get the dungeon details from the token contract.
        uint difficulty;
        uint seedGenes;
        (,, difficulty,,,,, seedGenes,) = dungeonTokenContract.dungeons(_dungeonId);

        // Checks for payment, any exceeding funds will be transferred back to the player.
        uint requiredFee = difficulty * challengeFeeMultiplier;
        require(msg.value >= requiredFee);

        // ** STORAGE UPDATE **
        // Increment the accumulated rewards for the dungeon.
        dungeonTokenContract.addDungeonRewards(_dungeonId, requiredFee);

        // Calculate any excess funds and make it available to be withdrawed by the player.
        asyncSend(msg.sender, msg.value - requiredFee);

        // Split the challenge function into multiple parts because of stack too deep error.
        _challengePart2(_dungeonId, _heroId);
    }

    /**
     * Split the challenge function into multiple parts because of stack too deep error.
     */
    function _challengePart2(uint _dungeonId, uint _heroId) private {
        uint floorNumber;
        uint rewards;
        uint floorGenes;
        (,,,, floorNumber,, rewards,, floorGenes) = dungeonTokenContract.dungeons(_dungeonId);

        // Get the hero gene, or claim first hero.
        uint heroGenes;
        (_heroId, heroGenes) = _getHeroGenesOrClaimFirstHero(_heroId);

        bool success = _getChallengeSuccess(heroGenes, _dungeonId, floorGenes);

        uint newFloorGenes;
        uint masterRewards;
        uint successRewards;
        uint newRewards;

        // Whether a challenge is success or not is determined by a simple comparison between hero power and floor power.
        if (success) {
            newFloorGenes = _getNewFloorGene(_dungeonId);

            masterRewards = rewards * masterRewardsPercent / 100;

            if (floorNumber < rushTimeFloorCount) { // rush time right after prepration period
                successRewards = rewards * rushTimeChallengeRewardsPercent / 100;

                // The dungeon rewards for new floor as total rewards - challenge rewards - devleoper fee.
                newRewards = rewards * (100 - rushTimeChallengeRewardsPercent - masterRewardsPercent) / 100;
            } else {
                successRewards = rewards * challengeRewardsPercent / 100;
                newRewards = rewards * (100 - challengeRewardsPercent - masterRewardsPercent) / 100;
            }

            // TRIPLE CONFIRM sanity check.
            require(successRewards + masterRewards + newRewards <= rewards);

            // ** STORAGE UPDATE **
            // Add new floor with the new floor genes and new rewards.
            dungeonTokenContract.addDungeonNewFloor(_dungeonId, newRewards, newFloorGenes);

            // Mark the challenge rewards available to be withdrawed by the player.
            asyncSend(msg.sender, successRewards);

            // Mark the master rewards available to be withdrawed by the dungeon master.
            asyncSend(dungeonTokenContract.ownerOf(_dungeonId), masterRewards);
        }

        // ** STORAGE UPDATE **
        // Trigger the cooldown for the hero.
        heroTokenContract.triggerCooldown(_heroId);

        // Emit the DungeonChallenged event.
        DungeonChallenged(now, msg.sender, _dungeonId, _heroId, heroGenes, floorNumber, floorGenes, success, newFloorGenes, successRewards, masterRewards);
    }

    /**
     * Split the challenge function into multiple parts because of stack too deep error.
     */
    function _getChallengeSuccess(uint _heroGenes, uint _dungeonId, uint _floorGenes) private view returns (bool) {
        // Determine if the player challenge successfuly the dungeon or not.
        uint heroPower = _getHeroPower(_heroGenes, _dungeonId);
        uint floorPower = _getDungeonPower(_floorGenes);

        return heroPower > floorPower;
    }

    /**
     * Split the challenge function into multiple parts because of stack too deep error.
     */
    function _getNewFloorGene(uint _dungeonId) private returns (uint) {
        uint seedGenes;
        uint floorGenes;
        (,,,,,, seedGenes, floorGenes) = dungeonTokenContract.dungeons(_dungeonId);

        // Calculate the new floor gene.
        uint floorPower = _getDungeonPower(floorGenes);

        // Call the external closed source secret function that determines the resulting floor "genes".
        uint newFloorGenes = challengeScienceContract.mixGenes(floorGenes, seedGenes);

        uint newFloorPower = _getDungeonPower(newFloorGenes);

        // If the power decreased, rollback to the current floor genes.
        if (newFloorPower < floorPower) {
            newFloorGenes = floorGenes;
        }

        return newFloorGenes;
    }


    /* ======== SETTER FUNCTIONS ======== */

    /**
     * @dev Updates the fee contribution multiplier required for calling challenge().
     */
    function setChallengeFeeMultiplier(uint _newChallengeFeeMultiplier) onlyOwner external {
        challengeFeeMultiplier = _newChallengeFeeMultiplier;
    }

    /**
     * @dev Updates the challenge rewards pecentage.
     */
    function setChallengeRewardsPercent(uint _newChallengeRewardsPercent) onlyOwner external {
        challengeRewardsPercent = _newChallengeRewardsPercent;
    }

    /**
     * @dev Updates the master rewards percentage.
     */
    function setMasterRewardsPercent(uint _newMasterRewardsPercent) onlyOwner external {
        masterRewardsPercent = _newMasterRewardsPercent;
    }

    /**
     * @dev Updates the challenge cooldown time.
     */
    function setChallengeCooldownTime(uint _newChallengeCooldownTime) onlyOwner external {
        challengeCooldownTime = _newChallengeCooldownTime;
    }

    /**
     * @dev Updates the challenge cooldown time.
     */
    function setDungeonPreparationTime(uint _newDungeonPreparationTime) onlyOwner external {
        dungeonPreparationTime = _newDungeonPreparationTime;
    }

    /**
     * @dev Updates the rush time challenge rewards percentage.
     */
    function setRushTimeChallengeRewardsPercent(uint _newRushTimeChallengeRewardsPercent) onlyOwner external {
        rushTimeChallengeRewardsPercent = _newRushTimeChallengeRewardsPercent;
    }

    /**
     * @dev Updates the rush time floor count.
     */
    function setRushTimeFloorCount(uint _newRushTimeFloorCount) onlyOwner external {
        rushTimeFloorCount = _newRushTimeFloorCount;
    }


    /* ======== MODIFIERS ======== */

    /**
     * @dev Throws if dungeon status do not allow challenge, also check for dungeon existence.
     *  Also check if the user is in the dungeon.
     *  Also check if the dungeon is not in preparation period.
     */
    modifier dungeonCanChallenge(uint _dungeonId) {
        require(_dungeonId < dungeonTokenContract.totalSupply());
        uint creationTime;
        uint status;
        (creationTime, status,,,,,,,) = dungeonTokenContract.dungeons(_dungeonId);
        require(status == 0 || status == 2);

        // Check if the user is in the dungeon.
        require(playerToDungeonID[msg.sender] == _dungeonId);

        // Check if the dungeon is not in preparation period.
        require(creationTime + dungeonPreparationTime <= now);
        _;
    }

    /**
     * @dev Throws if player does not own the hero, or it is still in cooldown.
     *  Unless the player does not have any hero yet, which will auto claim one during first challenge / train.
     */
    modifier heroAllowedToChallenge(uint _heroId) {
        if (heroTokenContract.balanceOf(msg.sender) > 0) {
            // You can only challenge with your own hero.
            require(heroTokenContract.ownerOf(_heroId) == msg.sender);

            uint cooldownStartTime;
            (, cooldownStartTime,,) = heroTokenContract.heroes(_heroId);
            require(cooldownStartTime + challengeCooldownTime <= now);
        }
        _;
    }

}
function setRushTimeFloorCount(uint _newRushTimeFloorCount) onlyOwner external {
    rushTimeFloorCount = _newRushTimeFloorCount;
}

/**
 * @dev Updates the rush time floor count.
 */