From 84d28ac4947a35ae8f361192dc1add55e8f607d3 Mon Sep 17 00:00:00 2001 From: LifeHckr Date: Fri, 18 Apr 2025 22:39:39 -0700 Subject: [PATCH 1/3] Status Effects Implemented Refactored damage taking into DamageInstances Added StatusEffect class Added Block Status as an example --- Classes/Notes/Note.cs | 12 +- Classes/StatusEffects/Assets/Status_Block.png | Bin 0 -> 181 bytes .../Assets/Status_Block.png.import | 34 ++++ Classes/StatusEffects/StatusEffect.cs | 144 ++++++++++++++ Classes/StatusEffects/StatusEffect.cs.uid | 1 + Classes/StatusEffects/StatusIcon.tscn | 36 ++++ Globals/FunkEngineNameSpace.cs | 2 + Globals/Scribe.cs | 25 ++- .../BattleDirector/Scripts/BattleDirector.cs | 179 +++++++++++++----- Scenes/Puppets/Enemies/BossBlood/Boss1.tscn | 10 +- Scenes/Puppets/Enemies/EnemyPuppet.tscn | 8 +- .../Puppets/Enemies/Parasifly/Parasifly.tscn | 8 +- Scenes/Puppets/Enemies/TheGWS/GWS.tscn | 10 +- Scenes/Puppets/PlayerPuppet.tscn | 8 +- Scenes/Puppets/Scripts/DamageInstance.cs | 26 +++ Scenes/Puppets/Scripts/DamageInstance.cs.uid | 1 + Scenes/Puppets/Scripts/PlayerPuppet.cs | 4 +- Scenes/Puppets/Scripts/PuppetTemplate.cs | 48 ++++- Scenes/Puppets/StatusContainer.tscn | 10 + 19 files changed, 486 insertions(+), 80 deletions(-) create mode 100644 Classes/StatusEffects/Assets/Status_Block.png create mode 100644 Classes/StatusEffects/Assets/Status_Block.png.import create mode 100644 Classes/StatusEffects/StatusEffect.cs create mode 100644 Classes/StatusEffects/StatusEffect.cs.uid create mode 100644 Classes/StatusEffects/StatusIcon.tscn create mode 100644 Scenes/Puppets/Scripts/DamageInstance.cs create mode 100644 Scenes/Puppets/Scripts/DamageInstance.cs.uid create mode 100644 Scenes/Puppets/StatusContainer.tscn diff --git a/Classes/Notes/Note.cs b/Classes/Notes/Note.cs index 57c463d7..466a5827 100644 --- a/Classes/Notes/Note.cs +++ b/Classes/Notes/Note.cs @@ -35,17 +35,7 @@ public Note( Id = id; Name = name; Owner = owner; - NoteEffect = - noteEffect - ?? ( - (BD, source, timing) => - { - Array.ForEach( - BD.GetTargets(source), //Ok, sure - enemy => enemy.TakeDamage((int)timing * source._baseVal) - ); - } - ); + NoteEffect = noteEffect; _baseVal = baseVal; Texture = texture; Tooltip = tooltip; diff --git a/Classes/StatusEffects/Assets/Status_Block.png b/Classes/StatusEffects/Assets/Status_Block.png new file mode 100644 index 0000000000000000000000000000000000000000..fe2d2f57ff4b785f90b8bf43d929b0002f4eff5a GIT binary patch literal 181 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|@;zM~Lo9le zQxXyqBIMWqo!`hKu;YW4^5b6>Z~vRe)v3GNH@8`+GjkiYi!f}S(y6n0A_MoXhK1=O zalFsB>pMRTe$(_YgP}@6aMIydOgUVIl0BEXPe^^($)zkSkgOf3ryzD9)*-JgDJ5Zn cjx{qw%_&tck;w-7K&u%%UHx3vIVCg!0AZmz&Hw-a literal 0 HcmV?d00001 diff --git a/Classes/StatusEffects/Assets/Status_Block.png.import b/Classes/StatusEffects/Assets/Status_Block.png.import new file mode 100644 index 00000000..ebdcbce5 --- /dev/null +++ b/Classes/StatusEffects/Assets/Status_Block.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cdlepg5fxfwn0" +path="res://.godot/imported/Status_Block.png-e8facdf93b546460a44dfea789c90919.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Classes/StatusEffects/Assets/Status_Block.png" +dest_files=["res://.godot/imported/Status_Block.png-e8facdf93b546460a44dfea789c90919.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/Classes/StatusEffects/StatusEffect.cs b/Classes/StatusEffects/StatusEffect.cs new file mode 100644 index 00000000..6b00010c --- /dev/null +++ b/Classes/StatusEffects/StatusEffect.cs @@ -0,0 +1,144 @@ +using System; +using System.Linq; +using FunkEngine; +using Godot; + +/// +/// Status Effect class. +/// Preferably set up as a static default status, then apply status using GetInstance, but custom statuses can be defined elsewhere. +/// Invoke StatusEnd to remove status. +/// +public partial class StatusEffect : TextureRect, IBattleEvent +{ + public static readonly string LoadPath = "res://Classes/StatusEffects/StatusIcon.tscn"; + public PuppetTemplate Sufferer { get; private set; } + public int Count { get; private set; } + + public string StatusName { get; private set; } + + [Export] + private Label CountLabel { get; set; } + + internal delegate void StatusEndHandler(StatusEffect status); + internal event StatusEndHandler StatusEnd; + + #region DefaultStatuses + private static readonly Action BlockEffect = (e, self) => + { + if (e is BattleDirector.Harbinger.OnDamageInstanceArgs dmgArgs) + { + if (dmgArgs.Dmg.Target != self.Sufferer || dmgArgs.Dmg.Damage <= 0) + return; + dmgArgs.Dmg.ModifyDamage(0, 0); + self.DecCount(); + } + }; + + /// + /// On the owner receiving a damage instance, if valid (correct target and dmg > 0) sets damage to 0 and reduces count. + /// + public static readonly StatusEffect Block = new StatusEffect() + .InitStatus( + "Block", + BlockEffect, + BattleEffectTrigger.OnDamageInstance, + GD.Load("res://Classes/StatusEffects/Assets/Status_Block.png") + ) + .SetTags(true); + + #endregion + + private BattleEffectTrigger _trigger; + private Action _effect; + + public BattleEffectTrigger GetTrigger() + { + return _trigger; + } + + public void OnTrigger(BattleEventArgs e) + { + _effect(e, this); + } + + public StatusEffect InitStatus( + string name, + Action effect, + BattleEffectTrigger trigger, + Texture2D texture = null + ) + { + _effect = effect; + _trigger = trigger; + StatusName = name; + Texture = texture; + return this; + } + + public StatusEffect GetInstance(int count = 1) + { + StatusEffect result = GD.Load(LoadPath).Instantiate(); + result.SetCount(count); + result.InitStatus(Name, _effect, _trigger, Texture); + result.SetTags(_stackable, _refreshes); + return result; + } + + public void SetOwner(PuppetTemplate owner) + { + Sufferer = owner; + } + + public void IncCount(int count = 1) + { + SetCount(Count + count); + } + + public void DecCount(int count = 1) + { + SetCount(Count - count); + } + + public void SetCount(int count) + { + Count = count; + CountLabel.Text = Count.ToString(); + if (Count <= 0) + { + StatusEnd?.Invoke(this); + } + } + + /// + /// Re-applying a status increases the count. + /// + private bool _stackable; + + /// + /// Re-applying a status sets the count to the higher counte + /// + private bool _refreshes; + + public StatusEffect SetTags(bool stackable = false, bool refreshes = false) + { + _stackable = stackable; + _refreshes = refreshes; + return this; + } + + //Called if a puppet is receiving a duplicate effect. + public void StackEffect(StatusEffect incomingEffect) + { + if (incomingEffect.StatusName != StatusName) + return; + if (_stackable) + { + IncCount(incomingEffect.Count); + } + + if (_refreshes && incomingEffect.Count >= Count) + { + SetCount(incomingEffect.Count); + } + } +} diff --git a/Classes/StatusEffects/StatusEffect.cs.uid b/Classes/StatusEffects/StatusEffect.cs.uid new file mode 100644 index 00000000..ed242236 --- /dev/null +++ b/Classes/StatusEffects/StatusEffect.cs.uid @@ -0,0 +1 @@ +uid://wawjisy70w1v diff --git a/Classes/StatusEffects/StatusIcon.tscn b/Classes/StatusEffects/StatusIcon.tscn new file mode 100644 index 00000000..9ca0495f --- /dev/null +++ b/Classes/StatusEffects/StatusIcon.tscn @@ -0,0 +1,36 @@ +[gd_scene load_steps=4 format=3 uid="uid://opqtl7khulko"] + +[ext_resource type="Script" uid="uid://wawjisy70w1v" path="res://Classes/StatusEffects/StatusEffect.cs" id="1_2adc3"] + +[sub_resource type="Gradient" id="Gradient_y1lef"] +offsets = PackedFloat32Array(1) +colors = PackedColorArray(1, 1, 1, 1) + +[sub_resource type="GradientTexture2D" id="GradientTexture2D_2adc3"] +gradient = SubResource("Gradient_y1lef") +width = 16 +height = 16 + +[node name="StatusIcon" type="TextureRect" node_paths=PackedStringArray("CountLabel")] +custom_minimum_size = Vector2(8, 8) +offset_right = 8.0 +offset_bottom = 8.0 +texture = SubResource("GradientTexture2D_2adc3") +script = ExtResource("1_2adc3") +CountLabel = NodePath("Count") + +[node name="Count" type="Label" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 2.0 +offset_top = 4.0 +offset_bottom = 2.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_font_sizes/font_size = 12 +text = "0" +horizontal_alignment = 2 +vertical_alignment = 2 diff --git a/Globals/FunkEngineNameSpace.cs b/Globals/FunkEngineNameSpace.cs index 7cf98d80..e1d76e7b 100644 --- a/Globals/FunkEngineNameSpace.cs +++ b/Globals/FunkEngineNameSpace.cs @@ -237,6 +237,8 @@ public enum BattleEffectTrigger OnLoop, OnBattleStart, OnBattleEnd, + OnDamageInstance, + OnDamageTaken, } public enum Stages diff --git a/Globals/Scribe.cs b/Globals/Scribe.cs index 1a5b8419..ff14c4c8 100644 --- a/Globals/Scribe.cs +++ b/Globals/Scribe.cs @@ -22,7 +22,8 @@ public partial class Scribe : Node 1, (director, note, timing) => { - director.Player.TakeDamage((3 - (int)timing) * note.GetBaseVal()); + int dmg = (3 - (int)timing) * note.GetBaseVal(); + director.Player.TakeDamage(new DamageInstance(dmg, null, director.Player)); } ), new Note( @@ -36,7 +37,7 @@ public partial class Scribe : Node { if (timing == Timing.Miss) return; - director.GetFirstEnemy()?.TakeDamage((int)timing * note.GetBaseVal()); + director.DealDamage(note, (int)timing * note.GetBaseVal(), director.Player); } ), new Note( @@ -50,7 +51,7 @@ public partial class Scribe : Node { if (timing == Timing.Miss) return; - director.GetFirstEnemy()?.TakeDamage(note.GetBaseVal() * (int)timing); + director.DealDamage(note, (int)timing * note.GetBaseVal(), director.Player); } ), new Note( @@ -78,8 +79,9 @@ public partial class Scribe : Node { if (timing == Timing.Miss) return; - director.Player.Heal((int)timing * note.GetBaseVal()); - director.GetFirstEnemy()?.TakeDamage((int)timing * note.GetBaseVal()); + int dmg = (int)timing * note.GetBaseVal(); + director.Player.Heal(dmg); + director.DealDamage(note, dmg, director.Player); } ), new Note( @@ -93,13 +95,13 @@ public partial class Scribe : Node { if (timing == Timing.Miss) return; - director.GetFirstEnemy()?.TakeDamage((int)timing + note.GetBaseVal()); + director.DealDamage(note, (int)timing * note.GetBaseVal(), director.Player); }, 0.25f ), new Note( 6, - "Play-c-HoldBlock", + "PlayerBlock", "Gives player one charge of block.", GD.Load("res://Classes/Notes/Assets/Note_PlayerBlock.png"), null, @@ -108,7 +110,7 @@ public partial class Scribe : Node { if (timing == Timing.Miss) return; - //director.Player.GainShield(note.GetBaseVal()); //todo: should scale with timing???? + director.AddStatus(Targetting.First, StatusEffect.Block.GetInstance(), true); //todo: should scale with timing???? } ), }; @@ -203,7 +205,10 @@ public partial class Scribe : Node 1, (e, self, val) => { - e.BD.GetFirstEnemy()?.TakeDamage(val); + if (e is not BattleDirector.Harbinger.NoteHitArgs noteHitArgs) + return; + if (noteHitArgs.Timing != Timing.Miss) + e.BD.DealDamage(Targetting.First, val, null); } ), } @@ -221,7 +226,7 @@ public partial class Scribe : Node 5, (e, self, val) => { - e.BD.GetFirstEnemy()?.TakeDamage(val); + e.BD.DealDamage(Targetting.First, val, null); } ), } diff --git a/Scenes/BattleDirector/Scripts/BattleDirector.cs b/Scenes/BattleDirector/Scripts/BattleDirector.cs index be7ac2ee..d7550fed 100644 --- a/Scenes/BattleDirector/Scripts/BattleDirector.cs +++ b/Scenes/BattleDirector/Scripts/BattleDirector.cs @@ -39,49 +39,6 @@ public partial class BattleDirector : Node2D #endregion - #region Note Handling - private bool PlayerAddNote(ArrowType type, Beat beat) - { - if (!NPB.CanPlaceNote()) - return false; - - Note noteToPlace = NPB.NotePlaced(); - noteToPlace.OnHit(this, Timing.Okay); - - CD.AddPlayerNote(noteToPlace, type, beat); - Harbinger.Instance.InvokeNotePlaced(new ArrowData(type, beat, noteToPlace)); - Harbinger.Instance.InvokeNoteHit(noteToPlace, Timing.Okay); //TODO: test how this feels? maybe take it out later - return true; - } - - public PuppetTemplate[] GetTargets(Note note) - { - if (!note.IsPlayerNote()) - return [Player]; - switch (note.TargetType) - { - case Targetting.First: - if (GetFirstEnemy() != null) - return [GetFirstEnemy()]; - return []; - case Targetting.All: - return _enemies.Where(x => x.GetCurrentHealth() > 0).ToArray(); - } - return null; - } - - public PuppetTemplate GetFirstEnemy() - { - foreach (var enemy in _enemies) - { - if (enemy.GetCurrentHealth() > 0) - return enemy; - } - - return null; - } - #endregion - #region Initialization private void SyncStartWithMix() { @@ -179,6 +136,20 @@ public override void _UnhandledInput(InputEvent @event) } } + private bool PlayerAddNote(ArrowType type, Beat beat) + { + if (!NPB.CanPlaceNote()) + return false; + + Note noteToPlace = NPB.NotePlaced(); + noteToPlace.OnHit(this, Timing.Okay); + + CD.AddPlayerNote(noteToPlace, type, beat); + Harbinger.Instance.InvokeNotePlaced(new ArrowData(type, beat, noteToPlace)); + Harbinger.Instance.InvokeNoteHit(noteToPlace, Timing.Okay); //TODO: test how this feels? maybe take it out later + return true; + } + //Only called from CD signal when a note is processed private void OnTimedInput(ArrowData data, double beatDif) { @@ -206,7 +177,7 @@ private void ForceMiss(ArrowType type) { NPB.HandleTiming(Timing.Miss, type); CM.ComboText(Timing.Miss, type, NPB.GetCurrentCombo()); - Player.TakeDamage(4); + Player.TakeDamage(new DamageInstance(4, null, Player)); } private Timing CheckTiming(double beatDif) @@ -280,10 +251,81 @@ private void TransitionOutOfBattle() } #endregion + #region Battles + public void DealDamage(Note note, int damage, PuppetTemplate source) + { + PuppetTemplate[] targets = GetTargets(note.TargetType, !note.IsPlayerNote()); + foreach (PuppetTemplate target in targets) + { + target.TakeDamage(new DamageInstance(damage, source, target)); + } + } + + public void DealDamage( + Targetting targetting, + int damage, + PuppetTemplate source, + bool targetPlayer = false + ) + { + PuppetTemplate[] targets = GetTargets(targetting, targetPlayer); + foreach (PuppetTemplate target in targets) + { + target.TakeDamage(new DamageInstance(damage, source, target)); + } + } + + public void AddStatus(Targetting targetting, StatusEffect status, bool targetPlayer = false) + { + PuppetTemplate[] targets = GetTargets(targetting, targetPlayer); + foreach (PuppetTemplate target in targets) + { + target.AddStatusEffect(status); + } + + status.StatusEnd += RemoveStatus; + AddEvent(status); + } + + public void RemoveStatus(StatusEffect status) + { + status.Sufferer.RemoveStatusEffect(status); + status.StatusEnd -= RemoveStatus; + RemoveEvent(status); + } + + private PuppetTemplate[] GetTargets(Targetting targetting, bool targetPlayer = false) + { + if (targetPlayer) + return [Player]; + switch (targetting) + { + case Targetting.First: + if (GetFirstEnemy() != null) + return [GetFirstEnemy()]; + return []; + case Targetting.All: + return _enemies.Where(x => x.GetCurrentHealth() > 0).ToArray(); + } + return null; + } + + private PuppetTemplate GetFirstEnemy() + { + foreach (var enemy in _enemies) + { + if (enemy.GetCurrentHealth() > 0) + return enemy; + } + + return null; + } + #endregion + #region BattleEffect Handling private void AddEvent(IBattleEvent bEvent) { - switch (bEvent.GetTrigger()) //TODO: Look into a way to get eventhandler from string + switch (bEvent.GetTrigger()) { case BattleEffectTrigger.NotePlaced: Harbinger.Instance.NotePlaced += bEvent.OnTrigger; @@ -297,6 +339,31 @@ private void AddEvent(IBattleEvent bEvent) case BattleEffectTrigger.OnBattleEnd: Harbinger.Instance.BattleEnded += bEvent.OnTrigger; break; + case BattleEffectTrigger.OnDamageInstance: + Harbinger.Instance.OnDamageInstance += bEvent.OnTrigger; + break; + } + } + + private void RemoveEvent(IBattleEvent bEvent) + { + switch (bEvent.GetTrigger()) + { + case BattleEffectTrigger.NotePlaced: + Harbinger.Instance.NotePlaced -= bEvent.OnTrigger; + break; + case BattleEffectTrigger.OnLoop: + Harbinger.Instance.ChartLooped -= bEvent.OnTrigger; + break; + case BattleEffectTrigger.NoteHit: + Harbinger.Instance.NoteHit -= bEvent.OnTrigger; + break; + case BattleEffectTrigger.OnBattleEnd: + Harbinger.Instance.BattleEnded -= bEvent.OnTrigger; + break; + case BattleEffectTrigger.OnDamageInstance: + Harbinger.Instance.OnDamageInstance -= bEvent.OnTrigger; + break; } } @@ -414,13 +481,33 @@ public void InvokeBattleEnded() { BattleEnded?.Invoke(new BattleEventArgs(_curDirector)); } + + /// + /// Event Args to handle a damage instance being dealt. Happens before taking damage. + /// This allows damage to be intercepted, to be reduced/increased, to counter, or heal based on incoming damage. + /// + /// The BattleDirector calling the event. + /// The damage instance being thrown. + public class OnDamageInstanceArgs(BattleDirector bd, DamageInstance dmg) + : BattleEventArgs(bd) + { + public DamageInstance Dmg = dmg; + } + + internal delegate void OnDamageInstanceHandler(OnDamageInstanceArgs e); + internal event OnDamageInstanceHandler OnDamageInstance; + + public void InvokeOnDamageInstance(DamageInstance dmg) + { + OnDamageInstance?.Invoke(new OnDamageInstanceArgs(_curDirector, dmg)); + } } private void DebugKillEnemy() { foreach (EnemyPuppet enemy in _enemies) { - enemy.TakeDamage(1000); + enemy.TakeDamage(new DamageInstance(1000, null, enemy)); } } } diff --git a/Scenes/Puppets/Enemies/BossBlood/Boss1.tscn b/Scenes/Puppets/Enemies/BossBlood/Boss1.tscn index d200454d..fd5648c8 100644 --- a/Scenes/Puppets/Enemies/BossBlood/Boss1.tscn +++ b/Scenes/Puppets/Enemies/BossBlood/Boss1.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=8 format=3 uid="uid://bi5iqbwpsd381"] +[gd_scene load_steps=9 format=3 uid="uid://bi5iqbwpsd381"] [ext_resource type="Script" uid="uid://bpyrrnhvisxgv" path="res://Scenes/Puppets/Enemies/BossBlood/P_BossBlood.cs" id="1_qj2oj"] +[ext_resource type="PackedScene" uid="uid://cdoguwlxehbpg" path="res://Scenes/Puppets/StatusContainer.tscn" id="2_b8x7h"] [ext_resource type="Texture2D" uid="uid://veedngaorx3l" path="res://Scenes/Puppets/Enemies/BossBlood/Assets/Boss1.png" id="2_mul30"] [ext_resource type="Texture2D" uid="uid://b2iptr3o8rg4t" path="res://Scenes/Puppets/Enemies/BossBlood/Assets/Boss1EmissionShape.tres" id="3_yxnso"] [ext_resource type="PackedScene" uid="uid://bgomxovxs7sr8" path="res://Scenes/Puppets/HealthBar.tscn" id="4_ffkxf"] @@ -22,11 +23,16 @@ scale_min = 4.0 scale_max = 4.0 turbulence_noise_strength = 14.2 -[node name="EnemPuppet" type="Node2D" node_paths=PackedStringArray("HealthBar", "Sprite")] +[node name="EnemPuppet" type="Node2D" node_paths=PackedStringArray("HealthBar", "Sprite", "_statusContainer")] script = ExtResource("1_qj2oj") HealthBar = NodePath("ProgressBar") Sprite = NodePath("Sprite") InitScale = Vector2(2, 2) +_statusContainer = NodePath("StatusContainer") + +[node name="StatusContainer" parent="." instance=ExtResource("2_b8x7h")] +offset_top = -105.0 +offset_bottom = -69.0 [node name="Sprite" type="Sprite2D" parent="."] position = Vector2(0, -32) diff --git a/Scenes/Puppets/Enemies/EnemyPuppet.tscn b/Scenes/Puppets/Enemies/EnemyPuppet.tscn index c7c6b849..fe97e6ba 100644 --- a/Scenes/Puppets/Enemies/EnemyPuppet.tscn +++ b/Scenes/Puppets/Enemies/EnemyPuppet.tscn @@ -1,12 +1,14 @@ -[gd_scene load_steps=3 format=3 uid="uid://cwm0n2wu851nx"] +[gd_scene load_steps=4 format=3 uid="uid://cwm0n2wu851nx"] [ext_resource type="Script" uid="uid://bnfo57c070cll" path="res://Scenes/Puppets/Scripts/PuppetTemplate.cs" id="1_kgmh0"] [ext_resource type="PackedScene" uid="uid://bgomxovxs7sr8" path="res://Scenes/Puppets/HealthBar.tscn" id="1_sxlrs"] +[ext_resource type="PackedScene" uid="uid://cdoguwlxehbpg" path="res://Scenes/Puppets/StatusContainer.tscn" id="3_hguc7"] -[node name="EnemPuppet" type="Node2D" node_paths=PackedStringArray("HealthBar", "Sprite")] +[node name="EnemPuppet" type="Node2D" node_paths=PackedStringArray("HealthBar", "Sprite", "_statusContainer")] script = ExtResource("1_kgmh0") HealthBar = NodePath("ProgressBar") Sprite = NodePath("Sprite") +_statusContainer = NodePath("StatusContainer") [node name="Sprite" type="Sprite2D" parent="."] @@ -15,3 +17,5 @@ offset_left = -50.0 offset_top = 32.0 offset_right = 50.0 offset_bottom = 52.0 + +[node name="StatusContainer" parent="." instance=ExtResource("3_hguc7")] diff --git a/Scenes/Puppets/Enemies/Parasifly/Parasifly.tscn b/Scenes/Puppets/Enemies/Parasifly/Parasifly.tscn index d5ef4a24..7b2b873f 100644 --- a/Scenes/Puppets/Enemies/Parasifly/Parasifly.tscn +++ b/Scenes/Puppets/Enemies/Parasifly/Parasifly.tscn @@ -1,13 +1,17 @@ -[gd_scene load_steps=4 format=3 uid="uid://uvlux4t6h5de"] +[gd_scene load_steps=5 format=3 uid="uid://uvlux4t6h5de"] [ext_resource type="Script" uid="uid://btaqgieybx0ep" path="res://Scenes/Puppets/Enemies/Parasifly/P_Parasifly.cs" id="1_ci2ca"] +[ext_resource type="PackedScene" uid="uid://cdoguwlxehbpg" path="res://Scenes/Puppets/StatusContainer.tscn" id="2_dq3sr"] [ext_resource type="Texture2D" uid="uid://pngu3pw1pu4o" path="res://Scenes/Puppets/Enemies/Parasifly/Assets/Parasifly.png" id="2_g4o48"] [ext_resource type="PackedScene" uid="uid://bgomxovxs7sr8" path="res://Scenes/Puppets/HealthBar.tscn" id="3_f74ri"] -[node name="EnemPuppet" type="Node2D" node_paths=PackedStringArray("HealthBar", "Sprite")] +[node name="EnemPuppet" type="Node2D" node_paths=PackedStringArray("HealthBar", "Sprite", "_statusContainer")] script = ExtResource("1_ci2ca") HealthBar = NodePath("ProgressBar") Sprite = NodePath("Sprite") +_statusContainer = NodePath("StatusContainer") + +[node name="StatusContainer" parent="." instance=ExtResource("2_dq3sr")] [node name="Sprite" type="Sprite2D" parent="."] position = Vector2(0, -12) diff --git a/Scenes/Puppets/Enemies/TheGWS/GWS.tscn b/Scenes/Puppets/Enemies/TheGWS/GWS.tscn index 20f76e45..c77cac57 100644 --- a/Scenes/Puppets/Enemies/TheGWS/GWS.tscn +++ b/Scenes/Puppets/Enemies/TheGWS/GWS.tscn @@ -1,13 +1,19 @@ -[gd_scene load_steps=4 format=3 uid="uid://d1puw6fvmkrb5"] +[gd_scene load_steps=5 format=3 uid="uid://d1puw6fvmkrb5"] [ext_resource type="Script" uid="uid://3axgcdtdevtx" path="res://Scenes/Puppets/Enemies/TheGWS/P_TheGWS.cs" id="1_dlike"] [ext_resource type="Texture2D" uid="uid://ci0a2h2eatjht" path="res://Scenes/Puppets/Enemies/TheGWS/Assets/GhostWolfSnake.png" id="2_77v4w"] +[ext_resource type="PackedScene" uid="uid://cdoguwlxehbpg" path="res://Scenes/Puppets/StatusContainer.tscn" id="2_c7cx1"] [ext_resource type="PackedScene" uid="uid://bgomxovxs7sr8" path="res://Scenes/Puppets/HealthBar.tscn" id="3_mr0it"] -[node name="EnemPuppet" type="Node2D" node_paths=PackedStringArray("HealthBar", "Sprite")] +[node name="EnemPuppet" type="Node2D" node_paths=PackedStringArray("HealthBar", "Sprite", "_statusContainer")] script = ExtResource("1_dlike") HealthBar = NodePath("ProgressBar") Sprite = NodePath("Sprite") +_statusContainer = NodePath("StatusContainer") + +[node name="StatusContainer" parent="." instance=ExtResource("2_c7cx1")] +offset_top = -6.0 +offset_bottom = 30.0 [node name="Sprite" type="Sprite2D" parent="."] position = Vector2(0, -54) diff --git a/Scenes/Puppets/PlayerPuppet.tscn b/Scenes/Puppets/PlayerPuppet.tscn index 2fd0a348..e0caec7c 100644 --- a/Scenes/Puppets/PlayerPuppet.tscn +++ b/Scenes/Puppets/PlayerPuppet.tscn @@ -1,13 +1,15 @@ -[gd_scene load_steps=4 format=3 uid="uid://eus17omen6yk"] +[gd_scene load_steps=5 format=3 uid="uid://eus17omen6yk"] [ext_resource type="Script" uid="uid://kkb4qp3s86n3" path="res://Scenes/Puppets/Scripts/PlayerPuppet.cs" id="1_f4rea"] [ext_resource type="Texture2D" uid="uid://b6fkei0i83vte" path="res://SharedAssets/Character1.png" id="2_affso"] [ext_resource type="PackedScene" uid="uid://bgomxovxs7sr8" path="res://Scenes/Puppets/HealthBar.tscn" id="3_rechw"] +[ext_resource type="PackedScene" uid="uid://cdoguwlxehbpg" path="res://Scenes/Puppets/StatusContainer.tscn" id="4_o8yrn"] -[node name="PlayerPuppet" type="Node2D" node_paths=PackedStringArray("HealthBar", "Sprite")] +[node name="PlayerPuppet" type="Node2D" node_paths=PackedStringArray("HealthBar", "Sprite", "_statusContainer")] script = ExtResource("1_f4rea") HealthBar = NodePath("ProgressBar") Sprite = NodePath("Sprite") +_statusContainer = NodePath("StatusContainer") [node name="Sprite" type="Sprite2D" parent="."] texture = ExtResource("2_affso") @@ -17,3 +19,5 @@ offset_left = -50.0 offset_top = 32.0 offset_right = 50.0 offset_bottom = 52.0 + +[node name="StatusContainer" parent="." instance=ExtResource("4_o8yrn")] diff --git a/Scenes/Puppets/Scripts/DamageInstance.cs b/Scenes/Puppets/Scripts/DamageInstance.cs new file mode 100644 index 00000000..f5406864 --- /dev/null +++ b/Scenes/Puppets/Scripts/DamageInstance.cs @@ -0,0 +1,26 @@ +using System; +using Godot; + +/// +/// A class to transmit damage with the source of damage and intended target. +/// Source should never be null, null target represents an indirect source of damage. +/// +public class DamageInstance +{ + public int Damage { get; private set; } + public PuppetTemplate Target { get; private set; } + public PuppetTemplate Source { get; private set; } + + public DamageInstance(int damage, PuppetTemplate source, PuppetTemplate target) + { + Damage = damage; + Source = source; + Target = target; + } + + public DamageInstance ModifyDamage(int increase, int multiplier = 1) + { + Damage = (Damage + increase) * multiplier; + return this; + } +} diff --git a/Scenes/Puppets/Scripts/DamageInstance.cs.uid b/Scenes/Puppets/Scripts/DamageInstance.cs.uid new file mode 100644 index 00000000..5f754b86 --- /dev/null +++ b/Scenes/Puppets/Scripts/DamageInstance.cs.uid @@ -0,0 +1 @@ +uid://cj3nureovetrp diff --git a/Scenes/Puppets/Scripts/PlayerPuppet.cs b/Scenes/Puppets/Scripts/PlayerPuppet.cs index 6c283c88..513c7965 100644 --- a/Scenes/Puppets/Scripts/PlayerPuppet.cs +++ b/Scenes/Puppets/Scripts/PlayerPuppet.cs @@ -19,9 +19,9 @@ public override void _Ready() base._Ready(); } - public override void TakeDamage(int amount) + public override void TakeDamage(DamageInstance dmg) { - base.TakeDamage(amount); + base.TakeDamage(dmg); Stats.CurrentHealth = CurrentHealth; } diff --git a/Scenes/Puppets/Scripts/PuppetTemplate.cs b/Scenes/Puppets/Scripts/PuppetTemplate.cs index 24651cc9..5257d67f 100644 --- a/Scenes/Puppets/Scripts/PuppetTemplate.cs +++ b/Scenes/Puppets/Scripts/PuppetTemplate.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using Godot; /** @@ -113,8 +115,11 @@ protected virtual void Kill() } #endregion - public virtual void TakeDamage(int amount) + public virtual void TakeDamage(DamageInstance dmg) { + BattleDirector.Harbinger.Instance.InvokeOnDamageInstance(dmg); + int amount = dmg.Damage; + amount = Math.Max(0, amount); //Should not be able to heal from damage. if (CurrentHealth <= 0 || amount == 0) return; //Only check if hp would change @@ -148,4 +153,45 @@ public int GetCurrentHealth() { return CurrentHealth; } + + #region Status Effects + /// + /// The visual indicators for status effects. + /// + [Export] + protected GridContainer _statusContainer; + const int MaxStatuses = 8; + protected List StatusEffects = new List(); + + /// + /// Returns true if it could be successfully added as a new status effect. False if it is stacking, or can't add. + /// ONLY call this from within BattleDirector's AddStatus + /// + /// The status effect to add. + /// + public bool AddStatusEffect(StatusEffect effect) + { + int index = StatusEffects.FindIndex(sEff => sEff.StatusName == effect.StatusName); + if (index != -1) //If status of same name -> stack -> return false + { + StatusEffects[index].StackEffect(effect); + return false; + } + if (StatusEffects.Count >= MaxStatuses) + return false; //Max status effects -> return false + + //Add status effect -> true + _statusContainer.AddChild(effect); + effect.SetOwner(this); + StatusEffects.Add(effect); + return true; + } + + public void RemoveStatusEffect(StatusEffect effect) + { + _statusContainer.RemoveChild(effect); + StatusEffects.Remove(effect); + effect.QueueFree(); + } + #endregion } diff --git a/Scenes/Puppets/StatusContainer.tscn b/Scenes/Puppets/StatusContainer.tscn new file mode 100644 index 00000000..4a462b46 --- /dev/null +++ b/Scenes/Puppets/StatusContainer.tscn @@ -0,0 +1,10 @@ +[gd_scene format=3 uid="uid://cdoguwlxehbpg"] + +[node name="StatusContainer" type="GridContainer"] +offset_left = -38.0 +offset_top = -69.0 +offset_right = 38.0 +offset_bottom = -33.0 +theme_override_constants/h_separation = 4 +theme_override_constants/v_separation = 4 +columns = 4 From 1ab9834135a4385dc275eaf95c073eb9b8d07e53 Mon Sep 17 00:00:00 2001 From: LifeHckr Date: Fri, 18 Apr 2025 22:46:04 -0700 Subject: [PATCH 2/3] Refactored targetting to include Player targetting --- Classes/StatusEffects/StatusEffect.cs | 2 +- Globals/FunkEngineNameSpace.cs | 1 + Globals/Scribe.cs | 2 +- Scenes/BattleDirector/Scripts/BattleDirector.cs | 17 +++++++++-------- 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Classes/StatusEffects/StatusEffect.cs b/Classes/StatusEffects/StatusEffect.cs index 6b00010c..2a571066 100644 --- a/Classes/StatusEffects/StatusEffect.cs +++ b/Classes/StatusEffects/StatusEffect.cs @@ -9,7 +9,7 @@ /// Invoke StatusEnd to remove status. /// public partial class StatusEffect : TextureRect, IBattleEvent -{ +{ //TODO: Status effects that are permanent, and status effects that don't take up a slot/are invisible public static readonly string LoadPath = "res://Classes/StatusEffects/StatusIcon.tscn"; public PuppetTemplate Sufferer { get; private set; } public int Count { get; private set; } diff --git a/Globals/FunkEngineNameSpace.cs b/Globals/FunkEngineNameSpace.cs index e1d76e7b..a280d737 100644 --- a/Globals/FunkEngineNameSpace.cs +++ b/Globals/FunkEngineNameSpace.cs @@ -224,6 +224,7 @@ public enum Timing public enum Targetting { + Player, First, All, } diff --git a/Globals/Scribe.cs b/Globals/Scribe.cs index ff14c4c8..d0f49fdb 100644 --- a/Globals/Scribe.cs +++ b/Globals/Scribe.cs @@ -110,7 +110,7 @@ public partial class Scribe : Node { if (timing == Timing.Miss) return; - director.AddStatus(Targetting.First, StatusEffect.Block.GetInstance(), true); //todo: should scale with timing???? + director.AddStatus(Targetting.Player, StatusEffect.Block.GetInstance()); //todo: should scale with timing???? } ), }; diff --git a/Scenes/BattleDirector/Scripts/BattleDirector.cs b/Scenes/BattleDirector/Scripts/BattleDirector.cs index d7550fed..8e105c1b 100644 --- a/Scenes/BattleDirector/Scripts/BattleDirector.cs +++ b/Scenes/BattleDirector/Scripts/BattleDirector.cs @@ -254,7 +254,7 @@ private void TransitionOutOfBattle() #region Battles public void DealDamage(Note note, int damage, PuppetTemplate source) { - PuppetTemplate[] targets = GetTargets(note.TargetType, !note.IsPlayerNote()); + PuppetTemplate[] targets = GetTargets(note.TargetType); foreach (PuppetTemplate target in targets) { target.TakeDamage(new DamageInstance(damage, source, target)); @@ -268,16 +268,16 @@ public void DealDamage( bool targetPlayer = false ) { - PuppetTemplate[] targets = GetTargets(targetting, targetPlayer); + PuppetTemplate[] targets = GetTargets(targetting); foreach (PuppetTemplate target in targets) { target.TakeDamage(new DamageInstance(damage, source, target)); } } - public void AddStatus(Targetting targetting, StatusEffect status, bool targetPlayer = false) + public void AddStatus(Targetting targetting, StatusEffect status) { - PuppetTemplate[] targets = GetTargets(targetting, targetPlayer); + PuppetTemplate[] targets = GetTargets(targetting); foreach (PuppetTemplate target in targets) { target.AddStatusEffect(status); @@ -294,20 +294,21 @@ public void RemoveStatus(StatusEffect status) RemoveEvent(status); } - private PuppetTemplate[] GetTargets(Targetting targetting, bool targetPlayer = false) + private PuppetTemplate[] GetTargets(Targetting targetting) { - if (targetPlayer) - return [Player]; switch (targetting) { + case Targetting.Player: + return [Player]; case Targetting.First: if (GetFirstEnemy() != null) return [GetFirstEnemy()]; return []; case Targetting.All: return _enemies.Where(x => x.GetCurrentHealth() > 0).ToArray(); + default: + return null; } - return null; } private PuppetTemplate GetFirstEnemy() From a80fe118afd041c2dcc8f38340a124cf2495ea55 Mon Sep 17 00:00:00 2001 From: LifeHckr Date: Sat, 19 Apr 2025 11:45:41 -0700 Subject: [PATCH 3/3] Add Mulligan status effect Allows the player to ignore the combo penalty of a missed note --- .../StatusEffects/Assets/Status_Mulligan.png | Bin 0 -> 201 bytes Classes/StatusEffects/StatusEffect.cs | 19 ++++++++++++++++ .../Scripts/NotePlacementBar.cs | 21 ++++++++++++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 Classes/StatusEffects/Assets/Status_Mulligan.png diff --git a/Classes/StatusEffects/Assets/Status_Mulligan.png b/Classes/StatusEffects/Assets/Status_Mulligan.png new file mode 100644 index 0000000000000000000000000000000000000000..5d84a8ac879aa198be18036a37bdff7fcf4c1f56 GIT binary patch literal 201 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`jKx9jP7LeL$-D$|nmt_{Lo9le zQxXyqEdIQCS MulliganEffect = (e, self) => + { + if (e is not BattleDirector.Harbinger.NoteHitArgs { Timing: Timing.Miss }) + return; + e.BD.NPB.SetIgnoreMiss(true); //Intercept the miss + self.DecCount(); + }; + + /// + /// If the player missed, take damage, but don't receive combo penalty. + /// + public static readonly StatusEffect Mulligan = new StatusEffect() + .InitStatus( + "Mulligan", + MulliganEffect, + BattleEffectTrigger.NoteHit, + GD.Load("res://Classes/StatusEffects/Assets/Status_Mulligan.png") + ) + .SetTags(true); #endregion private BattleEffectTrigger _trigger; diff --git a/Scenes/BattleDirector/Scripts/NotePlacementBar.cs b/Scenes/BattleDirector/Scripts/NotePlacementBar.cs index ea285e1e..56d149db 100644 --- a/Scenes/BattleDirector/Scripts/NotePlacementBar.cs +++ b/Scenes/BattleDirector/Scripts/NotePlacementBar.cs @@ -186,6 +186,12 @@ public int GetCurrentCombo() return _currentCombo; } + public void ResetCurrentCombo() + { + _currentCombo = 0; + _bonusMult = 0; + } + //For external events to directly change mult public void IncreaseBonusMult(int amount = 1) { @@ -236,13 +242,24 @@ private void HitNote(ArrowType type) UpdateComboMultText(); } + public bool IgnoreMiss; //a one time safe miss + // Missing a note resets combo private void MissNote() { - _currentCombo = 0; - _bonusMult = 0; + if (IgnoreMiss) + { + IgnoreMiss = false; + return; + } + ResetCurrentCombo(); UpdateComboMultText(); } + + public void SetIgnoreMiss(bool ignore) + { + IgnoreMiss = ignore; + } #endregion #region Shake