Initial commit: Tiny Tackle Heroes Unity project

This commit is contained in:
2026-09-15 17:37:42 +09:00
commit 591fa9a826
1623 changed files with 182420 additions and 0 deletions
@@ -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