/// <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); }
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); }
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); } }
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); } }); }
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); }
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); }