diff --git a/Classes/Note.cs b/Classes/Note.cs deleted file mode 100644 index 45007630..00000000 --- a/Classes/Note.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using FunkEngine; -using Godot; - -/** - * @class Note - * @brief Data structure class for holding data and methods for a battle time note. WIP - */ -public partial class Note : Resource -{ - private string _effect; - - //public Puppet_Template Owner; - - public Note(string effect = "") - { - _effect = effect; - } - - public string GetEffect() - { - return _effect; - } -} diff --git a/Classes/Notes/Note.cs b/Classes/Notes/Note.cs new file mode 100644 index 00000000..f5ac1ea4 --- /dev/null +++ b/Classes/Notes/Note.cs @@ -0,0 +1,47 @@ +using System; +using FunkEngine; +using Godot; + +/** + * @class Note + * @brief Data structure class for holding data and methods for a battle time note. WIP + */ +public partial class Note : Resource +{ + public PuppetTemplate Owner; + public string Name; + private int _baseVal; + private Action NoteEffect; //TODO: Where/How to deal with timing. + + //public string Tooltip; + + public Note( + string name, + PuppetTemplate owner = null, + int baseVal = 1, + Action noteEffect = null + ) + { + Name = name; + Owner = owner; + NoteEffect = + noteEffect + ?? ( + (BD, source, Timing) => + { + BD.GetTarget(this).TakeDamage(source._baseVal); + } + ); + _baseVal = baseVal; + } + + public void OnHit(BattleDirector BD, Timing timing) + { + NoteEffect(BD, this, timing); + } + + public Note Clone() + { + return (Note)MemberwiseClone(); + } +} diff --git a/Classes/RelicTemplate.cs b/Classes/RelicTemplate.cs deleted file mode 100644 index 66c3b8a9..00000000 --- a/Classes/RelicTemplate.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using Godot; - -public partial class RelicTemplate : Resource -{ - public string[] EffectTags; - public string Name; - - //public Texture2D Texture - //public string Tooltip - public RelicTemplate(string Name = "", string[] EffectTags = null) { } -} diff --git a/Classes/Relics/RelicEffect.cs b/Classes/Relics/RelicEffect.cs new file mode 100644 index 00000000..93ade4f0 --- /dev/null +++ b/Classes/Relics/RelicEffect.cs @@ -0,0 +1,31 @@ +using System; +using FunkEngine; +using Godot; + +public partial class RelicEffect : IBattleEvent +{ + private BattleEffectTrigger Trigger { get; set; } + public int BaseValue; + private Action OnRelicEffect; + + public RelicEffect( + BattleEffectTrigger trigger, + int val, + Action onRelicEffect + ) + { + BaseValue = val; + Trigger = trigger; + OnRelicEffect = onRelicEffect; + } + + public void OnTrigger(BattleDirector battleDirector) + { + OnRelicEffect(battleDirector, BaseValue); + } + + public BattleEffectTrigger GetTrigger() + { + return Trigger; + } +} diff --git a/Classes/Relics/RelicTemplate.cs b/Classes/Relics/RelicTemplate.cs new file mode 100644 index 00000000..892e1bdf --- /dev/null +++ b/Classes/Relics/RelicTemplate.cs @@ -0,0 +1,22 @@ +using System; +using FunkEngine; +using Godot; + +public partial class RelicTemplate : Resource +{ + public RelicEffect[] Effects; + public string Name; + + //public Texture2D Texture + //public string Tooltip + public RelicTemplate(string Name = "", RelicEffect[] EffectTags = null) + { + Effects = EffectTags; + this.Name = Name; + } + + public RelicTemplate Clone() + { + return (RelicTemplate)MemberwiseClone(); + } +} diff --git a/Globals/FunkEngineNameSpace.cs b/Globals/FunkEngineNameSpace.cs index 1b0eaa9c..6bf17004 100644 --- a/Globals/FunkEngineNameSpace.cs +++ b/Globals/FunkEngineNameSpace.cs @@ -10,6 +10,37 @@ public enum ArrowType Right = 3, } +public enum BattleEffectTrigger +{ + NotePlaced, + NoteHit, + SelfNoteHit, +} + +public enum Timing +{ + Miss = 0, + Bad = 1, + Okay = 2, + Good = 3, + Perfect = 4, +} + +public enum Stages +{ + Title, + Battle, + Quit, + Map, +} + +public struct SongData +{ + public int Bpm; + public double SongLength; + public int NumLoops; +} + public struct ArrowData { public Color Color; @@ -17,3 +48,9 @@ public struct ArrowData public NoteChecker Node; public ArrowType Type; } + +public interface IBattleEvent +{ + void OnTrigger(BattleDirector BD); + BattleEffectTrigger GetTrigger(); +} diff --git a/Globals/Scribe.cs b/Globals/Scribe.cs new file mode 100644 index 00000000..b1b9884a --- /dev/null +++ b/Globals/Scribe.cs @@ -0,0 +1,49 @@ +using System; +using FunkEngine; +using Godot; + +/** + * Global for storing defined data, e.g. Notes and Relic Dictionaries. + */ +public partial class Scribe : Node +{ + public static readonly Note[] NoteDictionary = new[] + { + new Note( + "EnemyBase", + null, + 1, + (director, note, timing) => + { + director.Player.TakeDamage(4 - (int)timing); + } + ), + new Note( + "PlayerBase", + null, + 1, + (director, note, timing) => + { + director.Enemy.TakeDamage((int)timing); + } + ), + }; + + public static readonly RelicTemplate[] RelicDictionary = new[] + { + new RelicTemplate( + "Good Vibes", + new RelicEffect[] + { + new RelicEffect( + BattleEffectTrigger.NotePlaced, + 5, + (director, val) => + { + director.Player.Heal(val); + } + ), + } + ), + }; +} diff --git a/Globals/StageProducer.cs b/Globals/StageProducer.cs new file mode 100644 index 00000000..6b34afe1 --- /dev/null +++ b/Globals/StageProducer.cs @@ -0,0 +1,128 @@ +using System; +using System.Linq; +using FunkEngine; +using Godot; + +public partial class StageProducer : Node +{ + //Generate a map, starting as a width x height grid, pick a starting spot and do (path) paths from that to the last + //row, connecting the path, then connect all at the end to the boss room. + public static RandomNumberGenerator GlobalRng = new RandomNumberGenerator(); + private ulong _seed; + private ulong _lastRngState; + + private Stages _curStage = Stages.Title; + private Node _curScene; + + private MapGrid _map = new MapGrid(); + + public class MapGrid + { + private int[,] _map; + private Room[] _rooms; + private int _curIdx = 0; + private int _curRoom = 0; + + public class Room + { + public Room(int idx, int x, int y) + { + Idx = idx; + X = x; + Y = y; + } + + public void SetType(string type) + { + Type = type; + } + + public void AddChild(int newIdx) + { + if (Children.Contains(newIdx)) + return; + Children = Children.Append(newIdx).ToArray(); + } + + private int Idx; + private int[] Children = Array.Empty(); + private int X; + private int Y; + private string Type; + } + + public void InitMapGrid(int width, int height, int paths) + { + _curIdx = 0; + _rooms = Array.Empty(); + _map = new int[width, height]; //x,y + + int startX = GlobalRng.RandiRange(0, width - 1); //TODO: Replace with seeding + _rooms = _rooms.Append(new Room(_curIdx, startX, 0)).ToArray(); + _map[startX, 0] = _curIdx++; + + for (int i = 0; i < paths; i++) + { + GeneratePath_r(startX, 0, width, height); + } + + AddBossRoom(width, height); + } + + //Start at x, y, assume prev room exists. Picks new x pos within +/- 1, attaches recursively + private void GeneratePath_r(int x, int y, int width, int height) + { + int nextX = GlobalRng.RandiRange(Math.Max(x - 1, 0), Math.Min(x + 1, width - 1)); + if (_map[nextX, y + 1] == 0) + { + _rooms = _rooms.Append(new Room(_curIdx, nextX, y + 1)).ToArray(); + _map[nextX, y + 1] = _curIdx; + _rooms[_map[x, y]].AddChild(_curIdx++); + } + else + { + _rooms[_map[x, y]].AddChild(_map[nextX, y + 1]); + } + if (y < height - 2) + { + GeneratePath_r(nextX, y + 1, width, height); + } + } + + private void AddBossRoom(int width, int height) + { + _rooms = _rooms.Append(new Room(_curIdx, 0, height)).ToArray(); + _rooms[_curIdx].SetType("Boss"); + for (int i = 0; i < width; i++) //Attach all last rooms to a boss room + { + if (_map[i, height - 1] != 0) + { + _rooms[_map[i, height - 1]].AddChild(_curIdx); + } + } + } + } + + public void StartGame() + { + _map.InitMapGrid(2, 2, 1); + _seed = GlobalRng.Seed; + _lastRngState = GlobalRng.State; + } + + public void TransitionStage(Stages nextStage) + { + GD.Print(GetTree().CurrentScene); + switch (nextStage) + { + case Stages.Title: + GetTree().ChangeSceneToFile("res://scenes/SceneTransitions/TitleScreen.tscn"); + break; + case Stages.Battle: + GetTree().ChangeSceneToFile("res://scenes/BattleDirector/test_battle_scene.tscn"); + break; + } + + _curStage = nextStage; + } +} diff --git a/project.godot b/project.godot index 9ebf0d8e..76f74626 100644 --- a/project.godot +++ b/project.godot @@ -11,13 +11,15 @@ config_version=5 [application] config/name="Funk Engine" -run/main_scene="res://scenes/BattleDirector/test_battle_scene.tscn" +run/main_scene="res://scenes/SceneTransitions/TitleScreen.tscn" config/features=PackedStringArray("4.3", "C#", "Forward Plus") config/icon="res://icon.svg" [autoload] TimeKeeper="*res://Globals/TimeKeeper.cs" +Scribe="*res://Globals/Scribe.cs" +StageProducer="*res://Globals/StageProducer.cs" [display] diff --git a/scenes/BattleDirector/scripts/BattleDirector.cs b/scenes/BattleDirector/scripts/BattleDirector.cs index c603a2cc..8338ddfb 100644 --- a/scenes/BattleDirector/scripts/BattleDirector.cs +++ b/scenes/BattleDirector/scripts/BattleDirector.cs @@ -12,18 +12,18 @@ public partial class BattleDirector : Node2D { //TODO: Maybe move some Director functionality to a sub node. #region Declarations - //private Puppet_Template[] ActivePuppets; - private PuppetTemplate Player; - private PuppetTemplate Enemy; + + public PlayerPuppet Player; + public PuppetTemplate Enemy; [Export] private ChartManager CM; [Export] - private InputHandler IH; + private NotePlacementBar NotePlacementBar; [Export] - private NotePlacementBar NotePlacementBar; + private Conductor CD; [Export] private AudioStreamPlayer Audio; @@ -32,89 +32,34 @@ public partial class BattleDirector : Node2D private SongData _curSong; - public struct SongData - { - public int Bpm; - public double SongLength; - public int NumLoops; - } #endregion #region Note Handling - //Assume queue structure for notes in each lane. - //Can eventually make this its own structure - private NoteArrow[][] _laneData = Array.Empty(); - private int[] _laneLastBeat = new int[] - { //Temporary (hopefully) measure to bridge from note queue structure to ordered array - 0, - 0, - 0, - 0, - }; - private Note[] _notes = Array.Empty(); - - //Returns first note of lane without modifying lane data - private Note GetNoteAt(ArrowType dir, int beat) - { - return GetNote(_laneData[(int)dir][beat]); - } - - //Get note of a note arrow - private Note GetNote(NoteArrow arrow) - { - return _notes[arrow.NoteIdx]; - } - - private bool IsNoteActive(ArrowType type, int beat) - { - return _laneData[(int)type][beat] != null && _laneData[(int)type][beat].IsActive; - } - - private bool AddNoteToLane(ArrowType type, int beat, bool isActive = true) + private void PlayerAddNote(ArrowType type, int beat) { - beat %= CM.BeatsPerLoop; - //Don't add dupe notes //Beat at 0 is too messy. - if (beat == 0 || _laneData[(int)type][beat] != null) - { - return false; - } - //Get noteArrow from CM - NoteArrow arrow; - if (isActive) - { - arrow = CM.AddArrowToLane(type, beat, _notes.Length - 1); - } - else + GD.Print($"Player trying to place {type} typed note at beat: " + beat); + if (!NotePlacementBar.CanPlaceNote()) + return; + if (CD.AddNoteToLane(type, beat % CM.BeatsPerLoop, false)) { - arrow = CM.AddArrowToLane(type, beat, _notes.Length - 1, new Color(1, 0.43f, 0.26f)); + NotePlacementBar.PlacedNote(); + NotePlaced?.Invoke(this); + GD.Print("Note Placed."); } - arrow.IsActive = isActive; - _laneData[(int)type][beat] = arrow; - return true; } - #endregion - private void AddExampleNotes() + public PuppetTemplate GetTarget(Note note) { - GD.Print(CM.BeatsPerLoop); - for (int i = 1; i < 15; i++) - { - AddNoteToLane(ArrowType.Up, i * 4); - } - for (int i = 1; i < 15; i++) + if (note.Owner == Player) { - AddNoteToLane(ArrowType.Left, 4 * i + 1); - } - for (int i = 0; i < 10; i++) - { - AddNoteToLane(ArrowType.Right, 3 * i + 32); - } - for (int i = 0; i < 3; i++) - { - AddNoteToLane(ArrowType.Down, 8 * i + 16); + return Enemy; } + + return Player; } + #endregion + #region Initialization public override void _Ready() { _curSong = new SongData @@ -123,15 +68,19 @@ public override void _Ready() SongLength = Audio.Stream.GetLength(), NumLoops = 5, }; + TimeKeeper.Bpm = _curSong.Bpm; Player = new PlayerPuppet(); AddChild(Player); - Player.Init( - GD.Load("res://scenes/BattleDirector/assets/Character1.png"), - "Player" - ); - Player.SetPosition(new Vector2(80, 0)); - Player.Sprite.Position += Vector2.Down * 30; //TEMP + EventizeRelics(); + //TODO: Refine + foreach (var note in Player.Stats.CurNotes) + { + note.Owner = Player; + CD.Notes = CD.Notes.Append(note).ToArray(); + } + Note enemNote = Scribe.NoteDictionary[0].Clone(); + CD.Notes = CD.Notes.Append(enemNote).ToArray(); Enemy = new PuppetTemplate(); Enemy.SetPosition(new Vector2(400, 0)); @@ -147,16 +96,10 @@ public override void _Ready() private void Begin() { CM.PrepChart(_curSong); - _laneData = new NoteArrow[][] - { - new NoteArrow[CM.BeatsPerLoop], - new NoteArrow[CM.BeatsPerLoop], - new NoteArrow[CM.BeatsPerLoop], - new NoteArrow[CM.BeatsPerLoop], - }; - AddExampleNotes(); + CD.Prep(); + CD.TimedInput += OnTimedInput; - //TEMP + //TEMP TODO: Make enemies, can put this in an enemy subclass var enemTween = CreateTween(); enemTween.TweenProperty(Enemy.Sprite, "position", Vector2.Down * 5, 1f).AsRelative(); enemTween.TweenProperty(Enemy.Sprite, "position", Vector2.Up * 5, 1f).AsRelative(); @@ -174,102 +117,85 @@ private void Begin() public override void _Process(double delta) { TimeKeeper.CurrentTime = Audio.GetPlaybackPosition(); - CheckMiss(); + CD.CheckMiss(); } + #endregion #region Input&Timing private void OnNotePressed(ArrowType type) { - CheckNoteTiming(type); + CD.CheckNoteTiming(type); } private void OnNoteReleased(ArrowType arrowType) { } - //Check all lanes for misses from missed inputs - private void CheckMiss() + private void OnTimedInput(Note note, ArrowType arrowType, int beat, double beatDif) { - //On current beat, if prev beat is active and not inputted - double realBeat = TimeKeeper.CurrentTime / (60 / (double)_curSong.Bpm) % CM.BeatsPerLoop; - for (int i = 0; i < _laneData.Length; i++) + GD.Print(arrowType + " " + beat + " difference: " + beatDif); + if (note == null) { - if ( - !(_laneLastBeat[i] < Math.Floor(realBeat)) - && (_laneLastBeat[i] != CM.BeatsPerLoop - 1 || Math.Floor(realBeat) != 0) - ) - continue; - if (_laneData[i][_laneLastBeat[i]] == null || !_laneData[i][_laneLastBeat[i]].IsActive) - { - _laneLastBeat[i] = (_laneLastBeat[i] + 1) % CM.BeatsPerLoop; - continue; - } - //Note exists and has been missed - _laneData[i][_laneLastBeat[i]].NoteHit(); - HandleTiming(1); - _laneLastBeat[i] = (_laneLastBeat[i] + 1) % CM.BeatsPerLoop; + PlayerAddNote(arrowType, beat); + return; } - } + //TODO: Evaluate Timing as a function + Timing timed = CheckTiming(beatDif); + GD.Print(timed); - private void CheckNoteTiming(ArrowType type) - { - double realBeat = TimeKeeper.CurrentTime / (60 / (double)_curSong.Bpm) % CM.BeatsPerLoop; - int curBeat = (int)Math.Round(realBeat); - GD.Print("Cur beat " + curBeat + "Real: " + realBeat.ToString("#.###")); - if (_laneData[(int)type][curBeat % CM.BeatsPerLoop] == null) + if (timed == Timing.Miss) { - PlayerAddNote(type, curBeat); - return; + note.OnHit(this, timed); + NotePlacementBar.MissNote(); } - if (!_laneData[(int)type][curBeat % CM.BeatsPerLoop].IsActive) - return; - double beatDif = Math.Abs(realBeat - curBeat); - _laneData[(int)type][curBeat % CM.BeatsPerLoop].NoteHit(); - _laneLastBeat[(int)type] = (curBeat) % CM.BeatsPerLoop; - HandleTiming(beatDif); + else + { + note.OnHit(this, timed); + NotePlacementBar.HitNote(); + } + NotePlacementBar.ComboText(timed.ToString()); } - private void HandleTiming(double beatDif) + private Timing CheckTiming(double beatDif) { if (beatDif < _timingInterval * 1) { - GD.Print("Perfect"); - Enemy.TakeDamage(3); - NotePlacementBar.HitNote(); - NotePlacementBar.ComboText("Perfect!"); - } - else if (beatDif < _timingInterval * 2) - { - GD.Print("Good"); - Enemy.TakeDamage(1); - NotePlacementBar.HitNote(); - NotePlacementBar.ComboText("Good"); + return Timing.Perfect; } - else if (beatDif < _timingInterval * 3) + + if (beatDif < _timingInterval * 2) { - GD.Print("Ok"); - Player.TakeDamage(1); - NotePlacementBar.HitNote(); - NotePlacementBar.ComboText("Okay"); + return Timing.Good; } - else + + if (beatDif < _timingInterval * 3) { - GD.Print("Miss"); - Player.TakeDamage(2); - NotePlacementBar.MissNote(); - NotePlacementBar.ComboText("Miss"); + return Timing.Okay; } + + return Timing.Miss; } + #endregion - private void PlayerAddNote(ArrowType type, int beat) + #region BattleEffect Handling + + private delegate void NotePlacedHandler(BattleDirector BD); + private event NotePlacedHandler NotePlaced; + + private void EventizeRelics() { - // can also add some sort of keybind here to also have pressed - // in case the user just presses the note too early and spawns a note - GD.Print($"Player trying to place {type} typed note at beat: " + beat); - if (NotePlacementBar.CanPlaceNote()) + foreach (var relic in Player.Stats.CurRelics) { - if (AddNoteToLane(type, beat % CM.BeatsPerLoop, false)) - NotePlacementBar.PlacedNote(); - GD.Print("Note Placed."); + GetNode