ccee580789
- On leaving PvP, LoadSavedHotbar for all 10 bars (via TryRestorePvEHotbarsAfterLeavePvP in Framework update) and re-apply for ~2s so live Hotbars show PvE - GetSlotData always reads from live StandardHotbars so combo updates (e.g. Pictomancer) and icons work normally - Misc: Show Action ID option in Misc -> Tooltips; hotbar/party cooldown tooltips pass TooltipIdKind for Action vs Status IDs Co-authored-by: Cursor <cursoragent@cursor.com>
593 lines
23 KiB
C#
593 lines
23 KiB
C#
using Dalamud.Game.ClientState.Objects.Enums;
|
|
using Dalamud.Game.ClientState.Objects.SubKinds;
|
|
using Dalamud.Game.ClientState.Objects.Types;
|
|
using Dalamud.Utility;
|
|
using HSUI.Config;
|
|
using HSUI.Enums;
|
|
using HSUI.Helpers;
|
|
using HSUI.Interface.GeneralElements;
|
|
using Dalamud.Bindings.ImGui;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Numerics;
|
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
|
using LuminaStatus = Lumina.Excel.Sheets.Status;
|
|
using StatusStruct = FFXIVClientStructs.FFXIV.Client.Game.Status;
|
|
|
|
namespace HSUI.Interface.StatusEffects
|
|
{
|
|
public class StatusEffectsListHud : ParentAnchoredDraggableHudElement, IHudElementWithActor, IHudElementWithAnchorableParent, IHudElementWithPreview, IHudElementWithMouseOver, IHudElementWithVisibilityConfig
|
|
{
|
|
protected StatusEffectsListConfig Config => (StatusEffectsListConfig)_config;
|
|
public VisibilityConfig? VisibilityConfig => Config is UnitFrameStatusEffectsListConfig config ? config.VisibilityConfig : null;
|
|
|
|
private LayoutInfo _layoutInfo;
|
|
|
|
internal static int StatusEffectListsSize = 60;
|
|
private StatusStruct[]? _fakeEffects = null;
|
|
|
|
private LabelHud _durationLabel;
|
|
private LabelHud _stacksLabel;
|
|
public IGameObject? Actor { get; set; } = null;
|
|
|
|
private bool _wasHovering = false;
|
|
private bool NeedsSpecialInput => !ClipRectsHelper.Instance.Enabled || ClipRectsHelper.Instance.Mode == WindowClippingMode.Performance;
|
|
|
|
protected override bool AnchorToParent => Config is UnitFrameStatusEffectsListConfig config ? config.AnchorToUnitFrame : false;
|
|
protected override DrawAnchor ParentAnchor => Config is UnitFrameStatusEffectsListConfig config ? config.UnitFrameAnchor : DrawAnchor.Center;
|
|
|
|
public StatusEffectsListHud(StatusEffectsListConfig config, string? displayName = null) : base(config, displayName)
|
|
{
|
|
_config.ValueChangeEvent += OnConfigPropertyChanged;
|
|
|
|
_durationLabel = new LabelHud(config.IconConfig.DurationLabelConfig);
|
|
_stacksLabel = new LabelHud(config.IconConfig.StacksLabelConfig);
|
|
|
|
UpdatePreview();
|
|
}
|
|
|
|
~StatusEffectsListHud()
|
|
{
|
|
_config.ValueChangeEvent -= OnConfigPropertyChanged;
|
|
}
|
|
|
|
public void StopPreview()
|
|
{
|
|
Config.Preview = false;
|
|
UpdatePreview();
|
|
}
|
|
|
|
protected override (List<Vector2>, List<Vector2>) ChildrenPositionsAndSizes()
|
|
{
|
|
Vector2 pos = LayoutHelper.CalculateStartPosition(Config.Position, Config.Size, LayoutHelper.GrowthDirectionsFromIndex(Config.Directions));
|
|
return (new List<Vector2>() { pos + Config.Size / 2f }, new List<Vector2>() { Config.Size });
|
|
}
|
|
|
|
public void StopMouseover()
|
|
{
|
|
if (_wasHovering && NeedsSpecialInput)
|
|
{
|
|
InputsHelper.Instance.StopHandlingInputs();
|
|
_wasHovering = false;
|
|
}
|
|
}
|
|
|
|
private uint CalculateLayout(List<StatusEffectData> list)
|
|
{
|
|
var effectCount = (uint)list.Count;
|
|
var count = Config.Limit >= 0 ? Math.Min((uint)Config.Limit, effectCount) : effectCount;
|
|
|
|
if (count <= 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
_layoutInfo = LayoutHelper.CalculateLayout(
|
|
Config.Size,
|
|
Config.IconConfig.Size,
|
|
count,
|
|
Config.IconPadding,
|
|
LayoutHelper.GetFillsRowsFirst(Config.FillRowsFirst, LayoutHelper.GrowthDirectionsFromIndex(Config.Directions))
|
|
);
|
|
|
|
return count;
|
|
}
|
|
|
|
protected string GetStatusActorName(StatusStruct status)
|
|
{
|
|
var character = Plugin.ObjectTable.SearchById(status.SourceObject.Id);
|
|
return character == null ? "" : character.Name.ToString();
|
|
}
|
|
|
|
protected virtual List<StatusEffectData> StatusEffectsData()
|
|
{
|
|
var list = StatusEffectDataList(Actor);
|
|
|
|
// sort by duration
|
|
if (Config.SortByDuration)
|
|
{
|
|
list.Sort((a, b) =>
|
|
{
|
|
float aTime = a.Data.IsPermanent || a.Data.IsFcBuff ? float.MaxValue : a.Status.RemainingTime;
|
|
float bTime = b.Data.IsPermanent || b.Data.IsFcBuff ? float.MaxValue : b.Status.RemainingTime;
|
|
|
|
if (Config.DurationSortType == StatusEffectDurationSortType.Ascending)
|
|
{
|
|
return aTime.CompareTo(bTime);
|
|
}
|
|
else
|
|
{
|
|
return bTime.CompareTo(aTime);
|
|
}
|
|
});
|
|
}
|
|
// show mine or permanent first
|
|
else if (Config.ShowMineFirst || Config.ShowPermanentFirst)
|
|
{
|
|
return OrderByMineOrPermanentFirst(list);
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
protected unsafe List<StatusEffectData> StatusEffectDataList(IGameObject? actor)
|
|
{
|
|
List<StatusEffectData> list = new List<StatusEffectData>();
|
|
IPlayerCharacter? player = Plugin.ObjectTable.LocalPlayer;
|
|
IBattleChara? character = null;
|
|
int count = StatusEffectListsSize;
|
|
|
|
if (_fakeEffects == null)
|
|
{
|
|
if (actor == null || actor is not IBattleChara battleChara)
|
|
{
|
|
return list;
|
|
}
|
|
|
|
if (Config.HideWhenDead && (battleChara.IsDead || battleChara.CurrentHp <= 0))
|
|
{
|
|
return list;
|
|
}
|
|
|
|
character = (IBattleChara)actor;
|
|
|
|
try
|
|
{
|
|
count = Math.Min(count, character.StatusList.Length);
|
|
}
|
|
catch { }
|
|
}
|
|
else
|
|
{
|
|
count = Config.Limit == -1 ? _fakeEffects.Length : Math.Min(Config.Limit, _fakeEffects.Length);
|
|
}
|
|
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
// status
|
|
StatusStruct* status = null;
|
|
|
|
if (_fakeEffects != null)
|
|
{
|
|
var fakeStruct = _fakeEffects![i];
|
|
status = &fakeStruct;
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
status = character?.StatusList[i] == null ? null : (StatusStruct*)character.StatusList[i]!.Address;
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
if (status == null || status->StatusId == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// data
|
|
LuminaStatus? data = null;
|
|
|
|
if (_fakeEffects != null)
|
|
{
|
|
data = Plugin.DataManager.GetExcelSheet<LuminaStatus>()?.GetRow(status->StatusId);
|
|
}
|
|
else
|
|
{
|
|
try
|
|
{
|
|
data = character?.StatusList[i]?.GameData.Value;
|
|
} catch { }
|
|
}
|
|
|
|
if (data == null || !data.HasValue)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// filter "invisible" status effects
|
|
if (data.Value.Icon == 0 || data.Value.Name.ToString().Length == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// dont filter anything on preview mode
|
|
if (_fakeEffects != null)
|
|
{
|
|
list.Add(new StatusEffectData(*status, data.Value));
|
|
continue;
|
|
}
|
|
|
|
// buffs
|
|
if (!Config.ShowBuffs && data.Value.StatusCategory == 1)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// debuffs
|
|
if (!Config.ShowDebuffs && data.Value.StatusCategory != 1)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// permanent
|
|
if (!Config.ShowPermanentEffects && data.Value.IsPermanent)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// only mine
|
|
var mine = player?.GameObjectId == status->SourceObject.Id;
|
|
|
|
if (Config.IncludePetAsOwn)
|
|
{
|
|
mine = player?.GameObjectId == status->SourceObject.Id || IsStatusFromPlayerPet(*status);
|
|
}
|
|
|
|
if (Config.ShowOnlyMine && !mine)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// blacklist
|
|
if (Config.BlacklistConfig.Enabled && !Config.BlacklistConfig.StatusAllowed(data.Value))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
list.Add(new StatusEffectData(*status, data.Value));
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
protected bool IsStatusFromPlayerPet(StatusStruct status)
|
|
{
|
|
var buddy = Plugin.BuddyList.PetBuddy;
|
|
|
|
if (buddy == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return buddy.EntityId == status.SourceObject.Id;
|
|
}
|
|
|
|
protected List<StatusEffectData> OrderByMineOrPermanentFirst(List<StatusEffectData> list)
|
|
{
|
|
var player = Plugin.ObjectTable.LocalPlayer;
|
|
if (player == null)
|
|
{
|
|
return list;
|
|
}
|
|
|
|
if (Config.ShowMineFirst && Config.ShowPermanentFirst)
|
|
{
|
|
return list.OrderByDescending(x => x.Status.SourceObject.Id == player.GameObjectId && x.Data.IsPermanent || x.Data.IsFcBuff)
|
|
.ThenByDescending(x => x.Status.SourceObject.Id == player.GameObjectId)
|
|
.ThenByDescending(x => x.Data.IsPermanent)
|
|
.ThenByDescending(x => x.Data.IsFcBuff)
|
|
.ToList();
|
|
}
|
|
else if (Config.ShowMineFirst && !Config.ShowPermanentFirst)
|
|
{
|
|
return list.OrderByDescending(x => x.Status.SourceObject.Id == player.GameObjectId)
|
|
.ToList();
|
|
}
|
|
else if (!Config.ShowMineFirst && Config.ShowPermanentFirst)
|
|
{
|
|
return list.OrderByDescending(x => x.Data.IsPermanent)
|
|
.ThenByDescending(x => x.Data.IsFcBuff)
|
|
.ToList();
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
public override void DrawChildren(Vector2 origin)
|
|
{
|
|
if (!Config.Enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_fakeEffects == null && (Actor == null || Actor.ObjectKind != ObjectKind.Player && Actor.ObjectKind != ObjectKind.BattleNpc))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// calculate layout
|
|
List<StatusEffectData> list = StatusEffectsData();
|
|
|
|
// area
|
|
GrowthDirections growthDirections = LayoutHelper.GrowthDirectionsFromIndex(Config.Directions);
|
|
Vector2 position = origin + GetAnchoredPosition(Config.Position, Config.Size, DrawAnchor.TopLeft);
|
|
Vector2 areaPos = LayoutHelper.CalculateStartPosition(position, Config.Size, growthDirections);
|
|
Vector2 margin = new Vector2(14, 10);
|
|
|
|
ImDrawListPtr drawList = ImGui.GetWindowDrawList();
|
|
|
|
// no need to do anything else if there are no effects
|
|
if (list.Count == 0)
|
|
{
|
|
if (_wasHovering && NeedsSpecialInput)
|
|
{
|
|
_wasHovering = false;
|
|
InputsHelper.Instance.StopHandlingInputs();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// calculate icon positions
|
|
uint count = CalculateLayout(list);
|
|
var (iconPositions, minPos, maxPos) = LayoutHelper.CalculateIconPositions(
|
|
growthDirections,
|
|
count,
|
|
position,
|
|
Config.Size,
|
|
Config.IconConfig.Size,
|
|
Config.IconPadding,
|
|
LayoutHelper.GetFillsRowsFirst(Config.FillRowsFirst, growthDirections),
|
|
_layoutInfo
|
|
);
|
|
|
|
// window
|
|
// imgui clips the left and right borders inside windows for some reason
|
|
// we make the window bigger so the actual drawable size is the expected one
|
|
Vector2 windowPos = minPos - margin;
|
|
Vector2 windowSize = maxPos - minPos;
|
|
|
|
AddDrawAction(Config.StrataLevel, () =>
|
|
{
|
|
DrawHelper.DrawInWindow(ID, windowPos, windowSize + margin * 2, !Config.DisableInteraction, (drawList) =>
|
|
{
|
|
// area
|
|
if (Config.Preview)
|
|
{
|
|
drawList.AddRectFilled(areaPos, areaPos + Config.Size, 0x88000000);
|
|
}
|
|
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
Vector2 iconPos = iconPositions[i];
|
|
var statusEffectData = list[i];
|
|
|
|
// shadow
|
|
if (Config.IconConfig.ShadowConfig! != null && Config.IconConfig.ShadowConfig.Enabled)
|
|
{
|
|
// Right Side
|
|
drawList.AddRectFilled(iconPos + new Vector2(Config.IconConfig.Size.X, Config.IconConfig.ShadowConfig.Offset), iconPos + Config.IconConfig.Size + new Vector2(Config.IconConfig.ShadowConfig.Offset, Config.IconConfig.ShadowConfig.Offset) + new Vector2(Config.IconConfig.ShadowConfig.Thickness - 1, Config.IconConfig.ShadowConfig.Thickness - 1), Config.IconConfig.ShadowConfig.Color.Base);
|
|
|
|
// Bottom Size
|
|
drawList.AddRectFilled(iconPos + new Vector2(Config.IconConfig.ShadowConfig.Offset, Config.IconConfig.Size.Y), iconPos + Config.IconConfig.Size + new Vector2(Config.IconConfig.ShadowConfig.Offset, Config.IconConfig.ShadowConfig.Offset) + new Vector2(Config.IconConfig.ShadowConfig.Thickness - 1, Config.IconConfig.ShadowConfig.Thickness - 1), Config.IconConfig.ShadowConfig.Color.Base);
|
|
}
|
|
|
|
// icon
|
|
var cropIcon = Config.IconConfig.CropIcon;
|
|
int stackCount = cropIcon ? 1 : statusEffectData.Data.MaxStacks > 0 ? statusEffectData.Status.Param : 0;
|
|
DrawHelper.DrawIcon<LuminaStatus>(drawList, statusEffectData.Data, iconPos, Config.IconConfig.Size, false, cropIcon, stackCount);
|
|
|
|
// border
|
|
var borderConfig = GetBorderConfig(statusEffectData);
|
|
if (borderConfig != null && cropIcon)
|
|
{
|
|
drawList.AddRect(iconPos, iconPos + Config.IconConfig.Size, borderConfig.Color.Base, 0, ImDrawFlags.None, borderConfig.Thickness);
|
|
}
|
|
|
|
// Draw dispell indicator above dispellable status effect on uncropped icons
|
|
if (borderConfig != null && !cropIcon && statusEffectData.Data.CanDispel)
|
|
{
|
|
var dispellIndicatorColor = new Vector4(141f / 255f, 206f / 255f, 229f / 255f, 100f / 100f);
|
|
// 24x32
|
|
drawList.AddRectFilled(
|
|
iconPos + new Vector2(Config.IconConfig.Size.X * .07f, Config.IconConfig.Size.Y * .07f),
|
|
iconPos + new Vector2(Config.IconConfig.Size.X * .93f, Config.IconConfig.Size.Y * .14f),
|
|
ImGui.ColorConvertFloat4ToU32(dispellIndicatorColor),
|
|
8f
|
|
);
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
StatusEffectData? hoveringData = null;
|
|
IGameObject? character = Actor;
|
|
|
|
// labels need to be drawn separated since they have their own window for clipping
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
Vector2 iconPos = iconPositions[i];
|
|
StatusEffectData statusEffectData = list[i];
|
|
|
|
// duration
|
|
if (Config.IconConfig.DurationLabelConfig.Enabled &&
|
|
!statusEffectData.Data.IsPermanent &&
|
|
!statusEffectData.Data.IsFcBuff)
|
|
{
|
|
AddDrawAction(Config.IconConfig.DurationLabelConfig.StrataLevel, () =>
|
|
{
|
|
double duration = Math.Round(Math.Abs(statusEffectData.Status.RemainingTime));
|
|
Config.IconConfig.DurationLabelConfig.SetText(Utils.DurationToString(duration));
|
|
_durationLabel.Draw(iconPos, Config.IconConfig.Size, character);
|
|
});
|
|
}
|
|
|
|
// stacks
|
|
if (Config.IconConfig.StacksLabelConfig.Enabled &&
|
|
statusEffectData.Data.MaxStacks > 0 &&
|
|
statusEffectData.Status.Param > 0 &&
|
|
!statusEffectData.Data.IsFcBuff)
|
|
{
|
|
AddDrawAction(Config.IconConfig.StacksLabelConfig.StrataLevel, () =>
|
|
{
|
|
Config.IconConfig.StacksLabelConfig.SetText($"{statusEffectData.Status.Param}");
|
|
_stacksLabel.Draw(iconPos, Config.IconConfig.Size, character);
|
|
});
|
|
}
|
|
|
|
// tooltips / interaction
|
|
if (ImGui.IsMouseHoveringRect(iconPos, iconPos + Config.IconConfig.Size))
|
|
{
|
|
hoveringData = statusEffectData;
|
|
}
|
|
}
|
|
|
|
if (hoveringData.HasValue)
|
|
{
|
|
StatusEffectData data = hoveringData.Value;
|
|
|
|
if (NeedsSpecialInput)
|
|
{
|
|
_wasHovering = true;
|
|
InputsHelper.Instance.StartHandlingInputs();
|
|
}
|
|
|
|
// tooltip
|
|
if (Config.ShowTooltips)
|
|
{
|
|
uint? iconId = data.Data.Icon > 0 ? data.Data.Icon : null;
|
|
TooltipsHelper.Instance.ShowTooltipOnCursor(
|
|
EncryptedStringsHelper.GetString(data.Data.Description.ToDalamudString().ToString()),
|
|
EncryptedStringsHelper.GetString(data.Data.Name.ToString()),
|
|
data.Status.StatusId,
|
|
GetStatusActorName(data.Status),
|
|
iconId,
|
|
HSUI.Helpers.TooltipIdKind.Status
|
|
);
|
|
}
|
|
|
|
bool leftClick = InputsHelper.Instance.HandlingMouseInputs ? InputsHelper.Instance.LeftButtonClicked : ImGui.GetIO().MouseClicked[0];
|
|
bool rightClick = InputsHelper.Instance.HandlingMouseInputs ? InputsHelper.Instance.RightButtonClicked : ImGui.GetIO().MouseClicked[1];
|
|
|
|
// remove buff on right click
|
|
bool isFromPlayer = data.Status.SourceObject.Id == Plugin.ObjectTable.LocalPlayer?.GameObjectId;
|
|
bool isTheEcho = data.Status.SourceObject.Id is 42 or 239;
|
|
|
|
if (data.Data.StatusCategory == 1 && (isFromPlayer || isTheEcho) && rightClick)
|
|
{
|
|
StatusManager.ExecuteStatusOff(data.Status.StatusId, data.Status.SourceObject.ObjectId);
|
|
|
|
if (NeedsSpecialInput)
|
|
{
|
|
_wasHovering = false;
|
|
InputsHelper.Instance.StopHandlingInputs();
|
|
}
|
|
}
|
|
|
|
// automatic add to black list with ctrl+alt+shift click
|
|
if (Config.BlacklistConfig.Enabled &&
|
|
ImGui.GetIO().KeyCtrl && ImGui.GetIO().KeyAlt && ImGui.GetIO().KeyShift && leftClick)
|
|
{
|
|
Config.BlacklistConfig.AddNewEntry(data.Data);
|
|
ConfigurationManager.Instance.ForceNeedsSave();
|
|
|
|
if (NeedsSpecialInput)
|
|
{
|
|
_wasHovering = false;
|
|
InputsHelper.Instance.StopHandlingInputs();
|
|
}
|
|
}
|
|
}
|
|
else if (_wasHovering && NeedsSpecialInput)
|
|
{
|
|
_wasHovering = false;
|
|
InputsHelper.Instance.StopHandlingInputs();
|
|
}
|
|
}
|
|
|
|
public StatusEffectIconBorderConfig? GetBorderConfig(StatusEffectData statusEffectData)
|
|
{
|
|
StatusEffectIconBorderConfig? borderConfig = null;
|
|
|
|
bool isFromPlayerPet = false;
|
|
if (Config.IncludePetAsOwn)
|
|
{
|
|
isFromPlayerPet = IsStatusFromPlayerPet(statusEffectData.Status);
|
|
}
|
|
|
|
if (Config.IconConfig.OwnedBorderConfig.Enabled && (statusEffectData.Status.SourceObject.Id == Plugin.ObjectTable.LocalPlayer?.GameObjectId || isFromPlayerPet))
|
|
{
|
|
borderConfig = Config.IconConfig.OwnedBorderConfig;
|
|
}
|
|
else if (Config.IconConfig.DispellableBorderConfig.Enabled && statusEffectData.Data.CanDispel)
|
|
{
|
|
borderConfig = Config.IconConfig.DispellableBorderConfig;
|
|
}
|
|
else if (Config.IconConfig.BorderConfig.Enabled)
|
|
{
|
|
borderConfig = Config.IconConfig.BorderConfig;
|
|
}
|
|
|
|
return borderConfig;
|
|
}
|
|
|
|
private void OnConfigPropertyChanged(object? sender, OnChangeBaseArgs args)
|
|
{
|
|
if (args.PropertyName == "Preview")
|
|
{
|
|
UpdatePreview();
|
|
}
|
|
}
|
|
|
|
private unsafe void UpdatePreview()
|
|
{
|
|
if (!Config.Preview)
|
|
{
|
|
_fakeEffects = null;
|
|
return;
|
|
}
|
|
|
|
var RNG = new Random((int)ImGui.GetTime());
|
|
_fakeEffects = new StatusStruct[StatusEffectListsSize];
|
|
|
|
for (int i = 0; i < StatusEffectListsSize; i++)
|
|
{
|
|
var fakeStruct = new StatusStruct();
|
|
|
|
// forcing "triplecast" buff first to always be able to test stacks
|
|
fakeStruct.StatusId = i == 0 ? (ushort)1211 : (ushort)RNG.Next(1, 200);
|
|
fakeStruct.RemainingTime = RNG.Next(1, 30);
|
|
fakeStruct.Param = (byte)RNG.Next(1, 3);
|
|
fakeStruct.SourceObject.Id = 0;
|
|
|
|
_fakeEffects[i] = fakeStruct;
|
|
}
|
|
}
|
|
}
|
|
|
|
public struct StatusEffectData
|
|
{
|
|
public StatusStruct Status;
|
|
public LuminaStatus Data;
|
|
|
|
public StatusEffectData(StatusStruct status, LuminaStatus data)
|
|
{
|
|
Status = status;
|
|
Data = data;
|
|
}
|
|
}
|
|
}
|