/// <summary> /// Initializes a new instance of the <see cref="AIBase"/> class. /// </summary> /// <param name="actor">Character for this AI module to control.</param> /// <exception cref="ArgumentNullException"><paramref name="actor" /> is <c>null</c>.</exception> protected AIBase(Character actor) { if (actor == null) throw new ArgumentNullException("actor"); _actor = actor; }
/// <summary> /// If the character is currently casting a skill, then attempts to cancel the skill. /// </summary> /// <returns>True if there was a skill being casted and it was successfully stopped; false if there was /// no skill being casted or the skill could not be canceled.</returns> public bool TryCancelCastingSkill() { if (!IsCastingSkill) return false; Debug.Assert(_currentCastingSkill != null); // Clear the casting status variables _currentCastingSkill = null; _castingSkillTarget = null; // Tell the caster that they stopped casting if (_character is INetworkSender) { using (var pw = ServerPacket.SkillStopCasting_ToUser()) { ((INetworkSender)_character).Send(pw, ServerMessageType.GUIUserStatus); } } // Tell everyone that the casting stopped using (var pw = ServerPacket.SkillStopCasting_ToMap(_character.MapEntityIndex)) { _character.Map.Send(pw, ServerMessageType.MapDynamicEntityProperty); } return true; }
/// <summary> /// Initializes a new instance of the <see cref="CharacterSPSynchronizer"/> class. /// </summary> /// <param name="character">The Character to synchronize the values of.</param> /// <exception cref="ArgumentNullException"><paramref name="character" /> is <c>null</c>.</exception> public CharacterSPSynchronizer(Character character) { if (character == null) throw new ArgumentNullException("character"); _character = character; _isUser = (_character is User); }
/// <summary> /// When overridden in the derived class, makes the <paramref name="user"/> Character use this skill. /// </summary> /// <param name="user">The Character that used this skill. Will never be null.</param> /// <param name="target">The optional Character that the skill was used on. Can be null if there was /// no targeted Character.</param> /// <returns>True if the skill was successfully used; otherwise false.</returns> protected override bool HandleUse(Character user, Character target) { if (target == null) target = user; var power = user.ModStats[StatType.Int] * 2 + 5; target.HP += power; return true; }
/// <summary> /// When overridden in the derived class, makes the <paramref name="user"/> Character use this skill. /// </summary> /// <param name="user">The Character that used this skill. Will never be null.</param> /// <param name="target">The optional Character that the skill was used on. Can be null if there was /// no targeted Character.</param> /// <returns>True if the skill was successfully used; otherwise false.</returns> protected override bool HandleUse(Character user, Character target) { if (target == null) target = user; int power = user.ModStats[StatType.Int]; var successful = target.StatusEffects.TryAdd(StatusEffectType.Strengthen, (ushort)power); return successful; }
/// <summary> /// Handles the real updating of the AI. /// </summary> protected override void DoUpdate() { var time = GetTime(); // Ensure the target is still valid, or enough time has elapsed to check for a better target if ((_target != null && !IsValidTarget(_target)) || (_lastTargetUpdateTime + _targetUpdateRate < time)) { _lastTargetUpdateTime = time; _target = GetClosestHostile(); } // Check if we have a target or not if (_target == null) UpdateNoTarget(); else UpdateWithTarget(); }
static Character GetTargetCharacter(Character user, MapEntityIndex? index) { if (!index.HasValue) return null; // Check for a valid user if (user == null || user.Map == null) return null; // Check for a valid target index var target = user.Map.GetDynamicEntity<Character>(index.Value); if (target == null || target.Map != user.Map) return null; // Check for a valid distance if (user.GetDistance(target) > GameData.MaxTargetDistance) return null; return target; }
/// <summary> /// Handles attacking with a melee weapon. /// </summary> /// <param name="weapon">The weapon to attack with. Cannot be null.</param> /// <param name="target">The target to attack. Can be null.</param> void AttackMelee(IItemTable weapon, Character target) { if (weapon == null) { Debug.Fail("Weapon should not be null..."); return; } OnAttacked(); if (Attacked != null) Attacked.Raise(this, EventArgs.Empty); // If melee and no target was defined, try to find one automatically if (target == null) { var hitArea = GameData.GetMeleeAttackArea(this, weapon.Range); target = Map.Spatial.Get<Character>(hitArea, x => x != this && x.IsAlive && Alliance.CanAttack(x.Alliance)); } // Display the attack var targetID = (target != null ? target.MapEntityIndex : (MapEntityIndex?)null); using (var charAttack = ServerPacket.CharAttack(MapEntityIndex, targetID, weapon.ActionDisplayID)) { Map.SendToArea(this, charAttack, ServerMessageType.MapEffect); } // Check that we managed to find a target if (target == null) return; // We found a target, so attack it AttackApplyReal(target); }
/// <summary> /// Initializes a new instance of the <see cref="TestAI"/> class. /// </summary> /// <param name="actor">Character for this AI module to control.</param> public TestAI(Character actor) : base(actor) { }
/// <summary> /// Handles attacking with a ranged weapon. /// </summary> /// <param name="weapon">The weapon to attack with. Cannot be null.</param> /// <param name="target">The target to attack. Can be null.</param> void AttackRanged(ItemEntity weapon, Character target) { if (weapon == null) { Debug.Fail("Weapon should not be null..."); return; } // We can't do anything with ranged attacks if no target is given if (target == null) { TrySend(GameMessage.CannotAttackNeedTarget, ServerMessageType.GUI); return; } Ray2D ray = new Ray2D(this, Position, target.Position, Map.Spatial); Vector2 rayCollideWall; // FUTURE: Use to create some sort of wasted ammo on a wall or something. e.g. Grenade item explodes on walls. bool hasHitWall = ray.Intersects<WallEntity>(out rayCollideWall); if (hasHitWall) { TrySend(GameMessage.CannotAttackNotInSight, ServerMessageType.GUI); return; } List<ISpatial> rayCollideCharacters; // Use IntersectsMany here if you want to damage all characters in the attack path bool hasHitCharacter = ray.Intersects<Character>(out rayCollideCharacters); if (hasHitCharacter) { var ammoUsed = false; // Check for the needed ammo switch (weapon.WeaponType) { case WeaponType.Projectile: // Grab projectile ammo out of the inventory first if possible to avoid having to constantly reload var invAmmo = Inventory.FirstOrDefault(x => weapon.CanStack(x.Value)); if (invAmmo.Value != null) Inventory.DecreaseItemAmount(invAmmo.Key); else weapon.Destroy(); ammoUsed = true; break; case WeaponType.Ranged: // By default, guns won't use ammo. But if you want to require guns to use ammo, you can do so here ammoUsed = true; break; } if (!ammoUsed) return; foreach (var character in rayCollideCharacters) { var c = character as Character; if (!Alliance.CanAttack(c.Alliance)) continue; // Attack using (var charAttack = ServerPacket.CharAttack(MapEntityIndex, c.MapEntityIndex, weapon.ActionDisplayID)) { Map.SendToArea(this, charAttack, ServerMessageType.MapEffect); } OnAttacked(); if (Attacked != null) Attacked.Raise(this, EventArgs.Empty); AttackApplyReal(c); } } }
/// <summary> /// Removes the explicit hostility towards a specific <see cref="Character"/>. Only applies to /// hostility set through <see cref="SetHostileTowards"/> and has no affect on hostility created through /// the <see cref="Alliance"/>. /// </summary> /// <param name="target">The <see cref="Character"/> to remove the explicit hostility on.</param> /// <returns>True if the explicit hostility towards the <paramref name="target"/> was removed; false if the /// hostility towards the <paramref name="target"/> is implicitly defined through the <see cref="Alliance"/> /// settings or there was no explicit hostility set on the <paramref name="target"/>.</returns> public bool RemoveHostileTowards(Character target) { if (target == null) { Debug.Fail("target parameter should not be null."); return false; } return _explicitHostiles.Remove(target); }
/// <summary> /// Gets if the actor is hostile towards the given <see cref="Character"/>. /// </summary> /// <param name="character">The character to check if the actor is hostile towards.</param> /// <returns>True if the actor is hostile towards the <paramref name="character"/>; otherwise false.</returns> public virtual bool IsHostileTowards(Character character) { var ret = Actor.Alliance.IsHostile(character.Alliance); if (!ret) { if (_explicitHostiles.ContainsKey(character)) ret = true; } return ret; }
/// <summary> /// Handles when a Character is added to the Map. This is an extension of EntityAdded that handles /// special stuff just for Characters. /// </summary> /// <param name="character">The Character that was added to the map.</param> /// <exception cref="TypeException">Unknown Character type - not a NPC or User...?</exception> void CharacterAdded(Character character) { // If the character was already on a map, so remove them from the old map if (character.Map != null && character.Map != this) { const string errmsg = "Character `{0}` [{1}] added to new map, but is already on a map!"; if (log.IsWarnEnabled) log.WarnFormat(errmsg, character, character.MapEntityIndex); Debug.Fail(string.Format(errmsg, character, character.MapEntityIndex)); character.Map.RemoveEntity(character); } // Added character is a User var user = character as User; if (user != null) { EventCounterManager.Map.Increment(ID, MapEventCounterType.UserAdded); Debug.Assert(!Users.Contains(user), string.Format("Users list already contains `{0}`!", user)); _users.Add(user); SendMapData(user); return; } // Added character is a NPC var npc = character as NPC; if (npc != null) { EventCounterManager.Map.Increment(ID, MapEventCounterType.NPCAdded); Debug.Assert(!NPCs.Contains(npc), string.Format("NPCs list already contains `{0}`!", npc)); _npcs.Add(npc); return; } // Unknown added character type - not actually an error, but it is likely an oversight throw new TypeException("Unknown Character type - not a NPC or User...?"); }
/// <summary> /// Initializes a new instance of the <see cref="CharacterSkillCaster"/> class. /// </summary> /// <param name="character">The character.</param> public CharacterSkillCaster(Character character) { _character = character; _cooldownManager = new SkillCooldownManager(); }
/// <summary> /// Calls a skill to be used, and applies the respective cooldown values. /// </summary> /// <param name="skill">The skill to be used.</param> /// <param name="target">The optional character to use the skill on. Can be null.</param> /// <param name="castedImmediately">True if this skill was casted immediately (that is, there was no delay before casting); /// false if the skill had a delay before casting.</param> void UseSkill(ISkill<SkillType, StatType, Character> skill, Character target, bool castedImmediately) { Debug.Assert(skill != null); Debug.Assert(castedImmediately || _currentCastingSkill == skill); Debug.Assert(castedImmediately || _castingSkillTarget == target); // Clear the casting status variables _currentCastingSkill = null; _castingSkillTarget = null; // Tell the caster that they stopped casting if (_character is INetworkSender) { using (var pw = ServerPacket.SkillStopCasting_ToUser()) { ((INetworkSender)_character).Send(pw, ServerMessageType.GUIUserStatus); } } // If the skill had a casting time, tell everyone that the casting stopped if (!castedImmediately) { using (var pw = ServerPacket.SkillStopCasting_ToMap(_character.MapEntityIndex)) { _character.Map.Send(pw, ServerMessageType.MapDynamicEntityProperty); } } // Actually use the skill var skillSuccessfullyUsed = skill.Use(_character, target); // If the skill was not used for whatever reason, then return if (!skillSuccessfullyUsed) return; // Only set the cooldown if it was successfully used _cooldownManager.SetCooldown(skill.CooldownGroup, skill.CooldownTime, _character.GetTime()); // Update the character's skill cooldown if (_character is INetworkSender) { using (var pw = ServerPacket.SkillSetGroupCooldown(skill.CooldownGroup, skill.CooldownTime)) { ((INetworkSender)_character).Send(pw, ServerMessageType.GUIUserStatus); } } // Notify the clients on the map that the character used the skill var targetEntityIndex = target != null ? (MapEntityIndex?)target.MapEntityIndex : null; using (var pw = ServerPacket.SkillUse(_character.MapEntityIndex, targetEntityIndex, skill.SkillType)) { _character.Map.Send(pw, ServerMessageType.MapDynamicEntityProperty); } }
/// <summary> /// Initializes a new instance of the <see cref="CharacterKillEventArgs"/> class. /// </summary> /// <param name="killer">The <see cref="Character"/> that killed the sender.</param> public CharacterKillEventArgs(Character killer) { _killer = killer; }
/// <summary> /// When overridden in the derived class, gets the MP cost of using this Skill. /// </summary> /// <param name="user">The Character using the skill. Will not be null.</param> /// <param name="target">The optional Character that the skill was used on. Can be null if there was /// no targeted Character.</param> /// <returns>The MP cost of using this Skill.</returns> public override int GetMPCost(Character user, Character target) { return 2; }
/// <summary> /// Gets the amount of damage for a normal attack. /// </summary> /// <param name="target">Character being attacked.</param> /// <returns>The amount of damage to inflict for a normal attack.</returns> /// <exception cref="ArgumentNullException"><paramref name="target" /> is <c>null</c>.</exception> public int GetAttackDamage(Character target) { if (target == null) throw new ArgumentNullException("target"); int minHit = ModStats[StatType.MinHit]; int maxHit = ModStats[StatType.MaxHit]; if (minHit > maxHit) maxHit = minHit; var damage = Rand.Next(minHit, maxHit); // Apply the defence, and ensure the damage is in a valid range int defence = target.ModStats[StatType.Defence]; damage -= defence / 2; if (damage < 1) damage = 1; return damage; }
/// <summary> /// Handles when a Character is removed from the Map. This is an extension of EntityRemoved that handles /// special stuff just for Characters. /// </summary> /// <param name="character">The Character that was removed from the Map.</param> void CharacterRemoved(Character character) { User user; NPC npc; if ((user = character as User) != null) _users.Remove(user); else if ((npc = character as NPC) != null) _npcs.Remove(npc); }
/// <summary> /// Tells the skill user to start using the given <paramref name="skill"/>. This is more of a suggestion than a /// request since the skill user does not actually have to start casting the skill. If this skill user decides /// to use the skill, the CurrentCastingSkill must be set to the <paramref name="skill"/>. /// </summary> /// <param name="skill">The skill to be used.</param> /// <param name="target">The optional character to use the skill on. Can be null.</param> /// <returns>True if the <paramref name="skill"/> started being casted; otherwise false. This does not indicate /// whether or not the skill was or will be successfully used, just that it was attempted to be used. Common /// times this will return false is if there is a skill already being casted, or if the skill that was /// attempted to be used still needs to cool down.</returns> public bool TryStartCastingSkill(ISkill<SkillType, StatType, Character> skill, Character target) { if (!_character.IsAlive) return false; // Check that the character knows the skill if (!_character.KnownSkills.Knows(skill.SkillType)) return false; // Don't interrupt a skill that the character is already casting if (IsCastingSkill) return false; // Check that the group is available for usage if (_cooldownManager.IsCoolingDown(skill.CooldownGroup, _character.GetTime())) return false; // Only allow immediate-usage skills when moving if (_character.Velocity != Vector2.Zero && skill.CastingTime > 0) return false; if (skill.CastingTime == 0) { // The skill to use has no usage delay, so use it immediately UseSkill(skill, target, true); } else { // The skill does have a delay, so queue it for usage _currentCastingSkill = skill; _castingSkillTarget = target; var castingTime = skill.CastingTime; _castingSkillUsageTime = _character.GetTime() + castingTime; // Tell the character the details about the skill they are casting if (_character is INetworkSender) { using (var pw = ServerPacket.SkillStartCasting_ToUser(skill.SkillType, castingTime)) { ((INetworkSender)_character).Send(pw, ServerMessageType.GUIUserStatus); } } // Tell the users on the map that the character is casting using (var pw = ServerPacket.SkillStartCasting_ToMap(_character.MapEntityIndex, skill.SkillType)) { _character.Map.Send(pw, ServerMessageType.MapDynamicEntityProperty); } } return true; }
/// <summary> /// Checks if the <paramref name="target"/> is valid. /// </summary> /// <param name="target">The target <see cref="Character"/>.</param> /// <returns>True if the <paramref name="target"/> is valid; otherwise false.</returns> protected virtual bool IsValidTarget(Character target) { if (target == null) return false; if (!target.IsAlive) return false; if (target.Map != Actor.Map) return false; if (target.IsDisposed) return false; return true; }
/// <summary> /// Checks if the User can start a dialog with the given <paramref name="npc"/>. /// </summary> /// <param name="npc">The NPC to start the dialog with.</param> /// <returns>True if the User can start a dialog with the given <paramref name="npc"/>; otherwise false.</returns> /// <exception cref="ArgumentNullException"><paramref name="npc" /> is <c>null</c>.</exception> bool CanStartChat(Character npc) { const string errmsg = "Cannot start chat between `{0}` and `{1}` - {2}."; // Check if a chat is already going if (IsChatting) { if (log.IsInfoEnabled) log.InfoFormat(errmsg, _user, npc, "Chat dialog already open"); return false; } // Check for valid states if (npc == null) throw new ArgumentNullException("npc"); if (!_user.IsAlive) { if (log.IsInfoEnabled) log.InfoFormat(errmsg, _user, npc, "User is dead"); return false; } if (!npc.IsAlive) { if (log.IsInfoEnabled) log.InfoFormat(errmsg, _user, npc, "NPC is dead"); return false; } if (!_user.IsOnGround) { if (log.IsInfoEnabled) log.InfoFormat(errmsg, _user, npc, "User is not on the ground"); return false; } if (_user.Map == null) { if (log.IsInfoEnabled) log.InfoFormat(errmsg, _user, npc, "User's map is null"); return false; } if (npc.ChatDialog == null) { if (log.IsInfoEnabled) log.InfoFormat(errmsg, _user, npc, "NPC has no chat dialog"); return false; } // Check for a valid distance if (_user.Map != npc.Map) { if (log.IsInfoEnabled) log.InfoFormat(errmsg, _user, npc, "Characters are on different maps"); return false; } if (_user.GetDistance(npc) > GameData.MaxNPCChatDistance) { if (log.IsInfoEnabled) log.InfoFormat(errmsg, _user, npc, "Too far away from the NPC to chat"); return false; } return true; }
/// <summary> /// Explicitly sets the AI to be hostile towards a specific <see cref="Character"/> for a set amount /// of time. Has no affect if the target <see cref="Character"/>'s <see cref="Alliance"/> already makes /// the <see cref="AIBase.Actor"/> hostile towards them, or if they cannot be attacked by the /// <see cref="AIBase.Actor"/>. /// </summary> /// <param name="target">The <see cref="Character"/> to set as hostile towards.</param> /// <param name="timeout">How long, in milliseconds, this hostility will last. It is recommended that /// this value remains relatively low (5-10 minutes max). Must be greater than 0.</param> /// <param name="increaseTimeOnly">If true, the given <paramref name="timeout"/> will not be /// used if the <paramref name="target"/> is already marked as being hostile towards, and the /// existing timeout is greater than the given <paramref name="timeout"/>. That is, the longer /// of the two timeouts will be used.</param> public void SetHostileTowards(Character target, TickCount timeout, bool increaseTimeOnly) { if (target == null) { Debug.Fail("target parameter should not be null."); return; } if (timeout < 0) { Debug.Fail("timeout parameter should be greater than 0."); return; } // Ignore characters that are already marked as hostile by the alliance, along with those // who cannot be attacked if (Actor.Alliance.IsHostile(target.Alliance) || !Actor.Alliance.CanAttack(target.Alliance)) return; // Get the absolute timeout time var timeoutTime = GetTime() + timeout; if (_explicitHostiles.ContainsKey(target)) { // Target already exists in the list, so update the time try { if (!increaseTimeOnly) _explicitHostiles[target] = timeoutTime; else _explicitHostiles[target] = Math.Max(timeoutTime, _explicitHostiles[target]); } catch (KeyNotFoundException ex) { const string errmsg = "Possible thread-safety issue in AI's ExplicitHostile. Exception: {0}"; if (log.IsErrorEnabled) log.ErrorFormat(errmsg, ex); Debug.Fail(string.Format(errmsg, ex)); // Retry SetHostileTowards(target, timeoutTime, increaseTimeOnly); } } else { // Target not in the list yet, so add them try { _explicitHostiles.Add(target, timeoutTime); } catch (ArgumentException ex) { const string errmsg = "Possible thread-safety issue in AI's ExplicitHostile. Exception: {0}"; if (log.IsErrorEnabled) log.ErrorFormat(errmsg, ex); Debug.Fail(string.Format(errmsg, ex)); // Retry SetHostileTowards(target, timeoutTime, increaseTimeOnly); } } }
/// <summary> /// Initializes a new instance of the <see cref="UserInventory"/> class. /// </summary> /// <param name="user">User that the inventory is for</param> public UserInventory(Character user) : base(user) { _inventoryUpdater = new UserInventoryUpdater(this); }
/// <summary> /// Gets if the actor can attack the given <see cref="Character"/>. /// </summary> /// <param name="character">The <see cref="Character"/> to check if the actor can attack.</param> /// <returns>True if the actor can attack the <paramref name="character"/>; otherwise false.</returns> public virtual bool CanAttack(Character character) { return Actor.Alliance.CanAttack(character.Alliance); }
/// <summary> /// Initializes a new instance of the <see cref="NPCInventory"/> class. /// </summary> /// <param name="npc">The NPC.</param> public NPCInventory(Character npc) : base(npc) { }
static bool TryGetMap(Character user, out Map map) { // Check for a valid user if (user == null) { const string errmsg = "user is null."; if (log.IsErrorEnabled) log.Error(errmsg); Debug.Fail(errmsg); map = null; return false; } // Get the map map = user.Map; if (map == null) { // Invalid map const string errorMsg = "Received UseWorld from user `{0}`, but their map is null."; if (log.IsWarnEnabled) log.WarnFormat(errorMsg, user); Debug.Fail(string.Format(errorMsg, user)); return false; } // Valid map return true; }
/// <summary> /// Initializes a new instance of the <see cref="NonPersistentCharacterStatusEffects"/> class. /// </summary> /// <param name="character">The <see cref="Character"/> that this collection belongs to.</param> public NonPersistentCharacterStatusEffects(Character character) : base(character) { }
/// <summary> /// Initializes a new instance of the <see cref="CharacterAttackEventArgs"/> class. /// </summary> /// <param name="attacked">The <see cref="Character"/> that was attacked.</param> /// <param name="damage">The amount of damage inflicted on the <paramref name="attacked"/> by /// the attacker.</param> public CharacterAttackEventArgs(Character attacked, int damage) { _attacked = attacked; _damage = damage; }
/// <summary> /// Performs the actual attacking of a specific character. This should only be called by /// <see cref="AttackMelee"/> or <see cref="AttackRanged"/>. /// </summary> /// <param name="target">Character to attack.</param> void AttackApplyReal(Character target) { // Get the damage var damage = GetAttackDamage(target); // Apply the damage to the target target.Damage(this, damage); // Raise attack events OnAttackedCharacter(target, damage); if (AttackedCharacter != null) AttackedCharacter.Raise(this, new CharacterAttackEventArgs(target, damage)); target.OnAttackedByCharacter(this, damage); if (target.AttackedByCharacter != null) target.AttackedByCharacter.Raise(this, new CharacterAttackEventArgs(target, damage)); }