Skip to content

Commit

Permalink
Added feature to hide players
Browse files Browse the repository at this point in the history
Required for the spectator feature. Added chat commands which are helpful for testing and required for game masters anyway.
  • Loading branch information
sven-n committed Jul 7, 2024
1 parent 25ee970 commit 28fadb3
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 4 deletions.
6 changes: 6 additions & 0 deletions src/GameLogic/Attributes/Stats.cs
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,12 @@ public class Stats
/// </summary>
public static AttributeDefinition TransformationSkin { get; } = new (new Guid("E5B886B0-B1A6-4EA2-8EF9-08D27AADB7C3"), "Character to Monster transformation", "This value is > 0, when the character got transformed into a monster, by wearing a transformation ring. The value specifies the type of monster (skin).");

/// <summary>
/// Gets the <see cref="IsInvisible"/> attribute which defines if the player is invisible.
/// This value is > 0, when the character is either a game master which used the hide-command, or when a player is spectating a duel.
/// </summary>
public static AttributeDefinition IsInvisible { get; } = new(new Guid("8B0721BC-7AC6-4677-B488-ED4319AE9A56"), "Is invisible", "This value is > 0, when the character is either a game master which used the hide-command, or when a player is spectating a duel.");

/// <summary>
/// Gets the attribute for a strength requirement reduction. Items with this option require less strength, according to the option's value.
/// </summary>
Expand Down
15 changes: 15 additions & 0 deletions src/GameLogic/LocateableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace MUnique.OpenMU.GameLogic;

using MUnique.OpenMU.GameLogic.Attributes;
using MUnique.OpenMU.Pathfinding;

/// <summary>
Expand All @@ -25,6 +26,20 @@ public static IEnumerable<T> WhereActive<T>(this IEnumerable<T> locateables)
return locateables.Where(l => l.IsActive());
}

/// <summary>
/// Filters out invisible locateables.
/// </summary>
/// <typeparam name="T">Type of elements.</typeparam>
/// <param name="locateables">The locateables.</param>
/// <returns>
/// All visible locateables of the given enumeration.
/// </returns>
public static IEnumerable<T> WhereNotInvisible<T>(this IEnumerable<T> locateables)
where T : IAttackable
{
return locateables.Where(l => l.Attributes[Stats.IsInvisible] == 0);
}

/// <summary>
/// Determines whether this instance is active (alive).
/// </summary>
Expand Down
3 changes: 2 additions & 1 deletion src/GameLogic/NPC/BasicMonsterIntelligence.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ protected virtual void Dispose(bool managed)
}

var possibleTargets = tempObservers.OfType<IAttackable>()
.Where(a => a.IsActive() && !a.IsAtSafezone())
.Where(a => a.IsActive() && !a.IsAtSafezone() && a is not Player { IsInvisible: true })
.ToList();
var summons = possibleTargets.OfType<Player>()
.Select(p => p.Summon?.Item1)
Expand Down Expand Up @@ -218,6 +218,7 @@ private async ValueTask TickAsync()
{
// Old Target out of Range?
if (!target.IsAlive
|| target is Player { IsInvisible: true }
|| target.IsTeleporting
|| target.IsAtSafezone()
|| !target.IsInRange(this.Monster.Position, this.Npc.Definition.ViewRange)
Expand Down
6 changes: 3 additions & 3 deletions src/GameLogic/ObserverToWorldViewAdapter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public async ValueTask LocateableAddedAsync(ILocateable item)
return;
}

if (item is Player player)
if (item is Player { IsInvisible: false } player)
{
await this._adaptee.InvokeViewPlugInAsync<INewPlayersInScopePlugIn>(p => p.NewPlayersInScopeAsync(player.GetAsEnumerable())).ConfigureAwait(false);
}
Expand All @@ -75,7 +75,7 @@ public async ValueTask LocateableAddedAsync(ILocateable item)
// no action required.
}

if (item is IObservable observable)
if (item is IObservable observable and not Player { IsInvisible: true })
{
using (await this._observingLock.WriterLockAsync())
{
Expand Down Expand Up @@ -184,7 +184,7 @@ public async ValueTask NewLocateablesInScopeAsync(IEnumerable<ILocateable> newOb
newItems.ForEach(item => this._observingObjects.Add(item));
}

var players = newItems.OfType<Player>().WhereActive();
var players = newItems.OfType<Player>().WhereActive().WhereNotInvisible();
if (players.Any())
{
await this._adaptee.InvokeViewPlugInAsync<INewPlayersInScopePlugIn>(p => p.NewPlayersInScopeAsync(players, false)).ConfigureAwait(false);
Expand Down
83 changes: 83 additions & 0 deletions src/GameLogic/Player.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ public Player(IGameContext gameContext)
/// <inheritdoc />
public Point WalkTarget => this._walker.CurrentTarget;

/// <summary>
/// Gets a value indicating whether this instance is invisible to other players.
/// </summary>
public bool IsInvisible => this.Attributes?[Stats.IsInvisible] > 0;

/// <summary>
/// Gets the skill hit validator.
/// </summary>
Expand Down Expand Up @@ -800,6 +805,47 @@ public virtual bool TryAddMoney(int value)
return true;
}

public async ValueTask AddInvisibleEffectAsync()
{
var invisibleEffect = this.GameContext.Configuration.MagicEffects.FirstOrDefault(e => e.PowerUpDefinitions.Any(e => e.TargetAttribute == Stats.IsInvisible));
if (invisibleEffect is null)
{
this.Logger.LogError("Invisible effect not found!");
}
else
{
var (duration, powerUps) = this.CreateMagicEffectPowerUp(invisibleEffect);
var magicEffect = new MagicEffect(TimeSpan.FromSeconds(duration.Value), invisibleEffect, powerUps.Select(p => new MagicEffect.ElementWithTarget(p.BuffPowerUp, p.Target)).ToArray());
await this.MagicEffectList.AddEffectAsync(magicEffect).ConfigureAwait(false);

if (this._currentMap is { } currentMap)
{
await currentMap.RespawnAsync(this).ConfigureAwait(false);
}
}
}

public async ValueTask RemoveInvisibleEffectAsync()
{
var invisibleEffect = this.GameContext.Configuration.MagicEffects.FirstOrDefault(e => e.PowerUpDefinitions.Any(e => e.TargetAttribute == Stats.IsInvisible));
if (invisibleEffect is null)
{
return;
}

var activeEffect = this.MagicEffectList.ActiveEffects.Values.FirstOrDefault(e => e.Definition == invisibleEffect);
if (activeEffect is null)
{
return;
}

await activeEffect.DisposeAsync().ConfigureAwait(false);
if (this._currentMap is { } currentMap)
{
await currentMap.RespawnAsync(this).ConfigureAwait(false);
}
}

/// <summary>
/// Moves the player to the specified gate.
/// </summary>
Expand Down Expand Up @@ -1175,6 +1221,11 @@ public async ValueTask DisconnectAsync()
/// <inheritdoc/>
public async ValueTask AddObserverAsync(IWorldObserver observer)
{
if (this.IsInvisible && observer != this)
{
return;
}

using var _ = await this.ObserverLock.WriterLockAsync();
this.Observers.Add(observer);
}
Expand Down Expand Up @@ -1291,6 +1342,38 @@ public void CreateMagicEffectPowerUp(SkillEntry skillEntry)
skillEntry.PowerUps = result;
}

/// <summary>
/// Creates the magic effect power up for the given definition.
/// </summary>
/// <param name="magicEffectDefinition">The definition for a magic effect.</param>
/// <returns></returns>
public (IElement DurationInSeconds, (AttributeDefinition Target, IElement BuffPowerUp)[] PowerUps) CreateMagicEffectPowerUp(MagicEffectDefinition magicEffectDefinition)
{
ArgumentNullException.ThrowIfNull(magicEffectDefinition);

if (magicEffectDefinition.PowerUpDefinitions.Any(d => d.Boost is null))
{
throw new InvalidOperationException($"Magic effect definition {magicEffectDefinition.Name} ({magicEffectDefinition.Number}) is without a PowerUpDefinition.");
}

if (magicEffectDefinition.Duration is null)
{
throw new InvalidOperationException($"Magic effect definition {magicEffectDefinition.Name} ({magicEffectDefinition.Number}) has no duration.");
}

int i = 0;
var result = new (AttributeDefinition Target, IElement BuffPowerUp)[magicEffectDefinition.PowerUpDefinitions.Count];
foreach (var powerUpDef in magicEffectDefinition.PowerUpDefinitions)
{
IElement powerUp = this.Attributes!.CreateElement(powerUpDef.Boost!);

result[i] = (powerUpDef.TargetAttribute!, powerUp);
i++;
}

return (this.Attributes!.CreateElement(magicEffectDefinition.Duration), result);
}

/// <summary>
/// Creates a summoned monster for the player.
/// </summary>
Expand Down
27 changes: 27 additions & 0 deletions src/GameLogic/PlugIns/ChatCommands/HideChatCommandPlugIn.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;

namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands;

/// <summary>
/// A chat command plugin which handles hide commands.
/// </summary>
[Guid("7CE1CA66-C6B1-4840-9997-EF15C49FAB49")]
[PlugIn("Hide command", "Handles the chat command '/hide'. Hides the own player from others.")]
[ChatCommandHelp(Command, "Hides the own player from others.", CharacterStatus.GameMaster)]
public class HideChatCommandPlugIn : IChatCommandPlugIn
{
private const string Command = "/hide";

/// <inheritdoc />
public string Key => Command;

/// <inheritdoc/>
public CharacterStatus MinCharacterStatusRequirement => CharacterStatus.GameMaster;

/// <inheritdoc />
public async ValueTask HandleCommandAsync(Player player, string command)
{
await player.AddInvisibleEffectAsync().ConfigureAwait(false);
}
}
27 changes: 27 additions & 0 deletions src/GameLogic/PlugIns/ChatCommands/UnHideChatCommandPlugIn.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Runtime.InteropServices;
using MUnique.OpenMU.PlugIns;

namespace MUnique.OpenMU.GameLogic.PlugIns.ChatCommands;

/// <summary>
/// A chat command plugin which handles unhide commands.
/// </summary>
[Guid("0F0ADAC6-88C7-4EC0-94A2-A289173DEDA7")]
[PlugIn("Hide command", "Handles the chat command '/unhide'. Unhides the own player from others.")]
[ChatCommandHelp(Command, "Unhides the own player from others.", CharacterStatus.GameMaster)]
public class UnHideChatCommandPlugIn : IChatCommandPlugIn
{
private const string Command = "/unhide";

/// <inheritdoc />
public string Key => Command;

/// <inheritdoc/>
public CharacterStatus MinCharacterStatusRequirement => CharacterStatus.GameMaster;

/// <inheritdoc />
public async ValueTask HandleCommandAsync(Player player, string command)
{
await player.RemoveInvisibleEffectAsync().ConfigureAwait(false);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// <copyright file="InvisibleEffectInitializer.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>

namespace MUnique.OpenMU.Persistence.Initialization.Skills;

using MUnique.OpenMU.AttributeSystem;
using MUnique.OpenMU.DataModel.Attributes;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.GameLogic.Attributes;

/// <summary>
/// Initializer which initializes the invisible effect.
/// </summary>
public class InvisibleEffectInitializer : InitializerBase
{
/// <summary>
/// Initializes a new instance of the <see cref="InvisibleEffectInitializer"/> class.
/// </summary>
/// <param name="context">The context.</param>
/// <param name="gameConfiguration">The game configuration.</param>
public InvisibleEffectInitializer(IContext context, GameConfiguration gameConfiguration)
: base(context, gameConfiguration)
{
}

/// <inheritdoc/>
public override void Initialize()
{
var magicEffect = this.Context.CreateNew<MagicEffectDefinition>();
this.GameConfiguration.MagicEffects.Add(magicEffect);
magicEffect.Number = (byte)MagicEffectNumber.Transparency;
magicEffect.Name = "Invisible";
magicEffect.InformObservers = false;
magicEffect.SendDuration = false;
magicEffect.StopByDeath = false;
magicEffect.Duration = this.Context.CreateNew<PowerUpDefinitionValue>();
magicEffect.Duration.ConstantValue.Value = (float)TimeSpan.FromDays(1).TotalSeconds;

var invisibleEffect = this.Context.CreateNew<PowerUpDefinition>();
magicEffect.PowerUpDefinitions.Add(invisibleEffect);
invisibleEffect.TargetAttribute = Stats.IsInvisible.GetPersistent(this.GameConfiguration);
invisibleEffect.Boost = this.Context.CreateNew<PowerUpDefinitionValue>();
invisibleEffect.Boost.ConstantValue.Value = 1;
invisibleEffect.Boost.ConstantValue.AggregateType = AggregateType.AddRaw;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@ private void InitializeEffects()
new ShieldRecoverEffectInitializer(this.Context, this.GameConfiguration).Initialize();
new InfiniteArrowEffectInitializer(this.Context, this.GameConfiguration).Initialize();
new DefenseReductionEffectInitializer(this.Context, this.GameConfiguration).Initialize();
new InvisibleEffectInitializer(this.Context, this.GameConfiguration).Initialize();
}

private void MapSkillsToEffects()
Expand Down

0 comments on commit 28fadb3

Please sign in to comment.