public CreatureStatus(Xml.Status xmlstatus, Creature target, Xml.Castable castable = null, Creature source = null, int duration = -1, int tickFrequency = -1, double intensity = 1.0) { Target = target; XmlStatus = xmlstatus; Start = DateTime.Now; Target = target; Source = source; Duration = duration == -1 ? xmlstatus.Duration : duration; Tick = tickFrequency == -1 ? xmlstatus.Tick : tickFrequency; Intensity = intensity; // Calculate damage/heal effects. Note that a castable MUST be passed here for a status // to have damage effects as the castable itself has fields we need to access // (intensity, etc) in order to do damage calculations. if (castable != null) { var start = CalculateNumericEffects(castable, xmlstatus.Effects.OnApply, source); var tick = CalculateNumericEffects(castable, xmlstatus.Effects.OnTick, source); var end = CalculateNumericEffects(castable, xmlstatus.Effects.OnRemove, source); var expire = CalculateNumericEffects(castable, xmlstatus.Effects.OnExpire, source); OnStartEffect = new SimpleStatusEffect(start.Heal, start.Damage); OnTickEffect = new SimpleStatusEffect(tick.Heal, tick.Damage); OnRemoveEffect = new SimpleStatusEffect(end.Heal, end.Damage); OnExpireEffect = new SimpleStatusEffect(expire.Heal, expire.Damage); BonusModifiers = NumberCruncher.CalculateStatusModifiers(castable, intensity, xmlstatus.Effects.OnApply.StatModifiers, source, target); } }
private (double Heal, DamageOutput Damage) CalculateNumericEffects(Xml.Castable castable, Xml.ModifierEffect effect, Creature source) { double heal = 0; var dmg = new DamageOutput(); if (effect == null) { return(heal, dmg); } if (effect.Heal != null) { heal = NumberCruncher.CalculateHeal(castable, effect, Target, source, Name); } if (effect.Damage != null) { dmg = NumberCruncher.CalculateDamage(castable, effect, Target, source, Name); } return(heal, dmg); }
public virtual bool UseCastable(Xml.Castable castObject, Creature target = null, Xml.SpawnCastable spawnCastable = null) { if (!Condition.CastingAllowed) { return(false); } if (this is User) { GameLog.UserActivityInfo($"UseCastable: {Name} begin casting {castObject.Name} on target: {target?.Name ?? "no target"} CastingAllowed: {Condition.CastingAllowed}"); } var damage = castObject.Effects.Damage; List <Creature> targets = new List <Creature>(); if (this is Monster) { if (spawnCastable != null) { damage = new Xml.CastableDamage { Simple = new Xml.SimpleQuantity { Min = (uint)spawnCastable.MinDmg, Max = (uint)spawnCastable.MaxDmg } }; castObject.Effects.Damage = damage; //set damage based on spawncastable settings. castObject.Element = spawnCastable.Element; //handle defined element without redoing a ton of code. } } targets = GetTargets(castObject, target); // Quick checks // If no targets and is not an assail, do nothing if (targets.Count() == 0 && castObject.IsAssail == false && string.IsNullOrEmpty(castObject.Script)) { GameLog.UserActivityInfo($"UseCastable: {Name}: no targets and not assail"); return(false); } // Is this a pvpable spell? If so, is pvp enabled? // We do these next steps to ensure effects are displayed uniformly and as fast as possible var deadMobs = new List <Creature>(); if (castObject.Effects?.Animations?.OnCast != null) { foreach (var tar in targets) { foreach (var user in tar.viewportUsers.ToList()) { GameLog.UserActivityInfo($"UseCastable: Sending {user.Name} effect for {Name}: {castObject.Effects.Animations.OnCast.Target.Id}"); user.SendEffect(tar.Id, castObject.Effects.Animations.OnCast.Target.Id, castObject.Effects.Animations.OnCast.Target.Speed); } } if (castObject.Effects?.Animations?.OnCast?.SpellEffect != null) { GameLog.UserActivityInfo($"UseCastable: Sending spelleffect for {Name}: {castObject.Effects.Animations.OnCast.SpellEffect.Id}"); Effect(castObject.Effects.Animations.OnCast.SpellEffect.Id, castObject.Effects.Animations.OnCast.SpellEffect.Speed); } } if (castObject.Effects?.Sound != null) { PlaySound(castObject.Effects.Sound.Id); } GameLog.UserActivityInfo($"UseCastable: {Name} casting {castObject.Name}, {targets.Count()} targets"); if (!string.IsNullOrEmpty(castObject.Script)) { // If a script is defined we fire it immediately, and let it handle targeting / etc if (Game.World.ScriptProcessor.TryGetScript(castObject.Script, out Script script)) { return(script.ExecuteFunction("OnUse", this)); } else { GameLog.UserActivityError($"UseCastable: {Name} casting {castObject.Name}: castable script {castObject.Script} missing"); return(false); } } if (targets.Count == 0) { GameLog.UserActivityError("{Name}: {castObject.Name}: hey fam no targets"); } foreach (var tar in targets) { if (castObject.Effects?.ScriptOverride == true) { // TODO: handle castables with scripting // DoStuff(); continue; } if (!castObject.Effects.Damage.IsEmpty) { Xml.Element attackElement; var damageOutput = NumberCruncher.CalculateDamage(castObject, tar, this); if (castObject.Element == Xml.Element.Random) { Random rnd = new Random(); var Elements = Enum.GetValues(typeof(Xml.Element)); attackElement = (Xml.Element)Elements.GetValue(rnd.Next(Elements.Length)); } else if (castObject.Element != Xml.Element.None) { attackElement = castObject.Element; } else { attackElement = (Stats.OffensiveElementOverride != Xml.Element.None ? Stats.OffensiveElementOverride : Stats.BaseOffensiveElement); } GameLog.UserActivityInfo($"UseCastable: {Name} casting {castObject.Name} - target: {tar.Name} damage: {damageOutput}, element {attackElement}"); tar.Damage(damageOutput.Amount, attackElement, damageOutput.Type, damageOutput.Flags, this, false); if (this is Monster) { if (tar is User) { (tar as User).SendSystemMessage($"{this.Name} attacks you with {castObject.Name}."); } } if (this is User) { if (Equipment.Weapon != null && !Equipment.Weapon.Undamageable) { Equipment.Weapon.Durability -= 1 / (Equipment.Weapon.MaximumDurability * ((100 - Stats.Ac) == 0 ? 1 : (100 - Stats.Ac))); } } if (tar.Stats.Hp <= 0) { deadMobs.Add(tar); } } // Note that we ignore castables with both damage and healing effects present - one or the other. // A future improvement might be to allow more complex effects. else if (!castObject.Effects.Heal.IsEmpty) { var healOutput = NumberCruncher.CalculateHeal(castObject, tar, this); tar.Heal(healOutput, this); if (this is User) { GameLog.UserActivityInfo($"UseCastable: {Name} casting {castObject.Name} - target: {tar.Name} healing: {healOutput}"); if (Equipment.Weapon != null && !Equipment.Weapon.Undamageable) { Equipment.Weapon.Durability -= 1 / (Equipment.Weapon.MaximumDurability * ((100 - Stats.Ac) == 0 ? 1 : (100 - Stats.Ac))); } } } // Handle statuses foreach (var status in castObject.Effects.Statuses.Add.Where(e => e.Value != null)) { Xml.Status applyStatus; if (World.WorldData.TryGetValue <Xml.Status>(status.Value.ToLower(), out applyStatus)) { var duration = status.Duration == 0 ? applyStatus.Duration : status.Duration; GameLog.UserActivityInfo($"UseCastable: {Name} casting {castObject.Name} - applying status {status.Value} - duration {duration}"); tar.ApplyStatus(new CreatureStatus(applyStatus, tar, castObject, this, duration)); } else { GameLog.UserActivityError($"UseCastable: {Name} casting {castObject.Name} - failed to add status {status.Value}, does not exist!"); } } foreach (var status in castObject.Effects.Statuses.Remove) { Xml.Status applyStatus; if (World.WorldData.TryGetValue <Xml.Status>(status.ToLower(), out applyStatus)) { GameLog.UserActivityError($"UseCastable: {Name} casting {castObject.Name} - removing status {status}"); tar.RemoveStatus(applyStatus.Icon); } else { GameLog.UserActivityError($"UseCastable: {Name} casting {castObject.Name} - failed to remove status {status}, does not exist!"); } } } // Now flood away foreach (var dead in deadMobs) { World.ControlMessageQueue.Add(new HybrasylControlMessage(ControlOpcodes.HandleDeath, dead)); } Condition.Casting = false; return(true); }
public virtual List <Creature> GetTargets(Xml.Castable castable, Creature target = null) { IEnumerable <Creature> actualTargets = new List <Creature>(); var intents = castable.Intents; List <VisibleObject> possibleTargets = new List <VisibleObject>(); Creature origin; foreach (var intent in intents) { if (intent.IsShapeless) { //GameLog.UserActivityInfo("GetTarget: Shapeless"); // No shapes specified. // If UseType=Target, exact clicked target. // If UseType=NoTarget Target=Self, caster. // If UseType=NoTarget Target=Group, *entire* group regardless of location on map. // Otherwise, no target. if (intent.UseType == Xml.SpellUseType.Target) { // Exact clicked target possibleTargets.Add(target); //GameLog.UserActivityInfo("GetTarget: exact clicked target"); } else if (intent.UseType == Xml.SpellUseType.NoTarget) { possibleTargets.Add(this); //GameLog.UserActivityInfo("GetTarget: notarget, self"); if (intent.Flags.Contains(Xml.IntentFlags.Group)) { // Add group members if (this is User uo) { if (uo.Group != null) { possibleTargets.AddRange(uo.Group.Members.Where(x => x.Connected)); } } } } else if (intent.UseType != Xml.SpellUseType.NoTarget) { GameLog.UserActivityWarning($"Unhandled intent type {intent.UseType}, ignoring"); } } if (intent.Map != null) { // add entire map //GameLog.UserActivityInfo("GetTarget: adding map targets"); possibleTargets.AddRange(Map.EntityTree.GetAllObjects().Where(e => e is Creature)); } if (intent.UseType == Xml.SpellUseType.NoTarget) { origin = this; //GameLog.UserActivityInfo($"GetTarget: origin is {this.Name} at {this.X}, {this.Y}"); } else { //GameLog.UserActivityInfo($"GetTarget: origin is {target.Name} at {target.X}, {target.Y}"); origin = target; } // Handle shapes foreach (var cross in intent.Cross) { // Process cross targets foreach (Xml.Direction direction in Enum.GetValues(typeof(Xml.Direction))) { //GameLog.UserActivityInfo($"GetTarget: cross, {direction}, origin {origin.Name}, radius {cross.Radius}"); possibleTargets.AddRange(origin.GetDirectionalTargets(direction, cross.Radius)); } // Add origin and let flags sort it out possibleTargets.Add(origin); } foreach (var line in intent.Line) { // Process line targets //GameLog.UserActivityInfo($"GetTarget: line, {line.Direction}, origin {origin.Name}, length {line.Length}"); possibleTargets.AddRange(origin.GetDirectionalTargets(origin.GetIntentDirection(line.Direction), line.Length)); // Similar to above, add origin possibleTargets.Add(origin); } foreach (var square in intent.Square) { // Process square targets var r = (square.Side - 1) / 2; var rect = new Rectangle(origin.X - r, origin.Y - r, square.Side, square.Side); //GameLog.UserActivityInfo($"GetTarget: square, {origin.X - r}, {origin.Y - r} - origin {origin.Name}, side length {square.Side}"); possibleTargets.AddRange(origin.Map.EntityTree.GetObjects(rect).Where(e => e is Creature)); } foreach (var tile in intent.Tile) { // Process tile targets, which can have either direction OR relative x/y if (tile.Direction == Xml.IntentDirection.None) { if (tile.RelativeX == 0 && tile.RelativeY == 0) { //GameLog.UserActivityInfo($"GetTarget: tile, origin {origin.Name}, RelativeX && RelativeY == 0, skipping"); continue; } else { //GameLog.UserActivityInfo($"GetTarget: tile, ({origin.X + tile.RelativeX}, {origin.Y + tile.RelativeY}, origin {origin.Name}"); possibleTargets.AddRange(origin.Map.GetTileContents(origin.X + tile.RelativeX, origin.Y + tile.RelativeY).Where(e => e is Creature)); } } else { //GameLog.UserActivityInfo($"GetTarget: tile, intent {tile.Direction}, direction {origin.GetIntentDirection(tile.Direction)}, origin {origin.Name}"); possibleTargets.Add(origin.GetDirectionalTarget(origin.GetIntentDirection(tile.Direction))); } } List <Creature> possible = intent.MaxTargets > 0 ? possibleTargets.Take(intent.MaxTargets).OfType <Creature>().ToList() : possibleTargets.OfType <Creature>().ToList(); if (possible != null && possible.Count > 0) { actualTargets = actualTargets.Concat(possible); } else { GameLog.UserActivityInfo("GetTarget: No targets found"); } // Remove all merchants // TODO: perhaps improve with a flag or extend in the future actualTargets = actualTargets.SkipWhile(e => e is Merchant); // Process intent flags var this_id = this.Id; if (this is Monster) { // No hostile flag: remove players if (!intent.Flags.Contains(Xml.IntentFlags.Hostile)) { actualTargets = actualTargets.SkipWhile(e => e is User); } // No friendly flag: remove monsters if (!intent.Flags.Contains(Xml.IntentFlags.Friendly)) { actualTargets = actualTargets.SkipWhile(e => e is Monster); } // Group / pvp: n/a } else if (this is User userobj) { // No PVP flag: remove PVP flagged players // No hostile flag: remove monsters // No friendly flag: remove non-PVP flagged players // No group flag: remove group members if (!intent.Flags.Contains(Xml.IntentFlags.Hostile)) { actualTargets = actualTargets.SkipWhile(e => e is Monster); } if (!intent.Flags.Contains(Xml.IntentFlags.Friendly)) { actualTargets = actualTargets.SkipWhile(e => e is User && !(e as User).Condition.PvpEnabled && e.Id != this_id); } if (!intent.Flags.Contains(Xml.IntentFlags.Pvp)) { actualTargets = actualTargets.SkipWhile(e => (e is User) && (e as User).Condition.PvpEnabled && e.Id != this_id); } if (!intent.Flags.Contains(Xml.IntentFlags.Group)) { // Remove group members if (userobj.Group != null) { actualTargets = actualTargets.SkipWhile(e => (e is User) && userobj.Group.Contains(e as User)); } } } // No Self flag: remove self if (!intent.Flags.Contains(Xml.IntentFlags.Self)) { //GameLog.UserActivityInfo($"Trying to remove self: my id is {this.Id} and actualtargets contains {String.Join(',',actualTargets.Select(e => e.Id).ToList())}"); actualTargets = actualTargets.Where(e => e.Id != this_id); //GameLog.UserActivityInfo($"did it happen :o - my id is {this.Id} and actualtargets contains {String.Join(',', actualTargets.Select(e => e.Id).ToList())}"); } } return(actualTargets.ToList()); }
public virtual List <Creature> GetTargets(Xml.Castable castable, Creature target = null) { List <Creature> actualTargets = new List <Creature>(); /* INTENT HANDLING FOR TARGETING * * This is particularly confusing so it is documented here. * UseType=Target Radius=0 Direction=None -> exact clicked target * UseType=Target Radius=0 Direction=!None -> invalid * UseType=Target Radius=>0 Direction=None -> rect centered on target * UseType=Target Radius>0 Direction=(anything but none) -> directional rect target based on click x/y * UseType=NoTarget Radius=0 Direction=None -> self (wings of protection, maybe custom spells / mentoring / lore / etc)? * UseType=NoTarget Radius>0 Direction=None -> rect from self in all directions * UseType=NoTarget Radius>0 Direction=!None -> rect from self in specific direction */ var intents = castable.Intents; foreach (var intent in intents) { var possibleTargets = new List <VisibleObject>(); if (intent.UseType == Xml.SpellUseType.NoTarget && intent.Target.Contains(Xml.IntentTarget.Group)) { // Targeting group members var user = this as User; if (user != null && user.Group != null) { possibleTargets.AddRange(user.Group.Members.Where(m => m.Map.Id == Map.Id && m.Distance(this) < intent.Radius)); } } else if (intent.UseType == Xml.SpellUseType.Target && intent.Radius == 0 && intent.Direction == Xml.IntentDirection.None) { // Targeting the exact clicked target if (target == null) { GameLog.Error($"GetTargets: {castable.Name} - intent was for exact clicked target but no target was passed?"); } else // If we're doing damage, ensure the target is attackable if (!castable.Effects.Damage.IsEmpty && target.Condition.IsAttackable) { possibleTargets.Add(target); } else if (castable.Effects.Damage.IsEmpty) { possibleTargets.Add(target); } } else if (intent.UseType == Xml.SpellUseType.NoTarget && intent.Radius == 0 && intent.Direction == Xml.IntentDirection.None) { // Targeting self - which, currently, is only allowed for non-damaging spells if (castable.Effects.Damage.IsEmpty) { possibleTargets.Add(this); } } else { // Area targeting, directional or otherwise Rectangle rect = new Rectangle(0, 0, 0, 0); byte X = this.X; byte Y = this.Y; // Handle area targeting with click target as the source if (intent.UseType == Xml.SpellUseType.Target) { X = target.X; Y = target.Y; } switch (intent.Direction) { case Xml.IntentDirection.Front: { switch (Direction) { case Xml.Direction.North: { //facing north, attack north rect = new Rectangle(X, Y - intent.Radius, 1, intent.Radius); } break; case Xml.Direction.South: { //facing south, attack south rect = new Rectangle(X, Y, 1, 1 + intent.Radius); } break; case Xml.Direction.East: { //facing east, attack east rect = new Rectangle(X, Y, 1 + intent.Radius, 1); } break; case Xml.Direction.West: { //facing west, attack west rect = new Rectangle(X - intent.Radius, Y, intent.Radius, 1); } break; } } break; case Xml.IntentDirection.Back: { switch (Direction) { case Xml.Direction.North: { //facing north, attack south rect = new Rectangle(X, Y, 1, 1 + intent.Radius); } break; case Xml.Direction.South: { //facing south, attack north rect = new Rectangle(X, Y - intent.Radius, 1, intent.Radius); } break; case Xml.Direction.East: { //facing east, attack west rect = new Rectangle(X - intent.Radius, Y, intent.Radius, 1); } break; case Xml.Direction.West: { //facing west, attack east rect = new Rectangle(X, Y, 1 + intent.Radius, 1); } break; } } break; case Xml.IntentDirection.Left: { switch (Direction) { case Xml.Direction.North: { //facing north, attack west rect = new Rectangle(X - intent.Radius, Y, intent.Radius, 1); } break; case Xml.Direction.South: { //facing south, attack east rect = new Rectangle(X, Y, 1 + intent.Radius, 1); } break; case Xml.Direction.East: { //facing east, attack north rect = new Rectangle(X, Y, 1, 1 + intent.Radius); } break; case Xml.Direction.West: { //facing west, attack south rect = new Rectangle(X, Y - intent.Radius, 1, intent.Radius); } break; } } break; case Xml.IntentDirection.Right: { switch (Direction) { case Xml.Direction.North: { //facing north, attack east rect = new Rectangle(X, Y, 1 + intent.Radius, 1); } break; case Xml.Direction.South: { //facing south, attack west rect = new Rectangle(X - intent.Radius, Y, intent.Radius, 1); } break; case Xml.Direction.East: { //facing east, attack south rect = new Rectangle(X, Y - intent.Radius, 1, intent.Radius); } break; case Xml.Direction.West: { //facing west, attack north rect = new Rectangle(X, Y, 1, 1 + intent.Radius); } break; } } break; case Xml.IntentDirection.Nearby: case Xml.IntentDirection.None: { //attack radius rect = new Rectangle(X - intent.Radius, Y - intent.Radius, Math.Max(intent.Radius, (byte)1) * 2, Math.Max(intent.Radius, (byte)1) * 2); } break; } GameLog.Info($"Rectangle: x: {X - intent.Radius} y: {Y - intent.Radius}, radius: {intent.Radius} - LOCATION: {rect.Location} TOP: {rect.Top}, BOTTOM: {rect.Bottom}, RIGHT: {rect.Right}, LEFT: {rect.Left}"); if (rect.IsEmpty) { continue; } possibleTargets.AddRange(Map.EntityTree.GetObjects(rect).Where(obj => obj is Creature && obj != this)); } // Remove merchants possibleTargets = possibleTargets.Where(e => !(e is Merchant)).ToList(); // Handle intent flags if (this is Monster) { // No hostile flag: remove users // No friendly flag: remove monsters // Group / pvp: do not apply here if (!intent.Target.Contains(Xml.IntentTarget.Friendly)) { possibleTargets = possibleTargets.Where(e => !(e is Monster)).ToList(); } if (!intent.Target.Contains(Xml.IntentTarget.Hostile)) { possibleTargets = possibleTargets.Where(e => !(e is User)).ToList(); } } else if (this is User && intent.UseType != Xml.SpellUseType.NoTarget) { var user = this as User; // No hostile flag: remove monsters // No friendly flag: remove users with pvp disabled // No pvp: remove // If we aren't targeting friendlies or pvp, remove all users entirely if (!intent.Target.Contains(Xml.IntentTarget.Pvp)) { possibleTargets = possibleTargets.Where(e => !(e is User && (e as Creature).Condition.PvpEnabled == true)).ToList(); } if (!intent.Target.Contains(Xml.IntentTarget.Friendly)) { possibleTargets = possibleTargets.Where(e => !(e is User && (e as Creature).Condition.PvpEnabled == false)).ToList(); } // If we aren't targeting hostiles, remove all monsters if (!intent.Target.Contains(Xml.IntentTarget.Hostile)) { possibleTargets = possibleTargets.Where(e => !(e is Monster)).ToList(); } } // Finally, add the targets to our list List <Creature> possible = intent.MaxTargets > 0 ? possibleTargets.Take(intent.MaxTargets).OfType <Creature>().ToList() : possibleTargets.OfType <Creature>().ToList(); if (possible != null && possible.Count > 0) { actualTargets.AddRange(possible); } else { GameLog.Info("No targets found"); } } return(actualTargets); }