diff --git a/docs/Packets/C1-FC-HeykelSavasiTeamRoster_by-server.md b/docs/Packets/C1-FC-HeykelSavasiTeamRoster_by-server.md
new file mode 100644
index 0000000..5e12d47
--- /dev/null
+++ b/docs/Packets/C1-FC-HeykelSavasiTeamRoster_by-server.md
@@ -0,0 +1,30 @@
+# C1 FC - HeykelSavasiTeamRoster (by server)
+
+## Is sent when
+
+Periodically (about once every one to two seconds) while the Heykel Savasi (Statue War) event is open for registration, in its pre-battle countdown, or being played.
+
+## Causes the following actions on the client side
+
+The client tints each listed nearby player red or blue according to its team.
+
+## Structure
+
+| Index | Length | Data Type | Value | Description |
+|-------|--------|-----------|-------|-------------|
+| 0 | 1 | Byte | 0xC1 | [Packet type](PacketTypes.md) |
+| 1 | 1 | Byte | | Packet header - length of the packet |
+| 2 | 1 | Byte | 0xFC | Packet header - packet type identifier |
+| 3 | 1 | Byte | | Count; The number of player-team entries which follow. |
+| 4 | PlayerTeam.Length * Count | Array of PlayerTeam | | Players |
+
+### PlayerTeam Structure
+
+Maps a player's network id to its Heykel Savasi team.
+
+Length: 3 Bytes
+
+| Index | Length | Data Type | Value | Description |
+|-------|--------|-----------|-------|-------------|
+| 0 | 2 | ShortBigEndian | | PlayerId; The network id of the player (same id used in the viewport/appearance packets). |
+| 2 | 1 | Byte | | Team; 1 = red, 2 = blue. |
\ No newline at end of file
diff --git a/docs/Packets/ServerToClient.md b/docs/Packets/ServerToClient.md
index 8fa178f..84cd066 100644
--- a/docs/Packets/ServerToClient.md
+++ b/docs/Packets/ServerToClient.md
@@ -243,3 +243,4 @@
* [C3 F9 01 - OpenNpcDialog (by server)](C3-F9-01-OpenNpcDialog_by-server.md)
* [C1 FA - HeykelSavasiOpenTeamPanel (by server)](C1-FA-HeykelSavasiOpenTeamPanel_by-server.md)
* [C1 FB - HeykelSavasiHudState (by server)](C1-FB-HeykelSavasiHudState_by-server.md)
+ * [C1 FC - HeykelSavasiTeamRoster (by server)](C1-FC-HeykelSavasiTeamRoster_by-server.md)
diff --git a/src/GameLogic/MiniGames/HeykelSavasiContext.cs b/src/GameLogic/MiniGames/HeykelSavasiContext.cs
index e17dca3..badc021 100644
--- a/src/GameLogic/MiniGames/HeykelSavasiContext.cs
+++ b/src/GameLogic/MiniGames/HeykelSavasiContext.cs
@@ -218,31 +218,6 @@ public class HeykelSavasiContext : MiniGameContext
///
public override bool AllowPlayerKilling => this.State == MiniGameState.Playing;
- ///
- ///
- /// Disallows wings and capes (see ) once the battle has started
- /// (), so a player cannot re-equip one after
- /// auto-unequipped it at battle start (see ).
- /// This check is deliberately scoped to only (not
- /// or ): calls this method (via its
- /// private AreEquippedItemsAllowedAsync) while is still
- /// to decide whether to REJECT entry outright for a disallowed equipped item -
- /// which we do not want here (a player wearing wings should still be able to join; they get unequipped
- /// automatically once the battle starts). Scoping the check to keeps entry
- /// working while still blocking any (re-)equip attempt during the battle itself, via the other call site of
- /// this method in MoveItemAction.CanMoveAsync (which checks player.CurrentMiniGame.IsItemAllowedToEquip
- /// on every equip attempt, regardless of state).
- ///
- public override bool IsItemAllowedToEquip(Item item)
- {
- if (this.State == MiniGameState.Playing && IsWingOrCape(item))
- {
- return false;
- }
-
- return base.IsItemAllowedToEquip(item);
- }
-
///
/// Gets the current number of players assigned to the given team.
///
@@ -405,6 +380,9 @@ public class HeykelSavasiContext : MiniGameContext
{
if (args.Object is Player player)
{
+ // Revert the uniform battle form before dropping the player from the roster, so a player who
+ // leaves the event map mid-battle returns to their normal appearance.
+ await this.RemoveTransformAsync(player).ConfigureAwait(false);
this.RemoveTeam(player);
}
@@ -431,13 +409,18 @@ public class HeykelSavasiContext : MiniGameContext
await player.WarpToAsync(this.GetTeamSpawnGate(this.GetTeam(player))).ConfigureAwait(false);
}
- // Players must be wingless for the battle; auto-unequip any wing/cape now that the entrance is closed
- // and the teams are final (see IsItemAllowedToEquip/IsWingOrCape/UnequipWingsAndCapesAsync).
+ // Transform every participant into a single uniform battle form so their wings/mount/pet are
+ // visually hidden and everyone shares the same base skin; the client adds the red/blue tint on top
+ // via the per-player team roster (IHeykelSavasiTeamRosterPlugIn). Do this after the warp so the
+ // appearance broadcast reaches the players' new (base map) scope.
foreach (var player in players)
{
- await this.UnequipWingsAndCapesAsync(player).ConfigureAwait(false);
+ await this.TransformAsync(player).ConfigureAwait(false);
}
+ // Re-show the HUD panel promptly after the battle-start map change.
+ await this.BroadcastHudStateAsync().ConfigureAwait(false);
+
// Spawn the first (outermost) statue of each team's line; the opposing team attacks it.
await this.SpawnStatueAsync(HeykelSavasiTeam.Red, 0).ConfigureAwait(false);
await this.SpawnStatueAsync(HeykelSavasiTeam.Blue, 0).ConfigureAwait(false);
@@ -474,6 +457,12 @@ public class HeykelSavasiContext : MiniGameContext
}
}
+ // Revert the uniform battle form applied in OnGameStartAsync so players return to their normal appearance.
+ foreach (var player in finishers)
+ {
+ await this.RemoveTransformAsync(player).ConfigureAwait(false);
+ }
+
// Final HUD broadcast: by now MiniGameContext.State is already MiniGameState.Ended (StopAsync sets it
// before calling this method), so ComputeHudPhaseAndRemaining naturally reports phase 3/0 seconds. This
// is needed because the periodic loop (HudBroadcastLoopAsync) stops exactly at this same state
@@ -587,69 +576,6 @@ public class HeykelSavasiContext : MiniGameContext
_ => HeykelSavasiTeam.None,
};
- ///
- /// Determines whether is a wing or a cape, which are disallowed during the battle
- /// (see ) and auto-unequipped at battle start (see
- /// ).
- ///
- /// The item to check.
- /// true if is a wing or cape; otherwise, false.
- ///
- /// Almost all wings AND capes share ItemDefinition.Group 12 (see
- /// Persistence.Initialization.VersionSeasonSix.Items.Wings.CreateWing, which sets wing.Group = 12;
- /// for every wing/cape it creates); the single exception is "Cape of Lord" (group 12, number 30 at creation),
- /// which is deliberately reassigned to group 13 right after creation (capeOfLord.Group = 13; in
- /// Wings.Initialize) so it is special-cased here by its fixed (group, number) pair.
- ///
- private static bool IsWingOrCape(Item item)
- {
- if (item.Definition is not { } definition)
- {
- return false;
- }
-
- return (definition.Group, definition.Number) switch
- {
- (12, _) => true, // Wings, including most capes (Cape of Fighter/Emperor/Overrule, Poison/Warrior Cape, ...).
- (13, 30) => true, // Cape of Lord, the one cape reassigned to its own item group.
- _ => false,
- };
- }
-
- ///
- /// Auto-unequips any wing or cape (see ) currently equipped by ,
- /// moving it to a free general inventory slot so its stats/appearance are removed (see
- /// ), without deleting it. Called from
- /// once the battle starts. If no free inventory slot is available, the item is
- /// deliberately left equipped (logged) rather than risking data loss.
- ///
- /// The player whose equipped wings/capes should be unequipped.
- private async ValueTask UnequipWingsAndCapesAsync(Player player)
- {
- if (player.Inventory is not { } inventory)
- {
- return;
- }
-
- // Snapshot first: EquippedItems is a live view over the equip slots, and RemoveItemAsync below mutates it.
- var wingItems = inventory.EquippedItems.Where(IsWingOrCape).ToList();
- foreach (var item in wingItems)
- {
- var freeSlot = inventory.CheckInvSpace(item);
- if (freeSlot is null)
- {
- this.Logger.LogWarning("{context}: Player {player} has no free inventory slot to unequip {item}; leaving it equipped.", this, player, item);
- continue;
- }
-
- await inventory.RemoveItemAsync(item).ConfigureAwait(false);
- if (!await inventory.AddItemAsync(freeSlot.Value, item).ConfigureAwait(false))
- {
- this.Logger.LogError("{context}: Failed to move unequipped item {item} of player {player} to inventory slot {slot} after removal from the equip slot; the item may now be lost.", this, item, player, freeSlot.Value);
- }
- }
- }
-
///
/// Pure state transition shared by the runtime death handler () and the
/// test-only : records the attacker's progress and, once
@@ -857,9 +783,69 @@ public class HeykelSavasiContext : MiniGameContext
}
}
- /// Reapplies the team's earned buffs to a player who just (re)spawned.
+ ///
+ /// Reapplies the team's earned buffs to a player who just (re)spawned, and - while the battle is running -
+ /// re-transforms them into the uniform battle form (death clears the transformation skin) and re-shows the
+ /// HUD panel promptly after the respawn map change.
+ ///
/// The player who (re)spawned.
- public ValueTask OnPlayerRespawnedAsync(Player player) => this.ReapplyBuffsAsync(player);
+ public async ValueTask OnPlayerRespawnedAsync(Player player)
+ {
+ await this.ReapplyBuffsAsync(player).ConfigureAwait(false);
+
+ if (this.State == MiniGameState.Playing)
+ {
+ await this.TransformAsync(player).ConfigureAwait(false);
+ await this.BroadcastHudStateAsync().ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// The of the transformation skin every participant is turned into
+ /// for the duration of the battle: 14 = Skeleton Warrior, a caped, walking humanoid soldier that reads as a
+ /// reasonable uniform battle form and hides the player's wings/mount/pet. The red/blue distinction is added
+ /// separately by the client via the per-player team roster ().
+ ///
+ private const short EventFormSkin = 14;
+
+ ///
+ /// Transforms into the uniform battle form, mirroring
+ /// the exact mechanism of SkinChatCommandPlugIn (the /skin command): it composes an
+ /// element onto the player's
+ /// attribute and sets the attribute value, which fires Player.OnTransformationSkinChanged and
+ /// re-broadcasts the player's appearance to observers.
+ ///
+ /// The player to transform.
+ private ValueTask TransformAsync(Player player) => this.SetTransformationSkinAsync(player, EventFormSkin);
+
+ ///
+ /// Reverts 's transformation by resetting to
+ /// 0 (no transformation), which restores the player's normal appearance (wings/mount/pet included). Mirrors
+ /// how /skin 0 removes the added skin element.
+ ///
+ /// The player to revert.
+ private ValueTask RemoveTransformAsync(Player player) => this.SetTransformationSkinAsync(player, 0);
+
+ ///
+ /// Sets 's to exactly
+ /// as SkinChatCommandPlugIn does: clear any previously composed elements, add a single
+ /// element with the target value, then write the attribute value (which is
+ /// what actually triggers the appearance re-broadcast). A value of 0 fully reverts to the natural appearance.
+ ///
+ /// The player whose transformation skin to set.
+ /// The transformation skin number (0 = none/revert).
+ private ValueTask SetTransformationSkinAsync(Player player, short skin)
+ {
+ if (player.Attributes is { } attributes
+ && attributes.GetComposableAttribute(Stats.TransformationSkin) is { } attribute)
+ {
+ attribute.Elements.ToList().ForEach(attribute.RemoveElement);
+ attribute.AddElement(attributes.CreateElement(new MUnique.OpenMU.Persistence.BasicModel.PowerUpDefinitionValue { AggregateType = MUnique.OpenMU.AttributeSystem.AggregateType.AddRaw, Value = skin }, Stats.TransformationSkin));
+ attributes[Stats.TransformationSkin] = skin;
+ }
+
+ return ValueTask.CompletedTask;
+ }
///
/// Periodically (about once per second) broadcasts the Heykel Savasi HUD state
@@ -884,6 +870,7 @@ public class HeykelSavasiContext : MiniGameContext
do
{
await this.BroadcastHudStateAsync().ConfigureAwait(false);
+ await this.BroadcastTeamRosterAsync().ConfigureAwait(false);
}
while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false));
}
@@ -917,6 +904,23 @@ public class HeykelSavasiContext : MiniGameContext
.AsTask()).ConfigureAwait(false);
}
+ ///
+ /// Sends the current team roster (every participating player's network id and team) to every entered
+ /// player via , so the client can tint each nearby event player
+ /// red or blue. Broadcast on the same ~1s cadence as the HUD state (see ).
+ ///
+ private async ValueTask BroadcastTeamRosterAsync()
+ {
+ var entries = this._teams
+ .Where(kv => kv.Value != HeykelSavasiTeam.None)
+ .Select(kv => (kv.Key.Id, (byte)kv.Value))
+ .ToList();
+
+ await this.ForEachPlayerAsync(player => player.InvokeViewPlugInAsync(p =>
+ p.UpdateTeamRosterAsync(entries))
+ .AsTask()).ConfigureAwait(false);
+ }
+
///
/// Determines the current HUD phase and the estimated number of seconds remaining in it, purely from
/// and wall-clock timestamps recorded (lazily, on first observation) at
diff --git a/src/GameLogic/Views/MiniGames/IHeykelSavasiTeamRosterPlugIn.cs b/src/GameLogic/Views/MiniGames/IHeykelSavasiTeamRosterPlugIn.cs
new file mode 100644
index 0000000..5b57acc
--- /dev/null
+++ b/src/GameLogic/Views/MiniGames/IHeykelSavasiTeamRosterPlugIn.cs
@@ -0,0 +1,20 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameLogic.Views.MiniGames;
+
+///
+/// Interface of a view whose implementation sends the Heykel Savasi (Statue War) team roster, mapping every
+/// participating player's network id to its team so the client can tint each nearby player red or blue.
+///
+public interface IHeykelSavasiTeamRosterPlugIn : IViewPlugIn
+{
+ ///
+ /// Sends the current team roster.
+ ///
+ ///
+ /// The (player network id, team) pairs of all participating players. Team is 1 = red, 2 = blue.
+ ///
+ ValueTask UpdateTeamRosterAsync(IReadOnlyList<(ushort PlayerId, byte Team)> entries);
+}
diff --git a/src/GameServer/RemoteView/MiniGames/HeykelSavasiTeamRosterPlugIn.cs b/src/GameServer/RemoteView/MiniGames/HeykelSavasiTeamRosterPlugIn.cs
new file mode 100644
index 0000000..9eb3879
--- /dev/null
+++ b/src/GameServer/RemoteView/MiniGames/HeykelSavasiTeamRosterPlugIn.cs
@@ -0,0 +1,63 @@
+//
+// Licensed under the MIT License. See LICENSE file in the project root for full license information.
+//
+
+namespace MUnique.OpenMU.GameServer.RemoteView.MiniGames;
+
+using System.Runtime.InteropServices;
+using MUnique.OpenMU.GameLogic;
+using MUnique.OpenMU.GameLogic.Views.MiniGames;
+using MUnique.OpenMU.Network;
+using MUnique.OpenMU.Network.Packets.ServerToClient;
+using MUnique.OpenMU.PlugIns;
+
+///
+/// The default implementation of the which is forwarding the
+/// team roster to the game client with a data packet (code 0xFC).
+///
+[PlugIn]
+[Guid("b1f2a6d4-1c7e-4d3a-9b0e-6a2f5c8e4d11")]
+public class HeykelSavasiTeamRosterPlugIn : IHeykelSavasiTeamRosterPlugIn
+{
+ private readonly RemotePlayer _player;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The player.
+ public HeykelSavasiTeamRosterPlugIn(RemotePlayer player) => this._player = player;
+
+ ///
+ public async ValueTask UpdateTeamRosterAsync(IReadOnlyList<(ushort PlayerId, byte Team)> entries)
+ {
+ var connection = this._player.Connection;
+ if (connection is null)
+ {
+ return;
+ }
+
+ // A byte Count field caps the roster at 255 entries; the event never has that many players.
+ var count = entries.Count > byte.MaxValue ? byte.MaxValue : entries.Count;
+
+ int Write()
+ {
+ var size = HeykelSavasiTeamRosterRef.GetRequiredSize(count);
+ var span = connection.Output.GetSpan(size)[..size];
+ var packet = new HeykelSavasiTeamRosterRef(span)
+ {
+ Count = (byte)count,
+ };
+
+ for (var i = 0; i < count; i++)
+ {
+ var block = packet[i];
+ block.PlayerId = entries[i].PlayerId;
+ block.Team = entries[i].Team;
+ }
+
+ return size;
+ }
+
+ await connection.SendAsync(Write).ConfigureAwait(false);
+ }
+}
diff --git a/src/Network/Packets/ServerToClient/ServerToClientPackets.cs b/src/Network/Packets/ServerToClient/ServerToClientPackets.cs
index 4d5d3b0..9b3abf0 100644
--- a/src/Network/Packets/ServerToClient/ServerToClientPackets.cs
+++ b/src/Network/Packets/ServerToClient/ServerToClientPackets.cs
@@ -30892,6 +30892,133 @@ public readonly struct HeykelSavasiHudState
/// The packet as byte span.
public static implicit operator Memory(HeykelSavasiHudState packet) => packet._data;
}
+
+
+///
+/// Is sent by the server when: Periodically (about once every one to two seconds) while the Heykel Savasi (Statue War) event is open for registration, in its pre-battle countdown, or being played.
+/// Causes reaction on client side: The client tints each listed nearby player red or blue according to its team.
+///
+public readonly struct HeykelSavasiTeamRoster
+{
+ private readonly Memory _data;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The underlying data.
+ public HeykelSavasiTeamRoster(Memory data)
+ : this(data, true)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The underlying data.
+ /// If set to true, the header data is automatically initialized and written to the underlying span.
+ private HeykelSavasiTeamRoster(Memory data, bool initialize)
+ {
+ this._data = data;
+ if (initialize)
+ {
+ var header = this.Header;
+ header.Type = HeaderType;
+ header.Code = Code;
+ header.Length = (byte)data.Length;
+ }
+ }
+
+ ///
+ /// Gets the header type of this data packet.
+ ///
+ public static byte HeaderType => 0xC1;
+
+ ///
+ /// Gets the operation code of this data packet.
+ ///
+ public static byte Code => 0xFC;
+
+ ///
+ /// Gets the header of this packet.
+ ///
+ public C1Header Header => new (this._data);
+
+ ///
+ /// Gets or sets the number of player-team entries which follow.
+ ///
+ public byte Count
+ {
+ get => this._data.Span[3];
+ set => this._data.Span[3] = value;
+ }
+
+ ///
+ /// Gets the of the specified index.
+ ///
+ public PlayerTeam this[int index] => new (this._data.Slice(4 + index * PlayerTeam.Length));
+
+ ///
+ /// Performs an implicit conversion from a Memory of bytes to a .
+ ///
+ /// The packet as span.
+ /// The packet as struct.
+ public static implicit operator HeykelSavasiTeamRoster(Memory packet) => new (packet, false);
+
+ ///
+ /// Performs an implicit conversion from to a Memory of bytes.
+ ///
+ /// The packet as struct.
+ /// The packet as byte span.
+ public static implicit operator Memory(HeykelSavasiTeamRoster packet) => packet._data;
+
+ ///
+ /// Calculates the size of the packet for the specified count of .
+ ///
+ /// The count of from which the size will be calculated.
+
+ public static int GetRequiredSize(int playersCount) => playersCount * PlayerTeam.Length + 4;
+
+
+///
+/// Maps a player's network id to its Heykel Savasi team..
+///
+public readonly struct PlayerTeam
+{
+ private readonly Memory _data;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The underlying data.
+ public PlayerTeam(Memory data)
+ {
+ this._data = data;
+ }
+
+ ///
+ /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed.
+ ///
+ public static int Length => 3;
+
+ ///
+ /// Gets or sets the network id of the player (same id used in the viewport/appearance packets).
+ ///
+ public ushort PlayerId
+ {
+ get => ReadUInt16BigEndian(this._data.Span);
+ set => WriteUInt16BigEndian(this._data.Span, value);
+ }
+
+ ///
+ /// Gets or sets 1 = red, 2 = blue.
+ ///
+ public byte Team
+ {
+ get => this._data.Span[2];
+ set => this._data.Span[2] = value;
+ }
+}
+}
///
/// Defines the role of a guild member.
///
diff --git a/src/Network/Packets/ServerToClient/ServerToClientPackets.xml b/src/Network/Packets/ServerToClient/ServerToClientPackets.xml
index 02d62f5..48a6f7a 100644
--- a/src/Network/Packets/ServerToClient/ServerToClientPackets.xml
+++ b/src/Network/Packets/ServerToClient/ServerToClientPackets.xml
@@ -11139,6 +11139,50 @@
+
+ C1Header
+ FC
+ HeykelSavasiTeamRoster
+ ServerToClient
+ Periodically (about once every one to two seconds) while the Heykel Savasi (Statue War) event is open for registration, in its pre-battle countdown, or being played.
+ The client tints each listed nearby player red or blue according to its team.
+
+
+ 3
+ Byte
+ Count
+ The number of player-team entries which follow.
+
+
+ 4
+ Structure[]
+ PlayerTeam
+ Players
+ Count
+
+
+
+
+ PlayerTeam
+ Maps a player's network id to its Heykel Savasi team.
+ 3
+
+
+ 0
+ ShortBigEndian
+ PlayerId
+ The network id of the player (same id used in the viewport/appearance packets).
+
+
+ 2
+ Byte
+ Team
+ 1 = red, 2 = blue.
+
+
+
+
+
diff --git a/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs b/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs
index 81eb413..264a238 100644
--- a/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs
+++ b/src/Network/Packets/ServerToClient/ServerToClientPacketsRef.cs
@@ -29255,3 +29255,130 @@ public readonly ref struct HeykelSavasiHudStateRef
/// The packet as byte span.
public static implicit operator Span(HeykelSavasiHudStateRef packet) => packet._data;
}
+
+
+///
+/// Is sent by the server when: Periodically (about once every one to two seconds) while the Heykel Savasi (Statue War) event is open for registration, in its pre-battle countdown, or being played.
+/// Causes reaction on client side: The client tints each listed nearby player red or blue according to its team.
+///
+public readonly ref struct HeykelSavasiTeamRosterRef
+{
+ private readonly Span _data;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The underlying data.
+ public HeykelSavasiTeamRosterRef(Span data)
+ : this(data, true)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The underlying data.
+ /// If set to true, the header data is automatically initialized and written to the underlying span.
+ private HeykelSavasiTeamRosterRef(Span data, bool initialize)
+ {
+ this._data = data;
+ if (initialize)
+ {
+ var header = this.Header;
+ header.Type = HeaderType;
+ header.Code = Code;
+ header.Length = (byte)data.Length;
+ }
+ }
+
+ ///
+ /// Gets the header type of this data packet.
+ ///
+ public static byte HeaderType => 0xC1;
+
+ ///
+ /// Gets the operation code of this data packet.
+ ///
+ public static byte Code => 0xFC;
+
+ ///
+ /// Gets the header of this packet.
+ ///
+ public C1HeaderRef Header => new (this._data);
+
+ ///
+ /// Gets or sets the number of player-team entries which follow.
+ ///
+ public byte Count
+ {
+ get => this._data[3];
+ set => this._data[3] = value;
+ }
+
+ ///
+ /// Gets the of the specified index.
+ ///
+ public PlayerTeamRef this[int index] => new (this._data[(4 + index * PlayerTeamRef.Length)..]);
+
+ ///
+ /// Performs an implicit conversion from a Span of bytes to a .
+ ///
+ /// The packet as span.
+ /// The packet as struct.
+ public static implicit operator HeykelSavasiTeamRosterRef(Span packet) => new (packet, false);
+
+ ///
+ /// Performs an implicit conversion from to a Span of bytes.
+ ///
+ /// The packet as struct.
+ /// The packet as byte span.
+ public static implicit operator Span(HeykelSavasiTeamRosterRef packet) => packet._data;
+
+ ///
+ /// Calculates the size of the packet for the specified count of .
+ ///
+ /// The count of from which the size will be calculated.
+
+ public static int GetRequiredSize(int playersCount) => playersCount * PlayerTeamRef.Length + 4;
+
+
+///
+/// Maps a player's network id to its Heykel Savasi team..
+///
+public readonly ref struct PlayerTeamRef
+{
+ private readonly Span _data;
+
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ /// The underlying data.
+ public PlayerTeamRef(Span data)
+ {
+ this._data = data;
+ }
+
+ ///
+ /// Gets the initial length of this data packet. When the size is dynamic, this value may be bigger than actually needed.
+ ///
+ public static int Length => 3;
+
+ ///
+ /// Gets or sets the network id of the player (same id used in the viewport/appearance packets).
+ ///
+ public ushort PlayerId
+ {
+ get => ReadUInt16BigEndian(this._data);
+ set => WriteUInt16BigEndian(this._data, value);
+ }
+
+ ///
+ /// Gets or sets 1 = red, 2 = blue.
+ ///
+ public byte Team
+ {
+ get => this._data[2];
+ set => this._data[2] = value;
+ }
+}
+}
diff --git a/src/Persistence/Initialization/VersionSeasonSix/Maps/HeykelSavasiMap.cs b/src/Persistence/Initialization/VersionSeasonSix/Maps/HeykelSavasiMap.cs
index bca3769..132caaa 100644
--- a/src/Persistence/Initialization/VersionSeasonSix/Maps/HeykelSavasiMap.cs
+++ b/src/Persistence/Initialization/VersionSeasonSix/Maps/HeykelSavasiMap.cs
@@ -77,8 +77,8 @@ internal class HeykelSavasiMap : BaseMapInitializer
var statueAttributes = new Dictionary
{
{ Stats.Level, 100 },
- { Stats.MaximumHealth, 200000 }, // tuning
- { Stats.DefenseBase, 200 },
+ { Stats.MaximumHealth, 3_000_000 }, // tuning: big HP piƱata that takes sustained effort to break
+ { Stats.DefenseBase, 500 },
{ Stats.DefenseRatePvm, 100 },
};
statue.AddAttributes(statueAttributes, this.Context, this.GameConfiguration);