Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions Hybrasyl.Tests/Items.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
// For contributors and individual authors please refer to CONTRIBUTORS.MD.

using Hybrasyl.Internals.Enums;
using Hybrasyl.Objects;
using Hybrasyl.Subsystems.Formulas;
using Hybrasyl.Xml.Objects;
using System.Linq;
using Xunit;

namespace Hybrasyl.Tests;
Expand Down Expand Up @@ -192,4 +195,34 @@ public void UseItemBaseStats()
Assert.True(Fixture.TestUser.Stats.BaseManaSteal == 10,
$"ManaSteal: after item usage, should be 10, is {Fixture.TestUser.Stats.BaseManaSteal}");
}

[Fact]
public void ItemObjectHas12FormulaVariables()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the purpose of this test? Either something has an attribute or it doesn't - removing an attribute isn't very likely. Is this just to ensure there aren't any unnoticeable scripting changes?

{
var props = typeof(ItemObject).GetProperties()
.Where(p => p.IsDefined(typeof(FormulaVariable), false))
.ToList();

Assert.Equal(12, props.Count);
}

[Fact]
public void ItemObjectFormulaVariablesIncludeExpectedProperties()
{
var props = typeof(ItemObject).GetProperties()
.Where(p => p.IsDefined(typeof(FormulaVariable), false))
.Select(p => p.Name)
.ToHashSet();

var expected = new[]
{
"Weight", "MaximumDurability", "MinLevel", "MinAbility",
"MaxLevel", "MaxAbility", "MinLDamage", "MaxLDamage",
"MinSDamage", "MaxSDamage", "Value", "Durability"
};

foreach (var name in expected)
Assert.Contains(name, props);
}

}
41 changes: 41 additions & 0 deletions Hybrasyl.Tests/Monster.cs
Original file line number Diff line number Diff line change
Expand Up @@ -532,4 +532,45 @@ public void MonsterShouldAttackWithCorrectRotation()


}

[Fact]
public void MonsterAllocateStatsRestoresLevel()
{
Assert.True(Game.World.WorldData.TryGetValue<Creature>("Gabbaghoul", out var monsterXml),
"Gabbaghoul test monster not found");
var monster = new Monster(monsterXml, SpawnFlags.AiDisabled, 50);

Assert.Equal(50, monster.Stats.Level);
}

[Fact]
public void HigherLevelMonsterHasMoreHp()
{
Assert.True(Game.World.WorldData.TryGetValue<Creature>("Gabbaghoul", out var monsterXml),
"Gabbaghoul test monster not found");

var low = new Monster(monsterXml, SpawnFlags.AiDisabled, 10);
var high = new Monster(monsterXml, SpawnFlags.AiDisabled, 50);

Assert.True(high.Stats.MaximumHp > low.Stats.MaximumHp,
$"Level 50 HP ({high.Stats.MaximumHp}) should exceed level 10 HP ({low.Stats.MaximumHp})");
}

[Fact]
public void HigherLevelMonsterHasMoreStats()
{
Assert.True(Game.World.WorldData.TryGetValue<Creature>("Gabbaghoul", out var monsterXml),
"Gabbaghoul test monster not found");

var low = new Monster(monsterXml, SpawnFlags.AiDisabled, 5);
var high = new Monster(monsterXml, SpawnFlags.AiDisabled, 50);

var lowTotal = low.Stats.BaseStr + low.Stats.BaseInt + low.Stats.BaseWis +
low.Stats.BaseCon + low.Stats.BaseDex;
var highTotal = high.Stats.BaseStr + high.Stats.BaseInt + high.Stats.BaseWis +
high.Stats.BaseCon + high.Stats.BaseDex;

Assert.True(highTotal > lowTotal,
$"Level 50 total stats ({highTotal}) should exceed level 5 ({lowTotal})");
}
}
10 changes: 10 additions & 0 deletions Hybrasyl.Tests/Targeting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,14 @@ public void NoDuplicateTargets()
var targets2 = Fixture.TestUser.GetTargets(castable, bait2);
Assert.Single(targets2);
}

[Fact]
public void ConeRadiusNotCappedByViewport()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test needs to test the actual expected behavior versus checking for what is effectively a regression, and also doing it in a way that is fragile?

{
var source = System.IO.File.ReadAllText(
System.IO.Path.Combine(System.AppDomain.CurrentDomain.BaseDirectory,
"..", "..", "..", "..", "hybrasyl", "Objects", "Creature.cs"));

Assert.DoesNotContain("Math.Min(tile.Radius, Game.ActiveConfiguration.Constants.ViewportSize / 2)", source);
}
}
20 changes: 14 additions & 6 deletions hybrasyl/Interfaces/IPursuitable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,20 +110,28 @@ public sealed void DisplayPursuits(User invoker)

if (merchant?.Jobs.HasFlag(MerchantJob.Skills) ?? false)
{
optionsCount += 2;
optionsCount++;
options.Options.Add(new MerchantDialogOption
{ Id = (ushort)MerchantMenuItem.LearnSkillMenu, Text = "Learn Skill" });
options.Options.Add(new MerchantDialogOption
{ Id = (ushort)MerchantMenuItem.ForgetSkillMenu, Text = "Forget Skill" });
if (merchant.Template.Roles?.DisableForget != true)
{
optionsCount++;
options.Options.Add(new MerchantDialogOption
{ Id = (ushort)MerchantMenuItem.ForgetSkillMenu, Text = "Forget Skill" });
}
}

if (merchant?.Jobs.HasFlag(MerchantJob.Spells) ?? false)
{
optionsCount += 2;
optionsCount++;
options.Options.Add(new MerchantDialogOption
{ Id = (ushort)MerchantMenuItem.LearnSpellMenu, Text = "Learn Secret" });
options.Options.Add(new MerchantDialogOption
{ Id = (ushort)MerchantMenuItem.ForgetSpellMenu, Text = "Forget Secret" });
if (merchant.Template.Roles?.DisableForget != true)
{
optionsCount++;
options.Options.Add(new MerchantDialogOption
{ Id = (ushort)MerchantMenuItem.ForgetSpellMenu, Text = "Forget Secret" });
}
}

if (merchant?.Jobs.HasFlag(MerchantJob.Post) ?? false)
Expand Down
146 changes: 73 additions & 73 deletions hybrasyl/Objects/Creature.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,24 +200,24 @@ public Creature GetDirectionalTarget(Direction direction)

switch (direction)
{
case Direction.East:
{
obj = Map.EntityTree.FirstOrDefault(predicate: x => x.X == X + 1 && x.Y == Y && x is Creature);
case Direction.East:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this just tab / spacing changes?

{
obj = Map.EntityTree.FirstOrDefault(predicate: x => x.X == X + 1 && x.Y == Y && x is Creature);
}
break;
case Direction.West:
{
obj = Map.EntityTree.FirstOrDefault(predicate: x => x.X == X - 1 && x.Y == Y && x is Creature);
case Direction.West:
{
obj = Map.EntityTree.FirstOrDefault(predicate: x => x.X == X - 1 && x.Y == Y && x is Creature);
}
break;
case Direction.North:
{
obj = Map.EntityTree.FirstOrDefault(predicate: x => x.X == X && x.Y == Y - 1 && x is Creature);
case Direction.North:
{
obj = Map.EntityTree.FirstOrDefault(predicate: x => x.X == X && x.Y == Y - 1 && x is Creature);
}
break;
case Direction.South:
{
obj = Map.EntityTree.FirstOrDefault(predicate: x => x.X == X && x.Y == Y + 1 && x is Creature);
case Direction.South:
{
obj = Map.EntityTree.FirstOrDefault(predicate: x => x.X == X && x.Y == Y + 1 && x is Creature);
}
break;
default:
Expand All @@ -241,24 +241,24 @@ public List<Creature> GetDirectionalTargets(Direction direction, int radius = 1)

switch (direction)
{
case Direction.East:
{
rect = new Rectangle(X + 1, Y, radius, 1);
case Direction.East:
{
rect = new Rectangle(X + 1, Y, radius, 1);
}
break;
case Direction.West:
{
rect = new Rectangle(X - radius, Y, radius, 1);
case Direction.West:
{
rect = new Rectangle(X - radius, Y, radius, 1);
}
break;
case Direction.South:
{
rect = new Rectangle(X, Y + 1, 1, radius);
case Direction.South:
{
rect = new Rectangle(X, Y + 1, 1, radius);
}
break;
case Direction.North:
{
rect = new Rectangle(X, Y - radius, 1, radius);
case Direction.North:
{
rect = new Rectangle(X, Y - radius, 1, radius);
}
break;
}
Expand Down Expand Up @@ -400,7 +400,7 @@ public virtual List<Creature> GetTargets(Castable castable, Creature target = nu

foreach (var tile in intent.Cone)
{
var radius = Math.Min(tile.Radius, Game.ActiveConfiguration.Constants.ViewportSize / 2);
var radius = tile.Radius;
if (radius == 0)
continue;
var coneDirection = tile.Direction.Resolve(Direction);
Expand Down Expand Up @@ -434,50 +434,50 @@ public virtual List<Creature> GetTargets(Castable castable, Creature target = nu
switch (this)
{
// No hostile flag: remove players
case Monster when Condition.Charmed:
{
if (intent.Flags.Contains(IntentFlags.Hostile))
finalTargets.AddRange(actualTargets.OfType<Monster>());
// No friendly flag, or not charmed - remove monsters
if (intent.Flags.Contains(IntentFlags.Friendly))
finalTargets.AddRange(actualTargets.OfType<User>());
break;
case Monster when Condition.Charmed:
{
if (intent.Flags.Contains(IntentFlags.Hostile))
finalTargets.AddRange(actualTargets.OfType<Monster>());

// No friendly flag, or not charmed - remove monsters
if (intent.Flags.Contains(IntentFlags.Friendly))
finalTargets.AddRange(actualTargets.OfType<User>());
break;
}
// Group / pvp: n/a
case Monster:
{
if (intent.Flags.Contains(IntentFlags.Hostile))
finalTargets.AddRange(actualTargets.OfType<User>());
// No friendly flag, or not charmed - remove monsters
if (intent.Flags.Contains(IntentFlags.Friendly))
finalTargets.AddRange(actualTargets.OfType<Monster>());
break;
case Monster:
{
if (intent.Flags.Contains(IntentFlags.Hostile))
finalTargets.AddRange(actualTargets.OfType<User>());

// No friendly flag, or not charmed - remove monsters
if (intent.Flags.Contains(IntentFlags.Friendly))
finalTargets.AddRange(actualTargets.OfType<Monster>());
break;
}
case 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(IntentFlags.Hostile))
finalTargets.AddRange(actualTargets.OfType<Monster>());
if (intent.Flags.Contains(IntentFlags.Friendly))
finalTargets.AddRange(actualTargets.OfType<User>()
.Where(predicate: e => e.Condition.PvpEnabled == false && e.Id != Id));
if (intent.Flags.Contains(IntentFlags.Pvp))
finalTargets.AddRange(actualTargets.OfType<User>()
.Where(predicate: e => e.Condition.PvpEnabled && e.Id != Id));
if (intent.Flags.Contains(IntentFlags.Group))
// Remove group members
if (userobj.Group != null)
finalTargets.AddRange(actualTargets.OfType<User>()
.Where(predicate: e => userobj.Group.Contains(e)));
break;
case 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(IntentFlags.Hostile))
finalTargets.AddRange(actualTargets.OfType<Monster>());

if (intent.Flags.Contains(IntentFlags.Friendly))
finalTargets.AddRange(actualTargets.OfType<User>()
.Where(predicate: e => e.Condition.PvpEnabled == false && e.Id != Id));

if (intent.Flags.Contains(IntentFlags.Pvp))
finalTargets.AddRange(actualTargets.OfType<User>()
.Where(predicate: e => e.Condition.PvpEnabled && e.Id != Id));

if (intent.Flags.Contains(IntentFlags.Group))
// Remove group members
if (userobj.Group != null)
finalTargets.AddRange(actualTargets.OfType<User>()
.Where(predicate: e => userobj.Group.Contains(e)));
break;
}
}

Expand Down Expand Up @@ -523,13 +523,13 @@ public virtual bool UseCastable(Castable castableXml, Creature target = null)
if (castableXml.Effects?.Animations?.OnCast != null)
{
if (castableXml.Effects?.Animations?.OnCast.Target != null)
foreach (var tar in targets)
foreach (var user in tar.viewportUsers.ToList())
{
GameLog.UserActivityInfo(
$"UseCastable: Sending {user.Name} effect for {Name}: {castableXml.Effects.Animations.OnCast.Target.Id}");
user.SendEffect(tar.Id, castableXml.Effects.Animations.OnCast.Target.Id,
castableXml.Effects.Animations.OnCast.Target.Speed);
foreach (var tar in targets)
foreach (var user in tar.viewportUsers.ToList())
{
GameLog.UserActivityInfo(
$"UseCastable: Sending {user.Name} effect for {Name}: {castableXml.Effects.Animations.OnCast.Target.Id}");
user.SendEffect(tar.Id, castableXml.Effects.Animations.OnCast.Target.Id,
castableXml.Effects.Animations.OnCast.Target.Speed);
}

if (castableXml.Effects?.Animations?.OnCast?.SpellEffect != null)
Expand Down
12 changes: 12 additions & 0 deletions hybrasyl/Objects/ItemObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public ItemObjectType ItemObjectType
public string SlotName => Enum.GetName(typeof(EquipmentSlot), EquipmentSlot) ?? "None";
public string DisplayName => Name;

[FormulaVariable]
public int Weight => Template.Properties.Physical.Weight > int.MaxValue
? int.MaxValue
: Convert.ToInt32(Template.Properties.Physical.Weight);
Expand All @@ -111,6 +112,7 @@ public ItemObjectType ItemObjectType

public List<CastModifier> CastModifiers => Template.Properties.CastModifiers;

[FormulaVariable]
public uint MaximumDurability => Template.Properties?.Physical?.Durability > uint.MaxValue
? uint.MaxValue
: Convert.ToUInt32(Template.Properties.Physical.Durability);
Expand All @@ -130,9 +132,13 @@ public uint RepairCost
// Identifiable flag is set.
public bool Identified => true;

[FormulaVariable]
public byte MinLevel => Template.MinLevel;
[FormulaVariable]
public byte MinAbility => Template.MinAbility;
[FormulaVariable]
public byte MaxLevel => Template.MaxLevel;
[FormulaVariable]
public byte MaxAbility => Template.MaxAbility;

public Class Class => Template.Class;
Expand All @@ -145,12 +151,17 @@ public uint RepairCost

public ElementType Element => Template.Element;

[FormulaVariable]
public float MinLDamage => Template.MinLDamage;
[FormulaVariable]
public float MaxLDamage => Template.MaxLDamage;
[FormulaVariable]
public float MinSDamage => Template.MinSDamage;
[FormulaVariable]
public float MaxSDamage => Template.MaxSDamage;
public ushort DisplaySprite => Template.Properties.Appearance.DisplaySprite;

[FormulaVariable]
public uint Value => Template.Properties.Physical.Value > uint.MaxValue
? uint.MaxValue
: Convert.ToUInt32(Template.Properties.Physical.Value);
Expand Down Expand Up @@ -201,6 +212,7 @@ public int Count

private Lockable<double> _durability { get; set; }

[FormulaVariable]
public double Durability
{
get => _durability.Value;
Expand Down
Loading