Initial commit: Tiny Tackle Heroes Unity project
This commit is contained in:
@@ -0,0 +1,874 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BumpCombat.Core;
|
||||
using BumpCombat.Player;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BumpCombat.Combat
|
||||
{
|
||||
[DisallowMultipleComponent]
|
||||
[DefaultExecutionOrder(1000)]
|
||||
public sealed class ArtifactChargeVisual : MonoBehaviour
|
||||
{
|
||||
public const int FrameCount = 6;
|
||||
public const int FlashFrameCount = 4;
|
||||
public const int CellSize = 64;
|
||||
public const float PixelsPerUnit = 32f;
|
||||
public const float ChargeVisualY = 0f;
|
||||
public const float ReadyFlashDuration = 0.14f;
|
||||
public const float ReleaseVisualDuration = 0.24f;
|
||||
public const float SwitchVisualDuration = 0.36f;
|
||||
|
||||
private const int AuraSortingOffset = -1;
|
||||
private const int ReadyAuraSortingOffset = -1;
|
||||
private const int FlashSortingOffset = 1;
|
||||
private const int ReleaseSortingOffset = 1;
|
||||
private const string ResourceRoot = "Artifacts/ThreeColor-v1/Charge/";
|
||||
private const string AuraSuffix = "-Aura-v1";
|
||||
private const string ReadyAuraSuffix = "-ReadyAura-v1";
|
||||
private const string FlashSuffix = "-Flash-v1";
|
||||
private const string ReleaseSuffix = "-Release-v1";
|
||||
|
||||
private static readonly float[] AuraFrameDurations =
|
||||
{
|
||||
0.06f, 0.06f, 0.06f, 0.06f, 0.06f, 0.06f,
|
||||
};
|
||||
|
||||
private static readonly float[] ReadyAuraFrameDurations =
|
||||
{
|
||||
0.06f, 0.06f, 0.06f, 0.06f, 0.06f, 0.06f,
|
||||
};
|
||||
|
||||
private static readonly float[] FlashFrameDurations =
|
||||
{
|
||||
0.03f, 0.04f, 0.04f, 0.03f,
|
||||
};
|
||||
|
||||
private static readonly float[] ReleaseFrameDurations =
|
||||
{
|
||||
0.03f, 0.04f, 0.04f, 0.04f, 0.04f, 0.05f,
|
||||
};
|
||||
|
||||
private sealed class CachedFrames
|
||||
{
|
||||
public CachedFrames(Sprite[] frames, bool ownsSprites)
|
||||
{
|
||||
Frames = frames;
|
||||
OwnsSprites = ownsSprites;
|
||||
}
|
||||
|
||||
public Sprite[] Frames { get; }
|
||||
public bool OwnsSprites { get; }
|
||||
}
|
||||
|
||||
private sealed class VisualSlot
|
||||
{
|
||||
public VisualSlot(
|
||||
string objectName,
|
||||
int sortingOffset,
|
||||
float[] frameDurations)
|
||||
{
|
||||
ObjectName = objectName;
|
||||
SortingOffset = sortingOffset;
|
||||
FrameDurations = frameDurations;
|
||||
}
|
||||
|
||||
public string ObjectName { get; }
|
||||
public int SortingOffset { get; }
|
||||
public float[] FrameDurations { get; }
|
||||
public Sprite[] Frames;
|
||||
public GameObject Root;
|
||||
public SpriteRenderer Renderer;
|
||||
public int FrameIndex;
|
||||
public float FrameElapsed;
|
||||
public bool Loop;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, CachedFrames> FrameCache = new();
|
||||
|
||||
private readonly VisualSlot aura = new(
|
||||
"Artifact Charge Aura",
|
||||
AuraSortingOffset,
|
||||
AuraFrameDurations);
|
||||
private readonly VisualSlot readyAura = new(
|
||||
"Artifact Charge Ready Aura",
|
||||
ReadyAuraSortingOffset,
|
||||
ReadyAuraFrameDurations);
|
||||
private readonly VisualSlot flash = new(
|
||||
"Artifact Charge Flash",
|
||||
FlashSortingOffset,
|
||||
FlashFrameDurations);
|
||||
private readonly VisualSlot release = new(
|
||||
"Artifact Release",
|
||||
ReleaseSortingOffset,
|
||||
ReleaseFrameDurations);
|
||||
private readonly VisualSlot switchPulse = new(
|
||||
"Artifact Switch",
|
||||
FlashSortingOffset,
|
||||
AuraFrameDurations);
|
||||
|
||||
private ActiveArtifactController controller;
|
||||
private readonly Dictionary<string, Sprite[]> equipmentFrames = new();
|
||||
private readonly Dictionary<Sprite, Sprite> equipmentOriginals = new();
|
||||
private PlayerHealth playerHealth;
|
||||
private SpriteRenderer playerRenderer;
|
||||
private SpriteRenderer guardOutline;
|
||||
private readonly Dictionary<Sprite, Sprite> guardFrames = new();
|
||||
private RunManager subscribedRunManager;
|
||||
private ActiveArtifactDefinition chargingArtifact;
|
||||
private bool readyFlashPlayed;
|
||||
private int flashPlayCount;
|
||||
private int releasePlayCount;
|
||||
|
||||
public bool IsAuraVisible => IsVisible(aura);
|
||||
public bool IsSwitchVisible => IsVisible(switchPulse);
|
||||
public GameObject SwitchObject => switchPulse.Root;
|
||||
public bool IsReadyAuraVisible => IsVisible(readyAura);
|
||||
public bool IsFlashVisible => IsVisible(flash);
|
||||
public bool IsReleaseVisible => IsVisible(release);
|
||||
public GameObject AuraObject => aura.Root;
|
||||
public GameObject ReadyAuraObject => readyAura.Root;
|
||||
public GameObject FlashObject => flash.Root;
|
||||
public GameObject ReleaseObject => release.Root;
|
||||
public int FlashPlayCount => flashPlayCount;
|
||||
public int ReleasePlayCount => releasePlayCount;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void ResetFrameCache()
|
||||
{
|
||||
foreach (CachedFrames cached in FrameCache.Values)
|
||||
{
|
||||
if (!cached.OwnsSprites)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (Sprite sprite in cached.Frames)
|
||||
{
|
||||
if (sprite == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
Destroy(sprite);
|
||||
}
|
||||
else
|
||||
{
|
||||
DestroyImmediate(sprite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FrameCache.Clear();
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
controller = GetComponent<ActiveArtifactController>();
|
||||
playerHealth = GetComponent<PlayerHealth>();
|
||||
playerRenderer = FindPlayerRenderer();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (controller == null)
|
||||
{
|
||||
controller = GetComponent<ActiveArtifactController>();
|
||||
}
|
||||
|
||||
if (playerHealth == null)
|
||||
{
|
||||
playerHealth = GetComponent<PlayerHealth>();
|
||||
}
|
||||
|
||||
if (playerRenderer == null)
|
||||
{
|
||||
playerRenderer = FindPlayerRenderer();
|
||||
}
|
||||
|
||||
SubscribeController();
|
||||
SubscribePlayerHealth();
|
||||
RefreshRunManagerSubscription();
|
||||
RestoreCurrentChargeVisuals();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (guardOutline != null) guardOutline.enabled = false;
|
||||
RestoreEquipmentSprite();
|
||||
UnsubscribeController();
|
||||
UnsubscribePlayerHealth();
|
||||
UnsubscribeRunManager();
|
||||
ClearAllVisuals();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (guardOutline != null) Destroy(guardOutline.gameObject);
|
||||
UnsubscribeController();
|
||||
UnsubscribePlayerHealth();
|
||||
UnsubscribeRunManager();
|
||||
ClearAllVisuals();
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
RefreshEquipmentSprite();
|
||||
RefreshGuardOutline();
|
||||
RefreshRunManagerSubscription();
|
||||
if (subscribedRunManager != null
|
||||
&& (subscribedRunManager.IsSelectionOpen
|
||||
|| subscribedRunManager.IsGameOver
|
||||
|| subscribedRunManager.IsTitleScreen))
|
||||
{
|
||||
ClearAllVisuals();
|
||||
return;
|
||||
}
|
||||
|
||||
if (controller == null)
|
||||
{
|
||||
ClearAllVisuals();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!controller.isActiveAndEnabled)
|
||||
{
|
||||
ClearAllVisuals();
|
||||
return;
|
||||
}
|
||||
|
||||
if (controller.IsCharging)
|
||||
{
|
||||
UpdateChargeVisuals();
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearChargeHoldVisuals();
|
||||
}
|
||||
|
||||
SyncSlot(aura, true);
|
||||
SyncSlot(readyAura, true);
|
||||
SyncSlot(flash, true);
|
||||
SyncSlot(release, false);
|
||||
SyncSlot(switchPulse, true);
|
||||
AdvanceSlot(aura, Time.deltaTime);
|
||||
AdvanceSlot(readyAura, Time.deltaTime);
|
||||
AdvanceSlot(flash, Time.deltaTime);
|
||||
AdvanceSlot(release, Time.deltaTime);
|
||||
AdvanceSlot(switchPulse, Time.deltaTime);
|
||||
}
|
||||
|
||||
public void RefreshEquipmentSprite()
|
||||
{
|
||||
if (playerRenderer == null || playerRenderer.sprite == null) return;
|
||||
Sprite current = playerRenderer.sprite;
|
||||
Sprite original = equipmentOriginals.TryGetValue(current, out Sprite source) ? source : current;
|
||||
if (controller == null || !controller.isActiveAndEnabled || controller.CurrentArtifact == null)
|
||||
{
|
||||
playerRenderer.sprite = original;
|
||||
return;
|
||||
}
|
||||
string path = "Artifacts/ThreeColor-v1/Player/" + original.texture.name
|
||||
+ "-" + controller.CurrentArtifact.ArtifactColor + "-v1";
|
||||
if (!equipmentFrames.TryGetValue(path, out Sprite[] frames))
|
||||
{
|
||||
frames = Resources.LoadAll<Sprite>(path);
|
||||
equipmentFrames[path] = frames;
|
||||
}
|
||||
foreach (Sprite frame in frames)
|
||||
{
|
||||
if (frame.rect != original.rect) continue;
|
||||
equipmentOriginals[frame] = original;
|
||||
playerRenderer.sprite = frame;
|
||||
return;
|
||||
}
|
||||
playerRenderer.sprite = original;
|
||||
}
|
||||
|
||||
public void RefreshGuardOutline()
|
||||
{
|
||||
bool visible = isActiveAndEnabled && playerHealth != null
|
||||
&& playerHealth.isActiveAndEnabled && playerHealth.CurrentHealth > 0f
|
||||
&& playerHealth.IsGuarding && playerRenderer != null
|
||||
&& playerRenderer.enabled && playerRenderer.sprite != null
|
||||
&& (RunManager.Instance == null || !RunManager.Instance.IsGameOver);
|
||||
if (!visible)
|
||||
{
|
||||
if (guardOutline != null) guardOutline.enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (guardOutline == null)
|
||||
{
|
||||
var root = new GameObject("Player Guard Outline");
|
||||
root.transform.SetParent(playerRenderer.transform, false);
|
||||
guardOutline = root.AddComponent<SpriteRenderer>();
|
||||
}
|
||||
|
||||
Sprite current = playerRenderer.sprite;
|
||||
Sprite original = equipmentOriginals.TryGetValue(current, out Sprite source) ? source : current;
|
||||
if (!guardFrames.TryGetValue(original, out Sprite mask))
|
||||
{
|
||||
foreach (Sprite frame in Resources.LoadAll<Sprite>("Combat/Guard-v1/" + original.texture.name + "-outline"))
|
||||
{
|
||||
if (frame.rect != original.rect) continue;
|
||||
mask = frame;
|
||||
break;
|
||||
}
|
||||
guardFrames[original] = mask;
|
||||
}
|
||||
guardOutline.sprite = mask;
|
||||
guardOutline.enabled = mask != null;
|
||||
guardOutline.flipX = playerRenderer.flipX;
|
||||
guardOutline.flipY = playerRenderer.flipY;
|
||||
guardOutline.color = new Color(1f, 1f, 1f, playerRenderer.color.a);
|
||||
guardOutline.sharedMaterial = playerRenderer.sharedMaterial;
|
||||
guardOutline.sortingLayerID = playerRenderer.sortingLayerID;
|
||||
guardOutline.sortingOrder = playerRenderer.sortingOrder + 1;
|
||||
}
|
||||
|
||||
private void RestoreEquipmentSprite()
|
||||
{
|
||||
if (playerRenderer != null && playerRenderer.sprite != null
|
||||
&& equipmentOriginals.TryGetValue(playerRenderer.sprite, out Sprite original))
|
||||
playerRenderer.sprite = original;
|
||||
}
|
||||
|
||||
public void Connect(ActiveArtifactController artifactController)
|
||||
{
|
||||
if (controller == artifactController && controller != null)
|
||||
{
|
||||
SubscribeController();
|
||||
return;
|
||||
}
|
||||
|
||||
UnsubscribeController();
|
||||
controller = artifactController;
|
||||
SubscribeController();
|
||||
}
|
||||
|
||||
public void NotifyChargeEnded(bool preserveReadyFlash)
|
||||
{
|
||||
ClearChargeHoldVisuals();
|
||||
if (!preserveReadyFlash)
|
||||
{
|
||||
ClearSlot(flash);
|
||||
}
|
||||
}
|
||||
|
||||
public void NotifyArtifactUseFailed()
|
||||
{
|
||||
ClearAllVisuals();
|
||||
}
|
||||
|
||||
public void ClearAllVisuals()
|
||||
{
|
||||
ClearSlot(aura);
|
||||
ClearSlot(readyAura);
|
||||
ClearSlot(flash);
|
||||
ClearSlot(release);
|
||||
ClearSlot(switchPulse);
|
||||
}
|
||||
|
||||
private void SubscribeController()
|
||||
{
|
||||
if (controller == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
controller.OnStateChanged -= HandleControllerStateChanged;
|
||||
controller.OnArtifactChargeStarted -= HandleArtifactChargeStarted;
|
||||
controller.OnArtifactUseSucceeded -= HandleArtifactUseSucceeded;
|
||||
controller.OnArtifactSwitched -= HandleArtifactSwitched;
|
||||
controller.OnStateChanged += HandleControllerStateChanged;
|
||||
controller.OnArtifactChargeStarted += HandleArtifactChargeStarted;
|
||||
controller.OnArtifactUseSucceeded += HandleArtifactUseSucceeded;
|
||||
controller.OnArtifactSwitched += HandleArtifactSwitched;
|
||||
}
|
||||
|
||||
private void UnsubscribeController()
|
||||
{
|
||||
if (controller == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
controller.OnStateChanged -= HandleControllerStateChanged;
|
||||
controller.OnArtifactChargeStarted -= HandleArtifactChargeStarted;
|
||||
controller.OnArtifactUseSucceeded -= HandleArtifactUseSucceeded;
|
||||
controller.OnArtifactSwitched -= HandleArtifactSwitched;
|
||||
}
|
||||
|
||||
private void SubscribePlayerHealth()
|
||||
{
|
||||
if (playerHealth == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
playerHealth.OnDamaged -= HandlePlayerDamaged;
|
||||
playerHealth.OnDied -= HandlePlayerDied;
|
||||
playerHealth.OnDamaged += HandlePlayerDamaged;
|
||||
playerHealth.OnDied += HandlePlayerDied;
|
||||
}
|
||||
|
||||
private void UnsubscribePlayerHealth()
|
||||
{
|
||||
if (playerHealth == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
playerHealth.OnDamaged -= HandlePlayerDamaged;
|
||||
playerHealth.OnDied -= HandlePlayerDied;
|
||||
}
|
||||
|
||||
private void RefreshRunManagerSubscription()
|
||||
{
|
||||
RunManager current = RunManager.Instance;
|
||||
if (current == subscribedRunManager)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UnsubscribeRunManager();
|
||||
subscribedRunManager = current;
|
||||
if (subscribedRunManager != null)
|
||||
{
|
||||
subscribedRunManager.OnSelectionChanged += HandleSelectionChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsubscribeRunManager()
|
||||
{
|
||||
if (subscribedRunManager != null)
|
||||
{
|
||||
subscribedRunManager.OnSelectionChanged -= HandleSelectionChanged;
|
||||
subscribedRunManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleArtifactChargeStarted(ActiveArtifactDefinition definition)
|
||||
{
|
||||
ClearSlot(switchPulse);
|
||||
chargingArtifact = definition;
|
||||
readyFlashPlayed = false;
|
||||
ClearSlot(readyAura);
|
||||
ClearSlot(flash);
|
||||
ClearSlot(aura);
|
||||
if (!Application.isPlaying || definition == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
aura.Frames = LoadFrames(
|
||||
GetResourcePath(definition, AuraSuffix),
|
||||
FrameCount);
|
||||
readyAura.Frames = LoadFrames(
|
||||
GetResourcePath(definition, ReadyAuraSuffix),
|
||||
FrameCount);
|
||||
flash.Frames = LoadFrames(
|
||||
GetResourcePath(definition, FlashSuffix),
|
||||
FlashFrameCount);
|
||||
if (aura.Frames.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CreateSlot(aura, true, Vector2.zero, 1f);
|
||||
}
|
||||
|
||||
private void RestoreCurrentChargeVisuals()
|
||||
{
|
||||
if (!Application.isPlaying
|
||||
|| controller == null
|
||||
|| !controller.IsCharging)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
chargingArtifact = controller.CurrentArtifact;
|
||||
if (chargingArtifact == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
aura.Frames = LoadFrames(
|
||||
GetResourcePath(chargingArtifact, AuraSuffix),
|
||||
FrameCount);
|
||||
readyAura.Frames = LoadFrames(
|
||||
GetResourcePath(chargingArtifact, ReadyAuraSuffix),
|
||||
FrameCount);
|
||||
flash.Frames = LoadFrames(
|
||||
GetResourcePath(chargingArtifact, FlashSuffix),
|
||||
FlashFrameCount);
|
||||
UpdateChargeVisuals();
|
||||
}
|
||||
|
||||
private void HandleControllerStateChanged()
|
||||
{
|
||||
if (controller == null || !controller.IsCharging)
|
||||
{
|
||||
ClearChargeHoldVisuals();
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateChargeVisuals();
|
||||
}
|
||||
|
||||
private void HandleArtifactUseSucceeded(
|
||||
ActiveArtifactDefinition definition,
|
||||
bool charged)
|
||||
{
|
||||
ClearSlot(switchPulse);
|
||||
ClearChargeHoldVisuals();
|
||||
if (!Application.isPlaying || definition == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
release.Frames = LoadFrames(
|
||||
GetResourcePath(definition, ReleaseSuffix),
|
||||
FrameCount);
|
||||
if (release.Frames.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 origin = controller.LastArtifactUsePosition;
|
||||
CreateSlot(release, false, origin, charged ? 1.2f : 1f);
|
||||
releasePlayCount++;
|
||||
}
|
||||
|
||||
private void HandleArtifactSwitched(ActiveArtifactDefinition definition)
|
||||
{
|
||||
ClearSlot(switchPulse);
|
||||
if (!Application.isPlaying || definition == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
switchPulse.Frames = LoadFrames(
|
||||
GetResourcePath(definition, ReleaseSuffix), FrameCount);
|
||||
if (switchPulse.Frames.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Reuse the existing colored outward burst, following the player
|
||||
// for a single short cycle so movement does not leave it behind.
|
||||
CreateSlot(switchPulse, true, Vector2.zero, 1f);
|
||||
}
|
||||
|
||||
private void HandlePlayerDamaged(Vector2 _, float __)
|
||||
{
|
||||
ClearAllVisuals();
|
||||
}
|
||||
|
||||
private void HandlePlayerDied()
|
||||
{
|
||||
ClearAllVisuals();
|
||||
}
|
||||
|
||||
private void HandleSelectionChanged(bool isOpen)
|
||||
{
|
||||
if (isOpen)
|
||||
{
|
||||
ClearAllVisuals();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateChargeVisuals()
|
||||
{
|
||||
if (controller == null
|
||||
|| !controller.IsCharging
|
||||
|| chargingArtifact == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!controller.IsFullyCharged)
|
||||
{
|
||||
EnsureLoopSlot(aura, AuraSuffix, 1f);
|
||||
ClearSlot(readyAura);
|
||||
SetAlpha(
|
||||
aura,
|
||||
Mathf.Lerp(0.55f, 0.85f, controller.ChargeProgress));
|
||||
return;
|
||||
}
|
||||
|
||||
EnsureLoopSlot(readyAura, ReadyAuraSuffix, 1f);
|
||||
ClearSlot(aura);
|
||||
if (!readyFlashPlayed)
|
||||
{
|
||||
readyFlashPlayed = true;
|
||||
flashPlayCount++;
|
||||
if (flash.Frames.Length > 0 && Application.isPlaying)
|
||||
{
|
||||
CreateSlot(flash, true, Vector2.zero, 1f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureLoopSlot(
|
||||
VisualSlot slot,
|
||||
string suffix,
|
||||
float scale)
|
||||
{
|
||||
if (slot.Root != null || chargingArtifact == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
slot.Frames = LoadFrames(
|
||||
GetResourcePath(chargingArtifact, suffix),
|
||||
FrameCount);
|
||||
if (slot.Frames.Length == 0 || !Application.isPlaying)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CreateSlot(slot, true, Vector2.zero, scale);
|
||||
}
|
||||
|
||||
private void CreateSlot(
|
||||
VisualSlot slot,
|
||||
bool followPlayer,
|
||||
Vector2 worldOrigin,
|
||||
float scale)
|
||||
{
|
||||
ClearSlot(slot);
|
||||
slot.Root = new GameObject(slot.ObjectName);
|
||||
if (followPlayer)
|
||||
{
|
||||
slot.Root.transform.SetParent(transform, false);
|
||||
slot.Root.transform.localPosition = new Vector3(
|
||||
0f,
|
||||
ChargeVisualY,
|
||||
0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
slot.Root.transform.position = new Vector3(
|
||||
worldOrigin.x,
|
||||
worldOrigin.y + ChargeVisualY,
|
||||
transform.position.z);
|
||||
}
|
||||
|
||||
slot.Root.transform.localScale = Vector3.one * scale;
|
||||
slot.Renderer = slot.Root.AddComponent<SpriteRenderer>();
|
||||
slot.Renderer.color = Color.white;
|
||||
slot.Renderer.sprite = slot.Frames[0];
|
||||
slot.Renderer.enabled = true;
|
||||
slot.FrameIndex = 0;
|
||||
slot.FrameElapsed = 0f;
|
||||
slot.Loop = slot == aura || slot == readyAura;
|
||||
SyncSlot(slot, followPlayer);
|
||||
}
|
||||
|
||||
private void SyncSlot(VisualSlot slot, bool followPlayer)
|
||||
{
|
||||
if (slot.Root == null || slot.Renderer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (followPlayer)
|
||||
{
|
||||
slot.Root.transform.SetParent(transform, false);
|
||||
slot.Root.transform.localPosition = new Vector3(
|
||||
0f,
|
||||
ChargeVisualY,
|
||||
0f);
|
||||
}
|
||||
|
||||
if (playerRenderer == null)
|
||||
{
|
||||
playerRenderer = FindPlayerRenderer();
|
||||
}
|
||||
|
||||
if (playerRenderer != null)
|
||||
{
|
||||
slot.Renderer.sortingLayerID = playerRenderer.sortingLayerID;
|
||||
slot.Renderer.sortingOrder =
|
||||
playerRenderer.sortingOrder + slot.SortingOffset;
|
||||
slot.Renderer.sharedMaterial = playerRenderer.sharedMaterial;
|
||||
}
|
||||
|
||||
Color color = Color.white;
|
||||
if (slot == aura
|
||||
&& controller != null
|
||||
&& controller.IsCharging
|
||||
&& !controller.IsFullyCharged)
|
||||
{
|
||||
color.a = Mathf.Lerp(0.55f, 0.85f, controller.ChargeProgress);
|
||||
}
|
||||
slot.Renderer.color = color;
|
||||
slot.Renderer.enabled = true;
|
||||
}
|
||||
|
||||
private static void SetAlpha(VisualSlot slot, float alpha)
|
||||
{
|
||||
if (slot.Renderer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Color color = Color.white;
|
||||
color.a = Mathf.Clamp01(alpha);
|
||||
slot.Renderer.color = color;
|
||||
}
|
||||
|
||||
private static void AdvanceSlot(VisualSlot slot, float deltaTime)
|
||||
{
|
||||
if (slot.Root == null
|
||||
|| slot.Renderer == null
|
||||
|| slot.Frames == null
|
||||
|| slot.Frames.Length == 0
|
||||
|| deltaTime <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
slot.FrameElapsed += deltaTime;
|
||||
while (slot.FrameElapsed >= slot.FrameDurations[slot.FrameIndex])
|
||||
{
|
||||
slot.FrameElapsed -= slot.FrameDurations[slot.FrameIndex];
|
||||
int nextFrame = slot.FrameIndex + 1;
|
||||
if (nextFrame >= slot.Frames.Length)
|
||||
{
|
||||
if (!slot.Loop)
|
||||
{
|
||||
ClearSlot(slot);
|
||||
return;
|
||||
}
|
||||
|
||||
nextFrame = 0;
|
||||
}
|
||||
|
||||
slot.FrameIndex = nextFrame;
|
||||
slot.Renderer.sprite = slot.Frames[slot.FrameIndex];
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearChargeHoldVisuals()
|
||||
{
|
||||
ClearSlot(aura);
|
||||
ClearSlot(readyAura);
|
||||
}
|
||||
|
||||
private static bool IsVisible(VisualSlot slot)
|
||||
{
|
||||
return slot.Root != null
|
||||
&& slot.Renderer != null
|
||||
&& slot.Renderer.enabled;
|
||||
}
|
||||
|
||||
private SpriteRenderer FindPlayerRenderer()
|
||||
{
|
||||
SpriteRenderer rootRenderer = GetComponent<SpriteRenderer>();
|
||||
if (rootRenderer != null)
|
||||
{
|
||||
return rootRenderer;
|
||||
}
|
||||
|
||||
SpriteRenderer[] renderers = GetComponentsInChildren<SpriteRenderer>(true);
|
||||
foreach (SpriteRenderer renderer in renderers)
|
||||
{
|
||||
if (renderer != null && renderer.transform != transform)
|
||||
{
|
||||
return renderer;
|
||||
}
|
||||
}
|
||||
|
||||
return GetComponent<SpriteRenderer>();
|
||||
}
|
||||
|
||||
private static string GetResourcePath(
|
||||
ActiveArtifactDefinition definition,
|
||||
string suffix)
|
||||
{
|
||||
return ResourceRoot + definition.Effect + suffix;
|
||||
}
|
||||
|
||||
private static void ClearSlot(VisualSlot slot)
|
||||
{
|
||||
if (slot.Root != null)
|
||||
{
|
||||
slot.Root.SetActive(false);
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
Destroy(slot.Root);
|
||||
}
|
||||
else
|
||||
{
|
||||
DestroyImmediate(slot.Root);
|
||||
}
|
||||
}
|
||||
|
||||
slot.Root = null;
|
||||
slot.Renderer = null;
|
||||
slot.FrameIndex = 0;
|
||||
slot.FrameElapsed = 0f;
|
||||
}
|
||||
|
||||
private static Sprite[] LoadFrames(
|
||||
string resourcePath,
|
||||
int expectedFrameCount)
|
||||
{
|
||||
if (FrameCache.TryGetValue(resourcePath, out CachedFrames cached))
|
||||
{
|
||||
return cached.Frames;
|
||||
}
|
||||
|
||||
Sprite[] importedFrames = Resources.LoadAll<Sprite>(resourcePath);
|
||||
if (importedFrames != null
|
||||
&& importedFrames.Length >= expectedFrameCount)
|
||||
{
|
||||
Array.Sort(
|
||||
importedFrames,
|
||||
(left, right) => String.CompareOrdinal(left.name, right.name));
|
||||
Sprite[] frames = new Sprite[expectedFrameCount];
|
||||
Array.Copy(importedFrames, frames, expectedFrameCount);
|
||||
FrameCache[resourcePath] = new CachedFrames(frames, false);
|
||||
return frames;
|
||||
}
|
||||
|
||||
Texture2D texture = importedFrames != null
|
||||
&& importedFrames.Length > 0
|
||||
? importedFrames[0].texture
|
||||
: Resources.Load<Texture2D>(resourcePath);
|
||||
if (texture == null
|
||||
|| texture.width < CellSize * expectedFrameCount
|
||||
|| texture.height < CellSize
|
||||
|| texture.width % expectedFrameCount != 0)
|
||||
{
|
||||
Sprite[] missing = Array.Empty<Sprite>();
|
||||
FrameCache[resourcePath] = new CachedFrames(missing, false);
|
||||
return missing;
|
||||
}
|
||||
|
||||
int frameWidth = texture.width / expectedFrameCount;
|
||||
Sprite[] generatedFrames = new Sprite[expectedFrameCount];
|
||||
for (int i = 0; i < expectedFrameCount; i++)
|
||||
{
|
||||
generatedFrames[i] = Sprite.Create(
|
||||
texture,
|
||||
new Rect(i * frameWidth, 0f, frameWidth, CellSize),
|
||||
new Vector2(0.5f, 0.5f),
|
||||
PixelsPerUnit,
|
||||
0,
|
||||
SpriteMeshType.FullRect);
|
||||
generatedFrames[i].name =
|
||||
$"{resourcePath} Frame {i + 1}";
|
||||
generatedFrames[i].hideFlags = HideFlags.HideAndDontSave;
|
||||
}
|
||||
|
||||
FrameCache[resourcePath] = new CachedFrames(generatedFrames, true);
|
||||
return generatedFrames;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5de4a9b0cc4d4d5ab0e9a06ee7256bb9
|
||||
@@ -0,0 +1,312 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using BumpCombat.Enemies;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BumpCombat.Combat
|
||||
{
|
||||
[DefaultExecutionOrder(1000)]
|
||||
public sealed class ArtifactStatusVisual : MonoBehaviour
|
||||
{
|
||||
public const int StatusSortingOffset = 1;
|
||||
|
||||
private const int FrameCount = 6;
|
||||
private const float PixelsPerUnit = 32f;
|
||||
private const string IgniteResourcePath = "Artifacts/ThreeColor-v1/StatusEffects/Ignite-Warlock-v1";
|
||||
private const string ShockResourcePath = "Artifacts/ThreeColor-v1/StatusEffects/Shock-Warlock-v1";
|
||||
|
||||
private static readonly float[] IgniteFrameDurations =
|
||||
{
|
||||
0.1f,
|
||||
0.1f,
|
||||
0.1f,
|
||||
0.1f,
|
||||
0.1f,
|
||||
0.1f,
|
||||
};
|
||||
|
||||
private static readonly float[] ShockFrameDurations =
|
||||
{
|
||||
0.06f,
|
||||
0.04f,
|
||||
0.07f,
|
||||
0.05f,
|
||||
0.07f,
|
||||
0.11f,
|
||||
};
|
||||
|
||||
private sealed class CachedFrames
|
||||
{
|
||||
public CachedFrames(Sprite[] frames, bool ownsSprites)
|
||||
{
|
||||
Frames = frames;
|
||||
OwnsSprites = ownsSprites;
|
||||
}
|
||||
|
||||
public Sprite[] Frames { get; }
|
||||
public bool OwnsSprites { get; }
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, CachedFrames> FrameCache = new();
|
||||
|
||||
private sealed class StatusSlot
|
||||
{
|
||||
public StatusSlot(
|
||||
string resourcePath,
|
||||
string objectName,
|
||||
float[] frameDurations)
|
||||
{
|
||||
ResourcePath = resourcePath;
|
||||
ObjectName = objectName;
|
||||
FrameDurations = frameDurations;
|
||||
}
|
||||
|
||||
public string ResourcePath { get; }
|
||||
public string ObjectName { get; }
|
||||
public float[] FrameDurations { get; }
|
||||
public Sprite[] Frames;
|
||||
public GameObject Root;
|
||||
public SpriteRenderer Renderer;
|
||||
public int FrameIndex;
|
||||
public float FrameElapsed;
|
||||
}
|
||||
|
||||
private readonly StatusSlot ignite = new(
|
||||
IgniteResourcePath,
|
||||
"Ignite Status Visual",
|
||||
IgniteFrameDurations);
|
||||
private readonly StatusSlot shock = new(
|
||||
ShockResourcePath,
|
||||
"Shock Status Visual",
|
||||
ShockFrameDurations);
|
||||
|
||||
private EnemyController enemy;
|
||||
private SpriteRenderer sourceRenderer;
|
||||
|
||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
||||
private static void ResetFrameCache()
|
||||
{
|
||||
foreach (CachedFrames cached in FrameCache.Values)
|
||||
{
|
||||
if (!cached.OwnsSprites)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (Sprite sprite in cached.Frames)
|
||||
{
|
||||
if (sprite == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Application.isPlaying)
|
||||
{
|
||||
Destroy(sprite);
|
||||
}
|
||||
else
|
||||
{
|
||||
DestroyImmediate(sprite);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FrameCache.Clear();
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
enemy = GetComponent<EnemyController>();
|
||||
sourceRenderer = GetComponent<SpriteRenderer>();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (enemy == null)
|
||||
{
|
||||
enemy = GetComponent<EnemyController>();
|
||||
}
|
||||
|
||||
if (sourceRenderer == null)
|
||||
{
|
||||
sourceRenderer = GetComponent<SpriteRenderer>();
|
||||
}
|
||||
|
||||
if (enemy != null)
|
||||
{
|
||||
enemy.OnDied += HandleEnemyDied;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (enemy != null)
|
||||
{
|
||||
enemy.OnDied -= HandleEnemyDied;
|
||||
}
|
||||
|
||||
ClearSlot(ignite);
|
||||
ClearSlot(shock);
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (enemy == null
|
||||
|| sourceRenderer == null
|
||||
|| enemy.IsDead
|
||||
|| !gameObject.activeInHierarchy)
|
||||
{
|
||||
ClearSlot(ignite);
|
||||
ClearSlot(shock);
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateSlot(ignite, enemy.IsIgnited);
|
||||
UpdateSlot(shock, enemy.IsShocked);
|
||||
}
|
||||
|
||||
private void HandleEnemyDied(EnemyController deadEnemy)
|
||||
{
|
||||
ClearSlot(ignite);
|
||||
ClearSlot(shock);
|
||||
}
|
||||
|
||||
private void UpdateSlot(StatusSlot slot, bool active)
|
||||
{
|
||||
if (!active)
|
||||
{
|
||||
ClearSlot(slot);
|
||||
return;
|
||||
}
|
||||
|
||||
if (slot.Root == null)
|
||||
{
|
||||
slot.Frames ??= LoadFrames(slot.ResourcePath);
|
||||
if (slot.Frames.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CreateSlot(slot);
|
||||
}
|
||||
|
||||
SyncSlot(slot);
|
||||
AdvanceSlot(slot, Time.deltaTime);
|
||||
}
|
||||
|
||||
private void CreateSlot(StatusSlot slot)
|
||||
{
|
||||
slot.Root = new GameObject(slot.ObjectName);
|
||||
slot.Root.transform.SetParent(transform, false);
|
||||
slot.Root.transform.localPosition = Vector3.zero;
|
||||
slot.Renderer = slot.Root.AddComponent<SpriteRenderer>();
|
||||
slot.Renderer.color = Color.white;
|
||||
slot.FrameIndex = 0;
|
||||
slot.FrameElapsed = 0f;
|
||||
slot.Renderer.sprite = slot.Frames[slot.FrameIndex];
|
||||
}
|
||||
|
||||
private void SyncSlot(StatusSlot slot)
|
||||
{
|
||||
if (slot.Root == null || slot.Renderer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 groundAnchor = enemy.GroundAnchorPosition;
|
||||
slot.Root.transform.SetPositionAndRotation(
|
||||
new Vector3(groundAnchor.x, groundAnchor.y, transform.position.z),
|
||||
Quaternion.identity);
|
||||
slot.Root.transform.localScale = Vector3.one;
|
||||
slot.Renderer.sortingLayerID = sourceRenderer.sortingLayerID;
|
||||
slot.Renderer.sortingOrder =
|
||||
sourceRenderer.sortingOrder + StatusSortingOffset;
|
||||
slot.Renderer.flipX = sourceRenderer.flipX;
|
||||
slot.Renderer.flipY = sourceRenderer.flipY;
|
||||
slot.Renderer.sharedMaterial = sourceRenderer.sharedMaterial;
|
||||
slot.Renderer.enabled = true;
|
||||
}
|
||||
|
||||
private static void AdvanceSlot(StatusSlot slot, float deltaTime)
|
||||
{
|
||||
if (slot.Renderer == null
|
||||
|| slot.Frames == null
|
||||
|| slot.Frames.Length == 0
|
||||
|| deltaTime <= 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
slot.FrameElapsed += deltaTime;
|
||||
while (slot.FrameElapsed >= slot.FrameDurations[slot.FrameIndex])
|
||||
{
|
||||
slot.FrameElapsed -= slot.FrameDurations[slot.FrameIndex];
|
||||
slot.FrameIndex = (slot.FrameIndex + 1) % slot.Frames.Length;
|
||||
slot.Renderer.sprite = slot.Frames[slot.FrameIndex];
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClearSlot(StatusSlot slot)
|
||||
{
|
||||
if (slot.Root != null)
|
||||
{
|
||||
slot.Root.SetActive(false);
|
||||
Destroy(slot.Root);
|
||||
}
|
||||
|
||||
slot.Root = null;
|
||||
slot.Renderer = null;
|
||||
slot.FrameIndex = 0;
|
||||
slot.FrameElapsed = 0f;
|
||||
}
|
||||
|
||||
private static Sprite[] LoadFrames(string resourcePath)
|
||||
{
|
||||
if (FrameCache.TryGetValue(resourcePath, out CachedFrames cached))
|
||||
{
|
||||
return cached.Frames;
|
||||
}
|
||||
|
||||
Sprite[] importedFrames = Resources.LoadAll<Sprite>(resourcePath);
|
||||
if (importedFrames != null && importedFrames.Length >= FrameCount)
|
||||
{
|
||||
Sprite[] frames = new Sprite[FrameCount];
|
||||
Array.Copy(importedFrames, frames, FrameCount);
|
||||
FrameCache[resourcePath] = new CachedFrames(frames, false);
|
||||
return frames;
|
||||
}
|
||||
|
||||
Texture2D texture = importedFrames != null
|
||||
&& importedFrames.Length > 0
|
||||
? importedFrames[0].texture
|
||||
: Resources.Load<Texture2D>(resourcePath);
|
||||
if (texture == null
|
||||
|| texture.width < FrameCount
|
||||
|| texture.width % FrameCount != 0
|
||||
|| texture.height <= 0)
|
||||
{
|
||||
Sprite[] missing = Array.Empty<Sprite>();
|
||||
FrameCache[resourcePath] = new CachedFrames(missing, false);
|
||||
return missing;
|
||||
}
|
||||
|
||||
int frameWidth = texture.width / FrameCount;
|
||||
Sprite[] generatedFrames = new Sprite[FrameCount];
|
||||
for (int i = 0; i < FrameCount; i++)
|
||||
{
|
||||
generatedFrames[i] = Sprite.Create(
|
||||
texture,
|
||||
new Rect(i * frameWidth, 0f, frameWidth, texture.height),
|
||||
new Vector2(0.5f, 0.125f),
|
||||
PixelsPerUnit,
|
||||
0,
|
||||
SpriteMeshType.FullRect);
|
||||
generatedFrames[i].name = $"{resourcePath} Frame {i + 1}";
|
||||
generatedFrames[i].hideFlags = HideFlags.HideAndDontSave;
|
||||
}
|
||||
|
||||
FrameCache[resourcePath] = new CachedFrames(generatedFrames, true);
|
||||
return generatedFrames;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4a11e9bb7e724c4b8e3c1a7d0f8b6d21
|
||||
@@ -0,0 +1,83 @@
|
||||
using UnityEngine;
|
||||
using BumpCombat.Constants;
|
||||
|
||||
namespace BumpCombat.Combat
|
||||
{
|
||||
public static class BumpCombatMath
|
||||
{
|
||||
public static float FrontThreshold => GameplayConstants.Current.Combat.FrontThreshold;
|
||||
public static float BackThreshold => GameplayConstants.Current.Combat.BackThreshold;
|
||||
public static float PositionDamageMultiplier =>
|
||||
GameplayConstants.Current.Combat.PositionDamageMultiplier;
|
||||
public static float BackDamageMultiplier =>
|
||||
GameplayConstants.Current.Combat.BackDamageMultiplier;
|
||||
|
||||
public static HitSide ClassifySide(Vector2 enemyFacing, Vector2 enemyToPlayer)
|
||||
{
|
||||
float dot = Vector2.Dot(enemyFacing.normalized, enemyToPlayer.normalized);
|
||||
if (dot >= FrontThreshold)
|
||||
{
|
||||
return HitSide.Front;
|
||||
}
|
||||
|
||||
if (dot <= BackThreshold)
|
||||
{
|
||||
return HitSide.Back;
|
||||
}
|
||||
|
||||
return HitSide.Side;
|
||||
}
|
||||
|
||||
public static float CalculateLateralOffset(
|
||||
Vector2 attackDirection,
|
||||
Vector2 attackerPosition,
|
||||
Vector2 targetPosition)
|
||||
{
|
||||
if (attackDirection.sqrMagnitude <= 0f)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
Vector2 direction = attackDirection.normalized;
|
||||
Vector2 perpendicular = new(-direction.y, direction.x);
|
||||
return Mathf.Abs(Vector2.Dot(
|
||||
targetPosition - attackerPosition,
|
||||
perpendicular));
|
||||
}
|
||||
|
||||
public static bool IsOffsetHit(
|
||||
HitSide side,
|
||||
Vector2 attackDirection,
|
||||
Vector2 attackerPosition,
|
||||
Vector2 targetPosition,
|
||||
float minimumLateralOffset)
|
||||
{
|
||||
return side == HitSide.Front
|
||||
&& CalculateLateralOffset(
|
||||
attackDirection,
|
||||
attackerPosition,
|
||||
targetPosition) >= minimumLateralOffset;
|
||||
}
|
||||
|
||||
public static float ApplySideMultiplier(
|
||||
float baseDamage,
|
||||
HitSide side,
|
||||
bool isOffsetHit = false)
|
||||
{
|
||||
float multiplier = side == HitSide.Back
|
||||
? BackDamageMultiplier
|
||||
: side == HitSide.Side || isOffsetHit
|
||||
? PositionDamageMultiplier
|
||||
: 1f;
|
||||
return DamageCalculator.ApplyMultiplier(baseDamage, multiplier);
|
||||
}
|
||||
|
||||
public static bool AppliesPlayerImpactRecoil(
|
||||
HitSide side,
|
||||
bool isEventEnemy = false)
|
||||
{
|
||||
return side != HitSide.Back || isEventEnemy;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 82c7f2a4e56e603458720f402519a7c2
|
||||
@@ -0,0 +1,526 @@
|
||||
using System.Collections.Generic;
|
||||
using BumpCombat.Core;
|
||||
using BumpCombat.Constants;
|
||||
using BumpCombat.Enemies;
|
||||
using BumpCombat.Player;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BumpCombat.Combat
|
||||
{
|
||||
[RequireComponent(typeof(PlayerController))]
|
||||
[RequireComponent(typeof(PlayerStats))]
|
||||
public sealed class BumpCombatResolver : MonoBehaviour
|
||||
{
|
||||
private float baseDamage => GameplayConstants.Current.Combat.BaseDamage;
|
||||
private float dashDamage => GameplayConstants.Current.Combat.DashDamage;
|
||||
private float strongDashDamage => GameplayConstants.Current.Combat.StrongDashDamage;
|
||||
private float minimumAttackSpeed => GameplayConstants.Current.Combat.MinimumAttackSpeed;
|
||||
private float minimumApproachDot => GameplayConstants.Current.Combat.MinimumApproachDot;
|
||||
private float knockbackForce => GameplayConstants.Current.Combat.KnockbackForce;
|
||||
private float dashKnockbackForce => GameplayConstants.Current.Combat.DashKnockbackForce;
|
||||
private float strongDashKnockbackForce => GameplayConstants.Current.Combat.StrongDashKnockbackForce;
|
||||
private float strongDashLaunchHeight => GameplayConstants.Current.Combat.StrongDashLaunchHeight;
|
||||
private float strongDashLaunchDuration => GameplayConstants.Current.Combat.StrongDashLaunchDuration;
|
||||
private float playerImpactRecoilDistance => GameplayConstants.Current.Combat.PlayerImpactRecoilDistance;
|
||||
private float frontKnockbackMultiplier => GameplayConstants.Current.Combat.FrontKnockbackMultiplier;
|
||||
private float frontPlayerImpactRecoilDistance => GameplayConstants.Current.Combat.FrontPlayerImpactRecoilDistance;
|
||||
private float eventBackPlayerImpactRecoilDistance => GameplayConstants.Current.Combat.EventBackPlayerImpactRecoilDistance;
|
||||
private float shieldedPlayerImpactRecoilDistance => GameplayConstants.Current.Combat.ShieldedPlayerImpactRecoilDistance;
|
||||
private float minimumOffsetDistance => GameplayConstants.Current.Combat.MinimumOffsetDistance;
|
||||
private float normalHitInterval => GameplayConstants.Current.Combat.NormalHitInterval;
|
||||
private float dashContactSkin => GameplayConstants.Current.Combat.DashContactSkin;
|
||||
|
||||
private readonly HashSet<int> dashTargets = new();
|
||||
private readonly RaycastHit2D[] dashHits = new RaycastHit2D[64];
|
||||
private readonly Dictionary<int, float> nextNormalHitTimes = new();
|
||||
|
||||
private PlayerStats playerStats;
|
||||
private PlayerController playerController;
|
||||
private DashController dashController;
|
||||
private ActiveArtifactController activeArtifactController;
|
||||
private CircleCollider2D bodyCollider;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
playerStats = GetComponent<PlayerStats>();
|
||||
playerController = GetComponent<PlayerController>();
|
||||
dashController = GetComponent<DashController>();
|
||||
activeArtifactController = GetComponent<ActiveArtifactController>();
|
||||
bodyCollider = GetComponent<CircleCollider2D>();
|
||||
}
|
||||
|
||||
public void BeginDashPathContactSession()
|
||||
{
|
||||
dashTargets.Clear();
|
||||
}
|
||||
|
||||
private void OnCollisionEnter2D(Collision2D collision)
|
||||
{
|
||||
TryResolveNormalCollision(collision);
|
||||
}
|
||||
|
||||
private void OnCollisionStay2D(Collision2D collision)
|
||||
{
|
||||
TryResolveNormalCollision(collision);
|
||||
}
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
TryResolveCrowdTrigger(other);
|
||||
}
|
||||
|
||||
private void OnTriggerStay2D(Collider2D other)
|
||||
{
|
||||
TryResolveCrowdTrigger(other);
|
||||
}
|
||||
|
||||
private void TryResolveNormalCollision(
|
||||
Collision2D collision)
|
||||
{
|
||||
EnemyController enemy = collision.collider.GetComponentInParent<EnemyController>();
|
||||
if (enemy == null || enemy.IsCrowd)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 hitPosition = collision.contactCount > 0
|
||||
? collision.GetContact(0).point
|
||||
: Vector2.Lerp(transform.position, enemy.transform.position, 0.5f);
|
||||
TryResolveNormalContact(enemy, hitPosition);
|
||||
}
|
||||
|
||||
private void TryResolveCrowdTrigger(
|
||||
Collider2D other)
|
||||
{
|
||||
EnemyController enemy = other.GetComponentInParent<EnemyController>();
|
||||
if (enemy == null || !enemy.IsCrowd)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 hitPosition = Vector2.Lerp(
|
||||
transform.position,
|
||||
enemy.transform.position,
|
||||
0.5f);
|
||||
TryResolveNormalContact(enemy, hitPosition);
|
||||
}
|
||||
|
||||
private void TryResolveNormalContact(
|
||||
EnemyController enemy,
|
||||
Vector2 hitPosition)
|
||||
{
|
||||
if (!RunManager.GameplayInputEnabled
|
||||
|| playerController.IsHurtMovementLocked
|
||||
|| playerController.IsDamageKnockbackActive
|
||||
|| dashController.IsDashing
|
||||
|| (activeArtifactController != null
|
||||
&& activeArtifactController.LocksMovement))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (enemy.IsDead)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextNormalHitTimes.TryGetValue(
|
||||
enemy.GetInstanceID(),
|
||||
out float nextNormalHitTime)
|
||||
&& Time.time < nextNormalHitTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 toEnemy = ((Vector2)enemy.transform.position - (Vector2)transform.position).normalized;
|
||||
if (playerController.AttackIntentVelocity.magnitude < minimumAttackSpeed
|
||||
|| playerController.MoveDirection.sqrMagnitude <= 0f
|
||||
|| Vector2.Dot(playerController.MoveDirection, toEnemy) < minimumApproachDot)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 enemyToPlayer =
|
||||
((Vector2)transform.position - (Vector2)enemy.transform.position).normalized;
|
||||
HitSide side = BumpCombatMath.ClassifySide(
|
||||
enemy.FacingDirection,
|
||||
enemyToPlayer);
|
||||
if (enemy.IsGroggyTierGated && enemy.IsDamageInvulnerable)
|
||||
{
|
||||
playerController.ApplyImpactRecoil(
|
||||
enemyToPlayer,
|
||||
shieldedPlayerImpactRecoilDistance);
|
||||
nextNormalHitTimes[enemy.GetInstanceID()] =
|
||||
Time.time + normalHitInterval;
|
||||
CombatFeedback.ShowBlockedBump(
|
||||
gameObject,
|
||||
hitPosition,
|
||||
playerController.MoveDirection);
|
||||
return;
|
||||
}
|
||||
|
||||
ResolveEnemy(
|
||||
enemy,
|
||||
false,
|
||||
hitPosition,
|
||||
transform.position,
|
||||
playerController.MoveDirection,
|
||||
side);
|
||||
}
|
||||
|
||||
public void ResolveDashPath(
|
||||
Vector2 start,
|
||||
Vector2 direction,
|
||||
float distance,
|
||||
bool isStrongDash = false,
|
||||
float? damageOverride = null,
|
||||
float? knockbackOverride = null,
|
||||
string sourceId = null,
|
||||
DamageTag damageTag = DamageTag.Collision,
|
||||
ArtifactColor? artifactColor = null,
|
||||
int castIdentity = 0,
|
||||
ActiveArtifactEffect? artifactEffect = null)
|
||||
{
|
||||
ResolveDashPathInternal(
|
||||
start,
|
||||
direction,
|
||||
distance,
|
||||
isStrongDash,
|
||||
damageOverride,
|
||||
knockbackOverride,
|
||||
sourceId,
|
||||
damageTag,
|
||||
artifactColor,
|
||||
castIdentity,
|
||||
artifactEffect,
|
||||
true);
|
||||
}
|
||||
|
||||
public void ResolveDashPathSegment(
|
||||
Vector2 start,
|
||||
Vector2 direction,
|
||||
float distance,
|
||||
bool isStrongDash = false,
|
||||
float? damageOverride = null,
|
||||
float? knockbackOverride = null,
|
||||
string sourceId = null,
|
||||
DamageTag damageTag = DamageTag.Collision,
|
||||
ArtifactColor? artifactColor = null,
|
||||
int castIdentity = 0,
|
||||
ActiveArtifactEffect? artifactEffect = null)
|
||||
{
|
||||
ResolveDashPathInternal(
|
||||
start,
|
||||
direction,
|
||||
distance,
|
||||
isStrongDash,
|
||||
damageOverride,
|
||||
knockbackOverride,
|
||||
sourceId,
|
||||
damageTag,
|
||||
artifactColor,
|
||||
castIdentity,
|
||||
artifactEffect,
|
||||
false);
|
||||
}
|
||||
|
||||
private void ResolveDashPathInternal(
|
||||
Vector2 start,
|
||||
Vector2 direction,
|
||||
float distance,
|
||||
bool isStrongDash,
|
||||
float? damageOverride,
|
||||
float? knockbackOverride,
|
||||
string sourceId,
|
||||
DamageTag damageTag,
|
||||
ArtifactColor? artifactColor,
|
||||
int castIdentity,
|
||||
ActiveArtifactEffect? artifactEffect,
|
||||
bool resetDashTargets)
|
||||
{
|
||||
if (resetDashTargets)
|
||||
{
|
||||
dashTargets.Clear();
|
||||
}
|
||||
|
||||
Vector2 castStart = start;
|
||||
float radius = GameplayConstants.Current.Combat.DashFallbackCastRadius;
|
||||
if (bodyCollider != null)
|
||||
{
|
||||
Vector2 colliderOffset =
|
||||
(Vector2)bodyCollider.bounds.center - (Vector2)transform.position;
|
||||
castStart += colliderOffset;
|
||||
radius = Mathf.Max(
|
||||
bodyCollider.bounds.extents.x,
|
||||
bodyCollider.bounds.extents.y)
|
||||
+ dashContactSkin;
|
||||
}
|
||||
|
||||
int hitCount = Physics2D.CircleCast(
|
||||
castStart,
|
||||
radius,
|
||||
direction,
|
||||
ContactFilter2D.noFilter,
|
||||
dashHits,
|
||||
distance);
|
||||
|
||||
for (int i = 0; i < hitCount; i++)
|
||||
{
|
||||
EnemyController enemy = dashHits[i].collider.GetComponentInParent<EnemyController>();
|
||||
if (enemy == null || enemy.IsDead || !dashTargets.Add(enemy.GetInstanceID()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector2 hitPosition = dashHits[i].point;
|
||||
if (hitPosition == Vector2.zero)
|
||||
{
|
||||
hitPosition = dashHits[i].collider.ClosestPoint(
|
||||
castStart + direction * dashHits[i].distance);
|
||||
}
|
||||
Vector2 attackerPositionAtHit =
|
||||
castStart + direction * dashHits[i].distance;
|
||||
ResolveEnemy(
|
||||
enemy,
|
||||
true,
|
||||
hitPosition,
|
||||
attackerPositionAtHit,
|
||||
direction,
|
||||
null,
|
||||
isStrongDash,
|
||||
damageOverride,
|
||||
knockbackOverride,
|
||||
sourceId,
|
||||
damageTag,
|
||||
artifactColor,
|
||||
castIdentity,
|
||||
artifactEffect);
|
||||
}
|
||||
}
|
||||
|
||||
private bool ResolveEnemy(
|
||||
EnemyController enemy,
|
||||
bool isDash,
|
||||
Vector2 hitPosition,
|
||||
Vector2 attackerPosition,
|
||||
Vector2 attackDirection,
|
||||
HitSide? knownSide = null,
|
||||
bool isStrongDash = false,
|
||||
float? damageOverride = null,
|
||||
float? knockbackOverride = null,
|
||||
string sourceId = null,
|
||||
DamageTag damageTag = DamageTag.Collision,
|
||||
ArtifactColor? artifactColor = null,
|
||||
int castIdentity = 0,
|
||||
ActiveArtifactEffect? artifactEffect = null)
|
||||
{
|
||||
Vector2 enemyToPlayer =
|
||||
(attackerPosition - (Vector2)enemy.transform.position).normalized;
|
||||
HitSide side = knownSide
|
||||
?? BumpCombatMath.ClassifySide(enemy.FacingDirection, enemyToPlayer);
|
||||
bool isEventEnemy = enemy.IsEventEnemy;
|
||||
bool isOffsetHit = !isDash && BumpCombatMath.IsOffsetHit(
|
||||
side,
|
||||
attackDirection,
|
||||
attackerPosition,
|
||||
enemy.transform.position,
|
||||
minimumOffsetDistance);
|
||||
float rawDamage = damageOverride
|
||||
?? (isStrongDash
|
||||
? strongDashDamage
|
||||
: isDash
|
||||
? dashDamage
|
||||
: baseDamage);
|
||||
float finalDamage = BumpCombatMath.ApplySideMultiplier(
|
||||
playerStats.CalculateDamage(rawDamage, damageTag),
|
||||
side,
|
||||
isOffsetHit);
|
||||
Vector2 knockbackDirection = -enemyToPlayer;
|
||||
bool suppressOrdinaryEventReaction = !isDash && isEventEnemy;
|
||||
float appliedKnockback = knockbackOverride
|
||||
?? (isStrongDash
|
||||
? strongDashKnockbackForce
|
||||
: isDash
|
||||
? dashKnockbackForce
|
||||
: side == HitSide.Front
|
||||
? knockbackForce * frontKnockbackMultiplier
|
||||
: knockbackForce);
|
||||
if (suppressOrdinaryEventReaction)
|
||||
{
|
||||
appliedKnockback = 0f;
|
||||
}
|
||||
|
||||
EnemyArtifactHitResult artifactHitResult =
|
||||
EnemyArtifactHitResult.Rejected;
|
||||
bool acceptedHit;
|
||||
if (artifactColor.HasValue)
|
||||
{
|
||||
artifactHitResult = enemy.TryTakeArtifactHit(
|
||||
finalDamage,
|
||||
knockbackDirection,
|
||||
appliedKnockback,
|
||||
artifactColor.Value,
|
||||
castIdentity,
|
||||
!suppressOrdinaryEventReaction);
|
||||
acceptedHit = artifactHitResult
|
||||
!= EnemyArtifactHitResult.Rejected;
|
||||
}
|
||||
else
|
||||
{
|
||||
acceptedHit = enemy.TryTakeDamage(
|
||||
finalDamage,
|
||||
knockbackDirection,
|
||||
appliedKnockback,
|
||||
!suppressOrdinaryEventReaction,
|
||||
!isDash);
|
||||
}
|
||||
|
||||
if (!acceptedHit)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (artifactHitResult == EnemyArtifactHitResult.Shielded)
|
||||
{
|
||||
if (enemy.LastArtifactContactCounted
|
||||
&& artifactEffect.HasValue)
|
||||
{
|
||||
CombatEvents.RaiseArtifactEffectiveHit(
|
||||
enemy.gameObject,
|
||||
castIdentity,
|
||||
artifactEffect.Value,
|
||||
isStrongDash,
|
||||
true);
|
||||
CombatFeedback.ShowArtifactImpact(
|
||||
CombatHitResult.ForArtifactContact(
|
||||
gameObject,
|
||||
enemy.gameObject,
|
||||
artifactEffect.Value,
|
||||
isStrongDash,
|
||||
side,
|
||||
knockbackDirection,
|
||||
hitPosition,
|
||||
damageTag,
|
||||
sourceId,
|
||||
isOffsetHit));
|
||||
}
|
||||
|
||||
// The contact has already advanced the groggy stagger pip.
|
||||
// A shielded dash must not become a bump, interrupt, launch,
|
||||
// recoil, gauge charge, or damage-hit feedback.
|
||||
return false;
|
||||
}
|
||||
|
||||
float damageTaken = enemy.LastAppliedDamage;
|
||||
if (damageTaken <= 0f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (artifactColor.HasValue && artifactEffect.HasValue)
|
||||
{
|
||||
CombatEvents.RaiseArtifactEffectiveHit(
|
||||
enemy.gameObject,
|
||||
castIdentity,
|
||||
artifactEffect.Value,
|
||||
isStrongDash,
|
||||
false);
|
||||
}
|
||||
float reportedDamage = enemy.LastReportedDamage;
|
||||
|
||||
bool protectionBegan = enemy.IsDamageInvulnerable;
|
||||
if (protectionBegan)
|
||||
{
|
||||
appliedKnockback = 0f;
|
||||
}
|
||||
|
||||
if (!isDash)
|
||||
{
|
||||
playerController.TryPlayBumpAttackAnimation(
|
||||
side == HitSide.Back);
|
||||
}
|
||||
|
||||
if (!isDash)
|
||||
{
|
||||
nextNormalHitTimes[enemy.GetInstanceID()] =
|
||||
Time.time + normalHitInterval;
|
||||
}
|
||||
|
||||
bool launchSucceeded = false;
|
||||
if (!protectionBegan && isStrongDash)
|
||||
{
|
||||
enemy.TryInterruptAttackFromLaunch();
|
||||
if (CanLaunchAfterHit(enemy.CanBeLaunched, enemy.IsDead))
|
||||
{
|
||||
launchSucceeded = true;
|
||||
enemy.PlayLaunchVisual(
|
||||
knockbackDirection,
|
||||
strongDashLaunchHeight,
|
||||
strongDashLaunchDuration);
|
||||
}
|
||||
else if (ShouldShowLaunchResist(enemy.CanBeLaunched))
|
||||
{
|
||||
CombatEvents.RaiseLaunchResisted(enemy.gameObject);
|
||||
}
|
||||
}
|
||||
else if (!protectionBegan && side == HitSide.Back)
|
||||
{
|
||||
bool broke = enemy.TryInterruptAttackFromBackHit();
|
||||
if (broke && !isDash)
|
||||
{
|
||||
CombatEvents.RaiseRearAttackCancelled(enemy.gameObject);
|
||||
}
|
||||
if (suppressOrdinaryEventReaction && broke)
|
||||
{
|
||||
enemy.PlayHurtAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
if (!isDash && !protectionBegan)
|
||||
{
|
||||
if (!enemy.IsCrowd
|
||||
&& BumpCombatMath.AppliesPlayerImpactRecoil(side, isEventEnemy))
|
||||
{
|
||||
playerController.ApplyImpactRecoil(
|
||||
enemyToPlayer,
|
||||
side == HitSide.Front
|
||||
? frontPlayerImpactRecoilDistance
|
||||
: side == HitSide.Back && isEventEnemy
|
||||
? eventBackPlayerImpactRecoilDistance
|
||||
: playerImpactRecoilDistance);
|
||||
}
|
||||
activeArtifactController?.AddNormalHitCharge();
|
||||
}
|
||||
|
||||
CombatEvents.RaiseValidHit(new CombatHitResult(
|
||||
gameObject,
|
||||
enemy.gameObject,
|
||||
isDash,
|
||||
side,
|
||||
reportedDamage,
|
||||
knockbackDirection,
|
||||
appliedKnockback,
|
||||
hitPosition,
|
||||
isOffsetHit,
|
||||
isStrongDash,
|
||||
damageTag,
|
||||
sourceId,
|
||||
true,
|
||||
true,
|
||||
launchSucceeded,
|
||||
artifactEffect.HasValue,
|
||||
artifactEffect.GetValueOrDefault(),
|
||||
artifactEffect.HasValue && isStrongDash));
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool CanLaunchAfterHit(bool canBeLaunched, bool isDead)
|
||||
{
|
||||
return canBeLaunched && !isDead;
|
||||
}
|
||||
|
||||
public static bool ShouldShowLaunchResist(bool canBeLaunched)
|
||||
{
|
||||
return !canBeLaunched;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ff1740f8f4f9b1946b04764e9179117a
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8a7dac88bf4ade942a657edf639850b8
|
||||
@@ -0,0 +1,224 @@
|
||||
using System;
|
||||
using BumpCombat.Player;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BumpCombat.Combat
|
||||
{
|
||||
public readonly struct CombatHitResult
|
||||
{
|
||||
public CombatHitResult(
|
||||
GameObject attacker,
|
||||
GameObject target,
|
||||
bool isDash,
|
||||
HitSide side,
|
||||
float damage,
|
||||
Vector2 knockbackDirection,
|
||||
float knockbackForce,
|
||||
Vector2 hitPosition,
|
||||
bool isOffsetHit = false,
|
||||
bool isStrongDash = false,
|
||||
DamageTag damageTag = DamageTag.Collision,
|
||||
string sourceId = null,
|
||||
bool usesPositionBonus = true,
|
||||
bool showsHitFeedback = true,
|
||||
bool launchSucceeded = false,
|
||||
bool isArtifactHit = false,
|
||||
ActiveArtifactEffect artifactEffect = ActiveArtifactEffect.Dash,
|
||||
bool isChargedArtifact = false)
|
||||
{
|
||||
Attacker = attacker;
|
||||
Target = target;
|
||||
IsDash = isDash;
|
||||
Side = side;
|
||||
Damage = damage;
|
||||
KnockbackDirection = knockbackDirection;
|
||||
KnockbackForce = knockbackForce;
|
||||
HitPosition = hitPosition;
|
||||
IsOffsetHit = isOffsetHit;
|
||||
IsStrongDash = isStrongDash;
|
||||
DamageTag = damageTag;
|
||||
SourceId = sourceId;
|
||||
UsesPositionBonus = usesPositionBonus;
|
||||
ShowsHitFeedback = showsHitFeedback;
|
||||
LaunchSucceeded = launchSucceeded;
|
||||
IsArtifactHit = isArtifactHit;
|
||||
ArtifactEffect = artifactEffect;
|
||||
IsChargedArtifact = isChargedArtifact;
|
||||
}
|
||||
|
||||
public static CombatHitResult ForArtifactContact(
|
||||
GameObject attacker,
|
||||
GameObject target,
|
||||
ActiveArtifactEffect artifactEffect,
|
||||
bool isChargedArtifact,
|
||||
HitSide side,
|
||||
Vector2 knockbackDirection,
|
||||
Vector2 hitPosition,
|
||||
DamageTag damageTag,
|
||||
string sourceId,
|
||||
bool isOffsetHit = false)
|
||||
{
|
||||
bool isDash = artifactEffect == ActiveArtifactEffect.Dash;
|
||||
return new CombatHitResult(
|
||||
attacker,
|
||||
target,
|
||||
isDash,
|
||||
side,
|
||||
0f,
|
||||
knockbackDirection,
|
||||
0f,
|
||||
hitPosition,
|
||||
isOffsetHit: isOffsetHit,
|
||||
isStrongDash: isDash && isChargedArtifact,
|
||||
damageTag: damageTag,
|
||||
sourceId: sourceId,
|
||||
usesPositionBonus: false,
|
||||
showsHitFeedback: false,
|
||||
launchSucceeded: false,
|
||||
isArtifactHit: true,
|
||||
artifactEffect: artifactEffect,
|
||||
isChargedArtifact: isChargedArtifact);
|
||||
}
|
||||
|
||||
public GameObject Attacker { get; }
|
||||
public GameObject Target { get; }
|
||||
public bool IsDash { get; }
|
||||
public bool IsStrongDash { get; }
|
||||
public DamageTag DamageTag { get; }
|
||||
public string SourceId { get; }
|
||||
public bool UsesPositionBonus { get; }
|
||||
public bool ShowsHitFeedback { get; }
|
||||
/// <summary>True only when a strong dash actually created the launch state.</summary>
|
||||
public bool LaunchSucceeded { get; }
|
||||
public bool IsArtifactHit { get; }
|
||||
public ActiveArtifactEffect ArtifactEffect { get; }
|
||||
public bool IsChargedArtifact { get; }
|
||||
public bool IsOrdinaryBump => !IsDash && string.IsNullOrEmpty(SourceId);
|
||||
public HitSide Side { get; }
|
||||
public float Damage { get; }
|
||||
public bool HasBackBonus => UsesPositionBonus && Side == HitSide.Back;
|
||||
public bool IsOffsetHit { get; }
|
||||
public bool HasPositionBonus =>
|
||||
UsesPositionBonus && (Side == HitSide.Side || IsOffsetHit);
|
||||
public Vector2 KnockbackDirection { get; }
|
||||
public float KnockbackForce { get; }
|
||||
public Vector2 HitPosition { get; }
|
||||
}
|
||||
|
||||
/// <summary>A confirmed artifact effect on one enemy during one activation.</summary>
|
||||
public readonly struct ArtifactEffectiveHitResult
|
||||
{
|
||||
public ArtifactEffectiveHitResult(
|
||||
GameObject target,
|
||||
int castIdentity,
|
||||
ActiveArtifactEffect artifactEffect,
|
||||
bool isCharged,
|
||||
bool matchedShieldRequirement)
|
||||
{
|
||||
Target = target;
|
||||
CastIdentity = castIdentity;
|
||||
ArtifactEffect = artifactEffect;
|
||||
IsCharged = isCharged;
|
||||
MatchedShieldRequirement = matchedShieldRequirement;
|
||||
}
|
||||
|
||||
public GameObject Target { get; }
|
||||
public int CastIdentity { get; }
|
||||
public ActiveArtifactEffect ArtifactEffect { get; }
|
||||
public bool IsCharged { get; }
|
||||
public bool MatchedShieldRequirement { get; }
|
||||
}
|
||||
|
||||
public static class CombatEvents
|
||||
{
|
||||
public static event Action<CombatHitResult> OnValidHit;
|
||||
public static event Action<ArtifactEffectiveHitResult> OnArtifactEffectiveHit;
|
||||
public static event Action<GameObject> OnRearAttackCancelled;
|
||||
public static event Action<GameObject, float> OnStunApplied;
|
||||
public static event Action<GameObject, bool> OnAttackCancelled;
|
||||
public static event Action<GameObject, int, int, bool> OnEnemyStagger;
|
||||
public static event Action<GameObject> OnEnemyStaggerCleared;
|
||||
public static event Action<GameObject> OnLaunchResisted;
|
||||
public static event Action<GameObject, Vector2, Vector2> OnKnockbackCompleted;
|
||||
public static event Action<GameObject> OnEnemyRetired;
|
||||
public static event Action<GameObject> OnEnemyDied;
|
||||
public static event Action<Vector2, float> OnThunderShield;
|
||||
|
||||
public static void RaiseValidHit(CombatHitResult result)
|
||||
{
|
||||
OnValidHit?.Invoke(result);
|
||||
}
|
||||
|
||||
public static void RaiseArtifactEffectiveHit(
|
||||
GameObject target,
|
||||
int castIdentity,
|
||||
ActiveArtifactEffect artifactEffect,
|
||||
bool isCharged,
|
||||
bool matchedShieldRequirement = false)
|
||||
{
|
||||
OnArtifactEffectiveHit?.Invoke(new ArtifactEffectiveHitResult(
|
||||
target,
|
||||
castIdentity,
|
||||
artifactEffect,
|
||||
isCharged,
|
||||
matchedShieldRequirement));
|
||||
}
|
||||
|
||||
public static void RaiseRearAttackCancelled(GameObject target)
|
||||
{
|
||||
OnRearAttackCancelled?.Invoke(target);
|
||||
}
|
||||
|
||||
public static void RaiseStunApplied(GameObject target, float duration)
|
||||
{
|
||||
OnStunApplied?.Invoke(target, duration);
|
||||
}
|
||||
|
||||
public static void RaiseAttackCancelled(GameObject target, bool fromStun)
|
||||
{
|
||||
OnAttackCancelled?.Invoke(target, fromStun);
|
||||
}
|
||||
|
||||
public static void RaiseEnemyStagger(
|
||||
GameObject target,
|
||||
int current,
|
||||
int maximum,
|
||||
bool broke)
|
||||
{
|
||||
OnEnemyStagger?.Invoke(target, current, maximum, broke);
|
||||
}
|
||||
|
||||
public static void RaiseEnemyStaggerCleared(GameObject target)
|
||||
{
|
||||
OnEnemyStaggerCleared?.Invoke(target);
|
||||
}
|
||||
|
||||
public static void RaiseLaunchResisted(GameObject target)
|
||||
{
|
||||
OnLaunchResisted?.Invoke(target);
|
||||
}
|
||||
|
||||
public static void RaiseKnockbackCompleted(
|
||||
GameObject target,
|
||||
Vector2 start,
|
||||
Vector2 end)
|
||||
{
|
||||
OnKnockbackCompleted?.Invoke(target, start, end);
|
||||
}
|
||||
|
||||
public static void RaiseEnemyRetired(GameObject target)
|
||||
{
|
||||
OnEnemyRetired?.Invoke(target);
|
||||
}
|
||||
|
||||
public static void RaiseEnemyDied(GameObject target)
|
||||
{
|
||||
OnEnemyDied?.Invoke(target);
|
||||
}
|
||||
|
||||
public static void RaiseThunderShield(Vector2 center, float duration)
|
||||
{
|
||||
OnThunderShield?.Invoke(center, duration);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dfeb839829347ce488873cb652ceefa4
|
||||
@@ -0,0 +1,71 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace BumpCombat.Combat
|
||||
{
|
||||
public static class DamageCalculator
|
||||
{
|
||||
public static float FinalizeDamage(float damage)
|
||||
{
|
||||
return Mathf.Floor(damage);
|
||||
}
|
||||
|
||||
public static string FormatDamage(float damage)
|
||||
{
|
||||
return FinalizeDamage(damage).ToString("0");
|
||||
}
|
||||
|
||||
public static float ApplyMultiplier(float damage, float multiplier)
|
||||
{
|
||||
return damage * multiplier;
|
||||
}
|
||||
|
||||
public static float CalculateWithAdditiveModifiers(
|
||||
float baseDamage,
|
||||
float flatBonus,
|
||||
float increasedBonus)
|
||||
{
|
||||
float damage = baseDamage + flatBonus;
|
||||
return ApplyMultiplier(damage, 1f + increasedBonus);
|
||||
}
|
||||
|
||||
public static float ApplyMoreModifier(float damage, float moreBonus)
|
||||
{
|
||||
return ApplyMultiplier(damage, 1f + moreBonus);
|
||||
}
|
||||
|
||||
public static float ApplyRepeatedMultiplier(
|
||||
float damage,
|
||||
float multiplier,
|
||||
int applicationCount)
|
||||
{
|
||||
float result = damage;
|
||||
for (int i = 0; i < applicationCount; i++)
|
||||
{
|
||||
result = ApplyMultiplier(result, multiplier);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static float ApplyDamageTakenEffects(
|
||||
float damage,
|
||||
float shockIncrease,
|
||||
bool isShocked,
|
||||
float vulnerabilityIncrease,
|
||||
bool isBumpVulnerable)
|
||||
{
|
||||
float result = damage;
|
||||
if (isShocked)
|
||||
{
|
||||
result = ApplyMultiplier(result, 1f + shockIncrease);
|
||||
}
|
||||
|
||||
if (isBumpVulnerable)
|
||||
{
|
||||
result = ApplyMultiplier(result, 1f + vulnerabilityIncrease);
|
||||
}
|
||||
|
||||
return FinalizeDamage(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bb5039010a3d4f829e4bc4465959f4b5
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace BumpCombat.Combat
|
||||
{
|
||||
public enum DamageTag
|
||||
{
|
||||
Collision,
|
||||
Spell,
|
||||
}
|
||||
|
||||
public enum ArtifactColor
|
||||
{
|
||||
Green,
|
||||
Red,
|
||||
Blue,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 35a71b8b2a234e29b3ba91470cc7e61d
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace BumpCombat.Combat
|
||||
{
|
||||
public enum HitSide
|
||||
{
|
||||
Front,
|
||||
Side,
|
||||
Back,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0036ace962504d043a25e8c9b2b90045
|
||||
Reference in New Issue
Block a user