diff --git a/Hybrasyl.Tests/Items.cs b/Hybrasyl.Tests/Items.cs index 01976b69..c5cb3e0d 100644 --- a/Hybrasyl.Tests/Items.cs +++ b/Hybrasyl.Tests/Items.cs @@ -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; @@ -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() + { + 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); + } + } \ No newline at end of file diff --git a/Hybrasyl.Tests/Monster.cs b/Hybrasyl.Tests/Monster.cs index 32123c2d..67fe148b 100644 --- a/Hybrasyl.Tests/Monster.cs +++ b/Hybrasyl.Tests/Monster.cs @@ -532,4 +532,45 @@ public void MonsterShouldAttackWithCorrectRotation() } + + [Fact] + public void MonsterAllocateStatsRestoresLevel() + { + Assert.True(Game.World.WorldData.TryGetValue("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("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("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})"); + } } \ No newline at end of file diff --git a/Hybrasyl.Tests/Targeting.cs b/Hybrasyl.Tests/Targeting.cs index b1955d02..c134fd07 100644 --- a/Hybrasyl.Tests/Targeting.cs +++ b/Hybrasyl.Tests/Targeting.cs @@ -117,4 +117,14 @@ public void NoDuplicateTargets() var targets2 = Fixture.TestUser.GetTargets(castable, bait2); Assert.Single(targets2); } + + [Fact] + public void ConeRadiusNotCappedByViewport() + { + 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); + } } \ No newline at end of file diff --git a/hybrasyl/Interfaces/IPursuitable.cs b/hybrasyl/Interfaces/IPursuitable.cs index beca32b3..0b1e80a2 100644 --- a/hybrasyl/Interfaces/IPursuitable.cs +++ b/hybrasyl/Interfaces/IPursuitable.cs @@ -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) diff --git a/hybrasyl/Objects/Creature.cs b/hybrasyl/Objects/Creature.cs index ed749fc2..3521baac 100644 --- a/hybrasyl/Objects/Creature.cs +++ b/hybrasyl/Objects/Creature.cs @@ -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: + { + 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: @@ -241,24 +241,24 @@ public List 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; } @@ -400,7 +400,7 @@ public virtual List 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); @@ -434,50 +434,50 @@ public virtual List 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()); - - // No friendly flag, or not charmed - remove monsters - if (intent.Flags.Contains(IntentFlags.Friendly)) - finalTargets.AddRange(actualTargets.OfType()); - break; + case Monster when Condition.Charmed: + { + if (intent.Flags.Contains(IntentFlags.Hostile)) + finalTargets.AddRange(actualTargets.OfType()); + + // No friendly flag, or not charmed - remove monsters + if (intent.Flags.Contains(IntentFlags.Friendly)) + finalTargets.AddRange(actualTargets.OfType()); + break; } // Group / pvp: n/a - case Monster: - { - if (intent.Flags.Contains(IntentFlags.Hostile)) - finalTargets.AddRange(actualTargets.OfType()); - - // No friendly flag, or not charmed - remove monsters - if (intent.Flags.Contains(IntentFlags.Friendly)) - finalTargets.AddRange(actualTargets.OfType()); - break; + case Monster: + { + if (intent.Flags.Contains(IntentFlags.Hostile)) + finalTargets.AddRange(actualTargets.OfType()); + + // No friendly flag, or not charmed - remove monsters + if (intent.Flags.Contains(IntentFlags.Friendly)) + finalTargets.AddRange(actualTargets.OfType()); + 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()); - - if (intent.Flags.Contains(IntentFlags.Friendly)) - finalTargets.AddRange(actualTargets.OfType() - .Where(predicate: e => e.Condition.PvpEnabled == false && e.Id != Id)); - - if (intent.Flags.Contains(IntentFlags.Pvp)) - finalTargets.AddRange(actualTargets.OfType() - .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() - .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()); + + if (intent.Flags.Contains(IntentFlags.Friendly)) + finalTargets.AddRange(actualTargets.OfType() + .Where(predicate: e => e.Condition.PvpEnabled == false && e.Id != Id)); + + if (intent.Flags.Contains(IntentFlags.Pvp)) + finalTargets.AddRange(actualTargets.OfType() + .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() + .Where(predicate: e => userobj.Group.Contains(e))); + break; } } @@ -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) diff --git a/hybrasyl/Objects/ItemObject.cs b/hybrasyl/Objects/ItemObject.cs index 54819018..118d4f68 100644 --- a/hybrasyl/Objects/ItemObject.cs +++ b/hybrasyl/Objects/ItemObject.cs @@ -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); @@ -111,6 +112,7 @@ public ItemObjectType ItemObjectType public List CastModifiers => Template.Properties.CastModifiers; + [FormulaVariable] public uint MaximumDurability => Template.Properties?.Physical?.Durability > uint.MaxValue ? uint.MaxValue : Convert.ToUInt32(Template.Properties.Physical.Durability); @@ -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; @@ -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); @@ -201,6 +212,7 @@ public int Count private Lockable _durability { get; set; } + [FormulaVariable] public double Durability { get => _durability.Value; diff --git a/hybrasyl/Objects/Monster.cs b/hybrasyl/Objects/Monster.cs index 6d9809f9..282d2601 100644 --- a/hybrasyl/Objects/Monster.cs +++ b/hybrasyl/Objects/Monster.cs @@ -377,16 +377,16 @@ public override void OnDeath() User hitter = null; switch (LastHitter) { - case Monster monster: - { - var hitterStatuses = monster.Condition.Charmed - ? monster.CurrentStatuses.Values - : CurrentStatuses.Values; - var statuses = hitterStatuses.Cast().Where(predicate: x => - x.ConditionChanges != null && (x.ConditionChanges.Set & CreatureCondition.Charm) != 0) - .ToList(); - if (statuses.Count != 0) hitter = statuses.First().Source as User; - break; + case Monster monster: + { + var hitterStatuses = monster.Condition.Charmed + ? monster.CurrentStatuses.Values + : CurrentStatuses.Values; + var statuses = hitterStatuses.Cast().Where(predicate: x => + x.ConditionChanges != null && (x.ConditionChanges.Set & CreatureCondition.Charm) != 0) + .ToList(); + if (statuses.Count != 0) hitter = statuses.First().Source as User; + break; } case User: hitter = LastHitter as User; @@ -463,7 +463,7 @@ public override void OnDeath() Game.ReportException(e); } else - hitter?.SendCombatLogMessage(new NoLootEvent + hitter?.SendCombatLogMessage(new NoLootEvent { Reason = "You or your group must hit a monster to collect loot" }); Game.World.RemoveStatusCheck(this); @@ -598,43 +598,57 @@ private void MpHpIncrease() public void AllocateStats() { - var totalPoints = Stats.Level * 2; - if (BehaviorSet is null || string.IsNullOrEmpty(BehaviorSet.StatAlloc)) + var targetLevel = Stats.Level; + + for (byte level = 1; level <= targetLevel; level++) { - RandomlyAllocateStatPoints(totalPoints); + Stats.Level = level; + + if (BehaviorSet is null || string.IsNullOrEmpty(BehaviorSet.StatAlloc)) + { + RandomlyAllocateStatPoints(2); + } + else + { + AllocatePatternPoints(2); + } } - else + } + + private void AllocatePatternPoints(int points) + { + var allocPattern = BehaviorSet.StatAlloc.Trim().ToLower().Split(" "); + var patternIndex = 0; + + for (var i = 0; i < points; i++) { - var allocPattern = BehaviorSet.StatAlloc.Trim().ToLower().Split(" "); - while (totalPoints > 0) - foreach (var alloc in allocPattern) - { - switch (alloc) - { - case "str": - Stats.BaseStr += 1; - break; - case "int": - Stats.BaseInt += 1; - break; - case "wis": - Stats.BaseWis += 1; - break; - case "con": - Stats.BaseCon += 1; - break; - case "dex": - Stats.BaseDex += 1; - break; - default: - RandomlyAllocateStatPoints(1); - break; - } + var alloc = allocPattern[patternIndex % allocPattern.Length]; + patternIndex++; - totalPoints--; - if (totalPoints % 2 == 0) - MpHpIncrease(); - } + switch (alloc) + { + case "str": + Stats.BaseStr += 1; + break; + case "int": + Stats.BaseInt += 1; + break; + case "wis": + Stats.BaseWis += 1; + break; + case "con": + Stats.BaseCon += 1; + break; + case "dex": + Stats.BaseDex += 1; + break; + default: + RandomlyAllocateStatPoints(1); + break; + } + + if ((i + 1) % 2 == 0) + MpHpIncrease(); } } @@ -940,10 +954,10 @@ public void DetermineNextAction() _actionQueue.Enqueue(MobAction.Move); return; } - else + else { - _actionQueue.Enqueue(MobAction.Move); - return; + _actionQueue.Enqueue(MobAction.Move); + return; } } else @@ -1026,8 +1040,8 @@ public void ProcessActions() return; case MobAction.Move when !Condition.MovementAllowed: return; - case MobAction.Move when Condition.Blinded: - + case MobAction.Move when Condition.Blinded: + var rand = Random.Shared.NextDouble(); dir = (Direction)Random.Shared.Next(0, 4); if (rand > 0.33) @@ -1048,7 +1062,7 @@ public void ProcessActions() Walk(Direction); } break; - case MobAction.Move when ShouldWander: + case MobAction.Move when ShouldWander: { // Make sure mob is on a map, and the right map at that (we don't support mobs wandering between maps yet) if (SpawnPoint == null || SpawnPoint.MapId != Map.Id) @@ -1064,15 +1078,15 @@ public void ProcessActions() if (Destination == null || Destination != SpawnPoint || (SpawnPoint.X == X && SpawnPoint.Y == Y)) Destination = RandomDestination; - CurrentPath = AStarPathFind(Destination.X, Destination.Y, X, Y); - + CurrentPath = AStarPathFind(Destination.X, Destination.Y, X, Y); + if (Walk(AStarGetDirection())) CurrentPath = CurrentPath.Parent; else // Couldn't move, attempt to recalculate path CurrentPath = AStarPathFind(Destination.X, Destination.Y, X, Y); - break; + break; } case MobAction.Move: { diff --git a/hybrasyl/Objects/User.cs b/hybrasyl/Objects/User.cs index a2f4c0ac..b26d032c 100644 --- a/hybrasyl/Objects/User.cs +++ b/hybrasyl/Objects/User.cs @@ -454,15 +454,17 @@ public override void OnHear(SpokenEvent e) LastHeard = e; if (e.Speaker != this) MessagesReceived.Add(e); + var speakerName = e.Speaker is Merchant m && !string.IsNullOrWhiteSpace(m.DisplayName) + ? m.DisplayName : e.Speaker.Name; var x0D = new ServerPacket(0x0D); x0D.WriteBoolean(e.Shout); x0D.WriteUInt32(e.Speaker.Id); if (e.Shout) x0D.WriteString8( - !string.IsNullOrEmpty(e.From) ? $"{e.From}! {e.Message}" : $"{e.Speaker.Name}! {e.Message}"); + !string.IsNullOrEmpty(e.From) ? $"{e.From}! {e.Message}" : $"{speakerName}! {e.Message}"); else x0D.WriteString8( - !string.IsNullOrEmpty(e.From) ? $"{e.From}: {e.Message}" : $"{e.Speaker.Name}: {e.Message}"); + !string.IsNullOrEmpty(e.From) ? $"{e.From}: {e.Message}" : $"{speakerName}: {e.Message}"); Enqueue(x0D); } diff --git a/hybrasyl/Subsystems/Mundanes/MerchantController.cs b/hybrasyl/Subsystems/Mundanes/MerchantController.cs index 5053d155..4ae9166c 100644 --- a/hybrasyl/Subsystems/Mundanes/MerchantController.cs +++ b/hybrasyl/Subsystems/Mundanes/MerchantController.cs @@ -217,6 +217,7 @@ public void RepairAll(MerchantControllerRequest request) foreach (var item in user.Inventory) { if (item.MaximumDurability == 0 || item.Durability == item.MaximumDurability) continue; + if (!CanRepairItem(item)) continue; if (item.RepairCost > user.Stats.Gold) { Merchant.Say($"You'll need {item.RepairCost} more gold to repair all of it, I'm afraid."); @@ -231,6 +232,7 @@ public void RepairAll(MerchantControllerRequest request) foreach (var item in user.Equipment) { if (item.MaximumDurability == 0 || item.Durability == item.MaximumDurability) continue; + if (!CanRepairItem(item)) continue; if (item.RepairCost > user.Stats.Gold) { Merchant.Say($"You'll need {item.RepairCost} more gold to repair all of it, I'm afraid."); @@ -270,6 +272,11 @@ public void RepairItem(MerchantControllerRequest request) foreach (var (slot, obj) in slotList) { if (obj.MaximumDurability == 0 || obj.Durability == obj.MaximumDurability) continue; + if (!CanRepairItem(obj)) + { + Merchant.Say($"I don't repair that sort of thing."); + return; + } if (obj.RepairCost > user.Stats.Gold) { Merchant.Say($"You'll need {obj.RepairCost} more gold to repair that, I'm afraid."); @@ -284,6 +291,23 @@ public void RepairItem(MerchantControllerRequest request) } } + private bool CanRepairItem(ItemObject item) + { + var repairTypes = Merchant.Template.Roles?.Repair?.Type; + if (repairTypes == null || repairTypes.Count == 0 || repairTypes.Contains(NpcRepairType.All)) + return true; + + var slot = (ItemSlots)item.EquipmentSlot; + if (repairTypes.Contains(NpcRepairType.Weapon)) + if (slot is ItemSlots.Weapon or ItemSlots.Shield) + return true; + if (repairTypes.Contains(NpcRepairType.Armor)) + if (slot is not (ItemSlots.Weapon or ItemSlots.Shield or ItemSlots.None)) + return true; + + return false; + } + [RegexTrigger(@"deposit (?\d+) (coins|gold|coin)")] [MerchantRequiredJob(MerchantJob.Bank)] public void DepositGold(MerchantControllerRequest request) diff --git a/hybrasyl/Subsystems/Spawning/Monolith.cs b/hybrasyl/Subsystems/Spawning/Monolith.cs index 297588c7..c4fa2c88 100644 --- a/hybrasyl/Subsystems/Spawning/Monolith.cs +++ b/hybrasyl/Subsystems/Spawning/Monolith.cs @@ -302,7 +302,7 @@ public void Spawn(SpawnGroup spawnGroup) var tile = spawnmap.FindEmptyTile(); if (tile == (-1, -1)) { - GameLog.SpawnFatal($"{spawnmap.Name}: {spawn.Name} - no empty tiles, aborting"); + GameLog.SpawnWarning($"{spawnmap.Name}: {spawn.Name} - no empty tiles, skipping"); return; }