Example #1
0
        /// <summary>
        /// Runs validation checks to ensure activator can use a perk feat.
        /// Activation will fail if any of the following are true:
        ///     - Target is invalid
        ///     - Activator is a ship
        ///     - Feat is not a perk feat
        ///     - Cooldown has not passed
        /// </summary>
        /// <param name="activator">The creature activating a perk feat.</param>
        /// <param name="target">The target of the perk feat.</param>
        /// <param name="featID">The ID number of the feat being used.</param>
        /// <returns>true if able to use perk feat on target, false otherwise.</returns>
        public static bool CanUsePerkFeat(NWCreature activator, NWObject target, int featID)
        {
            var perkFeat = DataService.PerkFeat.GetByFeatIDOrDefault(featID);

            // There's no matching feat in the DB for this ability. Exit early.
            if (perkFeat == null)
            {
                return(false);
            }

            // Retrieve the perk information.
            Data.Entity.Perk perk = DataService.Perk.GetByIDOrDefault(perkFeat.PerkID);

            // No perk could be found. Exit early.
            if (perk == null)
            {
                return(false);
            }

            // Check to see if we are a spaceship.  Spaceships can't use abilities...
            if (activator.GetLocalInt("IS_SHIP") > 0 || activator.GetLocalInt("IS_GUNNER") > 0)
            {
                activator.SendMessage("You cannot use that ability while piloting a ship.");
                return(false);
            }

            // Retrieve the perk-specific handler logic.
            var handler = PerkService.GetPerkHandler(perkFeat.PerkID);

            // Get the creature's perk level.
            int creaturePerkLevel = PerkService.GetCreaturePerkLevel(activator, perk.ID);

            // If player is disabling an existing stance, remove that effect.
            if (perk.ExecutionTypeID == PerkExecutionType.Stance)
            {
                // Can't process NPC stances at the moment. Need to do some more refactoring before this is possible.
                // todo: handle NPC stances.
                if (!activator.IsPlayer)
                {
                    return(false);
                }

                PCCustomEffect stanceEffect = DataService.PCCustomEffect.GetByStancePerkOrDefault(activator.GlobalID, perk.ID);

                if (stanceEffect != null)
                {
                    if (CustomEffectService.RemoveStance(activator))
                    {
                        return(false);
                    }
                }
            }

            // Check for a valid perk level.
            if (creaturePerkLevel <= 0)
            {
                activator.SendMessage("You do not meet the prerequisites to use this ability.");
                return(false);
            }

            // Verify that this hostile action meets PVP sanctuary restriction rules.
            if (handler.IsHostile() && target.IsPlayer)
            {
                if (!PVPSanctuaryService.IsPVPAttackAllowed(activator.Object, target.Object))
                {
                    return(false);
                }
            }

            // Activator and target must be in the same area and within line of sight.
            if (activator.Area.Resref != target.Area.Resref ||
                _.LineOfSightObject(activator.Object, target.Object) == FALSE)
            {
                activator.SendMessage("You cannot see your target.");
                return(false);
            }

            // Run this perk's specific checks on whether the activator may use this perk on the target.
            string canCast = handler.CanCastSpell(activator, target, perkFeat.PerkLevelUnlocked);

            if (!string.IsNullOrWhiteSpace(canCast))
            {
                activator.SendMessage(canCast);
                return(false);
            }

            // Calculate the FP cost to use this ability. Verify activator has sufficient FP.
            int fpCost    = handler.FPCost(activator, handler.FPCost(activator, perkFeat.BaseFPCost, perkFeat.PerkLevelUnlocked), perkFeat.PerkLevelUnlocked);
            int currentFP = GetCurrentFP(activator);

            if (currentFP < fpCost)
            {
                activator.SendMessage("You do not have enough FP. (Required: " + fpCost + ". You have: " + currentFP + ")");
                return(false);
            }

            // Verify activator isn't busy or dead.
            if (activator.IsBusy || activator.CurrentHP <= 0)
            {
                activator.SendMessage("You are too busy to activate that ability.");
                return(false);
            }

            // If we're executing a concentration ability, check and see if the activator currently has this ability
            // active. If it's active, then we immediately remove its effect and bail out.
            // Any other ability (including other concentration abilities) execute as normal.
            if (perk.ExecutionTypeID == PerkExecutionType.ConcentrationAbility)
            {
                // Retrieve the concentration effect for this creature.
                var concentrationEffect = GetActiveConcentrationEffect(activator);
                if ((int)concentrationEffect.Type == perk.ID)
                {
                    // It's active. Time to disable it.
                    EndConcentrationEffect(activator);
                    activator.SendMessage("Concentration ability '" + perk.Name + "' deactivated.");
                    SendAOEMessage(activator, activator.Name + " deactivates concentration ability '" + perk.Name + "'.");
                    return(false);
                }
            }

            // Retrieve the cooldown information and determine the unlock time.
            int?     cooldownCategoryID = handler.CooldownCategoryID(activator, perk.CooldownCategoryID, perkFeat.PerkLevelUnlocked);
            DateTime now            = DateTime.UtcNow;
            DateTime unlockDateTime = cooldownCategoryID == null ? now : GetAbilityCooldownUnlocked(activator, (int)cooldownCategoryID);

            // Check if we've passed the unlock date. Exit early if we have not.
            if (unlockDateTime > now)
            {
                string timeToWait = TimeService.GetTimeToWaitLongIntervals(now, unlockDateTime, false);
                activator.SendMessage("That ability can be used in " + timeToWait + ".");
                return(false);
            }

            // Passed all checks. Return true.
            return(true);
        }
Example #2
0
        private static void ActivateAbility(
            NWCreature activator,
            NWObject target,
            Data.Entity.Perk entity,
            IPerkHandler perkHandler,
            int pcPerkLevel,
            PerkExecutionType executionType,
            int spellTier)
        {
            string uuid = Guid.NewGuid().ToString();
            float  baseActivationTime = perkHandler.CastingTime(activator, (float)entity.BaseCastingTime, spellTier);
            float  activationTime     = baseActivationTime;
            int    vfxID       = -1;
            int    animationID = -1;

            if (baseActivationTime > 0f && activationTime < 1.0f)
            {
                activationTime = 1.0f;
            }

            // Force ability armor penalties
            float armorPenalty = 0.0f;

            if (executionType == PerkExecutionType.ForceAbility ||
                executionType == PerkExecutionType.ConcentrationAbility)
            {
                string penaltyMessage = string.Empty;
                foreach (var item in activator.EquippedItems)
                {
                    if (item.CustomItemType == CustomItemType.HeavyArmor)
                    {
                        armorPenalty   = 2;
                        penaltyMessage = "Heavy armor slows your force cooldown by 100%.";
                        break;
                    }
                    else if (item.CustomItemType == CustomItemType.LightArmor)
                    {
                        armorPenalty   = 1.25f;
                        penaltyMessage = "Light armor slows your force cooldown by 25%.";
                    }
                }

                // If there's an armor penalty, send a message to the player.
                if (armorPenalty > 0.0f)
                {
                    activator.SendMessage(penaltyMessage);
                }
            }

            // If player is in stealth mode, force them out of stealth mode.
            if (_.GetActionMode(activator.Object, ACTION_MODE_STEALTH) == 1)
            {
                _.SetActionMode(activator.Object, ACTION_MODE_STEALTH, 0);
            }

            // Make the player face their target.
            _.ClearAllActions();
            BiowarePosition.TurnToFaceObject(target, activator);

            // Force and Concentration Abilities will display a visual effect during the casting process.
            if (executionType == PerkExecutionType.ForceAbility ||
                executionType == PerkExecutionType.ConcentrationAbility)
            {
                vfxID       = VFX_DUR_IOUNSTONE_YELLOW;
                animationID = ANIMATION_LOOPING_CONJURE1;
            }

            if (executionType == PerkExecutionType.ConcentrationAbility)
            {
                activator.SetLocalObject("CONCENTRATION_TARGET", target);
            }

            // If a VFX ID has been specified, play that effect instead of the default one.
            if (vfxID > -1)
            {
                var vfx = _.EffectVisualEffect(vfxID);
                vfx = _.TagEffect(vfx, "ACTIVATION_VFX");
                _.ApplyEffectToObject(DURATION_TYPE_TEMPORARY, vfx, activator.Object, activationTime + 0.2f);
            }

            // If an animation has been specified, make the player play that animation now.
            // bypassing if perk is throw saber due to couldn't get the animation to work via db table edit
            if (animationID > -1 && entity.ID != (int)PerkType.ThrowSaber)
            {
                activator.AssignCommand(() => _.ActionPlayAnimation(animationID, 1.0f, activationTime - 0.1f));
            }

            // Mark player as busy. Busy players can't take other actions (crafting, harvesting, etc.)
            activator.IsBusy = true;

            // Non-players can't be interrupted via movement.
            if (!activator.IsPlayer)
            {
                // Begin the check for spell interruption. If the activator moves, the spell will be canceled.
                CheckForSpellInterruption(activator, uuid, activator.Position);
            }

            activator.SetLocalInt(uuid, (int)SpellStatusType.Started);

            // If there's a casting delay, display a timing bar on-screen.
            if (activationTime > 0)
            {
                NWNXPlayer.StartGuiTimingBar(activator, (int)activationTime, string.Empty);
            }

            // Run the FinishAbilityUse event at the end of the activation time.
            int perkID = entity.ID;

            var @event = new OnFinishAbilityUse(activator, uuid, perkID, target, pcPerkLevel, spellTier, armorPenalty);

            activator.DelayEvent(activationTime + 0.2f, @event);
        }
Example #3
0
        private void OnFinishAbilityUse(OnFinishAbilityUse data)
        {
            using (new Profiler(nameof(FinishAbilityUse)))
            {
                // These arguments are sent from the AbilityService's ActivateAbility method.
                NWCreature activator    = data.Activator;
                string     spellUUID    = data.SpellUUID;
                int        perkID       = data.PerkID;
                NWObject   target       = data.Target;
                int        pcPerkLevel  = data.PCPerkLevel;
                int        spellTier    = data.SpellTier;
                float      armorPenalty = data.ArmorPenalty;

                // Get the relevant perk information from the database.
                Data.Entity.Perk dbPerk = DataService.Perk.GetByID(perkID);

                // The execution type determines how the perk behaves and the rules surrounding it.
                PerkExecutionType executionType = dbPerk.ExecutionTypeID;

                // Get the class which handles this perk's behaviour.
                IPerkHandler perk = PerkService.GetPerkHandler(perkID);

                // Pull back cooldown information.
                int?cooldownID            = perk.CooldownCategoryID(activator, dbPerk.CooldownCategoryID, spellTier);
                CooldownCategory cooldown = cooldownID == null ? null : DataService.CooldownCategory.GetByIDOrDefault((int)cooldownID);

                // If the activator interrupted the spell or died, we can bail out early.
                if (activator.GetLocalInt(spellUUID) == (int)SpellStatusType.Interrupted || // Moved during casting
                    activator.CurrentHP < 0 || activator.IsDead)                            // Or is dead/dying
                {
                    activator.DeleteLocalInt(spellUUID);
                    return;
                }

                // Remove the temporary UUID which is tracking this spell cast.
                activator.DeleteLocalInt(spellUUID);

                // Force Abilities, Combat Abilities, Stances, and Concentration Abilities
                if (executionType == PerkExecutionType.ForceAbility ||
                    executionType == PerkExecutionType.CombatAbility ||
                    executionType == PerkExecutionType.Stance ||
                    executionType == PerkExecutionType.ConcentrationAbility)
                {
                    // Run the impact script.
                    perk.OnImpact(activator, target, pcPerkLevel, spellTier);

                    // If an animation is specified for this perk, play it now.
                    if (dbPerk.CastAnimationID != null && dbPerk.CastAnimationID > 0)
                    {
                        var animation = (Animation)dbPerk.CastAnimationID;
                        activator.AssignCommand(() => { _.ActionPlayAnimation(animation, 1f, 1f); });
                    }

                    // If the target is an NPC, assign enmity towards this creature for that NPC.
                    if (target.IsNPC)
                    {
                        AbilityService.ApplyEnmity(activator, target.Object, dbPerk);
                    }
                }

                // Adjust creature's current FP, if necessary.
                // Adjust FP only if spell cost > 0
                PerkFeat perkFeat = DataService.PerkFeat.GetByPerkIDAndLevelUnlocked(perkID, spellTier);
                int      fpCost   = perk.FPCost(activator, perkFeat.BaseFPCost, spellTier);

                if (fpCost > 0)
                {
                    int currentFP = AbilityService.GetCurrentFP(activator);
                    int maxFP     = AbilityService.GetMaxFP(activator);
                    currentFP -= fpCost;
                    AbilityService.SetCurrentFP(activator, currentFP);
                    activator.SendMessage(ColorTokenService.Custom("FP: " + currentFP + " / " + maxFP, 32, 223, 219));
                }

                // Notify activator of concentration ability change and also update it in the DB.
                if (executionType == PerkExecutionType.ConcentrationAbility)
                {
                    AbilityService.StartConcentrationEffect(activator, perkID, spellTier);
                    activator.SendMessage("Concentration ability activated: " + dbPerk.Name);

                    // The Skill Increase effect icon and name has been overwritten. Apply the effect to the player now.
                    // This doesn't do anything - it simply gives a visual cue that the player has an active concentration effect.
                    _.ApplyEffectToObject(DurationType.Permanent, _.EffectSkillIncrease(Skill.UseMagicDevice, 1), activator);
                }

                // Handle applying cooldowns, if necessary.
                if (cooldown != null)
                {
                    AbilityService.ApplyCooldown(activator, cooldown, perk, spellTier, armorPenalty);
                }

                // Mark the creature as no longer busy.
                activator.IsBusy = false;

                // Mark the spell cast as complete.
                activator.SetLocalInt(spellUUID, (int)SpellStatusType.Completed);
            }
        }
Example #4
0
        public void OnModuleUseFeat()
        {
            NWPlayer   pc     = Object.OBJECT_SELF;
            NWCreature target = _nwnxEvents.OnFeatUsed_GetTarget().Object;
            int        featID = _nwnxEvents.OnFeatUsed_GetFeatID();

            Data.Entity.Perk perk = _data.GetAll <Data.Entity.Perk>().SingleOrDefault(x => x.FeatID == featID);
            if (perk == null)
            {
                return;
            }

            App.ResolveByInterface <IPerk>("Perk." + perk.ScriptName, (perkAction) =>
            {
                if (perkAction == null)
                {
                    return;
                }

                Player playerEntity = _data.Get <Player>(pc.GlobalID);
                int pcPerkLevel     = _perk.GetPCPerkLevel(pc, perk.ID);

                // If player is disabling an existing stance, remove that effect.
                if (perk.ExecutionTypeID == (int)PerkExecutionType.Stance)
                {
                    PCCustomEffect stanceEffect = _data.GetAll <PCCustomEffect>().SingleOrDefault(x =>
                    {
                        var customEffect = _data.Get <Data.Entity.CustomEffect>(x.CustomEffectID);

                        return(x.PlayerID == pc.GlobalID &&
                               customEffect.CustomEffectCategoryID == (int)CustomEffectCategoryType.Stance);
                    });

                    if (stanceEffect != null && perk.ID == stanceEffect.StancePerkID)
                    {
                        if (_customEffect.RemoveStance(pc))
                        {
                            return;
                        }
                    }
                }

                if (pcPerkLevel <= 0)
                {
                    pc.SendMessage("You do not meet the prerequisites to use this ability.");
                    return;
                }

                if (perkAction.IsHostile() && target.IsPlayer)
                {
                    if (!_pvpSanctuary.IsPVPAttackAllowed(pc, target.Object))
                    {
                        return;
                    }
                }

                if (pc.Area.Resref != target.Area.Resref ||
                    _.LineOfSightObject(pc.Object, target.Object) == 0)
                {
                    pc.SendMessage("You cannot see your target.");
                    return;
                }

                if (!perkAction.CanCastSpell(pc, target))
                {
                    pc.SendMessage(perkAction.CannotCastSpellMessage(pc, target) ?? "That ability cannot be used at this time.");
                    return;
                }

                int fpCost = perkAction.FPCost(pc, perkAction.FPCost(pc, perk.BaseFPCost));
                if (playerEntity.CurrentFP < fpCost)
                {
                    pc.SendMessage("You do not have enough FP. (Required: " + fpCost + ". You have: " + playerEntity.CurrentFP + ")");
                    return;
                }

                if (pc.IsBusy || pc.CurrentHP <= 0)
                {
                    pc.SendMessage("You are too busy to activate that ability.");
                    return;
                }

                // Check cooldown
                PCCooldown pcCooldown = _data.GetAll <PCCooldown>().SingleOrDefault(x => x.PlayerID == pc.GlobalID && x.CooldownCategoryID == perk.CooldownCategoryID);
                if (pcCooldown == null)
                {
                    pcCooldown = new PCCooldown
                    {
                        CooldownCategoryID = Convert.ToInt32(perk.CooldownCategoryID),
                        DateUnlocked       = DateTime.UtcNow.AddSeconds(-1),
                        PlayerID           = pc.GlobalID
                    };

                    _data.SubmitDataChange(pcCooldown, DatabaseActionType.Insert);
                }

                DateTime unlockDateTime = pcCooldown.DateUnlocked;
                DateTime now            = DateTime.UtcNow;

                if (unlockDateTime > now)
                {
                    string timeToWait = _time.GetTimeToWaitLongIntervals(now, unlockDateTime, false);
                    pc.SendMessage("That ability can be used in " + timeToWait + ".");
                    return;
                }

                // Force Abilities (aka Spells)
                if (perk.ExecutionTypeID == (int)PerkExecutionType.ForceAbility)
                {
                    ActivateAbility(pc, target, perk, perkAction, pcPerkLevel, PerkExecutionType.ForceAbility);
                }
                // Combat Abilities
                else if (perk.ExecutionTypeID == (int)PerkExecutionType.CombatAbility)
                {
                    ActivateAbility(pc, target, perk, perkAction, pcPerkLevel, PerkExecutionType.CombatAbility);
                }
                // Queued Weapon Skills
                else if (perk.ExecutionTypeID == (int)PerkExecutionType.QueuedWeaponSkill)
                {
                    HandleQueueWeaponSkill(pc, perk, perkAction);
                }
                // Stances
                else if (perk.ExecutionTypeID == (int)PerkExecutionType.Stance)
                {
                    ActivateAbility(pc, target, perk, perkAction, pcPerkLevel, PerkExecutionType.Stance);
                }
            });
        }
Example #5
0
        private void ActivateAbility(NWPlayer pc,
                                     NWObject target,
                                     Data.Entity.Perk entity,
                                     IPerk perk,
                                     int pcPerkLevel,
                                     PerkExecutionType executionType)
        {
            string uuid               = Guid.NewGuid().ToString();
            var    effectiveStats     = _playerStat.GetPlayerItemEffectiveStats(pc);
            int    itemBonus          = effectiveStats.CastingSpeed;
            float  baseActivationTime = perk.CastingTime(pc, (float)entity.BaseCastingTime);
            float  activationTime     = baseActivationTime;
            int    vfxID              = -1;
            int    animationID        = -1;

            // Activation Bonus % - Shorten activation time.
            if (itemBonus < 0)
            {
                float activationBonus = Math.Abs(itemBonus) * 0.01f;
                activationTime = activationTime - activationTime * activationBonus;
            }
            // Activation Penalty % - Increase activation time.
            else if (itemBonus > 0)
            {
                float activationPenalty = Math.Abs(itemBonus) * 0.01f;
                activationTime = activationTime + activationTime * activationPenalty;
            }

            if (baseActivationTime > 0f && activationTime < 0.5f)
            {
                activationTime = 0.5f;
            }

            // Force ability armor penalties
            if (executionType == PerkExecutionType.ForceAbility)
            {
                float  armorPenalty   = 0.0f;
                string penaltyMessage = string.Empty;
                foreach (var item in pc.EquippedItems)
                {
                    if (item.CustomItemType == CustomItemType.HeavyArmor)
                    {
                        armorPenalty   = 2;
                        penaltyMessage = "Heavy armor slows your force activation speed by 100%.";
                        break;
                    }
                    else if (item.CustomItemType == CustomItemType.LightArmor)
                    {
                        armorPenalty   = 1.25f;
                        penaltyMessage = "Light armor slows your force activation speed by 25%.";
                    }
                }

                if (armorPenalty > 0.0f)
                {
                    activationTime = baseActivationTime * armorPenalty;
                    pc.SendMessage(penaltyMessage);
                }
            }

            if (_.GetActionMode(pc.Object, ACTION_MODE_STEALTH) == 1)
            {
                _.SetActionMode(pc.Object, ACTION_MODE_STEALTH, 0);
            }

            _.ClearAllActions();
            _biowarePosition.TurnToFaceObject(target, pc);

            if (executionType == PerkExecutionType.ForceAbility)
            {
                vfxID       = VFX_DUR_IOUNSTONE_YELLOW;
                animationID = ANIMATION_LOOPING_CONJURE1;
            }

            if (vfxID > -1)
            {
                var vfx = _.EffectVisualEffect(vfxID);
                vfx = _.TagEffect(vfx, "ACTIVATION_VFX");
                _.ApplyEffectToObject(DURATION_TYPE_TEMPORARY, vfx, pc.Object, activationTime + 0.2f);
            }

            if (animationID > -1)
            {
                pc.AssignCommand(() => _.ActionPlayAnimation(animationID, 1.0f, activationTime - 0.1f));
            }

            pc.IsBusy = true;
            CheckForSpellInterruption(pc, uuid, pc.Position);
            pc.SetLocalInt(uuid, (int)SpellStatusType.Started);

            _nwnxPlayer.StartGuiTimingBar(pc, (int)activationTime, "");

            int perkID = entity.ID;

            pc.DelayEvent <FinishAbilityUse>(
                activationTime + 0.2f,
                pc,
                uuid,
                perkID,
                target,
                pcPerkLevel);
        }
Example #6
0
        private void BuildPerkDetails()
        {
            Model vm = GetDialogCustomData <Model>();

            Data.Entity.Perk perk   = PerkService.GetPerkByID(vm.SelectedPerkID);
            PCPerk           pcPerk = PerkService.GetPCPerkByID(GetPC().GlobalID, perk.ID);
            Player           player = PlayerService.GetPlayerEntity(GetPC().GlobalID);
            var perkLevels          = DataService.Where <PerkLevel>(x => x.PerkID == perk.ID).ToList();

            int       rank                          = pcPerk?.PerkLevel ?? 0;
            int       maxRank                       = perkLevels.Count();
            string    currentBonus                  = "N/A";
            string    currentFPCost                 = string.Empty;
            string    currentConcentrationCost      = string.Empty;
            string    currentSpecializationRequired = "None";
            string    nextBonus                     = "N/A";
            string    nextFPCost                    = "N/A";
            string    nextConcentrationCost         = string.Empty;
            string    price                         = "N/A";
            string    nextSpecializationRequired    = "None";
            PerkLevel currentPerkLevel              = PerkService.FindPerkLevel(perkLevels, rank);
            PerkLevel nextPerkLevel                 = PerkService.FindPerkLevel(perkLevels, rank + 1);

            SetResponseVisible("PerkDetailsPage", 1, PerkService.CanPerkBeUpgraded(GetPC(), vm.SelectedPerkID));

            // Player has purchased at least one rank in this perk. Show their current bonuses.
            if (rank > 0 && currentPerkLevel != null)
            {
                var currentPerkFeat = DataService.SingleOrDefault <PerkFeat>(x => x.PerkID == vm.SelectedPerkID &&
                                                                             x.PerkLevelUnlocked == currentPerkLevel.Level);
                currentBonus = currentPerkLevel.Description;

                // Not every perk is going to have a perk feat. Don't display this information if not necessary.
                if (currentPerkFeat != null)
                {
                    currentFPCost = currentPerkFeat.BaseFPCost > 0 ? (ColorTokenService.Green("Current FP: ") + currentPerkFeat.BaseFPCost + "\n") : string.Empty;

                    // If this perk level has a concentration cost and interval, display it on the menu.
                    if (currentPerkFeat.ConcentrationFPCost > 0 && currentPerkFeat.ConcentrationTickInterval > 0)
                    {
                        currentConcentrationCost = ColorTokenService.Green("Current Concentration FP: ") + currentPerkFeat.ConcentrationFPCost + " / " + currentPerkFeat.ConcentrationTickInterval + "s\n";
                    }
                }

                // If this perk level has required specialization, change the text to that.
                if (currentPerkLevel.SpecializationID > 0)
                {
                    // Convert ID to enum, then get the string of the enum value. If we ever get a specialization with
                    // more than one word, another process will need to be used.
                    currentSpecializationRequired = ((SpecializationType)currentPerkLevel.SpecializationID).ToString();
                }
            }

            // Player hasn't reached max rank and this perk has another perk level to display.
            if (rank + 1 <= maxRank && nextPerkLevel != null)
            {
                var nextPerkFeat = DataService.SingleOrDefault <PerkFeat>(x => x.PerkID == vm.SelectedPerkID &&
                                                                          x.PerkLevelUnlocked == rank + 1);
                nextBonus = nextPerkLevel.Description;
                price     = nextPerkLevel.Price + " SP (Available: " + player.UnallocatedSP + " SP)";

                if (nextPerkFeat != null)
                {
                    nextFPCost = nextPerkFeat.BaseFPCost > 0 ? (ColorTokenService.Green("Next FP: ") + nextPerkFeat.BaseFPCost + "\n") : string.Empty;

                    // If this perk level has a concentration cost and interval, display it on the menu.
                    if (nextPerkFeat.ConcentrationFPCost > 0 && nextPerkFeat.ConcentrationTickInterval > 0)
                    {
                        nextConcentrationCost = ColorTokenService.Green("Next Concentration FP: ") + nextPerkFeat.ConcentrationFPCost + " / " + nextPerkFeat.ConcentrationTickInterval + "s\n";
                    }
                }

                if (nextPerkLevel.SpecializationID > 0)
                {
                    nextSpecializationRequired = ((SpecializationType)nextPerkLevel.SpecializationID).ToString();
                }
            }
            var perkCategory     = DataService.Get <PerkCategory>(perk.PerkCategoryID);
            var cooldownCategory = perk.CooldownCategoryID == null ?
                                   null :
                                   DataService.Get <CooldownCategory>(perk.CooldownCategoryID);

            string header = ColorTokenService.Green("Name: ") + perk.Name + "\n" +
                            ColorTokenService.Green("Category: ") + perkCategory.Name + "\n" +
                            ColorTokenService.Green("Rank: ") + rank + " / " + maxRank + "\n" +
                            ColorTokenService.Green("Price: ") + price + "\n" +
                            currentFPCost +
                            currentConcentrationCost +
                            (cooldownCategory != null && cooldownCategory.BaseCooldownTime > 0 ? ColorTokenService.Green("Cooldown: ") + cooldownCategory.BaseCooldownTime + "s" : "") + "\n" +
                            ColorTokenService.Green("Description: ") + perk.Description + "\n" +
                            ColorTokenService.Green("Current Bonus: ") + currentBonus + "\n" +
                            ColorTokenService.Green("Requires Specialization: ") + currentSpecializationRequired + "\n" +
                            nextFPCost +
                            nextConcentrationCost +
                            ColorTokenService.Green("Next Bonus: ") + nextBonus + "\n" +
                            ColorTokenService.Green("Requires Specialization: ") + nextSpecializationRequired + "\n";


            if (nextPerkLevel != null)
            {
                List <PerkLevelSkillRequirement> requirements =
                    DataService.Where <PerkLevelSkillRequirement>(x => x.PerkLevelID == nextPerkLevel.ID).ToList();
                if (requirements.Count > 0)
                {
                    header += "\n" + ColorTokenService.Green("Next Upgrade Skill Requirements:\n\n");

                    bool hasRequirement = false;
                    foreach (PerkLevelSkillRequirement req in requirements)
                    {
                        if (req.RequiredRank > 0)
                        {
                            PCSkill pcSkill = SkillService.GetPCSkill(GetPC(), req.SkillID);
                            Skill   skill   = SkillService.GetSkill(pcSkill.SkillID);

                            string detailLine = skill.Name + " Rank " + req.RequiredRank;

                            if (pcSkill.Rank >= req.RequiredRank)
                            {
                                header += ColorTokenService.Green(detailLine) + "\n";
                            }
                            else
                            {
                                header += ColorTokenService.Red(detailLine) + "\n";
                            }

                            hasRequirement = true;
                        }
                    }

                    if (requirements.Count <= 0 || !hasRequirement)
                    {
                        header += "None\n";
                    }
                }
            }

            SetPageHeader("PerkDetailsPage", header);
        }