Skip to content
Merged
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
14 changes: 14 additions & 0 deletions src/GameLogic/AttackableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ public static class AttackableExtensions
{ Stats.CurrentAbility, Stats.AbilityUsageReduction },
};

extension(IAttackable attackable)
{
/// <summary>
/// Gets a value indicating whether this instance is a summoned monster.
/// </summary>
public bool IsSummonedMonster => attackable is Monster { SummonedBy: not null };
}

/// <summary>
/// Calculates the damage, using a skill.
/// </summary>
Expand Down Expand Up @@ -563,6 +571,12 @@ public static int GetRequiredValue(this IAttacker attacker, AttributeRequirement
/// <returns>The calculated base experience.</returns>
public static double CalculateBaseExperience(this IAttackable killedObject, float killerLevel)
{
if (killedObject.IsSummonedMonster)
{
// Summoned monsters should not yield experience.
return 0;
}

var targetLevel = killedObject.Attributes[Stats.Level];
var tempExperience = (targetLevel + 25) * targetLevel / 3.0;

Expand Down
11 changes: 7 additions & 4 deletions src/GameLogic/NPC/AttackableNpcBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -266,12 +266,15 @@ protected virtual async ValueTask OnDeathAsync(IAttacker attacker)
await plugInPoint.AttackableGotKilledAsync(this, attacker).ConfigureAwait(false);
}

if (player.SelectedCharacter!.State > HeroState.Normal)
if (!this.IsSummonedMonster && player.SelectedCharacter is { } selectedCharacter)
{
player.SelectedCharacter.StateRemainingSeconds -= (int)this.Attributes[Stats.Level];
}
if (selectedCharacter.State > HeroState.Normal)
{
selectedCharacter.StateRemainingSeconds -= (int)this.Attributes[Stats.Level];
}

_ = this.DropItemDelayedAsync(player, exp); // don't wait for completion.
_ = this.DropItemDelayedAsync(player, exp); // don't wait for completion.
}
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/GameLogic/NPC/Monster.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ public Monster(MonsterSpawnArea spawnInfo, MonsterDefinition stats, GameMap map,
/// <remarks>Monsters don't do combos.</remarks>
public ComboStateMachine? ComboState => null;

/// <inheritdoc/>
protected override bool CanSpawnInSafezone => base.CanSpawnInSafezone || this.SummonedBy is not null;

/// <summary>
/// Attacks the specified target.
/// </summary>
Expand Down
7 changes: 6 additions & 1 deletion src/GameLogic/NPC/NonPlayerCharacter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,11 @@ protected virtual ValueTask MoveAsync(Point target, MoveType type)
throw new NotSupportedException("NPCs can't be moved");
}

/// <summary>
/// Gets a value indicating whether this instance can spawn in a safe zone.
/// </summary>
protected virtual bool CanSpawnInSafezone => this.Definition.ObjectKind != NpcObjectKind.Monster && this.Definition.ObjectKind != NpcObjectKind.Trap;

/// <summary>
/// Gets the spawn direction.
/// </summary>
Expand Down Expand Up @@ -214,7 +219,7 @@ private static Direction GetSpawnDirection(Direction configuredDirection)

private bool IsValidSpawnPoint(Point spawnPoint)
{
var isSafezoneAllowed = this.Definition.ObjectKind != NpcObjectKind.Monster && this.Definition.ObjectKind != NpcObjectKind.Trap;
var isSafezoneAllowed = this.CanSpawnInSafezone;
var isInSafezone = this.CurrentMap.Terrain.SafezoneMap[spawnPoint.X, spawnPoint.Y];
var npcCanWalk = this.Definition.ObjectKind == NpcObjectKind.Monster || this.Definition.ObjectKind == NpcObjectKind.Guard;
var isWalkable = this.CurrentMap.Terrain.WalkMap[spawnPoint.X, spawnPoint.Y];
Expand Down
9 changes: 9 additions & 0 deletions src/GameLogic/NPC/SummonedMonsterIntelligence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ public sealed class SummonedMonsterIntelligence : BasicMonsterIntelligence
public SummonedMonsterIntelligence(Player owner)
{
this.Owner = owner;

// Summons should be allowed to walk within safezones to follow their owner.
this.CanWalkOnSafezone = true;
}

/// <summary>
Expand All @@ -27,6 +30,12 @@ public SummonedMonsterIntelligence(Player owner)
/// <inheritdoc />
public override void RegisterHit(IAttacker attacker)
{
if (attacker == this.Owner)
{
// Never attack the owner.
return;
}

if (this.CurrentTarget is null
|| attacker.IsInRange(this.Npc.Position, this.Npc.Definition.AttackRange))
{
Expand Down
7 changes: 7 additions & 0 deletions src/GameLogic/Party.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace MUnique.OpenMU.GameLogic;
using System.Diagnostics.Metrics;
using System.Threading;
using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.Views;
using MUnique.OpenMU.GameLogic.Views.Party;
using Nito.AsyncEx;
Expand Down Expand Up @@ -250,6 +251,12 @@ protected override void Dispose(bool disposing)

private async ValueTask<int> InternalDistributeExperienceAfterKillAsync(IAttackable killedObject, IObservable killer)
{
if (killedObject.IsSummonedMonster)
{
// Do not award experience or drop items for summoned monsters.
return 0;
}

using (await killer.ObserverLock.ReaderLockAsync())
{
// All players in the range of the player are getting experience.
Expand Down
4 changes: 2 additions & 2 deletions src/GameLogic/PlugIns/SelfDefensePlugIn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ public void ForceStart()
public void AttackableGotHit(IAttackable attackable, IAttacker attacker, HitInfo hitInfo)
{
var defender = attackable as Player ?? (attackable as Monster)?.SummonedBy;
var attackerPlayer = attacker as Player ?? (attackable as Monster)?.SummonedBy;
if (defender is null || attackerPlayer is null)
var attackerPlayer = attacker as Player ?? (attacker as Monster)?.SummonedBy;
if (defender is null || attackerPlayer is null || defender == attackerPlayer)
{
return;
}
Expand Down
37 changes: 37 additions & 0 deletions tests/MUnique.OpenMU.Tests/SelfDefensePlugInTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// <copyright file="SelfDefensePlugInTest.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.Tests;

using Moq;
using MUnique.OpenMU.GameLogic;
using MUnique.OpenMU.GameLogic.NPC;
using MUnique.OpenMU.GameLogic.PlugIns;
using MUnique.OpenMU.DataModel.Configuration;

/// <summary>
/// Tests for <see cref="SelfDefensePlugIn"/>.
/// </summary>
[TestFixture]
public class SelfDefensePlugInTest
{
/// <summary>
/// Ensures that hitting an own summon doesn't start self-defense.
/// </summary>
[Test]
public async ValueTask OwnSummonHitDoesNotStartSelfDefenseAsync()
{
var player = await TestHelper.CreatePlayerAsync().ConfigureAwait(false);

var summonMock = new Mock<IAttackable>();
var summonable = summonMock.As<ISummonable>();
summonable.Setup(s => s.SummonedBy).Returns(player);
summonable.Setup(s => s.Definition).Returns(new MonsterDefinition());

var plugIn = new SelfDefensePlugIn();
plugIn.AttackableGotHit(summonMock.Object, player, new HitInfo(1, 0, DamageAttributes.Undefined));

Assert.That(player.GameContext.SelfDefenseState, Is.Empty);
}
}
Loading