diff --git a/Classes/Notes/Assets/Note_GWS.png b/Classes/Notes/Assets/Note_GWS.png new file mode 100644 index 00000000..be47556e Binary files /dev/null and b/Classes/Notes/Assets/Note_GWS.png differ diff --git a/Classes/Notes/Assets/Note_GWS.png.import b/Classes/Notes/Assets/Note_GWS.png.import new file mode 100644 index 00000000..a91093be --- /dev/null +++ b/Classes/Notes/Assets/Note_GWS.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bqng2uofbio1x" +path="res://.godot/imported/Note_GWS.png-374fb66d7b5ccdab1c51d6ad3d49769c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://Classes/Notes/Assets/Note_GWS.png" +dest_files=["res://.godot/imported/Note_GWS.png-374fb66d7b5ccdab1c51d6ad3d49769c.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/Notes/Note.cs b/Classes/Notes/Note.cs index 59cfabdb..3dc196e3 100644 --- a/Classes/Notes/Note.cs +++ b/Classes/Notes/Note.cs @@ -22,7 +22,6 @@ public Note( int id, string name, Texture2D texture = null, - PuppetTemplate owner = null, int baseVal = 1, Action noteEffect = null, float costModifier = 1.0f, @@ -31,7 +30,6 @@ public Note( { Id = id; Name = name; - Owner = owner; NoteEffect = noteEffect; _baseVal = baseVal; Texture = texture; @@ -44,20 +42,17 @@ public void OnHit(BattleDirector BD, Timing timing) NoteEffect(BD, this, timing); } + public Note SetOwner(PuppetTemplate owner) + { + Owner = owner; + return this; + } + public Note Clone() { //Eventually could look into something more robust, but for now shallow copy is preferable. //We only would want val and name to be copied by value - Note newNote = new Note( - Id, - Name, - Texture, - Owner, - _baseVal, - NoteEffect, - CostModifier, - TargetType - ); + Note newNote = new Note(Id, Name, Texture, _baseVal, NoteEffect, CostModifier, TargetType); return newNote; } diff --git a/Globals/FunkEngineNameSpace.cs b/Globals/FunkEngineNameSpace.cs index db26af8c..f9d87b60 100644 --- a/Globals/FunkEngineNameSpace.cs +++ b/Globals/FunkEngineNameSpace.cs @@ -55,7 +55,7 @@ public ArrowData(ArrowType type, Beat beat, Note note, double length = 0) public Beat Beat; public readonly double Length; //in beats, should never be >= loop public readonly ArrowType Type; - public readonly Note NoteRef = null; + public Note NoteRef { get; private set; } = null; public static ArrowData Placeholder { get; private set; } = new(default, default, new Note(-1, "")); @@ -66,6 +66,18 @@ public ArrowData BeatFromLength() return this; } + public static ArrowData SetNote(ArrowData arrowData, Note note) + { + arrowData.NoteRef = note; + return arrowData; + } + + public ArrowData IncDecLoop(int amount) + { + Beat.IncDecLoop(amount); + return this; + } + public bool Equals(ArrowData other) { return Beat.Equals(other.Beat) && Type == other.Type; diff --git a/Globals/Scribe.cs b/Globals/Scribe.cs index e012edde..37e19213 100644 --- a/Globals/Scribe.cs +++ b/Globals/Scribe.cs @@ -17,7 +17,6 @@ public partial class Scribe : Node 0, "EnemyBase", null, - null, 1, (director, note, timing) => { @@ -29,7 +28,6 @@ public partial class Scribe : Node 1, "PlayerBase", GD.Load("res://Classes/Notes/Assets/Note_PlayerBasic.png"), - null, 4, (director, note, timing) => { @@ -42,7 +40,6 @@ public partial class Scribe : Node 2, "PlayerDouble", GD.Load("res://Classes/Notes/Assets/Note_PlayerDouble.png"), - null, 8, (director, note, timing) => { @@ -55,7 +52,6 @@ public partial class Scribe : Node 3, "PlayerHeal", GD.Load("res://Classes/Notes/Assets/Note_PlayerHeal.png"), - null, 1, (director, note, timing) => { @@ -68,7 +64,6 @@ public partial class Scribe : Node 4, "PlayerVampire", GD.Load("res://Classes/Notes/Assets/Note_PlayerVampire.png"), - null, 3, (director, note, timing) => { @@ -83,7 +78,6 @@ public partial class Scribe : Node 5, "PlayerQuarter", GD.Load("res://Classes/Notes/Assets/Note_PlayerQuarter.png"), - null, 3, (director, note, timing) => { @@ -97,7 +91,6 @@ public partial class Scribe : Node 6, "PlayerBlock", GD.Load("res://Classes/Notes/Assets/Note_PlayerBlock.png"), - null, 1, (director, note, timing) => { @@ -110,7 +103,6 @@ public partial class Scribe : Node 7, "PlayerExplosive", GD.Load("res://Classes/Notes/Assets/Note_PlayerExplosive.png"), - null, 4, (director, note, timing) => { @@ -125,7 +117,6 @@ public partial class Scribe : Node 8, "PlayerEcho", GD.Load("res://Classes/Notes/Assets/Note_PlayerEcho.png"), - null, 4, (director, note, timing) => { @@ -139,7 +130,6 @@ public partial class Scribe : Node 9, "PlayerPoison", GD.Load("res://Classes/Notes/Assets/Note_PlayerPoison.png"), - null, 1, (director, note, timing) => { @@ -148,6 +138,19 @@ public partial class Scribe : Node director.AddStatus(Targetting.First, StatusEffect.Poison.GetInstance((int)timing)); } ), + new Note( + 10, + "GWS", + GD.Load("res://Classes/Notes/Assets/Note_GWS.png"), + 1, + (director, note, timing) => + { + int dmg = 2 * (3 - (int)timing) * note.GetBaseVal() + TimeKeeper.LastBeat.Loop; //Double an enemy base plus the loop num, unless perfect + if (timing == Timing.Perfect) + dmg = 0; + director.DealDamage(Targetting.Player, dmg, note.Owner); + } + ), }; public static readonly RelicTemplate[] RelicDictionary = new[] diff --git a/Scenes/BattleDirector/Scripts/BattleDirector.cs b/Scenes/BattleDirector/Scripts/BattleDirector.cs index db8e3011..4a228c1f 100644 --- a/Scenes/BattleDirector/Scripts/BattleDirector.cs +++ b/Scenes/BattleDirector/Scripts/BattleDirector.cs @@ -96,7 +96,7 @@ public override void _Ready() InitPlayer(); InitEnemies(); InitScoringGuide(); - CD.Initialize(curSong); + CD.Initialize(curSong, _enemies); CD.NoteInputEvent += OnTimedInput; FocusedButton.GrabFocus(); @@ -214,12 +214,26 @@ public bool PlayerAddNote(ArrowType type, Beat beat) Note noteToPlace = NPB.NotePlaced(); noteToPlace.OnHit(this, Timing.Okay); - CD.AddPlayerNote(noteToPlace, type, beat); + CD.AddPlayerNote(noteToPlace.SetOwner(Player), 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 bool EnemyAddNote(ArrowType type, Beat beat, Note noteRef, float len, EnemyPuppet enemy) + { + noteRef.SetOwner(enemy); + Beat realBeat = TimeKeeper.GetBeatFromTime(Audio.GetPlaybackPosition()); + return CD.AddConcurrentNote(realBeat, noteRef, type, beat.IncDecLoop(realBeat.Loop), len); + } + + public void RandApplyNote(PuppetTemplate owner, int noteId, int amount) + { + if (owner == null || noteId > Scribe.NoteDictionary.Length || amount < 1) + return; + CD.SetRandBaseNoteToType(owner, (noteId, amount)); + } + //Only called from CD signal when a note is processed private void OnTimedInput(ArrowData data, double beatDif) { diff --git a/Scenes/BattleDirector/Scripts/Conductor.cs b/Scenes/BattleDirector/Scripts/Conductor.cs index 146bba7a..e8ff1331 100644 --- a/Scenes/BattleDirector/Scripts/Conductor.cs +++ b/Scenes/BattleDirector/Scripts/Conductor.cs @@ -18,7 +18,7 @@ public partial class Conductor : Node private bool _initialized; #region Initialization - public void Initialize(SongData curSong) + public void Initialize(SongData curSong, EnemyPuppet[] enemies = null) { if (_initialized) return; @@ -33,6 +33,8 @@ public void Initialize(SongData curSong) CM.Size.X / TimeKeeper.ChartWidth * TimeKeeper.BeatsPerLoop ); AddInitialNotes(); + AddInitialEnemyNotes(enemies); + SpawnInitialNotes(); _initialized = true; } @@ -46,11 +48,22 @@ private void AddInitialNotes() AddNoteData(Scribe.NoteDictionary[0], type, new Beat((int)Note.Beat), Note.Length); } } - SpawnInitialNotes(); + } + + private void AddInitialEnemyNotes(EnemyPuppet[] enemies) + { + if (enemies == null) + return; + foreach (EnemyPuppet enemy in enemies) + { + if (enemy.InitialNote.Amount > 0) + SetRandBaseNoteToType(enemy, enemy.InitialNote); + } } private void SpawnInitialNotes() { + _noteData.Sort(); //Isn't inherently necessary, but sort for safety for (int i = 1; i <= _beatSpawnOffset; i++) { SpawnNotesAtBeat(new Beat(i)); @@ -66,55 +79,180 @@ private void ReceiveNoteInput(ArrowData data) } #endregion + //Ignores sorting, use sparingly + private void AddNoteData(ArrowData data, int index) + { + if (index == -1) + { + GD.PushWarning( + "Specific invalid index attempted to be passed (is -1): " + + data.Type + + " " + + data.Beat + ); + return; + } + + if (index < -1 || index > _noteData.Count) + { + GD.PushWarning( + "Invalid index passed is: " + index + " data: " + data.Type + " " + data.Beat + ); + return; + } + _noteData.Insert(index, data); + } + private int AddNoteData(Note noteRef, ArrowType type, Beat beat, double length = 0) { ArrowData result = new ArrowData(type, beat, noteRef, length); + return AddNoteData(result); + } + + private int AddNoteData(ArrowData data) + { if (_noteData.Count == 0) { - _noteData.Add(result); + _noteData.Add(data); return 0; } - int index = _noteData.BinarySearch(result); //TODO: This sorts correctly, but we don't take advantage yet. + int index = GetIndexOfData(data); + if (index == -1) + { + GD.PushWarning( + "Attempted to add duplicate note! Current note: " + data.Type + " " + data.Beat + ); + return -1; + } + + _noteData.Insert(index, data); + return index; + } + + private int GetIndexOfData(ArrowData data) + { + int index = _noteData.BinarySearch(data); if (index > 0) { - GD.PushWarning("Duplicate note attempted add " + type + " " + beat); + GD.PushWarning(index + " Invalid index for: " + data.Type + " " + data.Beat); return -1; } - _noteData.Insert(~index, result); + return ~index; } - //TODO: Beat spawn redundancy checking, efficiency + //Assumes beat has beatPos floor'd private void SpawnNotesAtBeat(Beat beat) { - for (int i = 0; i < _noteData.Count; i++) + int startIdx = _noteData.BinarySearch(new ArrowData(ArrowType.Up, beat, null)); //first arrow of beat + if (startIdx < 0) + startIdx = ~startIdx; + for (int i = 0; i <= 40 && (int)_noteData[startIdx].Beat.BeatPos == (int)beat.BeatPos; i++) { - if ( - _noteData[i].Beat.Loop != beat.Loop - || (int)_noteData[i].Beat.BeatPos != (int)beat.BeatPos - ) - continue; - SpawnNote(i); - } + SpawnNote(startIdx); //Spawn pops notes, so stay in same idx + } //A tiny bit of defensive programming. I don't like this much more than the old way of looping and checking everything. + //Could be a while loop, but just in case have a safety counter, iterations per beat should max at 40, 4 directions * 10 increments per beat (0.1 accuracy for beatPos tracking) } private void SpawnNote(int index, bool newPlayerNote = false) { CM.AddNoteArrow(_noteData[index], newPlayerNote); - _noteData[index] = new ArrowData( - _noteData[index].Type, - _noteData[index].Beat.IncDecLoop(1), - _noteData[index].NoteRef, - _noteData[index].Length - ); //Structs make me sad sometimes + if (newPlayerNote) //Player notes are presorted + { + _noteData[index] = _noteData[index].IncDecLoop(1); + return; + } + _noteData.Add(_noteData[index].IncDecLoop(1)); + _noteData.RemoveAt(index); + } + + public void SetRandBaseNoteToType(PuppetTemplate owner, (int noteid, int amount) noteOfAmount) + { + RandomNumberGenerator noteRng = new RandomNumberGenerator(); + noteRng.Seed = StageProducer.GlobalRng.Seed; + noteRng.State = StageProducer.GlobalRng.State; + + for (int i = noteOfAmount.amount; i > 0; i--) + { + int iterationsLeft = 5; + while (iterationsLeft > 0) + { + int idx = noteRng.RandiRange(0, _noteData.Count - 1); + if (_noteData[idx].NoteRef.Id == 0) + { + Note newNoteRef = Scribe + .NoteDictionary[noteOfAmount.noteid] + .Clone() + .SetOwner(owner); + _noteData[idx] = ArrowData.SetNote(_noteData[idx], newNoteRef); + iterationsLeft = -1; + } + iterationsLeft--; + } + } + } + + /// + /// Attempts to add the new specified beat to current chart.

+ /// Should only be used from On Loop effects

+ /// EXPERIMENTAL - Currently does not check for: Hold note overlap whether placing or preexisting. + /// Notes that are far past when they should have been placed. And notes that will come around far in the future. + ///
+ /// Current time, should be the beat of the about to happen new loop. + /// What type of note + /// The lane + /// Beat to spawn new note at. Should be within a loop, but outside _beatSpawnOffset buffer. + /// Length of note, may get removed. + /// Whether placement was successful + public bool AddConcurrentNote( + Beat currentTime, + Note noteRef, + ArrowType type, + Beat beat, + float length = 0 + ) + { + if (beat < currentTime + _beatSpawnOffset) + { + GD.PushWarning( + "Attempted note far in past. Current beat: " + + currentTime + + " notedata: " + + type + + " " + + beat + ); + return false; + } + if (beat > currentTime.IncDecLoop(1) + _beatSpawnOffset) + { + GD.PushWarning( + "Attempted note too far in future. Current beat: " + + currentTime + + " notedata: " + + type + + " " + + beat + ); + return false; + } + //Beat should now be guaranteed to be coming up, at some point, from ProgressiveSpawnNote, does NOT need manual spawn + int index = AddNoteData(noteRef, type, beat, length); + if (index == -1) + return false; //Assumption: Dupe note. + return true; } public void AddPlayerNote(Note noteRef, ArrowType type, Beat beat) { - int index = AddNoteData(noteRef, type, beat); //Currently player notes aren't sorted correctly + Beat compBeat = new Beat(beat.BeatPos, beat.Loop + 1); + int index = GetIndexOfData(new ArrowData(type, compBeat, null)); //Player notes should sorted based on immediately incrementing loop if (index != -1) + { + AddNoteData(new ArrowData(type, beat, noteRef), index); SpawnNote(index, true); + } else GD.PushError("Duplicate player note was attempted. (This should be stopped by CM)"); } diff --git a/Scenes/Puppets/Enemies/EnemyPuppet.cs b/Scenes/Puppets/Enemies/EnemyPuppet.cs index 5301476c..4a9d245d 100644 --- a/Scenes/Puppets/Enemies/EnemyPuppet.cs +++ b/Scenes/Puppets/Enemies/EnemyPuppet.cs @@ -4,6 +4,7 @@ public partial class EnemyPuppet : PuppetTemplate { protected EnemyEffect[] BattleEvents = Array.Empty(); public int BaseMoney { get; protected set; } = 0; + public (int NoteId, int Amount) InitialNote = (0, 0); public virtual EnemyEffect[] GetBattleEvents() { diff --git a/Scenes/Puppets/Enemies/TheGWS/P_TheGWS.cs b/Scenes/Puppets/Enemies/TheGWS/P_TheGWS.cs index 9f0e237d..3cd877e8 100644 --- a/Scenes/Puppets/Enemies/TheGWS/P_TheGWS.cs +++ b/Scenes/Puppets/Enemies/TheGWS/P_TheGWS.cs @@ -1,4 +1,5 @@ using System; +using FunkEngine; using Godot; public partial class P_TheGWS : EnemyPuppet @@ -10,6 +11,7 @@ public override void _Ready() MaxHealth = 150; CurrentHealth = MaxHealth; BaseMoney = 10; + InitialNote = (10, 5); base._Ready(); var enemTween = CreateTween(); enemTween.TweenProperty(Sprite, "position", Vector2.Down * 10, 3f).AsRelative(); @@ -18,5 +20,35 @@ public override void _Ready() enemTween.SetEase(Tween.EaseType.InOut); enemTween.SetLoops(); enemTween.Play(); + + BattleEvents = new EnemyEffect[] + { + new EnemyEffect( + this, + BattleEffectTrigger.OnBattleStart, + 1, + (e, eff, val) => + { + e.BD.RandApplyNote(eff.Owner, 10, 2); + if (val == 0) + return; + e.BD.EnemyAddNote( + ArrowType.Up, + new Beat(3, 1), + Scribe.NoteDictionary[10].Clone(), + 0, + eff.Owner + ); + e.BD.EnemyAddNote( + ArrowType.Up, + new Beat(26), + Scribe.NoteDictionary[10].Clone(), + 0, + eff.Owner + ); + eff.Value = 0; + } + ), + }; } }