From 25c70564bd7fcc7f7a97e89d17f164a62685a306 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Thu, 22 Jun 2023 01:41:01 -0400 Subject: [PATCH 01/30] WIP on new note rendering, inputs. --- hmm.json | 6 +- source/funkin/Conductor.hx | 8 +- source/funkin/Controls.hx | 20 + source/funkin/Highscore.hx | 2 + source/funkin/LatencyState.hx | 13 +- source/funkin/Note.hx | 301 -- source/funkin/Paths.hx | 4 +- source/funkin/PlayerSettings.hx | 4 + source/funkin/Section.hx | 33 - source/funkin/SongLoad.hx | 325 -- source/funkin/import.hx | 1 + source/funkin/input/PreciseInputManager.hx | 303 ++ source/funkin/modding/events/ScriptEvent.hx | 18 +- source/funkin/noteStuff/NoteBasic.hx | 197 - source/funkin/noteStuff/NoteEvent.hx | 12 - source/funkin/noteStuff/NoteUtil.hx | 98 - source/funkin/play/PlayState.hx | 3220 +++++++++-------- source/funkin/play/Strumline.hx | 253 -- source/funkin/play/character/BaseCharacter.hx | 22 +- source/funkin/play/notes/NoteDirection.hx | 82 + source/funkin/play/notes/NoteSplash.hx | 90 + source/funkin/play/notes/NoteSprite.hx | 178 + source/funkin/play/notes/Strumline.hx | 565 +++ source/funkin/play/notes/StrumlineNote.hx | 187 + source/funkin/play/notes/SustainTrail.hx | 272 ++ source/funkin/play/song/Song.hx | 11 +- source/funkin/play/song/SongData.hx | 6 + source/funkin/ui/ColorsMenu.hx | 15 +- .../ui/debug/charting/ChartEditorState.hx | 9 +- source/funkin/util/Constants.hx | 12 +- source/funkin/util/SortUtil.hx | 5 +- source/funkin/util/WindowUtil.hx | 9 + source/funkin/util/tools/ArraySortTools.hx | 154 + source/funkin/util/tools/ArrayTools.hx | 15 + 34 files changed, 3578 insertions(+), 2872 deletions(-) delete mode 100644 source/funkin/Note.hx delete mode 100644 source/funkin/Section.hx delete mode 100644 source/funkin/SongLoad.hx create mode 100644 source/funkin/input/PreciseInputManager.hx delete mode 100644 source/funkin/noteStuff/NoteBasic.hx delete mode 100644 source/funkin/noteStuff/NoteEvent.hx delete mode 100644 source/funkin/noteStuff/NoteUtil.hx delete mode 100644 source/funkin/play/Strumline.hx create mode 100644 source/funkin/play/notes/NoteDirection.hx create mode 100644 source/funkin/play/notes/NoteSplash.hx create mode 100644 source/funkin/play/notes/NoteSprite.hx create mode 100644 source/funkin/play/notes/Strumline.hx create mode 100644 source/funkin/play/notes/StrumlineNote.hx create mode 100644 source/funkin/play/notes/SustainTrail.hx create mode 100644 source/funkin/util/tools/ArraySortTools.hx diff --git a/hmm.json b/hmm.json index f79a2ca56..a1d78a29f 100644 --- a/hmm.json +++ b/hmm.json @@ -95,8 +95,8 @@ "name": "lime", "type": "git", "dir": null, - "ref": "5634ad7", - "url": "https://github.com/openfl/lime" + "ref": "2447ae6", + "url": "https://github.com/elitemastereric/lime" }, { "name": "openfl", @@ -123,4 +123,4 @@ "version": null } ] -} \ No newline at end of file +} diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx index 7f7e2b356..5c9c23ee3 100644 --- a/source/funkin/Conductor.hx +++ b/source/funkin/Conductor.hx @@ -16,7 +16,11 @@ typedef BPMChangeEvent = */ class Conductor { - static final STEPS_PER_BEAT:Int = 4; + public static final PIXELS_PER_MS:Float = 0.45; + public static final HIT_WINDOW_MS:Float = 160; + public static final SECONDS_PER_MINUTE:Float = 60; + public static final MILLIS_PER_SECOND:Float = 1000; + public static final STEPS_PER_BEAT:Int = 4; // onBeatHit is called every quarter note // onStepHit is called every sixteenth note @@ -93,7 +97,7 @@ class Conductor static function get_beatLengthMs():Float { // Tied directly to BPM. - return ((60 / bpm) * 1000); + return ((SECONDS_PER_MINUTE / bpm) * MILLIS_PER_SECOND); } /** diff --git a/source/funkin/Controls.hx b/source/funkin/Controls.hx index 46681adbd..88b637e72 100644 --- a/source/funkin/Controls.hx +++ b/source/funkin/Controls.hx @@ -391,6 +391,26 @@ class Controls extends FlxActionSet return byName[name].check(); } + public function getKeysForAction(name:Action):Array<FlxKey> { + #if debug + if (!byName.exists(name)) + throw 'Invalid name: $name'; + #end + + return byName[name].inputs.map(function(input) return (input.device == KEYBOARD) ? input.inputID : null) + .filter(function(key) return key != null); + } + + public function getButtonsForAction(name:Action):Array<FlxGamepadInputID> { + #if debug + if (!byName.exists(name)) + throw 'Invalid name: $name'; + #end + + return byName[name].inputs.map(function(input) return (input.device == GAMEPAD) ? input.inputID : null) + .filter(function(key) return key != null); + } + public function getDialogueName(action:FlxActionDigital):String { var input = action.inputs[0]; diff --git a/source/funkin/Highscore.hx b/source/funkin/Highscore.hx index 904d2cb45..46e98d8dc 100644 --- a/source/funkin/Highscore.hx +++ b/source/funkin/Highscore.hx @@ -192,6 +192,7 @@ abstract Tallies(RawTallies) bad: 0, good: 0, sick: 0, + killer: 0, totalNotes: 0, totalNotesHit: 0, maxCombo: 0, @@ -213,6 +214,7 @@ typedef RawTallies = var bad:Int; var good:Int; var sick:Int; + var killer:Int; var maxCombo:Int; var isNewHighscore:Bool; diff --git a/source/funkin/LatencyState.hx b/source/funkin/LatencyState.hx index 347454253..bd78a4298 100644 --- a/source/funkin/LatencyState.hx +++ b/source/funkin/LatencyState.hx @@ -2,14 +2,15 @@ package funkin; import flixel.FlxSprite; import flixel.FlxSubState; -import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxGroup; +import flixel.group.FlxGroup.FlxTypedGroup; import flixel.math.FlxMath; import flixel.sound.FlxSound; import flixel.system.debug.stats.StatsGraph; import flixel.text.FlxText; import flixel.util.FlxColor; import funkin.audio.visualize.PolygonSpectogram; +import funkin.play.notes.NoteSprite; import funkin.ui.CoolStatsGraph; import haxe.Timer; import openfl.events.KeyboardEvent; @@ -17,7 +18,7 @@ import openfl.events.KeyboardEvent; class LatencyState extends MusicBeatSubState { var offsetText:FlxText; - var noteGrp:FlxTypedGroup<Note>; + var noteGrp:FlxTypedGroup<NoteSprite>; var strumLine:FlxSprite; var blocks:FlxTypedGroup<FlxSprite>; @@ -74,7 +75,7 @@ class LatencyState extends MusicBeatSubState Conductor.forceBPM(60); - noteGrp = new FlxTypedGroup<Note>(); + noteGrp = new FlxTypedGroup<NoteSprite>(); add(noteGrp); diffGrp = new FlxTypedGroup<FlxText>(); @@ -127,7 +128,7 @@ class LatencyState extends MusicBeatSubState for (i in 0...32) { - var note:Note = new Note(Conductor.beatLengthMs * i, 1); + var note:NoteSprite = new NoteSprite(Conductor.beatLengthMs * i); noteGrp.add(note); } @@ -246,8 +247,8 @@ class LatencyState extends MusicBeatSubState FlxG.resetState(); }*/ - noteGrp.forEach(function(daNote:Note) { - daNote.y = (strumLine.y - ((Conductor.songPosition - Conductor.audioOffset) - daNote.data.strumTime) * 0.45); + noteGrp.forEach(function(daNote:NoteSprite) { + daNote.y = (strumLine.y - ((Conductor.songPosition - Conductor.audioOffset) - daNote.noteData.time) * 0.45); daNote.x = strumLine.x + 30; if (daNote.y < strumLine.y) daNote.alpha = 0.5; diff --git a/source/funkin/Note.hx b/source/funkin/Note.hx deleted file mode 100644 index ea99449b1..000000000 --- a/source/funkin/Note.hx +++ /dev/null @@ -1,301 +0,0 @@ -package funkin; - -import funkin.play.Strumline.StrumlineArrow; -import flixel.FlxSprite; -import flixel.math.FlxMath; -import funkin.noteStuff.NoteBasic.NoteData; -import funkin.noteStuff.NoteBasic.NoteType; -import funkin.play.PlayState; -import funkin.play.Strumline.StrumlineStyle; -import funkin.shaderslmfao.ColorSwap; -import funkin.ui.PreferencesMenu; -import funkin.util.Constants; - -class Note extends FlxSprite -{ - public var data = new NoteData(); - - /** - * code colors for.... code.... - * i think goes in order of left to right - * - * left 0 - * down 1 - * up 2 - * right 3 - */ - public static var codeColors:Array<Int> = [0xFFFF22AA, 0xFF00EEFF, 0xFF00CC00, 0xFFCC1111]; - - public var mustPress:Bool = false; - public var followsTime:Bool = true; // used if you want the note to follow the time shit! - public var canBeHit:Bool = false; - public var tooLate:Bool = false; - public var wasGoodHit:Bool = false; - public var prevNote:Note; - - var willMiss:Bool = false; - - public var invisNote:Bool = false; - - public var isSustainNote:Bool = false; - - public var colorSwap:ColorSwap; - - /** the lowercase name of the note, for anim control, i.e. left right up down */ - public var dirName(get, never):String; - - inline function get_dirName() - return data.dirName; - - /** the uppercase name of the note, for anim control, i.e. left right up down */ - public var dirNameUpper(get, never):String; - - inline function get_dirNameUpper() - return data.dirNameUpper; - - /** the lowercase name of the note's color, for anim control, i.e. purple blue green red */ - public var colorName(get, never):String; - - inline function get_colorName() - return data.colorName; - - /** the lowercase name of the note's color, for anim control, i.e. purple blue green red */ - public var colorNameUpper(get, never):String; - - inline function get_colorNameUpper() - return data.colorNameUpper; - - public var highStakes(get, never):Bool; - - inline function get_highStakes() - return data.highStakes; - - public var lowStakes(get, never):Bool; - - inline function get_lowStakes() - return data.lowStakes; - - public static var swagWidth:Float = 160 * 0.7; - public static var PURP_NOTE:Int = 0; - public static var GREEN_NOTE:Int = 2; - public static var BLUE_NOTE:Int = 1; - public static var RED_NOTE:Int = 3; - - // SCORING STUFF - public static var HIT_WINDOW:Float = (10 / 60) * 1000; // 166.67 ms hit window (10 frames at 60fps) - // thresholds are fractions of HIT_WINDOW ^^ - // anything above bad threshold is shit - public static var BAD_THRESHOLD:Float = 0.8; // 125ms , 8 frames - public static var GOOD_THRESHOLD:Float = 0.55; // 91.67ms , 5.5 frames - public static var SICK_THRESHOLD:Float = 0.2; // 33.33ms , 2 frames - - public var noteSpeedMulti:Float = 1; - public var pastHalfWay:Bool = false; - - // anything below sick threshold is sick - public static var arrowColors:Array<Float> = [1, 1, 1, 1]; - - // Which note asset to load? - public var style:StrumlineStyle = NORMAL; - - public function new(strumTime:Float = 0, noteData:NoteType, ?prevNote:Note, ?sustainNote:Bool = false, ?style:StrumlineStyle = NORMAL) - { - super(); - - if (prevNote == null) prevNote = this; - - this.prevNote = prevNote; - isSustainNote = sustainNote; - - x += 50; - // MAKE SURE ITS DEFINITELY OFF SCREEN? - y -= 2000; - data.strumTime = strumTime; - - data.noteData = noteData; - - this.style = style; - - if (this.style == null) this.style = StrumlineStyle.NORMAL; - - // TODO: Make this logic more generic - switch (this.style) - { - case PIXEL: - loadGraphic(Paths.image('weeb/pixelUI/arrows-pixels'), true, 17, 17); - - animation.add('greenScroll', [6]); - animation.add('redScroll', [7]); - animation.add('blueScroll', [5]); - animation.add('purpleScroll', [4]); - - if (isSustainNote) - { - loadGraphic(Paths.image('weeb/pixelUI/arrowEnds'), true, 7, 6); - - animation.add('purpleholdend', [4]); - animation.add('greenholdend', [6]); - animation.add('redholdend', [7]); - animation.add('blueholdend', [5]); - - animation.add('purplehold', [0]); - animation.add('greenhold', [2]); - animation.add('redhold', [3]); - animation.add('bluehold', [1]); - } - - setGraphicSize(Std.int(width * Constants.PIXEL_ART_SCALE)); - updateHitbox(); - - default: - frames = Paths.getSparrowAtlas('NOTE_assets'); - - animation.addByPrefix('purpleScroll', 'purple instance'); - animation.addByPrefix('blueScroll', 'blue instance'); - animation.addByPrefix('greenScroll', 'green instance'); - animation.addByPrefix('redScroll', 'red instance'); - - animation.addByPrefix('purpleholdend', 'pruple end hold'); - animation.addByPrefix('greenholdend', 'green hold end'); - animation.addByPrefix('redholdend', 'red hold end'); - animation.addByPrefix('blueholdend', 'blue hold end'); - - animation.addByPrefix('purplehold', 'purple hold piece'); - animation.addByPrefix('greenhold', 'green hold piece'); - animation.addByPrefix('redhold', 'red hold piece'); - animation.addByPrefix('bluehold', 'blue hold piece'); - - setGraphicSize(Std.int(width * 0.7)); - updateHitbox(); - antialiasing = true; - - // colorSwap.colorToReplace = 0xFFF9393F; - // colorSwap.newColor = 0xFF00FF00; - - // color = FlxG.random.color(); - // color.saturation *= 4; - // replaceColor(0xFFC1C1C1, FlxColor.RED); - } - - colorSwap = new ColorSwap(); - shader = colorSwap.shader; - updateColors(); - - x += swagWidth * data.int; - animation.play(data.colorName + 'Scroll'); - - // trace(prevNote); - - if (isSustainNote && prevNote != null) - { - alpha = 0.6; - - if (PreferencesMenu.getPref('downscroll')) angle = 180; - - x += width / 2; - - animation.play(data.colorName + 'holdend'); - - updateHitbox(); - - x -= width / 2; - - if (PlayState.instance.currentStageId.startsWith('school')) x += 30; - - if (prevNote.isSustainNote) - { - prevNote.animation.play(prevNote.colorName + 'hold'); - prevNote.updateHitbox(); - - var scaleThing:Float = Math.round((Conductor.stepLengthMs) * (0.45 * FlxMath.roundDecimal(PlayState.instance.currentChart.scrollSpeed, 2))); - // get them a LIL closer together cuz the antialiasing blurs the edges - if (antialiasing) scaleThing *= 1.0 + (1.0 / prevNote.frameHeight); - prevNote.scale.y = scaleThing / prevNote.frameHeight; - prevNote.updateHitbox(); - } - } - } - - public function alignToSturmlineArrow(arrow:StrumlineArrow):Void - { - x = arrow.x; - - if (isSustainNote && prevNote != null) - { - if (prevNote.isSustainNote) - { - x = prevNote.x; - } - else - { - x += prevNote.width / 2; - x -= width / 2; - } - } - } - - override function destroy() - { - prevNote = null; - - super.destroy(); - } - - public function updateColors():Void - { - colorSwap.update(arrowColors[data.noteData]); - } - - override function update(elapsed:Float) - { - super.update(elapsed); - - // mustPress indicates the player is the one pressing the key - if (mustPress) - { - // miss on the NEXT frame so lag doesnt make u miss notes - if (willMiss && !wasGoodHit) - { - tooLate = true; - canBeHit = false; - } - else - { - if (!pastHalfWay && data.strumTime <= Conductor.songPosition) - { - pastHalfWay = true; - noteSpeedMulti *= 2; - } - - if (data.strumTime > Conductor.songPosition - HIT_WINDOW) - { - // * 0.5 if sustain note, so u have to keep holding it closer to all the way thru! - if (data.strumTime < Conductor.songPosition + (HIT_WINDOW * (isSustainNote ? 0.5 : 1))) canBeHit = true; - } - else - { - canBeHit = true; - willMiss = true; - } - } - } - else - { - canBeHit = false; - - if (data.strumTime <= Conductor.songPosition) wasGoodHit = true; - } - - if (tooLate) - { - if (alpha > 0.3) alpha = 0.3; - } - } - - static public function fromData(data:NoteData, prevNote:Note, isSustainNote = false) - { - var result = new Note(data.strumTime, data.noteData, prevNote, isSustainNote); - result.data = data; - return result; - } -} diff --git a/source/funkin/Paths.hx b/source/funkin/Paths.hx index 60dcfad38..3943d84ee 100644 --- a/source/funkin/Paths.hx +++ b/source/funkin/Paths.hx @@ -96,14 +96,14 @@ class Paths return getPath('music/$key.$SOUND_EXT', MUSIC, library); } - inline static public function voices(song:String, ?suffix:String) + inline static public function voices(song:String, ?suffix:String = '') { if (suffix == null) suffix = ""; // no suffix, for a sorta backwards compatibility with older-ish voice files return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.$SOUND_EXT'; } - inline static public function inst(song:String, ?suffix:String) + inline static public function inst(song:String, ?suffix:String = '') { return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.$SOUND_EXT'; } diff --git a/source/funkin/PlayerSettings.hx b/source/funkin/PlayerSettings.hx index b9ad87a93..1b64d26c2 100644 --- a/source/funkin/PlayerSettings.hx +++ b/source/funkin/PlayerSettings.hx @@ -2,6 +2,7 @@ package funkin; import funkin.Controls; import flixel.FlxCamera; +import funkin.input.PreciseInputManager; import flixel.input.actions.FlxActionInput; import flixel.input.gamepad.FlxGamepad; import flixel.util.FlxSignal; @@ -52,6 +53,9 @@ class PlayerSettings } if (useDefault) controls.setKeyboardScheme(Solo); + + // Apply loaded settings. + PreciseInputManager.instance.initializeKeys(controls); } function addGamepad(gamepad:FlxGamepad) diff --git a/source/funkin/Section.hx b/source/funkin/Section.hx deleted file mode 100644 index f239baaad..000000000 --- a/source/funkin/Section.hx +++ /dev/null @@ -1,33 +0,0 @@ -package funkin; - -import funkin.noteStuff.NoteBasic.NoteData; - -typedef SwagSection = -{ - var sectionNotes:Array<NoteData>; - var lengthInSteps:Int; - var typeOfSection:Int; - var mustHitSection:Bool; - var bpm:Float; - var changeBPM:Bool; - var altAnim:Bool; -} - -class Section -{ - public var sectionNotes:Array<Dynamic> = []; - - public var lengthInSteps:Int = 16; - public var typeOfSection:Int = 0; - public var mustHitSection:Bool = true; - - /** - * Copies the first section into the second section! - */ - public static var COPYCAT:Int = 0; - - public function new(lengthInSteps:Int = 16) - { - this.lengthInSteps = lengthInSteps; - } -} diff --git a/source/funkin/SongLoad.hx b/source/funkin/SongLoad.hx deleted file mode 100644 index ca3bc72d0..000000000 --- a/source/funkin/SongLoad.hx +++ /dev/null @@ -1,325 +0,0 @@ -package funkin; - -import funkin.Section.SwagSection; -import funkin.noteStuff.NoteBasic.NoteData; -import funkin.play.PlayState; -import haxe.Json; -import lime.utils.Assets; - -typedef SwagSong = -{ - var song:String; - var notes:FunnyNotes; - var difficulties:Array<String>; - var noteMap:Map<String, Array<SwagSection>>; - var bpm:Float; - var needsVoices:Bool; - var voiceList:Array<String>; - var speed:FunnySpeed; - var speedMap:Map<String, Float>; - - var player1:String; - var player2:String; - var validScore:Bool; - var extraNotes:Map<String, Array<SwagSection>>; -} - -typedef FunnySpeed = -{ - var ?easy:Float; - var ?normal:Float; - var ?hard:Float; -} - -typedef FunnyNotes = -{ - var ?easy:Array<SwagSection>; - var ?normal:Array<SwagSection>; - var ?hard:Array<SwagSection>; -} - -class SongLoad -{ - public static var curDiff:String = 'normal'; - public static var curNotes:Array<SwagSection>; - public static var songData:SwagSong; - - public static function loadFromJson(jsonInput:String, ?folder:String):SwagSong - { - var rawJson:String = null; - try - { - rawJson = Assets.getText(Paths.json('songs/${folder.toLowerCase()}/${jsonInput.toLowerCase()}')).trim(); - } - catch (e) - { - trace('Failed to load song data: ${e}'); - rawJson = null; - } - - if (rawJson == null) - { - return null; - } - - while (!rawJson.endsWith("}")) - { - rawJson = rawJson.substr(0, rawJson.length - 1); - } - - return parseJSONshit(rawJson); - } - - public static function getSong(?diff:String):Array<SwagSection> - { - if (diff == null) diff = SongLoad.curDiff; - - var songShit:Array<SwagSection> = []; - - // THIS IS OVERWRITTEN, WILL BE DEPRECTATED AND REPLACED SOOOOON - if (songData != null) - { - switch (diff) - { - case 'easy': - songShit = songData.notes.easy; - case 'normal': - songShit = songData.notes.normal; - case 'hard': - songShit = songData.notes.hard; - } - } - - checkAndCreateNotemap(curDiff); - - songShit = songData.noteMap[diff]; - - return songShit; - } - - public static function checkAndCreateNotemap(diff:String):Void - { - if (songData == null || songData.noteMap == null) return; - if (songData.noteMap[diff] == null) songData.noteMap[diff] = []; - } - - public static function getSpeed(?diff:String):Float - { - if (PlayState.instance != null && PlayState.instance.currentChart != null) - { - return getSpeed_NEW(diff); - } - - if (diff == null) diff = SongLoad.curDiff; - - var speedShit:Float = 1; - - // all this shit is overridden by the thing that loads it from speedMap Map object!!! - // replace and delete later! - switch (diff) - { - case 'easy': - speedShit = songData?.speed?.easy ?? 1.0; - case 'normal': - speedShit = songData?.speed?.normal ?? 1.0; - case 'hard': - speedShit = songData?.speed?.hard ?? 1.0; - } - - if (songData?.speedMap == null || songData?.speedMap[diff] == null) - { - speedShit = 1; - } - else - { - speedShit = songData.speedMap[diff]; - } - - return speedShit; - } - - public static function getSpeed_NEW(?diff:String):Float - { - if (PlayState.instance == null - || PlayState.instance.currentChart == null - || PlayState.instance.currentChart.scrollSpeed == 0.0) return 1.0; - - return PlayState.instance.currentChart.scrollSpeed; - } - - public static function getDefaultSwagSong():SwagSong - { - return { - song: 'Test', - notes: {easy: [], normal: [], hard: []}, - difficulties: ["easy", "normal", "hard"], - noteMap: new Map(), - speedMap: new Map(), - bpm: 150, - needsVoices: true, - player1: 'bf', - player2: 'dad', - speed: - { - easy: 1, - normal: 1, - hard: 1 - }, - validScore: false, - voiceList: ["BF", "BF-pixel"], - extraNotes: [] - }; - } - - public static function getDefaultNoteData():NoteData - { - return new NoteData(); - } - - /** - * Casts the an array to NOTE data (for LOADING shit from json usually) - */ - public static function castArrayToNoteData(noteStuff:Array<SwagSection>) - { - if (noteStuff == null) return; - - for (sectionIndex => section in noteStuff) - { - if (section == null || section.sectionNotes == null) continue; - for (noteIndex => noteDataArray in section.sectionNotes) - { - var arrayDipshit:Array<Dynamic> = cast noteDataArray; // crackhead - - if (arrayDipshit != null) // array isnt null, that means it loaded it as an array and needs to be manually parsed? - { - // at this point noteStuff[sectionIndex].sectionNotes[noteIndex] is an array because of the cast from the first line in this function - // so this line right here turns it back into the NoteData typedef type because of another bastard cast - noteStuff[sectionIndex].sectionNotes[noteIndex] = cast SongLoad.getDefaultNoteData(); // turn it from an array (because of the cast), back to noteData? yeah that works - - noteStuff[sectionIndex].sectionNotes[noteIndex].strumTime = arrayDipshit[0]; - noteStuff[sectionIndex].sectionNotes[noteIndex].noteData = arrayDipshit[1]; - noteStuff[sectionIndex].sectionNotes[noteIndex].sustainLength = arrayDipshit[2]; - if (arrayDipshit.length > 3) - { - noteStuff[sectionIndex].sectionNotes[noteIndex].noteKind = arrayDipshit[3]; - } - } - else if (noteDataArray != null) - { - // array is NULL, so it checks if noteDataArray (doesnt exactly NEED to be an 'array' is also null or not.) - // At this point it should be an OBJECT that can be easily casted!!! - - noteStuff[sectionIndex].sectionNotes[noteIndex] = cast noteDataArray; - } - else - throw "shit brokey"; // i actually dont know how throw works lol - } - } - } - - /** - * Cast notedata to ARRAY (usually used for level SAVING) - */ - public static function castNoteDataToArray(noteStuff:Array<SwagSection>) - { - if (noteStuff == null) return; - - for (sectionIndex => section in noteStuff) - { - for (noteIndex => noteTypeDefShit in section.sectionNotes) - { - var dipshitArray:Array<Dynamic> = [ - noteTypeDefShit.strumTime, - noteTypeDefShit.noteData, - noteTypeDefShit.sustainLength, - noteTypeDefShit.noteKind - ]; - - noteStuff[sectionIndex].sectionNotes[noteIndex] = cast dipshitArray; - } - } - } - - public static function castNoteDataToNoteData(noteStuff:Array<SwagSection>) - { - if (noteStuff == null) return; - - for (sectionIndex => section in noteStuff) - { - for (noteIndex => noteTypedefShit in section.sectionNotes) - { - trace(noteTypedefShit); - noteStuff[sectionIndex].sectionNotes[noteIndex] = noteTypedefShit; - } - } - } - - public static function parseJSONshit(rawJson:String):SwagSong - { - var songParsed:Dynamic; - try - { - songParsed = Json.parse(rawJson); - } - catch (e) - { - FlxG.log.warn("Error parsing JSON: " + e.message); - trace("Error parsing JSON: " + e.message); - return null; - } - - var swagShit:SwagSong = cast songParsed.song; - swagShit.difficulties = []; // reset it to default before load - swagShit.noteMap = new Map(); - swagShit.speedMap = new Map(); - for (diff in Reflect.fields(songParsed.song.notes)) - { - swagShit.difficulties.push(diff); - swagShit.noteMap[diff] = cast Reflect.field(songParsed.song.notes, diff); - - castArrayToNoteData(swagShit.noteMap[diff]); - - // castNoteDataToNoteData(swagShit.noteMap[diff]); - - /* - switch (diff) - { - case "easy": - castArrayToNoteData(swagShit.notes.hard); - - case "normal": - castArrayToNoteData(swagShit.notes.normal); - case "hard": - castArrayToNoteData(swagShit.notes.hard); - } - */ - } - - for (diff in swagShit.difficulties) - { - swagShit.speedMap[diff] = cast Reflect.field(songParsed.song.speed, diff); - } - - // trace(swagShit.noteMap.toString()); - // trace(swagShit.speedMap.toString()); - // trace('that was just notemap string lol'); - - swagShit.validScore = true; - - trace("SONG SHIT ABOUTTA WEEK AGOOO"); - for (field in Reflect.fields(Json.parse(rawJson).song.speed)) - { - // swagShit.speed[field] = Reflect.field(Json.parse(rawJson).song.speed, field); - // swagShit.notes[field] = Reflect.field(Json.parse(rawJson).song.notes, field); - // trace(swagShit.notes[field]); - } - - // swagShit.notes = cast Json.parse(rawJson).song.notes[SongLoad.curDiff]; // by default uses - - trace('THAT SHIT WAS JUST THE NORMAL NOTES!!!'); - songData = swagShit; - // curNotes = songData.notes.get('normal'); - - return swagShit; - } -} diff --git a/source/funkin/import.hx b/source/funkin/import.hx index f54ccea86..9aa99fade 100644 --- a/source/funkin/import.hx +++ b/source/funkin/import.hx @@ -9,6 +9,7 @@ import flixel.FlxG; // This one in particular causes a compile error if you're u using Lambda; using StringTools; using funkin.util.tools.ArrayTools; +using funkin.util.tools.ArraySortTools; using funkin.util.tools.IteratorTools; using funkin.util.tools.MapTools; using funkin.util.tools.StringTools; diff --git a/source/funkin/input/PreciseInputManager.hx b/source/funkin/input/PreciseInputManager.hx new file mode 100644 index 000000000..11a3c2007 --- /dev/null +++ b/source/funkin/input/PreciseInputManager.hx @@ -0,0 +1,303 @@ +package funkin.input; + +import openfl.ui.Keyboard; +import funkin.play.notes.NoteDirection; +import flixel.input.keyboard.FlxKeyboard.FlxKeyInput; +import openfl.events.KeyboardEvent; +import flixel.FlxG; +import flixel.input.FlxInput.FlxInputState; +import flixel.input.FlxKeyManager; +import flixel.input.keyboard.FlxKey; +import flixel.input.keyboard.FlxKeyList; +import flixel.util.FlxSignal.FlxTypedSignal; +import haxe.Int64; +import lime.ui.KeyCode; +import lime.ui.KeyModifier; + +/** + * A precise input manager that: + * - Records the exact timestamp of when a key was pressed or released + * - Only records key presses for keys bound to game inputs (up/down/left/right) + */ +class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList> +{ + public static var instance(get, null):PreciseInputManager; + + static function get_instance():PreciseInputManager + { + return instance ?? (instance = new PreciseInputManager()); + } + + static final MS_TO_US:Int64 = 1000; + static final US_TO_NS:Int64 = 1000; + static final MS_TO_NS:Int64 = MS_TO_US * US_TO_NS; + + static final DIRECTIONS:Array<NoteDirection> = [NoteDirection.LEFT, NoteDirection.DOWN, NoteDirection.UP, NoteDirection.RIGHT]; + + public var onInputPressed:FlxTypedSignal<PreciseInputEvent->Void>; + public var onInputReleased:FlxTypedSignal<PreciseInputEvent->Void>; + + /** + * The list of keys that are bound to game inputs (up/down/left/right). + */ + var _keyList:Array<FlxKey>; + + /** + * The direction that a given key is bound to. + */ + var _keyListDir:Map<FlxKey, NoteDirection>; + + /** + * The timestamp at which a given note direction was last pressed. + */ + var _dirPressTimestamps:Map<NoteDirection, Int64>; + + /** + * The timestamp at which a given note direction was last released. + */ + var _dirReleaseTimestamps:Map<NoteDirection, Int64>; + + public function new() + { + super(PreciseInputList.new); + + _keyList = []; + _dirPressTimestamps = new Map<NoteDirection, Int64>(); + _dirReleaseTimestamps = new Map<NoteDirection, Int64>(); + _keyListDir = new Map<FlxKey, NoteDirection>(); + + FlxG.stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown); + FlxG.stage.removeEventListener(KeyboardEvent.KEY_UP, onKeyUp); + FlxG.stage.application.window.onKeyDownPrecise.add(handleKeyDown); + FlxG.stage.application.window.onKeyUpPrecise.add(handleKeyUp); + + preventDefaultKeys = getPreventDefaultKeys(); + + onInputPressed = new FlxTypedSignal<PreciseInputEvent->Void>(); + onInputReleased = new FlxTypedSignal<PreciseInputEvent->Void>(); + } + + public static function getKeysForDirection(controls:Controls, noteDirection:NoteDirection) + { + return switch (noteDirection) + { + case NoteDirection.LEFT: controls.getKeysForAction(NOTE_LEFT); + case NoteDirection.DOWN: controls.getKeysForAction(NOTE_DOWN); + case NoteDirection.UP: controls.getKeysForAction(NOTE_UP); + case NoteDirection.RIGHT: controls.getKeysForAction(NOTE_RIGHT); + }; + } + + /** + * Returns a precise timestamp, measured in nanoseconds. + * Timestamp is only useful for comparing against other timestamps. + * + * @return Int64 + */ + @:access(lime._internal.backend.native.NativeCFFI) + public static function getCurrentTimestamp():Int64 + { + #if html5 + // NOTE: This timestamp isn't that precise on standard HTML5 builds. + // This is because of browser safeguards against timing attacks. + // See https://web.dev/coop-coep to enable headers which allow for more precise timestamps. + return js.Browser.window.performance.now() * MS_TO_NS; + #elseif cpp + // NOTE: If the game hard crashes on this line, rebuild Lime! + // `lime rebuild windows -clean` + return lime._internal.backend.native.NativeCFFI.lime_sdl_get_ticks() * MS_TO_NS; + #else + throw "Eric didn't implement precise timestamps on this platform!"; + #end + } + + static function getPreventDefaultKeys():Array<FlxKey> + { + return []; + } + + /** + * Call this whenever the user's inputs change. + */ + public function initializeKeys(controls:Controls):Void + { + clearKeys(); + + for (noteDirection in DIRECTIONS) + { + var keys = getKeysForDirection(controls, noteDirection); + for (key in keys) + { + var input = new FlxKeyInput(key); + _keyList.push(key); + _keyListArray.push(input); + _keyListMap.set(key, input); + _keyListDir.set(key, noteDirection); + } + } + } + + /** + * Get the time, in nanoseconds, since the given note direction was last pressed. + * @param noteDirection The note direction to check. + * @return An Int64 representing the time since the given note direction was last pressed. + */ + public function getTimeSincePressed(noteDirection:NoteDirection):Int64 + { + return getCurrentTimestamp() - _dirPressTimestamps.get(noteDirection); + } + + /** + * Get the time, in nanoseconds, since the given note direction was last released. + * @param noteDirection The note direction to check. + * @return An Int64 representing the time since the given note direction was last released. + */ + public function getTimeSinceReleased(noteDirection:NoteDirection):Int64 + { + return getCurrentTimestamp() - _dirReleaseTimestamps.get(noteDirection); + } + + // TODO: Why doesn't this work? + // @:allow(funkin.input.PreciseInputManager.PreciseInputList) + public function getInputByKey(key:FlxKey):FlxKeyInput + { + return _keyListMap.get(key); + } + + public function getDirectionForKey(key:FlxKey):NoteDirection + { + return _keyListDir.get(key); + } + + function handleKeyDown(keyCode:KeyCode, _:KeyModifier, timestamp:Int64):Void + { + var key:FlxKey = convertKeyCode(keyCode); + if (_keyList.indexOf(key) == -1) return; + + // TODO: Remove this line with SDL3 when timestamps change meaning. + // This is because SDL3's timestamps are measured in nanoseconds, not milliseconds. + timestamp *= MS_TO_NS; + + updateKeyStates(key, true); + + if (getInputByKey(key) ?.justPressed ?? false) + { + onInputPressed.dispatch( + { + noteDirection: getDirectionForKey(key), + timestamp: timestamp + }); + _dirPressTimestamps.set(getDirectionForKey(key), timestamp); + } + } + + function handleKeyUp(keyCode:KeyCode, _:KeyModifier, timestamp:Int64):Void + { + var key:FlxKey = convertKeyCode(keyCode); + if (_keyList.indexOf(key) == -1) return; + + // TODO: Remove this line with SDL3 when timestamps change meaning. + // This is because SDL3's timestamps are measured in nanoseconds, not milliseconds. + timestamp *= MS_TO_NS; + + updateKeyStates(key, false); + + if (getInputByKey(key) ?.justReleased ?? false) + { + onInputReleased.dispatch( + { + noteDirection: getDirectionForKey(key), + timestamp: timestamp + }); + _dirReleaseTimestamps.set(getDirectionForKey(key), timestamp); + } + } + + static function convertKeyCode(input:KeyCode):FlxKey + { + @:privateAccess + { + return Keyboard.__convertKeyCode(input); + } + } + + function clearKeys():Void + { + _keyListArray = []; + _keyListMap.clear(); + _keyListDir.clear(); + } +} + +class PreciseInputList extends FlxKeyList +{ + var _preciseInputManager:PreciseInputManager; + + public function new(state:FlxInputState, preciseInputManager:FlxKeyManager<Dynamic, Dynamic>) + { + super(state, preciseInputManager); + + _preciseInputManager = cast preciseInputManager; + } + + static function getKeysForDir(noteDir:NoteDirection):Array<FlxKey> + { + return PreciseInputManager.getKeysForDirection(PlayerSettings.player1.controls, noteDir); + } + + function isKeyValid(key:FlxKey):Bool + { + @:privateAccess + { + return _preciseInputManager._keyListMap.exists(key); + } + } + + public function checkFlxKey(key:FlxKey):Bool + { + if (isKeyValid(key)) return check(cast key); + return false; + } + + public function checkDir(noteDir:NoteDirection):Bool + { + for (key in getKeysForDir(noteDir)) + { + if (check(_preciseInputManager.getInputByKey(key) ?.ID)) return true; + } + return false; + } + + public var NOTE_LEFT(get, never):Bool; + + function get_NOTE_LEFT():Bool + return checkDir(NoteDirection.LEFT); + + public var NOTE_DOWN(get, never):Bool; + + function get_NOTE_DOWN():Bool + return checkDir(NoteDirection.DOWN); + + public var NOTE_UP(get, never):Bool; + + function get_NOTE_UP():Bool + return checkDir(NoteDirection.UP); + + public var NOTE_RIGHT(get, never):Bool; + + function get_NOTE_RIGHT():Bool + return checkDir(NoteDirection.RIGHT); +} + +typedef PreciseInputEvent = +{ + /** + * The direction of the input. + */ + noteDirection:NoteDirection, + + /** + * The timestamp of the input. Measured in nanoseconds. + */ + timestamp:Int64, +}; diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx index 95922ded1..3f29ad833 100644 --- a/source/funkin/modding/events/ScriptEvent.hx +++ b/source/funkin/modding/events/ScriptEvent.hx @@ -1,10 +1,12 @@ package funkin.modding.events; +import funkin.play.song.SongData.SongNoteData; import flixel.FlxState; import flixel.FlxSubState; -import funkin.noteStuff.NoteBasic.NoteDir; +import funkin.play.notes.NoteSprite; import funkin.play.cutscene.dialogue.Conversation; import funkin.play.Countdown.CountdownStep; +import funkin.play.notes.NoteDirection; import openfl.events.EventType; import openfl.events.KeyboardEvent; @@ -344,7 +346,7 @@ class NoteScriptEvent extends ScriptEvent * The note associated with this event. * You cannot replace it, but you can edit it. */ - public var note(default, null):Note; + public var note(default, null):NoteSprite; /** * The combo count as it is with this event. @@ -357,7 +359,7 @@ class NoteScriptEvent extends ScriptEvent */ public var playSound(default, default):Bool; - public function new(type:ScriptEventType, note:Note, comboCount:Int = 0, cancelable:Bool = false):Void + public function new(type:ScriptEventType, note:NoteSprite, comboCount:Int = 0, cancelable:Bool = false):Void { super(type, cancelable); this.note = note; @@ -379,7 +381,7 @@ class GhostMissNoteScriptEvent extends ScriptEvent /** * The direction that was mistakenly pressed. */ - public var dir(default, null):NoteDir; + public var dir(default, null):NoteDirection; /** * Whether there was a note within judgement range when this ghost note was pressed. @@ -407,7 +409,7 @@ class GhostMissNoteScriptEvent extends ScriptEvent */ public var playAnim(default, default):Bool; - public function new(dir:NoteDir, hasPossibleNotes:Bool, healthChange:Float, scoreChange:Int):Void + public function new(dir:NoteDirection, hasPossibleNotes:Bool, healthChange:Float, scoreChange:Int):Void { super(ScriptEvent.NOTE_GHOST_MISS, true); this.dir = dir; @@ -575,19 +577,19 @@ class SongLoadScriptEvent extends ScriptEvent * The note associated with this event. * You cannot replace it, but you can edit it. */ - public var notes(default, set):Array<Note>; + public var notes(default, set):Array<SongNoteData>; public var id(default, null):String; public var difficulty(default, null):String; - function set_notes(notes:Array<Note>):Array<Note> + function set_notes(notes:Array<SongNoteData>):Array<SongNoteData> { this.notes = notes; return this.notes; } - public function new(id:String, difficulty:String, notes:Array<Note>):Void + public function new(id:String, difficulty:String, notes:Array<SongNoteData>):Void { super(ScriptEvent.SONG_LOADED, false); this.id = id; diff --git a/source/funkin/noteStuff/NoteBasic.hx b/source/funkin/noteStuff/NoteBasic.hx deleted file mode 100644 index c1900710f..000000000 --- a/source/funkin/noteStuff/NoteBasic.hx +++ /dev/null @@ -1,197 +0,0 @@ -package funkin.noteStuff; - -import flixel.FlxSprite; -import flixel.text.FlxText; - -typedef RawNoteData = -{ - var strumTime:Float; - var noteData:NoteType; - var sustainLength:Float; - var altNote:String; - var noteKind:NoteKind; -} - -@:forward -abstract NoteData(RawNoteData) -{ - public function new(strumTime = 0.0, noteData:NoteType = 0, sustainLength = 0.0, altNote = "", noteKind = NORMAL) - { - this = - { - strumTime: strumTime, - noteData: noteData, - sustainLength: sustainLength, - altNote: altNote, - noteKind: noteKind - } - } - - public var note(get, never):NoteType; - - inline function get_note() - return this.noteData.value; - - public var int(get, never):Int; - - inline function get_int() - return this.noteData.int; - - public var dir(get, never):NoteDir; - - inline function get_dir() - return this.noteData.value; - - public var dirName(get, never):String; - - inline function get_dirName() - return dir.name; - - public var dirNameUpper(get, never):String; - - inline function get_dirNameUpper() - return dir.nameUpper; - - public var color(get, never):NoteColor; - - inline function get_color() - return this.noteData.value; - - public var colorName(get, never):String; - - inline function get_colorName() - return color.name; - - public var colorNameUpper(get, never):String; - - inline function get_colorNameUpper() - return color.nameUpper; - - public var highStakes(get, never):Bool; - - inline function get_highStakes() - return this.noteData.highStakes; - - public var lowStakes(get, never):Bool; - - inline function get_lowStakes() - return this.noteData.lowStakes; -} - -enum abstract NoteType(Int) from Int to Int -{ - // public var raw(get, never):Int; - // inline function get_raw() return this; - public var int(get, never):Int; - - inline function get_int() - return this < 0 ? -this : this % 4; - - public var value(get, never):NoteType; - - inline function get_value() - return int; - - public var highStakes(get, never):Bool; - - inline function get_highStakes() - return this > 3; - - public var lowStakes(get, never):Bool; - - inline function get_lowStakes() - return this < 0; -} - -@:forward -enum abstract NoteDir(NoteType) from Int to Int from NoteType -{ - var LEFT = 0; - var DOWN = 1; - var UP = 2; - var RIGHT = 3; - var value(get, never):NoteDir; - - inline function get_value() - return this.value; - - public var name(get, never):String; - - function get_name() - { - return switch (value) - { - case LEFT: "left"; - case DOWN: "down"; - case UP: "up"; - case RIGHT: "right"; - } - } - - public var nameUpper(get, never):String; - - function get_nameUpper() - { - return switch (value) - { - case LEFT: "LEFT"; - case DOWN: "DOWN"; - case UP: "UP"; - case RIGHT: "RIGHT"; - } - } -} - -@:forward -enum abstract NoteColor(NoteType) from Int to Int from NoteType -{ - var PURPLE = 0; - var BLUE = 1; - var GREEN = 2; - var RED = 3; - var value(get, never):NoteColor; - - inline function get_value() - return this.value; - - public var name(get, never):String; - - function get_name() - { - return switch (value) - { - case PURPLE: "purple"; - case BLUE: "blue"; - case GREEN: "green"; - case RED: "red"; - } - } - - public var nameUpper(get, never):String; - - function get_nameUpper() - { - return switch (value) - { - case PURPLE: "PURPLE"; - case BLUE: "BLUE"; - case GREEN: "GREEN"; - case RED: "RED"; - } - } -} - -enum abstract NoteKind(String) from String to String -{ - /** - * The default note type. - */ - var NORMAL = "normal"; - - // Testing shiz - var PYRO_LIGHT = "pyro_light"; - var PYRO_KICK = "pyro_kick"; - var PYRO_TOSS = "pyro_toss"; - var PYRO_COCK = "pyro_cock"; // lol - var PYRO_SHOOT = "pyro_shoot"; -} diff --git a/source/funkin/noteStuff/NoteEvent.hx b/source/funkin/noteStuff/NoteEvent.hx deleted file mode 100644 index 2d0c60073..000000000 --- a/source/funkin/noteStuff/NoteEvent.hx +++ /dev/null @@ -1,12 +0,0 @@ -package funkin.noteStuff; - -import funkin.noteStuff.NoteBasic.NoteType; -import funkin.play.Strumline.StrumlineStyle; - -class NoteEvent extends Note -{ - public function new(strumTime:Float = 0, noteData:NoteType, ?prevNote:Note, ?sustainNote:Bool = false, ?style:StrumlineStyle = NORMAL) - { - super(strumTime, noteData, prevNote, sustainNote, style); - } -} diff --git a/source/funkin/noteStuff/NoteUtil.hx b/source/funkin/noteStuff/NoteUtil.hx deleted file mode 100644 index a36c32482..000000000 --- a/source/funkin/noteStuff/NoteUtil.hx +++ /dev/null @@ -1,98 +0,0 @@ -package funkin.noteStuff; - -import haxe.Json; -import openfl.Assets; - -/** - * Just various functions that IDK where to put em!!! - * Semi-temp for now? the note stuff is super clutter-y right now - * so I am putting this new stuff here right now XDD - * - * A lot of this stuff can probably be moved to where appropriate! - * i dont care about NoteUtil.hx at all!!! - */ -class NoteUtil -{ - /** - * IDK THING FOR BOTH LOL! DIS SHIT HACK-Y - * @param jsonPath - * @return Map<Int, Array<SongEventInfo>> - */ - public static function loadSongEvents(jsonPath:String):Map<Int, Array<SongEventInfo>> - { - return parseSongEvents(loadSongEventFromJson(jsonPath)); - } - - public static function loadSongEventFromJson(jsonPath:String):Array<SongEvent> - { - var daEvents:Array<SongEvent>; - daEvents = cast Json.parse(Assets.getText(jsonPath)).events; // DUMB LIL DETAIL HERE: MAKE SURE THAT .events IS THERE?? - trace('GET JSON SONG EVENTS:'); - trace(daEvents); - return daEvents; - } - - /** - * Parses song event json stuff into a neater lil map grouping? - * @param songEvents - */ - public static function parseSongEvents(songEvents:Array<SongEvent>):Map<Int, Array<SongEventInfo>> - { - var songData:Map<Int, Array<SongEventInfo>> = new Map(); - - for (songEvent in songEvents) - { - trace(songEvent); - if (songData[songEvent.t] == null) songData[songEvent.t] = []; - - songData[songEvent.t].push({songEventType: songEvent.e, value: songEvent.v, activated: false}); - } - - trace("FINISH SONG EVENTS!"); - trace(songData); - - return songData; - } - - public static function checkSongEvents(songData:Map<Int, Array<SongEventInfo>>, time:Float) - { - for (eventGrp in songData.keys()) - { - if (time >= eventGrp) - { - for (events in songData[eventGrp]) - { - if (!events.activated) - { - // TURN TO NICER SWITCH STATEMENT CHECKER OF EVENT TYPES!! - trace(events.value); - trace(eventGrp); - trace(Conductor.songPosition); - events.activated = true; - } - } - } - } - } -} - -typedef SongEventInfo = -{ - var songEventType:SongEventType; - var value:Dynamic; - var activated:Bool; -} - -typedef SongEvent = -{ - var t:Int; - var e:SongEventType; - var v:Dynamic; -} - -enum abstract SongEventType(String) -{ - var FocusCamera; - var PlayCharAnim; - var Trace; -} diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 6dfbfcf65..5583b7fed 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1,5 +1,6 @@ package funkin.play; +import haxe.Int64; import flixel.addons.display.FlxPieDial; import flixel.addons.transition.FlxTransitionableState; import flixel.FlxCamera; @@ -7,24 +8,21 @@ import flixel.FlxObject; import flixel.FlxSprite; import flixel.FlxState; import flixel.FlxSubState; -import flixel.group.FlxGroup.FlxTypedGroup; import flixel.input.keyboard.FlxKey; import flixel.math.FlxMath; import flixel.math.FlxPoint; import flixel.math.FlxRect; -import flixel.sound.FlxSound; import flixel.text.FlxText; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import flixel.ui.FlxBar; import flixel.util.FlxColor; -import flixel.util.FlxSort; import flixel.util.FlxTimer; import funkin.audio.VoicesGroup; import funkin.Highscore.Tallies; +import funkin.input.PreciseInputManager; import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEventDispatcher; -import funkin.Note; import funkin.play.character.BaseCharacter; import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.cutscene.dialogue.Conversation; @@ -32,17 +30,17 @@ import funkin.play.cutscene.dialogue.ConversationDataParser; import funkin.play.cutscene.VanillaCutscenes; import funkin.play.cutscene.VideoCutscene; import funkin.play.event.SongEventData.SongEventParser; +import funkin.play.notes.NoteSprite; +import funkin.play.notes.NoteDirection; +import funkin.play.notes.Strumline; import funkin.play.scoring.Scoring; import funkin.play.song.Song; import funkin.play.song.SongData.SongDataParser; import funkin.play.song.SongData.SongEventData; import funkin.play.song.SongData.SongNoteData; import funkin.play.song.SongData.SongPlayableChar; -import funkin.play.song.SongValidator; import funkin.play.stage.Stage; import funkin.play.stage.StageData.StageDataParser; -import funkin.play.Strumline.StrumlineArrow; -import funkin.play.Strumline.StrumlineStyle; import funkin.ui.PopUpStuff; import funkin.ui.PreferencesMenu; import funkin.ui.stageBuildShit.StageOffsetSubState; @@ -93,6 +91,12 @@ class PlayState extends MusicBeatState */ public static var instance:PlayState = null; + /** + * This sucks. We need this because FlxG.resetState(); assumes the constructor has no arguments. + * @see https://github.com/HaxeFlixel/flixel/issues/2541 + */ + static var lastParams:PlayStateParams = null; + /** * PUBLIC INSTANCE VARIABLES * Public instance variables should be used for information that must be reset or dereferenced @@ -118,18 +122,6 @@ class PlayState extends MusicBeatState */ public var currentStage:Stage = null; - /** - * Data for the current difficulty for the current song. - * Includes chart data, scroll speed, and other information. - */ - public var currentChart(get, null):SongDifficulty; - - /** - * The internal ID of the currently active Stage. - * Used to retrieve the data required to build the `currentStage`. - */ - public var currentStageId(get, null):String; - /** * Gets set to true when the PlayState needs to reset (player opted to restart or died). * Gets disabled once resetting happens. @@ -145,23 +137,24 @@ class PlayState extends MusicBeatState /** * The player's current health. * The default maximum health is 2.0, and the default starting health is 1.0. + * TODO: Refactor this to [0.0, 1.0] */ public var health:Float = 1; /** * The player's current score. + * TODO: Move this to its own class. */ public var songScore:Int = 0; /** * An empty FlxObject contained in the scene. - * The current gameplay camera will be centered on this object. Tween its position to move the camera smoothly. + * The current gameplay camera will always follow this object. Tween its position to move the camera smoothly. * - * This is an FlxSprite for two reasons: - * 1. It needs to be an object in the scene for the camera to be configured to follow it. - * 2. It needs to be an FlxSprite to allow a graphic (optionally, for debug purposes) to be drawn on it. + * It needs to be an object in the scene for the camera to be configured to follow it. + * We optionally make this an FlxSprite so we can draw a debug graphic with it. */ - public var cameraFollowPoint:FlxSprite = new FlxSprite(0, 0); + public var cameraFollowPoint:FlxObject; /** * The camera follow point from the last stage. @@ -229,17 +222,23 @@ class PlayState extends MusicBeatState */ public var currentConversation:Conversation; + /** + * Key press inputs which have been received but not yet processed. + * These are encoded with an OS timestamp, so they + **/ + var inputPressQueue:Array<PreciseInputEvent> = []; + + /** + * Key release inputs which have been received but not yet processed. + * These are encoded with an OS timestamp, so they + **/ + var inputReleaseQueue:Array<PreciseInputEvent> = []; + /** * PRIVATE INSTANCE VARIABLES * Private instance variables should be used for information that must be reset or dereferenced * every time the state is reset, but should not be accessed externally. */ - /** - * The Array containing the notes that are not currently on the screen. - * The `update()` function regularly shifts these out to add new notes to the screen. - */ - var inactiveNotes:Array<Note>; - /** * The Array containing the upcoming song events. * The `update()` function regularly shifts these out to trigger events. @@ -279,14 +278,17 @@ class PlayState extends MusicBeatState */ var vocals:VoicesGroup; + #if discord_rpc + // Discord RPC variables + var storyDifficultyText:String = ''; + var iconRPC:String = ''; + var detailsText:String = ''; + var detailsPausedText:String = ''; + #end + /** * RENDER OBJECTS */ - /** - * The SpriteGroup containing the notes that are currently on the screen or are about to be on the screen. - */ - var activeNotes:FlxTypedGroup<Note> = null; - /** * The FlxText which displays the current score. */ @@ -322,7 +324,7 @@ class PlayState extends MusicBeatState /** * The sprite group containing opponent's strumline notes. */ - public var enemyStrumline:Strumline; + public var opponentStrumline:Strumline; /** * The camera which contains, and controls visibility of, the user interface elements. @@ -339,6 +341,14 @@ class PlayState extends MusicBeatState */ public var camCutscene:FlxCamera; + /** + * The combo popups. Includes the real-time combo counter and the rating. + */ + var comboPopUps:PopUpStuff; + + /** + * The circular sprite that appears while the user is holding down the Skip Cutscene button. + */ var skipTimer:FlxPieDial; /** @@ -360,34 +370,54 @@ class PlayState extends MusicBeatState return this.subState != null; } - var gfSpeed:Int = 1; - var generatedMusic:Bool = false; + /** + * Data for the current difficulty for the current song. + * Includes chart data, scroll speed, and other information. + */ + public var currentChart(get, null):SongDifficulty; - var grpNoteSplashes:FlxTypedGroup<NoteSplash>; - var comboPopUps:PopUpStuff; - var perfectMode:Bool = false; - var previousFrameTime:Int = 0; - var songTime:Float = 0; - - #if discord_rpc - // Discord RPC variables - var storyDifficultyText:String = ''; - var iconRPC:String = ''; - var songLength:Float = 0; - var detailsText:String = ''; - var detailsPausedText:String = ''; - #end + function get_currentChart():SongDifficulty + { + if (currentSong == null || currentDifficulty == null) return null; + return currentSong.getDifficulty(currentDifficulty); + } /** - * This sucks. We need this because FlxG.resetState(); assumes the constructor has no arguments. - * @see https://github.com/HaxeFlixel/flixel/issues/2541 + * The internal ID of the currently active Stage. + * Used to retrieve the data required to build the `currentStage`. */ - static var lastParams:PlayStateParams = null; + public var currentStageId(get, null):String; + function get_currentStageId():String + { + if (currentChart == null || currentChart.stage == null || currentChart.stage == '') return Constants.DEFAULT_STAGE; + return currentChart.stage; + } + + /** + * The length of the current song, in milliseconds. + */ + var currentSongLengthMs(get, never):Float; + + function get_currentSongLengthMs():Float + { + return FlxG?.sound?.music?.length; + } + + // TODO: Refactor or document + var generatedMusic:Bool = false; + var perfectMode:Bool = false; + + /** + * Instantiate a new PlayState. + * @param params The parameters used to initialize the PlayState. + * Includes information about what song to play and more. + */ public function new(params:PlayStateParams) { super(); + // Validate parameters. if (params == null && lastParams == null) { throw 'PlayState constructor called with no available parameters.'; @@ -402,34 +432,43 @@ class PlayState extends MusicBeatState lastParams = params; } + // Apply parameters. currentSong = params.targetSong; if (params.targetDifficulty != null) currentDifficulty = params.targetDifficulty; if (params.targetCharacter != null) currentPlayerId = params.targetCharacter; + + // Don't do anything else here! Wait until create() when we attach to the camera. } + /** + * Called when the PlayState is switched to. + */ public override function create():Void { super.create(); if (instance != null) { + // TODO: Do something in this case? IDK. trace('WARNING: PlayState instance already exists. This should not happen.'); } instance = this; if (currentSong != null) { + // Load and cache the song's charts. // TODO: Do this in the loading state. currentSong.cacheCharts(true); } // Returns null if the song failed to load or doesn't have the selected difficulty. - if (currentChart == null) + if (currentSong == null || currentChart == null) { + // We have encountered a critical error. Prevent Flixel from trying to run any gameplay logic. criticalFailure = true; + // Choose an error message. var message:String = 'There was a critical error. Click OK to return to the main menu.'; - if (currentSong == null) { message = 'The was a critical error loading this song\'s chart. Click OK to return to the main menu.'; @@ -443,16 +482,26 @@ class PlayState extends MusicBeatState message = 'The was a critical error retrieving data for this song on "$currentDifficulty" difficulty. Click OK to return to the main menu.'; } + // Display a popup. This blocks the application until the user clicks OK. lime.app.Application.current.window.alert(message, 'Error loading PlayState'); + + // Force the user back to the main menu. FlxG.switchState(new MainMenuState()); return; } - // Displays the camera follow point as a sprite for debug purposes. - // TODO: Put this on a toggle? - cameraFollowPoint.makeGraphic(8, 8, 0xFF00FF00); - cameraFollowPoint.visible = false; - cameraFollowPoint.zIndex = 1000000; + if (false) + { + // Displays the camera follow point as a sprite for debug purposes. + cameraFollowPoint = new FlxSprite(0, 0).makeGraphic(8, 8, 0xFF00FF00); + cameraFollowPoint.visible = false; + cameraFollowPoint.zIndex = 1000000; + } + else + { + // Camera follow point is an invisible point in space. + cameraFollowPoint = new FlxObject(0, 0); + } // Reduce physics accuracy (who cares!!!) to improve animation quality. FlxG.fixedTimestep = false; @@ -465,590 +514,82 @@ class PlayState extends MusicBeatState // Stop any pre-existing music. if (FlxG.sound.music != null) FlxG.sound.music.stop(); - // Prepare the current song to be played. + // Prepare the current song's instrumental and vocals to be played. if (currentChart != null) { - currentChart.cacheInst(); + currentChart.cacheInst(currentPlayerId); currentChart.cacheVocals(currentPlayerId); } - // Initialize stage stuff. - initCameras(); - + // Prepare the Conductor. Conductor.mapTimeChanges(currentChart.timeChanges); - Conductor.update(-5000); - // Once the song is loaded, we can continue and initialize the stage. - - var healthBarYPos:Float = PreferencesMenu.getPref('downscroll') ? FlxG.height * 0.1 : FlxG.height * 0.9; - healthBarBG = new FlxSprite(0, healthBarYPos).loadGraphic(Paths.image('healthBar')); - healthBarBG.screenCenter(X); - healthBarBG.scrollFactor.set(0, 0); - add(healthBarBG); - - healthBar = new FlxBar(healthBarBG.x + 4, healthBarBG.y + 4, RIGHT_TO_LEFT, Std.int(healthBarBG.width - 8), Std.int(healthBarBG.height - 8), this, - 'healthLerp', 0, 2); - healthBar.scrollFactor.set(); - healthBar.createFilledBar(Constants.COLOR_HEALTH_BAR_RED, Constants.COLOR_HEALTH_BAR_GREEN); - add(healthBar); - + // The song is now loaded. We can continue to initialize the play state. + initCameras(); + initHealthBar(); initStage(); initCharacters(); - #if discord_rpc - initDiscord(); - #end - - // Configure camera follow point. - if (previousCameraFollowPoint != null) - { - cameraFollowPoint.setPosition(previousCameraFollowPoint.x, previousCameraFollowPoint.y); - previousCameraFollowPoint = null; - } - add(cameraFollowPoint); + initStrumlines(); + // Initialize the judgements and combo meter. comboPopUps = new PopUpStuff(); comboPopUps.cameras = [camHUD]; add(comboPopUps); - buildStrumlines(); - - grpNoteSplashes = new FlxTypedGroup<NoteSplash>(); - - var noteSplash:NoteSplash = new NoteSplash(100, 100, 0); - grpNoteSplashes.add(noteSplash); - noteSplash.alpha = 0.1; - - add(grpNoteSplashes); - - generateSong(); - - resetCamera(); - - FlxG.worldBounds.set(0, 0, FlxG.width, FlxG.height); - - scoreText = new FlxText(healthBarBG.x + healthBarBG.width - 190, healthBarBG.y + 30, 0, '', 20); - scoreText.setFormat(Paths.font('vcr.ttf'), 16, FlxColor.WHITE, RIGHT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK); - scoreText.scrollFactor.set(); - add(scoreText); - - // Skip Video Cutscene + // The little dial that shows up when you hold the Skip Cutscene key. skipTimer = new FlxPieDial(16, 16, 32, FlxColor.WHITE, 36, CIRCLE, true, 24); skipTimer.amount = 0; skipTimer.zIndex = 1000; + add(skipTimer); // Renders only in video cutscene mode. skipTimer.cameras = [camCutscene]; - add(skipTimer); - // Attach the groups to the HUD camera so they are rendered independent of the stage. - grpNoteSplashes.cameras = [camHUD]; - activeNotes.cameras = [camHUD]; - healthBar.cameras = [camHUD]; - healthBarBG.cameras = [camHUD]; - iconP1.cameras = [camHUD]; - iconP2.cameras = [camHUD]; - scoreText.cameras = [camHUD]; - leftWatermarkText.cameras = [camHUD]; - rightWatermarkText.cameras = [camHUD]; + #if discord_rpc + // Initialize Discord Rich Presence. + initDiscord(); + #end - // Starting song! + // Read the song's note data and pass it to the strumlines. + generateSong(); + + // Reset the camera's zoom and force it to focus on the camera follow point. + resetCamera(); + + initPreciseInputs(); + + FlxG.worldBounds.set(0, 0, FlxG.width, FlxG.height); + + // The song is loaded and in the process of starting. + // This gets set back to false when the chart actually starts. startingSong = true; - // TODO: Softcode cutscenes. - // TODO: Alternatively: make a song script that allows startCountdown to be called, - // then cancels the countdown, hides the UI, plays the cutscene, - // then calls PlayState.startCountdown later? - if (currentSong != null) + // TODO: We hardcoded the transition into Winter Horrorland. Do this with a ScriptedSong instead. + if ((currentSong?.songId ?? '').toLowerCase() == 'winter-horrorland') { - switch (currentSong.songId.toLowerCase()) - { - case 'winter-horrorland': - VanillaCutscenes.playHorrorStartCutscene(); - // This one is softcoded now WOOOO! - // case 'senpai' | 'roses' | 'thorns': - // schoolIntro(doof); - // case 'ugh': - // VanillaCutscenes.playUghCutscene(); - // case 'stress': - // VanillaCutscenes.playStressCutscene(); - // case 'guns': - // VanillaCutscenes.playGunsCutscene(); - default: - // VanillaCutscenes will call startCountdown later. - startCountdown(); - } + // VanillaCutscenes will call startCountdown later. + VanillaCutscenes.playHorrorStartCutscene(); } else { + // Call a script event to start the countdown. + // Songs with cutscenes should call event.cancel(). + // As long as they call `PlayState.instance.startCountdown()` later, the countdown will start. startCountdown(); } - #if debug - this.rightWatermarkText.text = Constants.VERSION; - #end + leftWatermarkText.cameras = [camHUD]; + rightWatermarkText.cameras = [camHUD]; + // Initialize some debug stuff. #if debug + // Display the version number (and git commit hash) in the bottom right corner. + this.rightWatermarkText.text = Constants.VERSION; + FlxG.console.registerObject('playState', this); #end } - function get_currentChart():SongDifficulty - { - if (currentSong == null || currentDifficulty == null) return null; - return currentSong.getDifficulty(currentDifficulty); - } - - function get_currentStageId():String - { - if (currentChart == null || currentChart.stage == null || currentChart.stage == '') return Constants.DEFAULT_STAGE; - return currentChart.stage; - } - - /** - * Initializes the game and HUD cameras. - */ - function initCameras():Void - { - // Set the camera zoom. This gets overridden by the value in the stage data. - // defaultCameraZoom = FlxCamera.defaultZoom * 1.05; - - camGame = new SwagCamera(); - camHUD = new FlxCamera(); - camHUD.bgColor.alpha = 0; - camCutscene = new FlxCamera(); - camCutscene.bgColor.alpha = 0; - - FlxG.cameras.reset(camGame); - FlxG.cameras.add(camHUD, false); - FlxG.cameras.add(camCutscene, false); - } - - function initStage():Void - { - if (currentSong != null) - { - if (currentChart == null) - { - trace('Song difficulty could not be loaded.'); - } - - loadStage(currentStageId); - } - else - { - // Fallback. - loadStage('mainStage'); - } - } - - function initCharacters():Void - { - if (currentSong == null || currentChart == null) - { - trace('Song difficulty could not be loaded.'); - } - - // TODO: Switch playable character by manipulating this value. - // TODO: How to choose which one to use for story mode? - - var playableChars:Array<String> = currentChart.getPlayableChars(); - - if (playableChars.length == 0) - { - trace('WARNING: No playable characters found for this song.'); - } - else if (playableChars.indexOf(currentPlayerId) == -1) - { - currentPlayerId = playableChars[0]; - } - - var currentCharData:SongPlayableChar = currentChart.getPlayableChar(currentPlayerId); - - // - // GIRLFRIEND - // - var girlfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharData.girlfriend); - - if (girlfriend != null) - { - girlfriend.characterType = CharacterType.GF; - } - else if (currentCharData.girlfriend != '') - { - trace('WARNING: Could not load girlfriend character with ID ${currentCharData.girlfriend}, skipping...'); - } - else - { - // Chosen GF was '' so we don't load one. - } - - // - // DAD - // - var dad:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharData.opponent); - - if (dad != null) - { - dad.characterType = CharacterType.DAD; - } - - // - // OPPONENT HEALTH ICON - // - iconP2 = new HealthIcon(currentCharData.opponent, 1); - iconP2.y = healthBar.y - (iconP2.height / 2); - dad.initHealthIcon(true); - add(iconP2); - - // - // BOYFRIEND - // - var boyfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentPlayerId); - - if (boyfriend != null) - { - boyfriend.characterType = CharacterType.BF; - } - - // - // PLAYER HEALTH ICON - // - iconP1 = new HealthIcon(currentPlayerId, 0); - iconP1.y = healthBar.y - (iconP1.height / 2); - boyfriend.initHealthIcon(false); - add(iconP1); - - // - // ADD CHARACTERS TO SCENE - // - - if (currentStage != null) - { - // Characters get added to the stage, not the main scene. - if (girlfriend != null) - { - currentStage.addCharacter(girlfriend, GF); - - #if debug - FlxG.console.registerObject('gf', girlfriend); - #end - } - - if (boyfriend != null) - { - currentStage.addCharacter(boyfriend, BF); - - #if debug - FlxG.console.registerObject('bf', boyfriend); - #end - } - - if (dad != null) - { - currentStage.addCharacter(dad, DAD); - // Camera starts at dad. - cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y); - - #if debug - FlxG.console.registerObject('dad', dad); - #end - } - - // Rearrange by z-indexes. - currentStage.refresh(); - } - } - - /** - * Removes any references to the current stage, then clears the stage cache, - * then reloads all the stages. - * - * This is useful for when you want to edit a stage without reloading the whole game. - * Reloading works on both the JSON and the HXC, if applicable. - * - * Call this by pressing F5 on a debug build. - */ - override function debug_refreshModules():Void - { - // Remove the current stage. If the stage gets deleted while it's still in use, - // it'll probably crash the game or something. - if (this.currentStage != null) - { - remove(currentStage); - var event:ScriptEvent = new ScriptEvent(ScriptEvent.DESTROY, false); - ScriptEventDispatcher.callEvent(currentStage, event); - currentStage = null; - } - - // Stop the vocals. - if (vocals != null) - { - vocals.stop(); - } - - super.debug_refreshModules(); - - var event:ScriptEvent = new ScriptEvent(ScriptEvent.CREATE, false); - ScriptEventDispatcher.callEvent(currentSong, event); - } - - /** - * Pauses music and vocals easily. - */ - public function pauseMusic():Void - { - FlxG.sound.music.pause(); - vocals.pause(); - } - - /** - * Loads stage data from cache, assembles the props, - * and adds it to the state. - * @param id - */ - function loadStage(id:String):Void - { - currentStage = StageDataParser.fetchStage(id); - - if (currentStage != null) - { - // Actually create and position the sprites. - var event:ScriptEvent = new ScriptEvent(ScriptEvent.CREATE, false); - ScriptEventDispatcher.callEvent(currentStage, event); - - // Apply camera zoom level from stage data. - defaultCameraZoom = currentStage.camZoom; - - // Add the stage to the scene. - this.add(currentStage); - - #if debug - FlxG.console.registerObject('stage', currentStage); - #end - } - else - { - // lolol - lime.app.Application.current.window.alert('Nice job, you ignoramus. $id isn\'t a real stage.\nI\'m falling back to the default so the game doesn\'t shit itself.', - 'Stage Error'); - } - } - - function initDiscord():Void - { - #if discord_rpc - storyDifficultyText = difficultyString(); - iconRPC = currentSong.player2; - - // To avoid having duplicate images in Discord assets - switch (iconRPC) - { - case 'senpai-angry': - iconRPC = 'senpai'; - case 'monster-christmas': - iconRPC = 'monster'; - case 'mom-car': - iconRPC = 'mom'; - } - - // String that contains the mode defined here so it isn't necessary to call changePresence for each mode - detailsText = isStoryMode ? 'Story Mode: Week $storyWeek' : 'Freeplay'; - detailsPausedText = 'Paused - $detailsText'; - - // Updating Discord Rich Presence. - DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC); - #end - } - - function startSong():Void - { - dispatchEvent(new ScriptEvent(ScriptEvent.SONG_START)); - - startingSong = false; - - previousFrameTime = FlxG.game.ticks; - - if (!isGamePaused && currentChart != null) - { - currentChart.playInst(1.0, false); - } - - FlxG.sound.music.onComplete = endSong; - trace('Playing vocals...'); - add(vocals); - vocals.play(); - - #if discord_rpc - // Song duration in a float, useful for the time left feature - songLength = FlxG.sound.music.length; - - // Updating Discord Rich Presence (with Time Left) - DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC, true, songLength); - #end - } - - function generateSong():Void - { - if (currentChart == null) - { - trace('Song difficulty could not be loaded.'); - } - - Conductor.forceBPM(currentChart.getStartingBPM()); - - vocals = currentChart.buildVocals(currentPlayerId); - if (vocals.members.length == 0) - { - trace('WARNING: No vocals found for this song.'); - } - - // Create the rendered note group. - activeNotes = new FlxTypedGroup<Note>(); - activeNotes.zIndex = 1000; - add(activeNotes); - - regenNoteData(); - - generatedMusic = true; - } - - function regenNoteData():Void - { - Highscore.tallies.combo = 0; - Highscore.tallies = new Tallies(); - - // Reset song events. - songEvents = currentChart.getEvents(); - SongEventParser.resetEvents(songEvents); - - // Destroy inactive notes. - inactiveNotes = []; - - // Destroy active notes. - activeNotes.forEach(function(nt) { - nt.followsTime = false; - FlxTween.tween(nt, {y: FlxG.height + nt.y}, 0.5, - { - ease: FlxEase.expoIn, - onComplete: function(twn) { - nt.kill(); - activeNotes.remove(nt, true); - nt.destroy(); - } - }); - }); - - var noteData:Array<SongNoteData> = currentChart.notes; - - var oldNote:Note = null; - for (songNote in noteData) - { - var mustHitNote:Bool = songNote.getMustHitNote(); - - // TODO: Put this in the chart or something? - var strumlineStyle:StrumlineStyle = null; - switch (currentStageId) - { - case 'school': - strumlineStyle = PIXEL; - case 'schoolEvil': - strumlineStyle = PIXEL; - default: - strumlineStyle = NORMAL; - } - - var newNote:Note = new Note(songNote.time, songNote.data, oldNote, false, strumlineStyle); - newNote.mustPress = mustHitNote; - newNote.data.sustainLength = songNote.length; - newNote.data.noteKind = songNote.kind; - newNote.scrollFactor.set(0, 0); - - // Note positioning. - // TODO: Make this more robust. - if (newNote.mustPress) - { - newNote.alignToSturmlineArrow(playerStrumline.getArrow(songNote.getDirection())); - } - else - { - newNote.alignToSturmlineArrow(enemyStrumline.getArrow(songNote.getDirection())); - } - - inactiveNotes.push(newNote); - - oldNote = newNote; - - // Generate X sustain notes. - var sustainSections = Math.round(songNote.length / Conductor.stepLengthMs); - for (noteIndex in 0...sustainSections) - { - var noteTimeOffset:Float = Conductor.stepLengthMs + (Conductor.stepLengthMs * noteIndex); - var sustainNote:Note = new Note(songNote.time + noteTimeOffset, songNote.data, oldNote, true, strumlineStyle); - sustainNote.mustPress = mustHitNote; - sustainNote.data.noteKind = songNote.kind; - sustainNote.scrollFactor.set(0, 0); - - if (sustainNote.mustPress) - { - // Align with the strumline arrow. - sustainNote.alignToSturmlineArrow(playerStrumline.getArrow(songNote.getDirection())); - } - else - { - sustainNote.alignToSturmlineArrow(enemyStrumline.getArrow(songNote.getDirection())); - } - - inactiveNotes.push(sustainNote); - - oldNote = sustainNote; - } - } - - // Sorting is an expensive operation. - // TODO: Make this more efficient. - // DO NOT assume it was done in the chart file. Notes created artificially by sustains are in here too. - inactiveNotes.sort(function(a:Note, b:Note):Int { - return SortUtil.byStrumtime(FlxSort.ASCENDING, a, b); - }); - /** - **/ - } - - #if discord_rpc - override public function onFocus():Void - { - if (health > 0 && !paused && FlxG.autoPause) - { - if (Conductor.songPosition > 0.0) DiscordClient.changePresence(detailsText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC, true, - songLength - Conductor.songPosition); - else - DiscordClient.changePresence(detailsText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC); - } - - super.onFocus(); - } - - override public function onFocusLost():Void - { - if (health > 0 && !paused && FlxG.autoPause) DiscordClient.changePresence(detailsPausedText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC); - - super.onFocusLost(); - } - #end - - function resyncVocals():Void - { - if (_exiting || vocals == null) return; - - vocals.pause(); - - FlxG.sound.music.play(); - Conductor.update(); - - vocals.time = FlxG.sound.music.time; - vocals.play(); - } - public override function update(elapsed:Float):Void { if (criticalFailure) return; @@ -1130,13 +671,9 @@ class PlayState extends MusicBeatState if (!isGamePaused) { - songTime += FlxG.game.ticks - previousFrameTime; - previousFrameTime = FlxG.game.ticks; - // Interpolation type beat if (Conductor.lastSongPos != Conductor.songPosition) { - songTime = (songTime + Conductor.songPosition) / 2; Conductor.lastSongPos = Conductor.songPosition; } } @@ -1216,22 +753,7 @@ class PlayState extends MusicBeatState } FlxG.watch.addQuick('songPos', Conductor.songPosition); - // Handle GF dance speed. - // TODO: Add a song event for this. - if (currentSong.songId == 'fresh') - { - switch (Conductor.currentBeat) - { - case 16: - gfSpeed = 2; - case 48: - gfSpeed = 1; - case 80: - gfSpeed = 2; - case 112: - gfSpeed = 1; - } - } + // TODO: Add a song event for Handle GF dance speed. // Handle player death. if (!isInCutscene && !disableKeys && !_exiting) @@ -1287,128 +809,8 @@ class PlayState extends MusicBeatState } } - // Iterate over inactive notes. - while (inactiveNotes[0] != null && inactiveNotes[0].data.strumTime - Conductor.songPosition < 1800 / currentChart.scrollSpeed) - { - var dunceNote:Note = inactiveNotes[0]; - - if (dunceNote.mustPress && !dunceNote.isSustainNote) Highscore.tallies.totalNotes++; - - activeNotes.add(dunceNote); - - inactiveNotes.shift(); - } - - // Iterate over active notes. - if (generatedMusic && playerStrumline != null) - { - activeNotes.forEachAlive(function(daNote:Note) { - if ((PreferencesMenu.getPref('downscroll') && daNote.y < -daNote.height) - || (!PreferencesMenu.getPref('downscroll') && daNote.y > FlxG.height)) - { - daNote.active = false; - daNote.visible = false; - } - else - { - daNote.visible = true; - daNote.active = true; - } - - var strumLineMid:Float = playerStrumline.y + Note.swagWidth / 2; - - if (daNote.followsTime) - { - daNote.y = (Conductor.songPosition - daNote.data.strumTime) * (0.45 * FlxMath.roundDecimal(currentChart.scrollSpeed, 2) * daNote.noteSpeedMulti); - } - - if (PreferencesMenu.getPref('downscroll')) - { - daNote.y += playerStrumline.y; - if (daNote.isSustainNote) - { - if (daNote.animation.curAnim.name.endsWith('end') && daNote.prevNote != null) - { - daNote.y += daNote.prevNote.height; - } - else - { - daNote.y += daNote.height / 2; - } - - if ((!daNote.mustPress || (daNote.wasGoodHit || (daNote.prevNote.wasGoodHit && !daNote.canBeHit))) - && daNote.y - daNote.offset.y * daNote.scale.y + daNote.height >= strumLineMid) - { - applyClipRect(daNote); - } - } - } - else - { - if (daNote.followsTime) daNote.y = playerStrumline.y - daNote.y; - if (daNote.isSustainNote - && (!daNote.mustPress || (daNote.wasGoodHit || (daNote.prevNote.wasGoodHit && !daNote.canBeHit))) - && daNote.y + daNote.offset.y * daNote.scale.y <= strumLineMid) - { - applyClipRect(daNote); - } - } - - if (!daNote.mustPress && daNote.wasGoodHit && !daNote.tooLate) - { - var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, daNote, Highscore.tallies.combo, true); - dispatchEvent(event); - - // Calling event.cancelEvent() in a module should force the CPU to miss the note. - // This is useful for cool shit, including but not limited to: - // - Making the AI ignore notes which are hazardous. - // - Making the AI miss notes on purpose for aesthetic reasons. - if (event.eventCanceled) - { - daNote.tooLate = true; - } - } - - // WIP interpolation shit? Need to fix the pause issue - // daNote.y = (strumLine.y - (songTime - daNote.strumTime) * (0.45 * SONG.speed[.curDiff])); - - // removing this so whether the note misses or not is entirely up to Note class - // var noteMiss:Bool = daNote.y < -daNote.height; - - // if (PreferencesMenu.getPref('downscroll')) - // noteMiss = daNote.y > FlxG.height; - - if (daNote.isSustainNote && daNote.wasGoodHit) - { - if ((!PreferencesMenu.getPref('downscroll') && daNote.y < -daNote.height) - || (PreferencesMenu.getPref('downscroll') && daNote.y > FlxG.height)) - { - daNote.active = false; - daNote.visible = false; - - daNote.kill(); - activeNotes.remove(daNote, true); - daNote.destroy(); - } - } - if (daNote.wasGoodHit) - { - daNote.active = false; - daNote.visible = false; - - daNote.kill(); - activeNotes.remove(daNote, true); - daNote.destroy(); - } - - if (daNote.tooLate) - { - noteMiss(daNote); - } - }); - } - // Query and activate song events. + // TODO: Check that these work even when songPosition is less than 0. if (songEvents != null && songEvents.length > 0) { var songEventsToActivate:Array<SongEventData> = SongEventParser.queryEvents(songEvents, Conductor.songPosition); @@ -1430,16 +832,1408 @@ class PlayState extends MusicBeatState } // Handle keybinds. - if (!isInCutscene && !disableKeys) keyShit(true); + // if (!isInCutscene && !disableKeys) keyShit(true); + processInputQueue(); if (!isInCutscene && !disableKeys) debugKeyShit(); if (isInCutscene && !disableKeys) handleCutsceneKeys(elapsed); + // Moving notes into position is now done by Strumline.update(). + processNotes(); + // Dispatch the onUpdate event to scripted elements. dispatchEvent(new UpdateScriptEvent(elapsed)); } - static final CUTSCENE_KEYS:Array<FlxKey> = [SPACE, ESCAPE, ENTER]; + public override function dispatchEvent(event:ScriptEvent):Void + { + // ORDER: Module, Stage, Character, Song, Conversation, Note + // Modules should get the first chance to cancel the event. + // super.dispatchEvent(event) dispatches event to module scripts. + super.dispatchEvent(event); + + // Dispatch event to stage script. + ScriptEventDispatcher.callEvent(currentStage, event); + + // Dispatch event to character script(s). + if (currentStage != null) currentStage.dispatchToCharacters(event); + + // Dispatch event to song script. + ScriptEventDispatcher.callEvent(currentSong, event); + + // Dispatch event to conversation script. + ScriptEventDispatcher.callEvent(currentConversation, event); + + // TODO: Dispatch event to note scripts + } + + /** + * Function called before opening a new substate. + * @param subState The substate to open. + */ + public override function openSubState(subState:FlxSubState):Void + { + // If there is a substate which requires the game to continue, + // then make this a condition. + var shouldPause = true; + + if (shouldPause) + { + // Pause the music. + if (FlxG.sound.music != null) + { + FlxG.sound.music.pause(); + if (vocals != null) vocals.pause(); + } + + // Pause the countdown. + Countdown.pauseCountdown(); + } + + super.openSubState(subState); + } + + /** + * Function called before closing the current substate. + * @param subState + */ + public override function closeSubState():Void + { + if (isGamePaused) + { + var event:ScriptEvent = new ScriptEvent(ScriptEvent.RESUME, true); + + dispatchEvent(event); + + if (event.eventCanceled) return; + + if (FlxG.sound.music != null && !startingSong && !isInCutscene) resyncVocals(); + + // Resume the countdown. + Countdown.resumeCountdown(); + + #if discord_rpc + if (startTimer.finished) + { + DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC, true, + currentSongLengthMs - Conductor.songPosition); + } + else + { + DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC); + } + #end + } + + super.closeSubState(); + } + + #if discord_rpc + /** + * Function called when the game window gains focus. + */ + public override function onFocus():Void + { + if (health > 0 && !paused && FlxG.autoPause) + { + if (Conductor.songPosition > 0.0) DiscordClient.changePresence(detailsText, currentSong.song + + ' (' + + storyDifficultyText + + ')', iconRPC, true, + currentSongLengthMs + - Conductor.songPosition); + else + DiscordClient.changePresence(detailsText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC); + } + + super.onFocus(); + } + + /** + * Function called when the game window loses focus. + */ + public override function onFocusLost():Void + { + if (health > 0 && !paused && FlxG.autoPause) DiscordClient.changePresence(detailsPausedText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC); + + super.onFocusLost(); + } + #end + + /** + * This function is called whenever Flixel switches switching to a new FlxState. + * @return Whether to actually switch to the new state. + */ + override function switchTo(nextState:FlxState):Bool + { + var result:Bool = super.switchTo(nextState); + + if (result) + { + performCleanup(); + } + + return result; + } + + /** + * Removes any references to the current stage, then clears the stage cache, + * then reloads all the stages. + * + * This is useful for when you want to edit a stage without reloading the whole game. + * Reloading works on both the JSON and the HXC, if applicable. + * + * Call this by pressing F5 on a debug build. + */ + override function debug_refreshModules():Void + { + // Remove the current stage. If the stage gets deleted while it's still in use, + // it'll probably crash the game or something. + if (this.currentStage != null) + { + remove(currentStage); + var event:ScriptEvent = new ScriptEvent(ScriptEvent.DESTROY, false); + ScriptEventDispatcher.callEvent(currentStage, event); + currentStage = null; + } + + // Stop the vocals. + if (vocals != null) + { + vocals.stop(); + } + + super.debug_refreshModules(); + + var event:ScriptEvent = new ScriptEvent(ScriptEvent.CREATE, false); + ScriptEventDispatcher.callEvent(currentSong, event); + } + + override function stepHit():Bool + { + // super.stepHit() returns false if a module cancelled the event. + if (!super.stepHit()) return false; + + if (Math.abs(FlxG.sound.music.time - (Conductor.songPosition - Conductor.offset)) > 200 + || Math.abs(vocals.checkSyncError(Conductor.songPosition - Conductor.offset)) > 200) + { + trace("VOCALS NEED RESYNC"); + if (vocals != null) trace(vocals.checkSyncError(Conductor.songPosition - Conductor.offset)); + trace(FlxG.sound.music.time - (Conductor.songPosition - Conductor.offset)); + resyncVocals(); + } + + if (iconP1 != null) iconP1.onStepHit(Std.int(Conductor.currentStep)); + if (iconP2 != null) iconP2.onStepHit(Std.int(Conductor.currentStep)); + + return true; + } + + override function beatHit():Bool + { + // super.beatHit() returns false if a module cancelled the event. + if (!super.beatHit()) return false; + + if (generatedMusic) + { + // TODO: Sort more efficiently, or less often, to improve performance. + // activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING); + } + + // Only zoom camera if we are zoomed by less than 35%. + if (FlxG.camera.zoom < (1.35 * defaultCameraZoom) && cameraZoomRate > 0 && Conductor.currentBeat % cameraZoomRate == 0) + { + // Zoom camera in (1.5%) + FlxG.camera.zoom += cameraZoomIntensity * defaultCameraZoom; + // Hud zooms double (3%) + camHUD.zoom += hudCameraZoomIntensity * defaultHUDCameraZoom; + } + // trace('Not bopping camera: ${FlxG.camera.zoom} < ${(1.35 * defaultCameraZoom)} && ${cameraZoomRate} > 0 && ${Conductor.currentBeat} % ${cameraZoomRate} == ${Conductor.currentBeat % cameraZoomRate}}'); + + // That combo milestones that got spoiled that one time. + // Comes with NEAT visual and audio effects. + + // bruh this var is bonkers i thot it was a function lmfaooo + + // Break up into individual lines to aid debugging. + + var shouldShowComboText:Bool = false; + // TODO: Re-enable combo text (how to do this without sections?). + // if (currentSong != null) + // { + // shouldShowComboText = (Conductor.currentBeat % 8 == 7); + // var daSection = .getSong()[Std.int(Conductor.currentBeat / 16)]; + // shouldShowComboText = shouldShowComboText && (daSection != null && daSection.mustHitSection); + // shouldShowComboText = shouldShowComboText && (Highscore.tallies.combo > 5); + // + // var daNextSection = .getSong()[Std.int(Conductor.currentBeat / 16) + 1]; + // var isEndOfSong = .getSong().length < Std.int(Conductor.currentBeat / 16); + // shouldShowComboText = shouldShowComboText && (isEndOfSong || (daNextSection != null && !daNextSection.mustHitSection)); + // } + + if (shouldShowComboText) + { + var animShit:ComboMilestone = new ComboMilestone(-100, 300, Highscore.tallies.combo); + animShit.scrollFactor.set(0.6, 0.6); + animShit.cameras = [camHUD]; + add(animShit); + + var frameShit:Float = (1 / 24) * 2; // equals 2 frames in the animation + + new FlxTimer().start(((Conductor.beatLengthMs / 1000) * 1.25) - frameShit, function(tmr) { + animShit.forceFinish(); + }); + } + + if (playerStrumline != null) playerStrumline.onBeatHit(); + if (opponentStrumline != null) opponentStrumline.onBeatHit(); + + // Make the characters dance on the beat + danceOnBeat(); + + return true; + } + + override function destroy():Void + { + if (currentConversation != null) + { + remove(currentConversation); + currentConversation.kill(); + } + + super.destroy(); + } + + /** + * Handles characters dancing to the beat of the current song. + * + * TODO: Move some of this logic into `Bopper.hx`, or individual character scripts. + */ + function danceOnBeat():Void + { + if (currentStage == null) return; + + // TODO: Add HEY! song events to Tutorial. + if (Conductor.currentBeat % 16 == 15 + && currentStage.getDad().characterId == 'gf' + && Conductor.currentBeat > 16 + && Conductor.currentBeat < 48) + { + currentStage.getBoyfriend().playAnimation('hey', true); + currentStage.getDad().playAnimation('cheer', true); + } + } + + /** + * Initializes the game and HUD cameras. + */ + function initCameras():Void + { + camGame = new SwagCamera(); + camHUD = new FlxCamera(); + camHUD.bgColor.alpha = 0; // Show the game scene behind the camera. + camCutscene = new FlxCamera(); + camCutscene.bgColor.alpha = 0; // Show the game scene behind the camera. + + FlxG.cameras.reset(camGame); + FlxG.cameras.add(camHUD, false); + FlxG.cameras.add(camCutscene, false); + + // Configure camera follow point. + if (previousCameraFollowPoint != null) + { + cameraFollowPoint.setPosition(previousCameraFollowPoint.x, previousCameraFollowPoint.y); + previousCameraFollowPoint = null; + } + add(cameraFollowPoint); + } + + /** + * Initializes the health bar on the HUD. + */ + function initHealthBar():Void + { + var healthBarYPos:Float = PreferencesMenu.getPref('downscroll') ? FlxG.height * 0.1 : FlxG.height * 0.9; + healthBarBG = new FlxSprite(0, healthBarYPos).loadGraphic(Paths.image('healthBar')); + healthBarBG.screenCenter(X); + healthBarBG.scrollFactor.set(0, 0); + add(healthBarBG); + + healthBar = new FlxBar(healthBarBG.x + 4, healthBarBG.y + 4, RIGHT_TO_LEFT, Std.int(healthBarBG.width - 8), Std.int(healthBarBG.height - 8), this, + 'healthLerp', 0, 2); + healthBar.scrollFactor.set(); + healthBar.createFilledBar(Constants.COLOR_HEALTH_BAR_RED, Constants.COLOR_HEALTH_BAR_GREEN); + add(healthBar); + + // The score text below the health bar. + scoreText = new FlxText(healthBarBG.x + healthBarBG.width - 190, healthBarBG.y + 30, 0, '', 20); + scoreText.setFormat(Paths.font('vcr.ttf'), 16, FlxColor.WHITE, RIGHT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK); + scoreText.scrollFactor.set(); + add(scoreText); + + // Move the health bar to the HUD camera. + healthBar.cameras = [camHUD]; + healthBarBG.cameras = [camHUD]; + scoreText.cameras = [camHUD]; + } + + /** + * Generates the stage and all its props. + */ + function initStage():Void + { + loadStage(currentStageId); + } + + /** + * Loads stage data from cache, assembles the props, + * and adds it to the state. + * @param id + */ + function loadStage(id:String):Void + { + currentStage = StageDataParser.fetchStage(id); + + if (currentStage != null) + { + // Actually create and position the sprites. + var event:ScriptEvent = new ScriptEvent(ScriptEvent.CREATE, false); + ScriptEventDispatcher.callEvent(currentStage, event); + + // Apply camera zoom level from stage data. + defaultCameraZoom = currentStage.camZoom; + + // Add the stage to the scene. + this.add(currentStage); + + #if debug + FlxG.console.registerObject('stage', currentStage); + #end + } + else + { + // lolol + lime.app.Application.current.window.alert('Nice job, you ignoramus. $id isn\'t a real stage.\nI\'m falling back to the default so the game doesn\'t shit itself.', + 'Stage Error'); + } + } + + /** + * Generates the character sprites and adds them to the stage. + */ + function initCharacters():Void + { + if (currentSong == null || currentChart == null) + { + trace('Song difficulty could not be loaded.'); + } + + // Switch the character we are playing as by manipulating currentPlayerId. + // TODO: How to choose which one to use for story mode? + var playableChars:Array<String> = currentChart.getPlayableChars(); + + if (playableChars.length == 0) + { + trace('WARNING: No playable characters found for this song.'); + } + else if (playableChars.indexOf(currentPlayerId) == -1) + { + currentPlayerId = playableChars[0]; + } + + // + var currentCharData:SongPlayableChar = currentChart.getPlayableChar(currentPlayerId); + + // + // GIRLFRIEND + // + var girlfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharData.girlfriend); + + if (girlfriend != null) + { + girlfriend.characterType = CharacterType.GF; + } + else if (currentCharData.girlfriend != '') + { + trace('WARNING: Could not load girlfriend character with ID ${currentCharData.girlfriend}, skipping...'); + } + else + { + // Chosen GF was '' so we don't load one. + } + + // + // DAD + // + var dad:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharData.opponent); + + if (dad != null) + { + dad.characterType = CharacterType.DAD; + } + + // + // OPPONENT HEALTH ICON + // + iconP2 = new HealthIcon(currentCharData.opponent, 1); + iconP2.y = healthBar.y - (iconP2.height / 2); + dad.initHealthIcon(true); + add(iconP2); + iconP2.cameras = [camHUD]; + + // + // BOYFRIEND + // + var boyfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentPlayerId); + + if (boyfriend != null) + { + boyfriend.characterType = CharacterType.BF; + } + + // + // PLAYER HEALTH ICON + // + iconP1 = new HealthIcon(currentPlayerId, 0); + iconP1.y = healthBar.y - (iconP1.height / 2); + boyfriend.initHealthIcon(false); + add(iconP1); + iconP1.cameras = [camHUD]; + + // + // ADD CHARACTERS TO SCENE + // + + if (currentStage != null) + { + // Characters get added to the stage, not the main scene. + if (girlfriend != null) + { + currentStage.addCharacter(girlfriend, GF); + + #if debug + FlxG.console.registerObject('gf', girlfriend); + #end + } + + if (boyfriend != null) + { + currentStage.addCharacter(boyfriend, BF); + + #if debug + FlxG.console.registerObject('bf', boyfriend); + #end + } + + if (dad != null) + { + currentStage.addCharacter(dad, DAD); + // Camera starts at dad. + cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y); + + #if debug + FlxG.console.registerObject('dad', dad); + #end + } + + // Rearrange by z-indexes. + currentStage.refresh(); + } + } + + /** + * Constructs the strumlines for each player. + */ + function initStrumlines():Void + { + // var strumlineStyle:StrumlineStyle = NORMAL; + // + //// TODO: Put this in the chart or something? + // switch (currentStageId) + // { + // case 'school': + // strumlineStyle = PIXEL; + // case 'schoolEvil': + // strumlineStyle = PIXEL; + // } + + playerStrumline = new Strumline(true); + opponentStrumline = new Strumline(false); + add(playerStrumline); + add(opponentStrumline); + + // Position the player strumline on the right + playerStrumline.x = FlxG.width - playerStrumline.width - Constants.STRUMLINE_X_OFFSET; + playerStrumline.y = PreferencesMenu.getPref('downscroll') ? FlxG.height - playerStrumline.height - Constants.STRUMLINE_Y_OFFSET : Constants.STRUMLINE_Y_OFFSET; + playerStrumline.zIndex = 200; + playerStrumline.cameras = [camHUD]; + + // Position the opponent strumline on the left + opponentStrumline.x = Constants.STRUMLINE_X_OFFSET; + opponentStrumline.y = PreferencesMenu.getPref('downscroll') ? FlxG.height - opponentStrumline.height - Constants.STRUMLINE_Y_OFFSET : Constants.STRUMLINE_Y_OFFSET; + opponentStrumline.zIndex = 100; + opponentStrumline.cameras = [camHUD]; + + if (!PlayStatePlaylist.isStoryMode) + { + playerStrumline.fadeInArrows(); + } + + if (!PlayStatePlaylist.isStoryMode) + { + opponentStrumline.fadeInArrows(); + } + + this.refresh(); + } + + /** + * Initializes the Discord Rich Presence. + */ + function initDiscord():Void + { + #if discord_rpc + storyDifficultyText = difficultyString(); + iconRPC = currentSong.player2; + + // To avoid having duplicate images in Discord assets + switch (iconRPC) + { + case 'senpai-angry': + iconRPC = 'senpai'; + case 'monster-christmas': + iconRPC = 'monster'; + case 'mom-car': + iconRPC = 'mom'; + } + + // String that contains the mode defined here so it isn't necessary to call changePresence for each mode + detailsText = isStoryMode ? 'Story Mode: Week $storyWeek' : 'Freeplay'; + detailsPausedText = 'Paused - $detailsText'; + + // Updating Discord Rich Presence. + DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC); + #end + } + + function initPreciseInputs():Void + { + FlxG.keys.preventDefaultKeys = []; + PreciseInputManager.instance.onInputPressed.add(onKeyPress); + PreciseInputManager.instance.onInputReleased.add(onKeyRelease); + } + + /** + * Initializes the song (applying the chart, generating the notes, etc.) + * Should be done before the countdown starts. + */ + function generateSong():Void + { + if (currentChart == null) + { + trace('Song difficulty could not be loaded.'); + } + + Conductor.forceBPM(currentChart.getStartingBPM()); + + vocals = currentChart.buildVocals(currentPlayerId); + if (vocals.members.length == 0) + { + trace('WARNING: No vocals found for this song.'); + } + + regenNoteData(); + + generatedMusic = true; + } + + /** + * Read note data from the chart and generate the notes. + */ + function regenNoteData():Void + { + Highscore.tallies.combo = 0; + Highscore.tallies = new Tallies(); + + // Reset song events. + songEvents = currentChart.getEvents(); + SongEventParser.resetEvents(songEvents); + + // TODO: Put this in the chart or something? + // var strumlineStyle:StrumlineStyle = null; + // switch (currentStageId) + // { + // case 'school': + // strumlineStyle = PIXEL; + // case 'schoolEvil': + // strumlineStyle = PIXEL; + // default: + // strumlineStyle = NORMAL; + // } + + // Reset the notes on each strumline. + var noteData:Array<SongNoteData> = currentChart.notes; + var playerNoteData:Array<SongNoteData> = []; + var opponentNoteData:Array<SongNoteData> = []; + + for (songNote in currentChart.notes) + { + var strumTime:Float = songNote.time; + var noteData:Int = songNote.getDirection(); + + var playerNote:Bool = true; + + if (noteData > 3) playerNote = false; + + switch (songNote.getStrumlineIndex()) + { + case 0: + playerNoteData.push(songNote); + case 1: + opponentNoteData.push(songNote); + } + } + + playerStrumline.applyNoteData(playerNoteData); + opponentStrumline.applyNoteData(opponentNoteData); + } + + /** + * Prepares to start the countdown. + * Ends any running cutscenes, creates the strumlines, and starts the countdown. + * This is public so that scripts can call it. + */ + public function startCountdown():Void + { + // If Countdown.performCountdown returns false, then the countdown was canceled by a script. + var result:Bool = Countdown.performCountdown(currentStageId.startsWith('school')); + if (!result) return; + + isInCutscene = false; + camCutscene.visible = false; + camHUD.visible = true; + } + + /** + * Displays a dialogue cutscene with the given ID. + * This is used by song scripts to display dialogue. + */ + public function startConversation(conversationId:String):Void + { + isInCutscene = true; + + currentConversation = ConversationDataParser.fetchConversation(conversationId); + if (currentConversation == null) return; + + currentConversation.completeCallback = onConversationComplete; + currentConversation.cameras = [camCutscene]; + currentConversation.zIndex = 1000; + add(currentConversation); + refresh(); + + var event:ScriptEvent = new ScriptEvent(ScriptEvent.CREATE, false); + ScriptEventDispatcher.callEvent(currentConversation, event); + } + + /** + * Handler function called when a conversation ends. + */ + function onConversationComplete():Void + { + isInCutscene = true; + remove(currentConversation); + currentConversation = null; + + if (startingSong && !isInCountdown) + { + startCountdown(); + } + } + + /** + * Starts playing the song after the countdown has completed. + */ + function startSong():Void + { + dispatchEvent(new ScriptEvent(ScriptEvent.SONG_START)); + + startingSong = false; + + if (!isGamePaused && currentChart != null) + { + currentChart.playInst(1.0, false); + } + + FlxG.sound.music.onComplete = endSong; + trace('Playing vocals...'); + add(vocals); + vocals.play(); + + #if discord_rpc + // Updating Discord Rich Presence (with Time Left) + DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC, true, currentSongLengthMs); + #end + } + + /** + * Resyncronize the vocal tracks if they have become offset from the instrumental. + */ + function resyncVocals():Void + { + if (_exiting || vocals == null) return; + + vocals.pause(); + + FlxG.sound.music.play(); + Conductor.update(); + + vocals.time = FlxG.sound.music.time; + vocals.play(); + } + + /** + * Updates the position and contents of the score display. + */ + function updateScoreText():Void + { + // TODO: Add functionality for modules to update the score text. + scoreText.text = 'Score:' + songScore; + } + + /** + * Updates the values of the health bar. + */ + function updateHealthBar():Void + { + healthLerp = FlxMath.lerp(healthLerp, health, 0.15); + } + + /** + * Callback executed when one of the note keys is pressed. + */ + function onKeyPress(event:PreciseInputEvent):Void + { + // Do the minimal possible work here. + inputPressQueue.push(event); + } + + /** + * Callback executed when one of the note keys is released. + */ + function onKeyRelease(event:PreciseInputEvent):Void + { + // Do the minimal possible work here. + inputReleaseQueue.push(event); + } + + /** + * Handles opponent note hits and player note misses. + */ + function processNotes():Void + { + // Process notes on the opponent's side. + for (note in opponentStrumline.notes.members) + { + if (note == null) continue; + + var hitWindowStart = note.strumTime - Conductor.HIT_WINDOW_MS; + var hitWindowCenter = note.strumTime; + var hitWindowEnd = note.strumTime + Conductor.HIT_WINDOW_MS; + + if (Conductor.songPosition > hitWindowEnd) + { + note.tooEarly = false; + note.mayHit = false; + note.tooLate = true; + } + else if (Conductor.songPosition > hitWindowCenter) + { + // Call an event to allow canceling the note hit. + // NOTE: This is what handles the character animations! + var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, note, 0, true); + dispatchEvent(event); + + // Calling event.cancelEvent() skips all the other logic! Neat! + if (event.eventCanceled) continue; + + // Command the opponent to hit the note on time. + // NOTE: This is what handles the strumline and cleaning up the note itself! + opponentStrumline.hitNote(note); + + // scoreNote(); + } + else if (Conductor.songPosition > hitWindowStart) + { + note.tooEarly = false; + note.mayHit = true; + note.tooLate = false; + } + else + { + note.tooEarly = true; + note.mayHit = false; + note.tooLate = false; + } + } + + // Process notes on the player's side. + for (note in playerStrumline.notes.members) + { + if (note == null || note.hasBeenHit) continue; + + // If this is true, the note is already properly off the screen. + if (note.hasMissed) + { + // Call an event to allow canceling the note miss. + // NOTE: This is what handles the character animations! + var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_MISS, note, 0, true); + dispatchEvent(event); + + // Calling event.cancelEvent() skips all the other logic! Neat! + if (event.eventCanceled) continue; + + // Judge the miss. + // NOTE: This is what handles the scoring. + onNoteMiss(note); + + // Kill the note. + // NOTE: This is what handles recycling the note graphic. + playerStrumline.killNote(note); + } + } + } + + /** + * Spitting out the input for ravy 🙇♂️!! + */ + var inputSpitter:Array<ScoreInput> = []; + + /** + * PreciseInputEvents are put into a queue between update() calls, + * and then processed here. + */ + function processInputQueue():Void + { + if (inputPressQueue.length + inputReleaseQueue.length == 0) return; + + // Ignore inputs during cutscenes. + if (isInCutscene || disableKeys) + { + inputPressQueue = []; + inputReleaseQueue = []; + return; + } + + // Generate a list of notes within range. + var notesInRange:Array<NoteSprite> = playerStrumline.getNotesInRange(Conductor.songPosition, Conductor.HIT_WINDOW_MS); + + // If there are notes in range, pressing a key will cause a ghost miss. + // var canMiss:Bool = notesInRange.length > 0; + var canMiss:Bool = true; // Forced to true for consistency with other input systems. + + var notesByDirection:Array<Array<NoteSprite>> = [[], [], [], []]; + + for (note in notesInRange) + notesByDirection[note.direction].push(note); + + while (inputPressQueue.length > 0) + { + var input:PreciseInputEvent = inputPressQueue.shift(); + + var notesInDirection:Array<NoteSprite> = notesByDirection[input.noteDirection]; + + if (canMiss && notesInDirection.length == 0) + { + // Pressed a wrong key with notes in range. + // Perform a ghost miss. + ghostNoteMiss(input.noteDirection, notesInRange.length > 0); + + // Play the strumline animation. + playerStrumline.playPress(input.noteDirection); + } + else if (notesInDirection.length > 0) + { + // Choose the first note, deprioritizing low priority notes. + var targetNote:Null<NoteSprite> = notesInDirection.find((note) -> !note.lowPriority); + if (targetNote == null) targetNote = notesInDirection[0]; + if (targetNote == null) continue; + + // Judge and hit the note. + goodNoteHit(targetNote, input); + + targetNote.visible = false; + targetNote.kill(); + + // Play the strumline animation. + playerStrumline.playConfirm(input.noteDirection); + } + else + { + // Play the strumline animation. + playerStrumline.playPress(input.noteDirection); + } + } + + while (inputReleaseQueue.length > 0) + { + var input:PreciseInputEvent = inputReleaseQueue.shift(); + + // Play the strumline animation. + playerStrumline.playStatic(input.noteDirection); + } + } + + /** + * Handle player inputs. + */ + function keyShit(test:Bool):Void + { + // control arrays, order L D R U + var holdArray:Array<Bool> = [controls.NOTE_LEFT, controls.NOTE_DOWN, controls.NOTE_UP, controls.NOTE_RIGHT]; + var pressArray:Array<Bool> = [ + controls.NOTE_LEFT_P, + controls.NOTE_DOWN_P, + controls.NOTE_UP_P, + controls.NOTE_RIGHT_P + ]; + var releaseArray:Array<Bool> = [ + controls.NOTE_LEFT_R, + controls.NOTE_DOWN_R, + controls.NOTE_UP_R, + controls.NOTE_RIGHT_R + ]; + + // if (pressArray.contains(true)) + // { + // var lol:Array<Int> = cast pressArray; + // inputSpitter.push(Std.int(Conductor.songPosition) + ' ' + lol.join(' ')); + // } + + // HOLDS, check for sustain notes + if (holdArray.contains(true) && generatedMusic) + { + /* + activeNotes.forEachAlive(function(daNote:Note) { + if (daNote.isSustainNote && daNote.canBeHit && daNote.mustPress && holdArray[daNote.data.noteData]) goodNoteHit(daNote); + }); + */ + } + + // PRESSES, check for note hits + if (pressArray.contains(true) && generatedMusic) + { + Haptic.vibrate(100, 100); + + if (currentStage != null && currentStage.getBoyfriend() != null) + { + currentStage.getBoyfriend().holdTimer = 0; + } + + var possibleNotes:Array<NoteSprite> = []; // notes that can be hit + var directionList:Array<Int> = []; // directions that can be hit + var dumbNotes:Array<NoteSprite> = []; // notes to kill later + + /* + activeNotes.forEachAlive(function(daNote:Note) { + if (daNote.canBeHit && daNote.mustPress && !daNote.tooLate && !daNote.hasBeenHit) + { + if (directionList.contains(daNote.data.noteData)) + { + for (coolNote in possibleNotes) + { + if (coolNote.data.noteData == daNote.data.noteData && Math.abs(daNote.data.strumTime - coolNote.data.strumTime) < 10) + { // if it's the same note twice at < 10ms distance, just delete it + // EXCEPT u cant delete it in this loop cuz it fucks with the collection lol + dumbNotes.push(daNote); + break; + } + else if (coolNote.data.noteData == daNote.data.noteData && daNote.data.strumTime < coolNote.data.strumTime) + { // if daNote is earlier than existing note (coolNote), replace + possibleNotes.remove(coolNote); + possibleNotes.push(daNote); + break; + } + } + } + else + { + possibleNotes.push(daNote); + directionList.push(daNote.data.noteData); + } + } + }); + */ + + for (note in dumbNotes) + { + FlxG.log.add('killing dumb ass note at ' + note.noteData.time); + note.kill(); + // activeNotes.remove(note, true); + note.destroy(); + } + + possibleNotes.sort((a, b) -> Std.int(a.noteData.time - b.noteData.time)); + + if (perfectMode) + { + goodNoteHit(possibleNotes[0], null); + } + else if (possibleNotes.length > 0) + { + for (shit in 0...pressArray.length) + { // if a direction is hit that shouldn't be + if (pressArray[shit] && !directionList.contains(shit)) ghostNoteMiss(shit); + } + for (coolNote in possibleNotes) + { + if (pressArray[coolNote.noteData.getDirection()]) goodNoteHit(coolNote, null); + } + } + else + { + // HNGGG I really want to add an option for ghost tapping + // L + ratio + for (shit in 0...pressArray.length) + if (pressArray[shit]) ghostNoteMiss(shit, false); + } + } + + if (currentStage == null) return; + + for (keyId => isPressed in pressArray) + { + if (playerStrumline == null) continue; + + var dir:NoteDirection = Strumline.DIRECTIONS[keyId]; + + if (isPressed && !playerStrumline.isConfirm(dir)) playerStrumline.playPress(dir); + if (!holdArray[keyId]) playerStrumline.playStatic(dir); + } + } + + function goodNoteHit(note:NoteSprite, input:PreciseInputEvent):Void + { + if (!note.hasBeenHit) + { + var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, note, Highscore.tallies.combo + 1, true); + dispatchEvent(event); + + // Calling event.cancelEvent() skips all the other logic! Neat! + if (event.eventCanceled) return; + + if (!note.isSustainNote) + { + Highscore.tallies.combo++; + Highscore.tallies.totalNotesHit++; + + if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo; + + popUpScore(note, input); + } + + playerStrumline.playConfirm(note.noteData.getDirection()); + + note.hasBeenHit = true; + vocals.playerVolume = 1; + + if (!note.isSustainNote) + { + note.kill(); + // activeNotes.remove(note, true); + note.destroy(); + } + } + } + + /** + * Called when a note leaves the screen and is considered missed by the player. + * @param note + */ + function onNoteMiss(note:NoteSprite):Void + { + // a MISS is when you let a note scroll past you!! + Highscore.tallies.missed++; + + var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_MISS, note, Highscore.tallies.combo, true); + dispatchEvent(event); + // Calling event.cancelEvent() skips all the other logic! Neat! + if (event.eventCanceled) return; + + health -= 0.0775; + + if (!isPracticeMode) + { + songScore -= 10; + + // messy copy paste rn lol + var pressArray:Array<Bool> = [ + controls.NOTE_LEFT_P, + controls.NOTE_DOWN_P, + controls.NOTE_UP_P, + controls.NOTE_RIGHT_P + ]; + + var indices:Array<Int> = []; + for (i in 0...pressArray.length) + { + if (pressArray[i]) indices.push(i); + } + if (indices.length > 0) + { + for (i in 0...indices.length) + { + inputSpitter.push( + { + t: Std.int(Conductor.songPosition), + d: indices[i], + l: 20 + }); + } + } + else + { + inputSpitter.push( + { + t: Std.int(Conductor.songPosition), + d: -1, + l: 20 + }); + } + } + vocals.playerVolume = 0; + + if (Highscore.tallies.combo != 0) + { + Highscore.tallies.combo = comboPopUps.displayCombo(0); + } + + if (event.playSound) + { + vocals.playerVolume = 0; + FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2)); + } + + note.active = false; + note.visible = false; + + note.kill(); + // activeNotes.remove(note, true); + note.destroy(); + } + + /** + * Called when a player presses a key with no note present. + * Scripts can modify the amount of health/score lost, whether player animations or sounds are used, + * or even cancel the event entirely. + * + * @param direction + * @param hasPossibleNotes + */ + function ghostNoteMiss(direction:NoteDirection, hasPossibleNotes:Bool = true):Void + { + var event:GhostMissNoteScriptEvent = new GhostMissNoteScriptEvent(direction, // Direction missed in. + hasPossibleNotes, // Whether there was a note you could have hit. + - 0.035 * 2, // How much health to add (negative). + - 10 // Amount of score to add (negative). + ); + dispatchEvent(event); + + // Calling event.cancelEvent() skips animations and penalties. Neat! + if (event.eventCanceled) return; + + health += event.healthChange; + + if (!isPracticeMode) + { + songScore += event.scoreChange; + + var pressArray:Array<Bool> = [ + controls.NOTE_LEFT_P, + controls.NOTE_DOWN_P, + controls.NOTE_UP_P, + controls.NOTE_RIGHT_P + ]; + + var indices:Array<Int> = []; + for (i in 0...pressArray.length) + { + if (pressArray[i]) indices.push(i); + } + for (i in 0...indices.length) + { + inputSpitter.push( + { + t: Std.int(Conductor.songPosition), + d: indices[i], + l: 20 + }); + } + } + + if (event.playSound) + { + vocals.playerVolume = 0; + FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2)); + } + } + + /** + * Debug keys. Disabled while in cutscenes. + */ + function debugKeyShit():Void + { + #if !debug + perfectMode = false; + #else + if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible; + #end + + if (FlxG.keys.justPressed.F4) FlxG.switchState(new MainMenuState()); + + if (FlxG.keys.justPressed.F5) debug_refreshModules(); + + // Press U to open stage ditor. + if (FlxG.keys.justPressed.U) + { + // hack for HaxeUI generation, doesn't work unless persistentUpdate is false at state creation!! + disableKeys = true; + persistentUpdate = false; + openSubState(new StageOffsetSubState()); + } + + #if debug + // 1: End the song immediately. + if (FlxG.keys.justPressed.ONE) endSong(); + + // 2: Gain 10% health. + if (FlxG.keys.justPressed.TWO) health += 0.1 * 2.0; + + // 3: Lose 5% health. + if (FlxG.keys.justPressed.THREE) health -= 0.05 * 2.0; + #end + + // 7: Move to the charter. + if (FlxG.keys.justPressed.SEVEN) + { + lime.app.Application.current.window.alert("Press ~ on the main menu to get to the editor", 'LOL'); + } + + // 8: Move to the offset editor. + if (FlxG.keys.justPressed.EIGHT) FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState()); + + // 9: Toggle the old icon. + if (FlxG.keys.justPressed.NINE) iconP1.toggleOldIcon(); + + #if debug + // PAGEUP: Skip forward one section. + // SHIFT+PAGEUP: Skip forward ten sections. + if (FlxG.keys.justPressed.PAGEUP) changeSection(FlxG.keys.pressed.SHIFT ? 10 : 1); + // PAGEDOWN: Skip backward one section. Doesn't replace notes. + // SHIFT+PAGEDOWN: Skip backward ten sections. + if (FlxG.keys.justPressed.PAGEDOWN) changeSection(FlxG.keys.pressed.SHIFT ? -10 : -1); + #end + + if (FlxG.keys.justPressed.B) trace(inputSpitter.join('\n')); + } + + /** + * Handles health, score, and rating popups when a note is hit. + */ + function popUpScore(daNote:NoteSprite, input:PreciseInputEvent):Void + { + vocals.playerVolume = 1; + + // Calculate the input latency (do this as late as possible). + var inputLatencyMs:Float = haxe.Int64.toInt(PreciseInputManager.getCurrentTimestamp() - input.timestamp) / 1000.0 / 1000.0; + trace('Input: ${daNote.noteData.getDirectionName()} pressed ${inputLatencyMs}ms ago!'); + + // Get the offset and compensate for input latency. + // Round inward (trim remainder) for consistency. + var noteDiff:Int = Std.int(Conductor.songPosition - daNote.noteData.time - inputLatencyMs); + + var score = Scoring.scoreNote(noteDiff, PBOT1); + var daRating = Scoring.judgeNote(noteDiff, PBOT1); + + var isSick:Bool = false; + var healthMulti:Float = 0; + + switch (daRating) + { + case 'killer': + Highscore.tallies.killer += 1; + healthMulti = 0.033; + case 'sick': + Highscore.tallies.sick += 1; + healthMulti = 0.033; + case 'good': + Highscore.tallies.good += 1; + healthMulti = 0.033 * 0.78; + case 'bad': + Highscore.tallies.bad += 1; + healthMulti = 0.033 * 0.2; + case 'shit': + Highscore.tallies.shit += 1; + healthMulti = 0; + case 'miss': + Highscore.tallies.missed += 1; + healthMulti = 0; + } + + health += healthMulti; + if (daRating == "sick" || daRating == "killer") + { + playerStrumline.playNoteSplash(daNote.noteData.getDirection()); + } + // Only add the score if you're not on practice mode + if (!isPracticeMode) + { + songScore += score; + + // TODO: Input splitter uses old input system, make it pull from the precise input queue directly. + var pressArray:Array<Bool> = [ + controls.NOTE_LEFT_P, + controls.NOTE_DOWN_P, + controls.NOTE_UP_P, + controls.NOTE_RIGHT_P + ]; + + var indices:Array<Int> = []; + for (i in 0...pressArray.length) + { + if (pressArray[i]) indices.push(i); + } + if (indices.length > 0) + { + for (i in 0...indices.length) + { + inputSpitter.push( + { + t: Std.int(Conductor.songPosition), + d: indices[i], + l: 20 + }); + } + } + else + { + inputSpitter.push( + { + t: Std.int(Conductor.songPosition), + d: -1, + l: 20 + }); + } + } + comboPopUps.displayRating(daRating); + if (Highscore.tallies.combo >= 10 || Highscore.tallies.combo == 0) comboPopUps.displayCombo(Highscore.tallies.combo); + } + + /** + * Handle keyboard inputs during cutscenes. + * This includes advancing conversations and skipping videos. + * @param elapsed Time elapsed since last game update. + */ function handleCutsceneKeys(elapsed:Float):Void { if (currentConversation != null) @@ -1470,7 +2264,12 @@ class PlayState extends MusicBeatState } } - public function trySkipVideoCutscene(elapsed:Float):Void + /** + * Handle logic for the skip timer. + * If the skip button is being held, pass the amount of time elapsed since last game update. + * If the skip button has been released, pass a negative number. + */ + function trySkipVideoCutscene(elapsed:Float):Void { if (skipTimer == null || skipTimer.animation == null) return; @@ -1492,81 +2291,9 @@ class PlayState extends MusicBeatState } } - function applyClipRect(daNote:Note):Void - { - // clipRect is applied to graphic itself so use frame Heights - var swagRect:FlxRect = new FlxRect(0, 0, daNote.frameWidth, daNote.frameHeight); - var strumLineMid:Float = playerStrumline.y + Note.swagWidth / 2; - - if (PreferencesMenu.getPref('downscroll')) - { - swagRect.height = (strumLineMid - daNote.y) / daNote.scale.y; - swagRect.y = daNote.frameHeight - swagRect.height; - } - else - { - swagRect.y = (strumLineMid - daNote.y) / daNote.scale.y; - swagRect.height -= swagRect.y; - } - - daNote.clipRect = swagRect; - } - - function killCombo():Void - { - // Girlfriend gets sad if you combo break after hitting 5 notes. - if (currentStage != null && currentStage.getGirlfriend() != null) - { - if (Highscore.tallies.combo > 5 && currentStage.getGirlfriend().hasAnimation('sad')) - { - currentStage.getGirlfriend().playAnimation('sad'); - } - } - - if (Highscore.tallies.combo != 0) - { - Highscore.tallies.combo = comboPopUps.displayCombo(0); - } - } - - #if debug /** - * Jumps forward or backward a number of sections in the song. - * Accounts for BPM changes, does not prevent death from skipped notes. - * @param sections The number of sections to jump, negative to go backwards. + * End the song. Handle saving high scores and transitioning to the results screen. */ - function changeSection(sections:Int):Void - { - FlxG.sound.music.pause(); - - FlxG.sound.music.time += sections * Conductor.measureLengthMs; - - Conductor.update(FlxG.sound.music.time); - - /** - * - // TODO: Redo this for the new conductor. - var daBPM:Float = Conductor.bpm; - var daPos:Float = 0; - for (i in 0...(Std.int(Conductor.currentStep / 16 + sec))) - { - var section = .getSong()[i]; - if (section == null) continue; - if (section.changeBPM) - { - daBPM = .getSong()[i].bpm; - } - daPos += 4 * (1000 * 60 / daBPM); - } - Conductor.songPosition = FlxG.sound.music.time = daPos; - Conductor.songPosition += Conductor.offset; - - */ - - resyncVocals(); - } - #end - function endSong():Void { dispatchEvent(new ScriptEvent(ScriptEvent.SONG_END)); @@ -1675,6 +2402,31 @@ class PlayState extends MusicBeatState } } + /** + * Perform necessary cleanup before leaving the PlayState. + */ + function performCleanup():Void + { + if (currentChart != null) + { + // TODO: Uncache the song. + } + + // Remove reference to stage and remove sprites from it to save memory. + if (currentStage != null) + { + remove(currentStage); + currentStage.kill(); + dispatchEvent(new ScriptEvent(ScriptEvent.DESTROY, false)); + currentStage = null; + } + + GameOverSubState.reset(); + + // Clear the static reference to this state. + instance = null; + } + /** * Play the camera zoom animation and move to the results screen. */ @@ -1688,24 +2440,24 @@ class PlayState extends MusicBeatState // If the opponent is GF, zoom in on the opponent. // Else, if there is no GF, zoom in on BF. // Else, zoom in on GF. - var targetDad:Bool = PlayState.instance.currentStage.getDad() != null && PlayState.instance.currentStage.getDad().characterId == 'gf'; - var targetBF:Bool = PlayState.instance.currentStage.getGirlfriend() == null && !targetDad; + var targetDad:Bool = currentStage.getDad() != null && currentStage.getDad().characterId == 'gf'; + var targetBF:Bool = currentStage.getGirlfriend() == null && !targetDad; if (targetBF) { - FlxG.camera.follow(PlayState.instance.currentStage.getBoyfriend(), null, 0.05); + FlxG.camera.follow(currentStage.getBoyfriend(), null, 0.05); FlxG.camera.targetOffset.y -= 350; FlxG.camera.targetOffset.x += 20; } else if (targetDad) { - FlxG.camera.follow(PlayState.instance.currentStage.getDad(), null, 0.05); + FlxG.camera.follow(currentStage.getDad(), null, 0.05); FlxG.camera.targetOffset.y -= 350; FlxG.camera.targetOffset.x += 20; } else { - FlxG.camera.follow(PlayState.instance.currentStage.getGirlfriend(), null, 0.05); + FlxG.camera.follow(currentStage.getGirlfriend(), null, 0.05); FlxG.camera.targetOffset.y -= 350; FlxG.camera.targetOffset.x += 20; } @@ -1743,748 +2495,13 @@ class PlayState extends MusicBeatState }); } - // gives score and pops up rating - function popUpScore(strumtime:Float, daNote:Note):Void - { - var noteDiff:Float = Math.abs(strumtime - Conductor.songPosition); - // boyfriend.playAnimation('hey'); - vocals.playerVolume = 1; - - var isSick:Bool = false; - var score = Scoring.scoreNote(noteDiff, PBOT1); - var daRating = Scoring.judgeNote(noteDiff, PBOT1); - var healthMulti:Float = daNote.lowStakes ? 0.002 : 0.033; - - if (noteDiff > Note.HIT_WINDOW * Note.BAD_THRESHOLD) - { - healthMulti *= 0; // no health on shit note - daRating = 'shit'; - Highscore.tallies.shit += 1; - // score = 50; - } - else if (noteDiff > Note.HIT_WINDOW * Note.GOOD_THRESHOLD) - { - healthMulti *= 0.2; - daRating = 'bad'; - Highscore.tallies.bad += 1; - } - else if (noteDiff > Note.HIT_WINDOW * Note.SICK_THRESHOLD) - { - healthMulti *= 0.78; - daRating = 'good'; - Highscore.tallies.good += 1; - // score = 200; - } - else - { - isSick = true; - } - - health += healthMulti; - if (isSick) - { - Highscore.tallies.sick += 1; - var noteSplash:NoteSplash = grpNoteSplashes.recycle(NoteSplash); - noteSplash.setupNoteSplash(daNote.x, daNote.y, daNote.data.noteData); - // new NoteSplash(daNote.x, daNote.y, daNote.noteData); - grpNoteSplashes.add(noteSplash); - } - // Only add the score if you're not on practice mode - if (!isPracticeMode) - { - songScore += score; - - var pressArray:Array<Bool> = [ - controls.NOTE_LEFT_P, - controls.NOTE_DOWN_P, - controls.NOTE_UP_P, - controls.NOTE_RIGHT_P - ]; - - var indices:Array<Int> = []; - for (i in 0...pressArray.length) - { - if (pressArray[i]) indices.push(i); - } - if (indices.length > 0) - { - for (i in 0...indices.length) - { - inputSpitter.push( - { - t: Std.int(Conductor.songPosition), - d: indices[i], - l: 20 - }); - } - } - else - { - inputSpitter.push( - { - t: Std.int(Conductor.songPosition), - d: -1, - l: 20 - }); - } - } - comboPopUps.displayRating(daRating); - if (Highscore.tallies.combo >= 10 || Highscore.tallies.combo == 0) comboPopUps.displayCombo(Highscore.tallies.combo); - } - /** - * Spitting out the input for ravy 🙇♂️!! + * Pauses music and vocals easily. */ - var inputSpitter:Array<ScoreInput> = []; - - public function keyShit(test:Bool):Void + public function pauseMusic():Void { - if (PlayState.instance == null) return; - - // control arrays, order L D R U - var holdArray:Array<Bool> = [controls.NOTE_LEFT, controls.NOTE_DOWN, controls.NOTE_UP, controls.NOTE_RIGHT]; - var pressArray:Array<Bool> = [ - controls.NOTE_LEFT_P, - controls.NOTE_DOWN_P, - controls.NOTE_UP_P, - controls.NOTE_RIGHT_P - ]; - var releaseArray:Array<Bool> = [ - controls.NOTE_LEFT_R, - controls.NOTE_DOWN_R, - controls.NOTE_UP_R, - controls.NOTE_RIGHT_R - ]; - - // if (pressArray.contains(true)) - // { - // var lol:Array<Int> = cast pressArray; - // inputSpitter.push(Std.int(Conductor.songPosition) + ' ' + lol.join(' ')); - // } - - // HOLDS, check for sustain notes - if (holdArray.contains(true) && PlayState.instance.generatedMusic) - { - PlayState.instance.activeNotes.forEachAlive(function(daNote:Note) { - if (daNote.isSustainNote && daNote.canBeHit && daNote.mustPress && holdArray[daNote.data.noteData]) PlayState.instance.goodNoteHit(daNote); - }); - } - - // PRESSES, check for note hits - if (pressArray.contains(true) && PlayState.instance.generatedMusic) - { - Haptic.vibrate(100, 100); - - if (currentStage != null && currentStage.getBoyfriend() != null) - { - currentStage.getBoyfriend().holdTimer = 0; - } - - var possibleNotes:Array<Note> = []; // notes that can be hit - var directionList:Array<Int> = []; // directions that can be hit - var dumbNotes:Array<Note> = []; // notes to kill later - - PlayState.instance.activeNotes.forEachAlive(function(daNote:Note) { - if (daNote.canBeHit && daNote.mustPress && !daNote.tooLate && !daNote.wasGoodHit) - { - if (directionList.contains(daNote.data.noteData)) - { - for (coolNote in possibleNotes) - { - if (coolNote.data.noteData == daNote.data.noteData && Math.abs(daNote.data.strumTime - coolNote.data.strumTime) < 10) - { // if it's the same note twice at < 10ms distance, just delete it - // EXCEPT u cant delete it in this loop cuz it fucks with the collection lol - dumbNotes.push(daNote); - break; - } - else if (coolNote.data.noteData == daNote.data.noteData && daNote.data.strumTime < coolNote.data.strumTime) - { // if daNote is earlier than existing note (coolNote), replace - possibleNotes.remove(coolNote); - possibleNotes.push(daNote); - break; - } - } - } - else - { - possibleNotes.push(daNote); - directionList.push(daNote.data.noteData); - } - } - }); - - for (note in dumbNotes) - { - FlxG.log.add('killing dumb ass note at ' + note.data.strumTime); - note.kill(); - PlayState.instance.activeNotes.remove(note, true); - note.destroy(); - } - - possibleNotes.sort((a, b) -> Std.int(a.data.strumTime - b.data.strumTime)); - - if (PlayState.instance.perfectMode) PlayState.instance.goodNoteHit(possibleNotes[0]); - else if (possibleNotes.length > 0) - { - for (shit in 0...pressArray.length) - { // if a direction is hit that shouldn't be - if (pressArray[shit] && !directionList.contains(shit)) PlayState.instance.ghostNoteMiss(shit); - } - for (coolNote in possibleNotes) - { - if (pressArray[coolNote.data.noteData]) PlayState.instance.goodNoteHit(coolNote); - } - } - else - { - // HNGGG I really want to add an option for ghost tapping - // L + ratio - for (shit in 0...pressArray.length) - if (pressArray[shit]) PlayState.instance.ghostNoteMiss(shit, false); - } - } - - if (PlayState.instance == null || PlayState.instance.currentStage == null) return; - - for (keyId => isPressed in pressArray) - { - if (playerStrumline == null) continue; - var arrow:StrumlineArrow = PlayState.instance.playerStrumline.getArrow(keyId); - - if (isPressed && arrow.animation.curAnim.name != 'confirm') - { - arrow.playAnimation('pressed'); - } - if (!holdArray[keyId]) - { - arrow.playAnimation('static'); - } - } - } - - /** - * Debug keys. Disabled while in cutscenes. - */ - public function debugKeyShit():Void - { - #if !debug - perfectMode = false; - #else - if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible; - #end - - if (FlxG.keys.justPressed.F4) FlxG.switchState(new MainMenuState()); - - if (FlxG.keys.justPressed.F5) debug_refreshModules(); - - // Press U to open stage ditor. - if (FlxG.keys.justPressed.U) - { - // hack for HaxeUI generation, doesn't work unless persistentUpdate is false at state creation!! - disableKeys = true; - persistentUpdate = false; - openSubState(new StageOffsetSubState()); - } - - #if debug - // 1: End the song immediately. - if (FlxG.keys.justPressed.ONE) endSong(); - - // 2: Gain 10% health. - if (FlxG.keys.justPressed.TWO) health += 0.1 * 2.0; - - // 3: Lose 5% health. - if (FlxG.keys.justPressed.THREE) health -= 0.05 * 2.0; - #end - - // 7: Move to the charter. - if (FlxG.keys.justPressed.SEVEN) - { - lime.app.Application.current.window.alert("Press ~ on the main menu to get to the editor", 'LOL'); - } - - // 8: Move to the offset editor. - if (FlxG.keys.justPressed.EIGHT) FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState()); - - // 9: Toggle the old icon. - if (FlxG.keys.justPressed.NINE) iconP1.toggleOldIcon(); - - #if debug - // PAGEUP: Skip forward one section. - // SHIFT+PAGEUP: Skip forward ten sections. - if (FlxG.keys.justPressed.PAGEUP) changeSection(FlxG.keys.pressed.SHIFT ? 10 : 1); - // PAGEDOWN: Skip backward one section. Doesn't replace notes. - // SHIFT+PAGEDOWN: Skip backward ten sections. - if (FlxG.keys.justPressed.PAGEDOWN) changeSection(FlxG.keys.pressed.SHIFT ? -10 : -1); - #end - - if (FlxG.keys.justPressed.B) trace(inputSpitter.join('\n')); - } - - /** - * Called when a player presses a key with no note present. - * Scripts can modify the amount of health/score lost, whether player animations or sounds are used, - * or even cancel the event entirely. - * - * @param direction - * @param hasPossibleNotes - */ - function ghostNoteMiss(direction:funkin.noteStuff.NoteBasic.NoteType = 1, hasPossibleNotes:Bool = true):Void - { - var event:GhostMissNoteScriptEvent = new GhostMissNoteScriptEvent(direction, // Direction missed in. - hasPossibleNotes, // Whether there was a note you could have hit. - - 0.035 * 2, // How much health to add (negative). - - 10 // Amount of score to add (negative). - ); - dispatchEvent(event); - - // Calling event.cancelEvent() skips animations and penalties. Neat! - if (event.eventCanceled) return; - - health += event.healthChange; - - if (!isPracticeMode) - { - songScore += event.scoreChange; - - var pressArray:Array<Bool> = [ - controls.NOTE_LEFT_P, - controls.NOTE_DOWN_P, - controls.NOTE_UP_P, - controls.NOTE_RIGHT_P - ]; - - var indices:Array<Int> = []; - for (i in 0...pressArray.length) - { - if (pressArray[i]) indices.push(i); - } - for (i in 0...indices.length) - { - inputSpitter.push( - { - t: Std.int(Conductor.songPosition), - d: indices[i], - l: 20 - }); - } - } - - if (event.playSound) - { - vocals.playerVolume = 0; - FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2)); - } - } - - function noteMiss(note:Note):Void - { - // a MISS is when you let a note scroll past you!! - Highscore.tallies.missed++; - - var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_MISS, note, Highscore.tallies.combo, true); - dispatchEvent(event); - // Calling event.cancelEvent() skips all the other logic! Neat! - if (event.eventCanceled) return; - - health -= 0.0775; - - if (!isPracticeMode) - { - songScore -= 10; - - // messy copy paste rn lol - var pressArray:Array<Bool> = [ - controls.NOTE_LEFT_P, - controls.NOTE_DOWN_P, - controls.NOTE_UP_P, - controls.NOTE_RIGHT_P - ]; - - var indices:Array<Int> = []; - for (i in 0...pressArray.length) - { - if (pressArray[i]) indices.push(i); - } - if (indices.length > 0) - { - for (i in 0...indices.length) - { - inputSpitter.push( - { - t: Std.int(Conductor.songPosition), - d: indices[i], - l: 20 - }); - } - } - else - { - inputSpitter.push( - { - t: Std.int(Conductor.songPosition), - d: -1, - l: 20 - }); - } - } - vocals.playerVolume = 0; - - if (Highscore.tallies.combo != 0) - { - Highscore.tallies.combo = comboPopUps.displayCombo(0); - } - - if (event.playSound) - { - vocals.playerVolume = 0; - FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2)); - } - - note.active = false; - note.visible = false; - - note.kill(); - activeNotes.remove(note, true); - note.destroy(); - } - - function goodNoteHit(note:Note):Void - { - if (!note.wasGoodHit) - { - var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, note, Highscore.tallies.combo + 1, true); - dispatchEvent(event); - - // Calling event.cancelEvent() skips all the other logic! Neat! - if (event.eventCanceled) return; - - if (!note.isSustainNote) - { - Highscore.tallies.combo++; - Highscore.tallies.totalNotesHit++; - - if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo; - - popUpScore(note.data.strumTime, note); - } - - playerStrumline.getArrow(note.data.noteData).playAnimation('confirm', true); - - note.wasGoodHit = true; - vocals.playerVolume = 1; - - if (!note.isSustainNote) - { - note.kill(); - activeNotes.remove(note, true); - note.destroy(); - } - } - } - - override function stepHit():Bool - { - // super.stepHit() returns false if a module cancelled the event. - if (!super.stepHit()) return false; - - if (Math.abs(FlxG.sound.music.time - (Conductor.songPosition - Conductor.offset)) > 200 - || Math.abs(vocals.checkSyncError(Conductor.songPosition - Conductor.offset)) > 200) - { - trace("VOCALS NEED RESYNC"); - if (vocals != null) trace(vocals.checkSyncError(Conductor.songPosition - Conductor.offset)); - trace(FlxG.sound.music.time - (Conductor.songPosition - Conductor.offset)); - resyncVocals(); - } - - if (iconP1 != null) iconP1.onStepHit(Std.int(Conductor.currentStep)); - if (iconP2 != null) iconP2.onStepHit(Std.int(Conductor.currentStep)); - - return true; - } - - override function beatHit():Bool - { - // super.beatHit() returns false if a module cancelled the event. - if (!super.beatHit()) return false; - - if (generatedMusic) - { - // TODO: Sort more efficiently, or less often, to improve performance. - activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING); - } - - // Only zoom camera if we are zoomed by less than 35%. - if (FlxG.camera.zoom < (1.35 * defaultCameraZoom) && cameraZoomRate > 0 && Conductor.currentBeat % cameraZoomRate == 0) - { - // Zoom camera in (1.5%) - FlxG.camera.zoom += cameraZoomIntensity * defaultCameraZoom; - // Hud zooms double (3%) - camHUD.zoom += hudCameraZoomIntensity * defaultHUDCameraZoom; - } - // trace('Not bopping camera: ${FlxG.camera.zoom} < ${(1.35 * defaultCameraZoom)} && ${cameraZoomRate} > 0 && ${Conductor.currentBeat} % ${cameraZoomRate} == ${Conductor.currentBeat % cameraZoomRate}}'); - - // That combo milestones that got spoiled that one time. - // Comes with NEAT visual and audio effects. - - // bruh this var is bonkers i thot it was a function lmfaooo - - // Break up into individual lines to aid debugging. - - var shouldShowComboText:Bool = false; - // TODO: Re-enable combo text (how to do this without sections?). - // if (currentSong != null) - // { - // shouldShowComboText = (Conductor.currentBeat % 8 == 7); - // var daSection = .getSong()[Std.int(Conductor.currentBeat / 16)]; - // shouldShowComboText = shouldShowComboText && (daSection != null && daSection.mustHitSection); - // shouldShowComboText = shouldShowComboText && (Highscore.tallies.combo > 5); - // - // var daNextSection = .getSong()[Std.int(Conductor.currentBeat / 16) + 1]; - // var isEndOfSong = .getSong().length < Std.int(Conductor.currentBeat / 16); - // shouldShowComboText = shouldShowComboText && (isEndOfSong || (daNextSection != null && !daNextSection.mustHitSection)); - // } - - if (shouldShowComboText) - { - var animShit:ComboMilestone = new ComboMilestone(-100, 300, Highscore.tallies.combo); - animShit.scrollFactor.set(0.6, 0.6); - animShit.cameras = [camHUD]; - add(animShit); - - var frameShit:Float = (1 / 24) * 2; // equals 2 frames in the animation - - new FlxTimer().start(((Conductor.beatLengthMs / 1000) * 1.25) - frameShit, function(tmr) { - animShit.forceFinish(); - }); - } - - // Make the characters dance on the beat - danceOnBeat(); - - return true; - } - - /** - * Handles characters dancing to the beat of the current song. - * - * TODO: Move some of this logic into `Bopper.hx` - */ - public function danceOnBeat():Void - { - if (currentStage == null) return; - - // TODO: Add HEY! song events to Tutorial. - if (Conductor.currentBeat % 16 == 15 - && currentStage.getDad().characterId == 'gf' - && Conductor.currentBeat > 16 - && Conductor.currentBeat < 48) - { - currentStage.getBoyfriend().playAnimation('hey', true); - currentStage.getDad().playAnimation('cheer', true); - } - } - - /** - * Constructs the strumlines for each player. - */ - function buildStrumlines():Void - { - var strumlineStyle:StrumlineStyle = NORMAL; - - // TODO: Put this in the chart or something? - switch (currentStageId) - { - case 'school': - strumlineStyle = PIXEL; - case 'schoolEvil': - strumlineStyle = PIXEL; - } - - var strumlineYPos = Strumline.getYPos(); - - playerStrumline = new Strumline(0, strumlineStyle, 4); - playerStrumline.x = 50 + FlxG.width / 2; - playerStrumline.y = strumlineYPos; - // Set the z-index so they don't appear in front of notes. - playerStrumline.zIndex = 100; - add(playerStrumline); - playerStrumline.cameras = [camHUD]; - - if (!PlayStatePlaylist.isStoryMode) - { - playerStrumline.fadeInArrows(); - } - - enemyStrumline = new Strumline(1, strumlineStyle, 4); - enemyStrumline.x = 50; - enemyStrumline.y = strumlineYPos; - // Set the z-index so they don't appear in front of notes. - enemyStrumline.zIndex = 100; - add(enemyStrumline); - enemyStrumline.cameras = [camHUD]; - - if (!PlayStatePlaylist.isStoryMode) - { - enemyStrumline.fadeInArrows(); - } - - this.refresh(); - } - - /** - * Function called before opening a new substate. - * @param subState The substate to open. - */ - public override function openSubState(subState:FlxSubState):Void - { - // If there is a substate which requires the game to continue, - // then make this a condition. - var shouldPause = true; - - if (shouldPause) - { - // Pause the music. - if (FlxG.sound.music != null) - { - FlxG.sound.music.pause(); - if (vocals != null) vocals.pause(); - } - - // Pause the countdown. - Countdown.pauseCountdown(); - } - - super.openSubState(subState); - } - - /** - * Function called before closing the current substate. - * @param subState - */ - public override function closeSubState():Void - { - if (isGamePaused) - { - var event:ScriptEvent = new ScriptEvent(ScriptEvent.RESUME, true); - - dispatchEvent(event); - - if (event.eventCanceled) return; - - if (FlxG.sound.music != null && !startingSong && !isInCutscene) resyncVocals(); - - // Resume the countdown. - Countdown.resumeCountdown(); - - #if discord_rpc - if (startTimer.finished) - { - DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC, true, songLength - Conductor.songPosition); - } - else - { - DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC); - } - #end - } - - super.closeSubState(); - } - - /** - * Prepares to start the countdown. - * Ends any running cutscenes, creates the strumlines, and starts the countdown. - */ - public function startCountdown():Void - { - // If Countdown.performCountdown returns false, then the countdown was canceled by a script. - var result:Bool = Countdown.performCountdown(currentStageId.startsWith('school')); - if (!result) return; - - isInCutscene = false; - camCutscene.visible = false; - camHUD.visible = true; - } - - public override function dispatchEvent(event:ScriptEvent):Void - { - // ORDER: Module, Stage, Character, Song, Conversation, Note - // Modules should get the first chance to cancel the event. - - // super.dispatchEvent(event) dispatches event to module scripts. - super.dispatchEvent(event); - - // Dispatch event to stage script. - ScriptEventDispatcher.callEvent(currentStage, event); - - // Dispatch event to character script(s). - if (currentStage != null) currentStage.dispatchToCharacters(event); - - // Dispatch event to song script. - ScriptEventDispatcher.callEvent(currentSong, event); - - // Dispatch event to conversation script. - ScriptEventDispatcher.callEvent(currentConversation, event); - - // TODO: Dispatch event to note scripts - } - - public function startConversation(conversationId:String):Void - { - isInCutscene = true; - - currentConversation = ConversationDataParser.fetchConversation(conversationId); - if (currentConversation == null) return; - - currentConversation.completeCallback = onConversationComplete; - currentConversation.cameras = [camCutscene]; - currentConversation.zIndex = 1000; - add(currentConversation); - refresh(); - - var event:ScriptEvent = new ScriptEvent(ScriptEvent.CREATE, false); - ScriptEventDispatcher.callEvent(currentConversation, event); - } - - function onConversationComplete():Void - { - isInCutscene = true; - remove(currentConversation); - currentConversation = null; - - if (startingSong && !isInCountdown) - { - startCountdown(); - } - } - - override function destroy():Void - { - if (currentConversation != null) - { - remove(currentConversation); - currentConversation.kill(); - } - - super.destroy(); - } - - /** - * Updates the position and contents of the score display. - */ - function updateScoreText():Void - { - // TODO: Add functionality for modules to update the score text. - scoreText.text = 'Score:' + songScore; - } - - /** - * Updates the values of the health bar. - */ - function updateHealthBar():Void - { - healthLerp = FlxMath.lerp(healthLerp, health, 0.15); + FlxG.sound.music.pause(); + vocals.pause(); } /** @@ -2498,44 +2515,41 @@ class PlayState extends MusicBeatState FlxG.camera.focusOn(cameraFollowPoint.getPosition()); } + #if debug /** - * Perform necessary cleanup before leaving the PlayState. + * Jumps forward or backward a number of sections in the song. + * Accounts for BPM changes, does not prevent death from skipped notes. + * @param sections The number of sections to jump, negative to go backwards. */ - function performCleanup():Void + function changeSection(sections:Int):Void { - if (currentChart != null) - { - // TODO: Uncache the song. - } + FlxG.sound.music.pause(); - // Remove reference to stage and remove sprites from it to save memory. - if (currentStage != null) - { - remove(currentStage); - currentStage.kill(); - dispatchEvent(new ScriptEvent(ScriptEvent.DESTROY, false)); - currentStage = null; - } + FlxG.sound.music.time += sections * Conductor.measureLengthMs; - GameOverSubState.reset(); + Conductor.update(FlxG.sound.music.time); - // Clear the static reference to this state. - instance = null; - } - - /** - * This function is called whenever Flixel switches switching to a new FlxState. - * @return Whether to actually switch to the new state. - */ - override function switchTo(nextState:FlxState):Bool - { - var result:Bool = super.switchTo(nextState); - - if (result) - { - performCleanup(); - } - - return result; + /** + * + // TODO: Redo this for the new conductor. + var daBPM:Float = Conductor.bpm; + var daPos:Float = 0; + for (i in 0...(Std.int(Conductor.currentStep / 16 + sec))) + { + var section = .getSong()[i]; + if (section == null) continue; + if (section.changeBPM) + { + daBPM = .getSong()[i].bpm; + } + daPos += 4 * (1000 * 60 / daBPM); + } + Conductor.songPosition = FlxG.sound.music.time = daPos; + Conductor.songPosition += Conductor.offset; + + */ + + resyncVocals(); } + #end } diff --git a/source/funkin/play/Strumline.hx b/source/funkin/play/Strumline.hx deleted file mode 100644 index 4bbcc720a..000000000 --- a/source/funkin/play/Strumline.hx +++ /dev/null @@ -1,253 +0,0 @@ -package funkin.play; - -import flixel.FlxSprite; -import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; -import flixel.math.FlxPoint; -import flixel.tweens.FlxEase; -import flixel.tweens.FlxTween; -import funkin.noteStuff.NoteBasic.NoteColor; -import funkin.noteStuff.NoteBasic.NoteDir; -import funkin.noteStuff.NoteBasic.NoteType; -import funkin.ui.PreferencesMenu; -import funkin.util.Constants; - -/** - * A group controlling the individual notes of the strumline for a given player. - * - * FUN FACT: Setting the X and Y of a FlxSpriteGroup will move all the sprites in the group. - */ -class Strumline extends FlxTypedSpriteGroup<StrumlineArrow> -{ - /** - * The style of the strumline. - * Options are normal and pixel. - */ - var style:StrumlineStyle; - - /** - * The player this strumline belongs to. - * 0 is Player 1, etc. - */ - var playerId:Int; - - /** - * The number of notes in the strumline. - */ - var size:Int; - - public function new(playerId:Int = 0, style:StrumlineStyle = NORMAL, size:Int = 4) - { - super(0); - this.playerId = playerId; - this.style = style; - this.size = size; - - generateStrumline(); - } - - function generateStrumline():Void - { - for (index in 0...size) - { - createStrumlineArrow(index); - } - } - - function createStrumlineArrow(index:Int):Void - { - var arrow:StrumlineArrow = new StrumlineArrow(index, style); - add(arrow); - } - - /** - * Apply a small animation which moves the arrow down and fades it in. - * Only plays at the start of Free Play songs. - * - * Note that modifying the offset of the whole strumline won't have the - * @param arrow The arrow to animate. - * @param index The index of the arrow in the strumline. - */ - function fadeInArrow(arrow:FlxSprite):Void - { - arrow.y -= 10; - arrow.alpha = 0; - FlxTween.tween(arrow, {y: arrow.y + 10, alpha: 1}, 1, {ease: FlxEase.circOut, startDelay: 0.5 + (0.2 * arrow.ID)}); - } - - public function fadeInArrows():Void - { - for (arrow in this.members) - { - fadeInArrow(arrow); - } - } - - function updatePositions() - { - for (arrow in members) - { - arrow.x = Note.swagWidth * arrow.ID; - arrow.x += offset.x; - - arrow.y = 0; - arrow.y += offset.y; - } - } - - /** - * Retrieves the arrow at the given position in the strumline. - * @param index The index to retrieve. - * @return The corresponding FlxSprite. - */ - public inline function getArrow(value:Int):StrumlineArrow - { - // members maintains the order that the arrows were added. - return this.members[value]; - } - - public inline function getArrowByNoteType(value:NoteType):StrumlineArrow - { - return getArrow(value.int); - } - - public inline function getArrowByNoteDir(value:NoteDir):StrumlineArrow - { - return getArrow(value.int); - } - - public inline function getArrowByNoteColor(value:funkin.noteStuff.NoteBasic.NoteColor):StrumlineArrow - { - return getArrow(value.int); - } - - /** - * Get the default Y offset of the strumline. - * @return Int - */ - public static inline function getYPos():Int - { - return PreferencesMenu.getPref('downscroll') ? (FlxG.height - 150) : 50; - } -} - -class StrumlineArrow extends FlxSprite -{ - var style:StrumlineStyle; - - public function new(id:Int, style:StrumlineStyle) - { - super(0, 0); - - this.ID = id; - this.style = style; - - // TODO: Unhardcode this. Maybe use a note style system> - switch (style) - { - case PIXEL: - buildPixelGraphic(); - case NORMAL: - buildNormalGraphic(); - } - - this.updateHitbox(); - scrollFactor.set(0, 0); - animation.play('static'); - } - - public function playAnimation(anim:String, force:Bool = false) - { - animation.play(anim, force); - centerOffsets(); - centerOrigin(); - } - - /** - * Applies the default note style to an arrow. - */ - function buildNormalGraphic():Void - { - this.frames = Paths.getSparrowAtlas('NOTE_assets'); - - this.animation.addByPrefix('green', 'arrowUP'); - this.animation.addByPrefix('blue', 'arrowDOWN'); - this.animation.addByPrefix('purple', 'arrowLEFT'); - this.animation.addByPrefix('red', 'arrowRIGHT'); - - this.setGraphicSize(Std.int(this.width * 0.7)); - this.antialiasing = true; - - this.x += Note.swagWidth * this.ID; - - switch (Math.abs(this.ID)) - { - case 0: - this.animation.addByPrefix('static', 'arrow static instance 1'); - this.animation.addByPrefix('pressed', 'left press', 24, false); - this.animation.addByPrefix('confirm', 'left confirm', 24, false); - case 1: - this.animation.addByPrefix('static', 'arrow static instance 2'); - this.animation.addByPrefix('pressed', 'down press', 24, false); - this.animation.addByPrefix('confirm', 'down confirm', 24, false); - case 2: - this.animation.addByPrefix('static', 'arrow static instance 4'); - this.animation.addByPrefix('pressed', 'up press', 24, false); - this.animation.addByPrefix('confirm', 'up confirm', 24, false); - case 3: - this.animation.addByPrefix('static', 'arrow static instance 3'); - this.animation.addByPrefix('pressed', 'right press', 24, false); - this.animation.addByPrefix('confirm', 'right confirm', 24, false); - } - } - - /** - * Applies the pixel note style to an arrow. - */ - function buildPixelGraphic():Void - { - this.loadGraphic(Paths.image('weeb/pixelUI/arrows-pixels'), true, 17, 17); - - this.animation.add('purplel', [4]); - this.animation.add('blue', [5]); - this.animation.add('green', [6]); - this.animation.add('red', [7]); - - this.setGraphicSize(Std.int(this.width * Constants.PIXEL_ART_SCALE)); - this.updateHitbox(); - - // Forcibly disable anti-aliasing on pixel graphics to stop blur. - this.antialiasing = false; - - this.x += Note.swagWidth * this.ID; - - // TODO: Seems weird that these are hardcoded like this... no XML? - switch (Math.abs(this.ID)) - { - case 0: - this.animation.add('static', [0]); - this.animation.add('pressed', [4, 8], 12, false); - this.animation.add('confirm', [12, 16], 24, false); - case 1: - this.animation.add('static', [1]); - this.animation.add('pressed', [5, 9], 12, false); - this.animation.add('confirm', [13, 17], 24, false); - case 2: - this.animation.add('static', [2]); - this.animation.add('pressed', [6, 10], 12, false); - this.animation.add('confirm', [14, 18], 12, false); - case 3: - this.animation.add('static', [3]); - this.animation.add('pressed', [7, 11], 12, false); - this.animation.add('confirm', [15, 19], 24, false); - } - } -} - -/** - * TODO: Unhardcode this and make it part of the note style system. - */ -enum StrumlineStyle -{ - NORMAL; - PIXEL; -} diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index bdf7ef591..b27a46a0f 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -2,10 +2,10 @@ package funkin.play.character; import flixel.math.FlxPoint; import funkin.modding.events.ScriptEvent; -import funkin.noteStuff.NoteBasic.NoteDir; import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.character.CharacterData.CharacterRenderType; import funkin.play.stage.Bopper; +import funkin.play.notes.NoteDirection; /** * A Character is a stage prop which bops to the music as well as controlled by the strumlines. @@ -488,16 +488,16 @@ class BaseCharacter extends Bopper { super.onNoteHit(event); - if (event.note.mustPress && characterType == BF) + if (event.note.noteData.getMustHitNote() && characterType == BF) { // If the note is from the same strumline, play the sing animation. - this.playSingAnimation(event.note.data.dir, false); + this.playSingAnimation(event.note.noteData.getDirection(), false); holdTimer = 0; } - else if (!event.note.mustPress && characterType == DAD) + else if (!event.note.noteData.getMustHitNote() && characterType == DAD) { // If the note is from the same strumline, play the sing animation. - this.playSingAnimation(event.note.data.dir, false); + this.playSingAnimation(event.note.noteData.getDirection(), false); holdTimer = 0; } } @@ -510,17 +510,17 @@ class BaseCharacter extends Bopper { super.onNoteMiss(event); - if (event.note.mustPress && characterType == BF) + if (event.note.noteData.getMustHitNote() && characterType == BF) { // If the note is from the same strumline, play the sing animation. - this.playSingAnimation(event.note.data.dir, true); + this.playSingAnimation(event.note.noteData.getDirection(), true); } - else if (!event.note.mustPress && characterType == DAD) + else if (!event.note.noteData.getMustHitNote() && characterType == DAD) { // If the note is from the same strumline, play the sing animation. - this.playSingAnimation(event.note.data.dir, true); + this.playSingAnimation(event.note.noteData.getDirection(), true); } - else if (event.note.mustPress && characterType == GF) + else if (event.note.noteData.getMustHitNote() && characterType == GF) { var dropAnim = ''; @@ -575,7 +575,7 @@ class BaseCharacter extends Bopper * @param miss If true, play the miss animation instead of the sing animation. * @param suffix A suffix to append to the animation name, like `alt`. */ - public function playSingAnimation(dir:NoteDir, ?miss:Bool = false, ?suffix:String = ''):Void + public function playSingAnimation(dir:NoteDirection, ?miss:Bool = false, ?suffix:String = ''):Void { var anim:String = 'sing${dir.nameUpper}${miss ? 'miss' : ''}${suffix != '' ? '-${suffix}' : ''}'; diff --git a/source/funkin/play/notes/NoteDirection.hx b/source/funkin/play/notes/NoteDirection.hx new file mode 100644 index 000000000..8a0fb5ecc --- /dev/null +++ b/source/funkin/play/notes/NoteDirection.hx @@ -0,0 +1,82 @@ +package funkin.play.notes; + +import funkin.util.Constants; +import flixel.util.FlxColor; + +/** + * The direction of a note. + * This has implicit casting set up, so you can use this as an integer. + */ +enum abstract NoteDirection(Int) from Int to Int +{ + var LEFT = 0; + var DOWN = 1; + var UP = 2; + var RIGHT = 3; + public var name(get, never):String; + public var nameUpper(get, never):String; + public var color(get, never):FlxColor; + public var colorName(get, never):String; + + @:from + public static function fromInt(value:Int):NoteDirection + { + return switch (value % 4) + { + case 0: LEFT; + case 1: DOWN; + case 2: UP; + case 3: RIGHT; + default: LEFT; + } + } + + function get_name():String + { + return switch (abstract) + { + case LEFT: + 'left'; + case DOWN: + 'down'; + case UP: + 'up'; + case RIGHT: + 'right'; + default: + 'unknown'; + } + } + + function get_nameUpper():String + { + return abstract.name.toUpperCase(); + } + + function get_color():FlxColor + { + return Constants.COLOR_NOTES[this]; + } + + function get_colorName():String + { + return switch (abstract) + { + case LEFT: + 'purple'; + case DOWN: + 'blue'; + case UP: + 'green'; + case RIGHT: + 'red'; + default: + 'unknown'; + } + } + + public function toString():String + { + return abstract.name; + } +} diff --git a/source/funkin/play/notes/NoteSplash.hx b/source/funkin/play/notes/NoteSplash.hx new file mode 100644 index 000000000..90c9825e9 --- /dev/null +++ b/source/funkin/play/notes/NoteSplash.hx @@ -0,0 +1,90 @@ +package funkin.play.notes; + +import funkin.play.notes.NoteDirection; +import flixel.graphics.frames.FlxFramesCollection; +import flixel.FlxG; +import flixel.graphics.frames.FlxAtlasFrames; +import flixel.FlxSprite; + +class NoteSplash extends FlxSprite +{ + static final ALPHA:Float = 0.6; + static final FRAMERATE_DEFAULT:Int = 24; + static final FRAMERATE_VARIANCE:Int = 2; + + static var frameCollection:FlxFramesCollection; + + public static function preloadFrames():Void + { + frameCollection = Paths.getSparrowAtlas('noteSplashes'); + } + + public function new() + { + super(0, 0); + + setup(); + + this.alpha = ALPHA; + this.antialiasing = true; + this.animation.finishCallback = this.onAnimationFinished; + } + + /** + * Add ALL the animations to this sprite. We will recycle and reuse the FlxSprite multiple times. + */ + function setup():Void + { + if (frameCollection == null) preloadFrames(); + + this.frames = frameCollection; + + this.animation.addByPrefix('splash1Left', 'note impact 1 purple0', FRAMERATE_DEFAULT, false, false, false); + this.animation.addByPrefix('splash1Down', 'note impact 1 blue0', FRAMERATE_DEFAULT, false, false, false); + this.animation.addByPrefix('splash1Up', 'note impact 1 green0', FRAMERATE_DEFAULT, false, false, false); + this.animation.addByPrefix('splash1Right', 'note impact 1 red0', FRAMERATE_DEFAULT, false, false, false); + this.animation.addByPrefix('splash2Left', 'note impact 2 purple0', FRAMERATE_DEFAULT, false, false, false); + this.animation.addByPrefix('splash2Down', 'note impact 2 blue0', FRAMERATE_DEFAULT, false, false, false); + this.animation.addByPrefix('splash2Up', 'note impact 2 green0', FRAMERATE_DEFAULT, false, false, false); + this.animation.addByPrefix('splash2Right', 'note impact 2 red0', FRAMERATE_DEFAULT, false, false, false); + + if (this.animation.getAnimationList().length < 8) + { + trace('WARNING: NoteSplash failed to initialize all animations.'); + } + } + + public function playAnimation(name:String, force:Bool = false, reversed:Bool = false, startFrame:Int = 0):Void + { + this.animation.play(name, force, reversed, startFrame); + } + + public function play(direction:NoteDirection, variant:Int = null):Void + { + if (variant == null) variant = FlxG.random.int(1, 2); + + switch (direction) + { + case NoteDirection.LEFT: + this.playAnimation('splash${variant}Left'); + case NoteDirection.DOWN: + this.playAnimation('splash${variant}Down'); + case NoteDirection.UP: + this.playAnimation('splash${variant}Up'); + case NoteDirection.RIGHT: + this.playAnimation('splash${variant}Right'); + } + + // Vary the speed of the animation a bit. + animation.curAnim.frameRate = FRAMERATE_DEFAULT + FlxG.random.int(-FRAMERATE_VARIANCE, FRAMERATE_VARIANCE); + + // Center the animation on the note splash. + offset.set(width * 0.3, height * 0.3); + } + + public function onAnimationFinished(animationName:String):Void + { + // *lightning* *zap* *crackle* + this.kill(); + } +} diff --git a/source/funkin/play/notes/NoteSprite.hx b/source/funkin/play/notes/NoteSprite.hx new file mode 100644 index 000000000..e4b866cc4 --- /dev/null +++ b/source/funkin/play/notes/NoteSprite.hx @@ -0,0 +1,178 @@ +package funkin.play.notes; + +import funkin.play.song.SongData.SongNoteData; +import flixel.graphics.frames.FlxAtlasFrames; +import flixel.FlxSprite; + +class NoteSprite extends FlxSprite +{ + static final DIRECTION_COLORS:Array<String> = ['purple', 'blue', 'green', 'red']; + + public var holdNoteSprite:SustainTrail; + + /** + * The time at which the note should be hit, in milliseconds. + */ + public var strumTime(default, set):Float; + + function set_strumTime(value:Float):Float + { + this.strumTime = value; + return this.strumTime; + } + + /** + * The length of the note's sustain, in milliseconds. + * If 0, the note is a tap note. + */ + public var length(default, set):Float; + + function set_length(value:Float):Float + { + this.length = value; + this.isSustainNote = (this.length > 0); + return this.length; + } + + /** + * The time at which the note should be hit, in steps. + */ + public var stepTime(get, never):Float; + + function get_stepTime():Float + { + // TODO: Account for changes in BPM. + return this.strumTime / Conductor.stepLengthMs; + } + + /** + * An extra attribute for the note. + * For example, whether the note is an "alt" note, or whether it has custom behavior on hit. + */ + public var kind(default, set):String; + + function set_kind(value:String):String + { + this.kind = value; + return this.kind; + } + + /** + * The data of the note (i.e. the direction.) + */ + public var direction(default, set):NoteDirection; + + function set_direction(value:Int):Int + { + if (frames == null) return value; + + animation.play(DIRECTION_COLORS[value] + 'Scroll'); + + this.direction = value; + return this.direction; + } + + public var noteData:SongNoteData; + + public var isSustainNote:Bool = false; + + /** + * Set this flag to true when hitting the note to avoid scoring it multiple times. + */ + public var hasBeenHit:Bool = false; + + /** + * Register this note as hit only after any other notes + */ + public var lowPriority:Bool = false; + + /** + * This is true if the note has been fully missed by the player. + * It will be destroyed immediately. + */ + public var hasMissed:Bool; + + /** + * This is true if the note is earlier than 10 frames within the strumline. + * and thus can't be hit by the player. + * Managed by PlayState. + */ + public var tooEarly:Bool; + + /** + * This is true if the note is within 10 frames of the strumline, + * and thus may be hit by the player. + * Managed by PlayState. + */ + public var mayHit:Bool; + + /** + * This is true if the note is earlier than 10 frames after the strumline, + * and thus can't be hit by the player. + * Managed by PlayState. + */ + public var tooLate:Bool; + + public function new(strumTime:Float = 0, direction:Int = 0) + { + super(0, -9999); + this.strumTime = strumTime; + this.direction = direction; + + if (this.strumTime < 0) this.strumTime = 0; + + setupNoteGraphic(); + + // Disables the update() function for performance. + this.active = false; + } + + public static function buildNoteFrames(force:Bool = false):FlxAtlasFrames + { + // static variables inside functions are a cool of Haxe 4.3.0. + static var noteFrames:FlxAtlasFrames = null; + + if (noteFrames != null && !force) return noteFrames; + + noteFrames = Paths.getSparrowAtlas('NOTE_assets'); + + noteFrames.parent.persist = true; + + return noteFrames; + } + + function setupNoteGraphic():Void + { + this.frames = buildNoteFrames(); + + animation.addByPrefix('greenScroll', 'green instance'); + animation.addByPrefix('redScroll', 'red instance'); + animation.addByPrefix('blueScroll', 'blue instance'); + animation.addByPrefix('purpleScroll', 'purple instance'); + + animation.addByPrefix('purpleholdend', 'pruple end hold'); + animation.addByPrefix('greenholdend', 'green hold end'); + animation.addByPrefix('redholdend', 'red hold end'); + animation.addByPrefix('blueholdend', 'blue hold end'); + + animation.addByPrefix('purplehold', 'purple hold piece'); + animation.addByPrefix('greenhold', 'green hold piece'); + animation.addByPrefix('redhold', 'red hold piece'); + animation.addByPrefix('bluehold', 'blue hold piece'); + + setGraphicSize(Strumline.STRUMLINE_SIZE); + updateHitbox(); + antialiasing = true; + } + + public override function revive():Void + { + super.revive(); + this.active = false; + this.tooEarly = false; + this.hasBeenHit = false; + this.mayHit = false; + this.tooLate = false; + this.hasMissed = false; + } +} diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx new file mode 100644 index 000000000..7be17a4cb --- /dev/null +++ b/source/funkin/play/notes/Strumline.hx @@ -0,0 +1,565 @@ +package funkin.play.notes; + +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; +import funkin.ui.PreferencesMenu; +import funkin.play.notes.NoteSprite; +import flixel.util.FlxSort; +import funkin.play.notes.SustainTrail; +import funkin.util.SortUtil; +import funkin.play.song.SongData.SongNoteData; +import flixel.FlxG; +import flixel.group.FlxSpriteGroup; +import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; + +/** + * A group of sprites which handles the receptor, the note splashes, and the notes (with sustains) for a given player. + */ +class Strumline extends FlxSpriteGroup +{ + public static final DIRECTIONS:Array<NoteDirection> = [NoteDirection.LEFT, NoteDirection.DOWN, NoteDirection.UP, NoteDirection.RIGHT]; + public static final STRUMLINE_SIZE:Int = 112; + public static final NOTE_SPACING:Int = STRUMLINE_SIZE + 8; + + // Positional fixes for new strumline graphics. + static final INITIAL_OFFSET = -0.275 * STRUMLINE_SIZE; + static final NUDGE:Float = 2.0; + + static final KEY_COUNT:Int = 4; + static final NOTE_SPLASH_CAP:Int = 6; + + static var RENDER_DISTANCE_MS(get, null):Float; + + static function get_RENDER_DISTANCE_MS():Float + { + return FlxG.height / 0.45; + } + + public var isPlayer:Bool; + + /** + * The notes currently being rendered on the strumline. + * This group iterates over this every frame to update note positions. + * The PlayState also iterates over this to calculate user inputs. + */ + public var notes:FlxTypedSpriteGroup<NoteSprite>; + + public var holdNotes:FlxTypedSpriteGroup<SustainTrail>; + + var strumlineNotes:FlxTypedSpriteGroup<StrumlineNote>; + var noteSplashes:FlxTypedSpriteGroup<NoteSplash>; + var sustainSplashes:FlxTypedSpriteGroup<NoteSplash>; + + var noteData:Array<SongNoteData> = []; + var nextNoteIndex:Int = -1; + + public function new(isPlayer:Bool) + { + super(); + + this.isPlayer = isPlayer; + + this.strumlineNotes = new FlxTypedSpriteGroup<StrumlineNote>(); + this.add(this.strumlineNotes); + + // Hold notes are added first so they render behind regular notes. + this.holdNotes = new FlxTypedSpriteGroup<SustainTrail>(); + this.add(this.holdNotes); + + this.notes = new FlxTypedSpriteGroup<NoteSprite>(); + this.add(this.notes); + + this.noteSplashes = new FlxTypedSpriteGroup<NoteSplash>(0, 0, NOTE_SPLASH_CAP); + this.add(this.noteSplashes); + + for (i in 0...DIRECTIONS.length) + { + var child:StrumlineNote = new StrumlineNote(isPlayer, DIRECTIONS[i]); + child.x = getXPos(DIRECTIONS[i]); + child.x += INITIAL_OFFSET; + child.y = 0; + this.strumlineNotes.add(child); + } + + // This MUST be true for children to update! + this.active = true; + } + + override function get_width():Float + { + return 4 * Strumline.NOTE_SPACING; + } + + public override function update(elapsed:Float):Void + { + super.update(elapsed); + + updateNotes(); + } + + /** + * Get a list of notes within + or - the given strumtime. + * @param strumTime The current time. + * @param hitWindow The hit window to check. + */ + public function getNotesInRange(strumTime:Float, hitWindow:Float):Array<NoteSprite> + { + var hitWindowStart:Float = strumTime - hitWindow; + var hitWindowEnd:Float = strumTime + hitWindow; + + return notes.members.filter(function(note:NoteSprite) { + return note != null && note.alive && !note.hasBeenHit && note.strumTime >= hitWindowStart && note.strumTime <= hitWindowEnd; + }); + } + + public function getHoldNotesInRange(strumTime:Float, hitWindow:Float):Array<SustainTrail> + { + var hitWindowStart:Float = strumTime - hitWindow; + var hitWindowEnd:Float = strumTime + hitWindow; + + return holdNotes.members.filter(function(note:SustainTrail) { + return note != null + && note.alive + && note.strumTime >= hitWindowStart + && (note.strumTime + note.fullSustainLength) <= hitWindowEnd; + }); + } + + public function getNoteSprite(noteData:SongNoteData):NoteSprite + { + if (noteData == null) return null; + + for (note in notes.members) + { + if (note == null) continue; + if (note.alive) continue; + + if (note.noteData == noteData) return note; + } + + return null; + } + + public function getHoldNoteSprite(noteData:SongNoteData):SustainTrail + { + if (noteData == null || ((noteData.length ?? 0.0) <= 0.0)) return null; + + for (holdNote in holdNotes.members) + { + if (holdNote == null) continue; + if (holdNote.alive) continue; + + if (holdNote.noteData == noteData) return holdNote; + } + + return null; + } + + /** + * For a note's strumTime, calculate its Y position relative to the strumline. + * NOTE: Assumes Conductor and PlayState are both initialized. + * @param strumTime + * @return Float + */ + static function calculateNoteYPos(strumTime:Float):Float + { + // Make the note move faster visually as it moves offscreen. + var vwoosh:Float = (strumTime < Conductor.songPosition) ? 2.0 : 1.0; + var scrollSpeed:Float = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0; + + return Conductor.PIXELS_PER_MS * (Conductor.songPosition - strumTime) * scrollSpeed * vwoosh * (PreferencesMenu.getPref('downscroll') ? 1 : -1); + } + + function updateNotes():Void + { + if (noteData.length == 0) return; + + var renderWindowStart:Float = Conductor.songPosition + RENDER_DISTANCE_MS; + + for (noteIndex in nextNoteIndex...noteData.length) + { + var note:Null<SongNoteData> = noteData[noteIndex]; + + if (note == null) continue; + if (note.time > renderWindowStart) break; + + buildNoteSprite(note); + + if (note.length > 0) + { + buildHoldNoteSprite(note); + } + + nextNoteIndex++; // Increment the nextNoteIndex rather than splicing the array, because splicing is slow. + } + + // Update rendering of notes. + for (note in notes.members) + { + if (note == null || note.hasBeenHit) continue; + + note.y = this.y - INITIAL_OFFSET + calculateNoteYPos(note.strumTime); + + // Check if the note is outside the hit window, and if so, mark it as missed. + // TODO: Check to make sure this doesn't happen when the note is on screen because it'll probably get deleted. + if (Conductor.songPosition > (note.noteData.time + Conductor.HIT_WINDOW_MS)) + { + note.visible = false; + note.hasMissed = true; + if (note.holdNoteSprite != null) note.holdNoteSprite.missed = true; + } + else + { + note.visible = true; + note.hasMissed = false; + if (note.holdNoteSprite != null) note.holdNoteSprite.missed = false; + } + } + + // Update rendering of hold notes. + for (holdNote in holdNotes.members) + { + if (holdNote == null || !holdNote.alive) continue; + + var renderWindowEnd = holdNote.strumTime + holdNote.fullSustainLength + Conductor.HIT_WINDOW_MS + RENDER_DISTANCE_MS / 8; + + if (Conductor.songPosition >= renderWindowEnd || holdNote.sustainLength <= 0) + { + // Hold note is offscreen, kill it. + holdNote.visible = false; + holdNote.kill(); // Do not destroy! Recycling is faster. + } + else if (holdNote.sustainLength <= 0) + { + // Hold note is completed, kill it. + playStatic(holdNote.noteDirection); + holdNote.visible = false; + holdNote.kill(); + } + else if (holdNote.sustainLength <= 10) + { + // TODO: Better handle the weird edge case where the hold note is almost completed. + holdNote.visible = false; + } + else if (Conductor.songPosition > holdNote.strumTime && !holdNote.missed) + { + // Hold note is currently being hit, clip it off. + holdConfirm(holdNote.noteDirection); + holdNote.visible = true; + + holdNote.sustainLength = (holdNote.strumTime + holdNote.fullSustainLength) - Conductor.songPosition; + + if (PreferencesMenu.getPref('downscroll')) + { + holdNote.y = this.y - holdNote.height + STRUMLINE_SIZE / 2; + } + else + { + holdNote.y = this.y - INITIAL_OFFSET + STRUMLINE_SIZE / 2; + } + } + else if (holdNote.missed && (holdNote.fullSustainLength > holdNote.sustainLength)) + { + // Hold note was dropped before completing, keep it in its clipped state. + holdNote.visible = true; + + var yOffset:Float = (holdNote.fullSustainLength - holdNote.sustainLength) * Conductor.PIXELS_PER_MS; + + trace('yOffset: ' + yOffset); + trace('holdNote.fullSustainLength: ' + holdNote.fullSustainLength); + trace('holdNote.sustainLength: ' + holdNote.sustainLength); + + if (PreferencesMenu.getPref('downscroll')) + { + holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime) - holdNote.height + STRUMLINE_SIZE / 2; + } + else + { + holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime) + yOffset + STRUMLINE_SIZE / 2; + } + } + else + { + // Hold note is new, render it normally. + holdNote.visible = true; + + if (PreferencesMenu.getPref('downscroll')) + { + holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime) - holdNote.height + STRUMLINE_SIZE / 2; + } + else + { + holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime) + STRUMLINE_SIZE / 2; + } + } + } + } + + public function onBeatHit():Void + { + if (notes.members.length > 1) notes.members.insertionSort(compareNoteSprites.bind(FlxSort.ASCENDING)); + + if (holdNotes.members.length > 1) holdNotes.members.insertionSort(compareHoldNoteSprites.bind(FlxSort.ASCENDING)); + } + + public function applyNoteData(data:Array<SongNoteData>):Void + { + this.notes.clear(); + + this.noteData = data.copy(); + this.nextNoteIndex = 0; + + // Sort the notes by strumtime. + this.noteData.insertionSort(compareNoteData.bind(FlxSort.ASCENDING)); + } + + public function hitNote(note:NoteSprite):Void + { + playConfirm(note.direction); + killNote(note); + } + + public function killNote(note:NoteSprite):Void + { + note.visible = false; + notes.remove(note, false); + note.kill(); + + if (note.holdNoteSprite != null) + { + holdNoteSprite.missed = true; + holdNoteSprite.alpha = 0.6; + } + } + + public function getByIndex(index:Int):StrumlineNote + { + return this.strumlineNotes.members[index]; + } + + public function getByDirection(direction:NoteDirection):StrumlineNote + { + return getByIndex(DIRECTIONS.indexOf(direction)); + } + + public function playStatic(direction:NoteDirection):Void + { + getByDirection(direction).playStatic(); + } + + public function playPress(direction:NoteDirection):Void + { + getByDirection(direction).playPress(); + } + + public function playConfirm(direction:NoteDirection):Void + { + getByDirection(direction).playConfirm(); + } + + public function holdConfirm(direction:NoteDirection):Void + { + getByDirection(direction).holdConfirm(); + } + + public function isConfirm(direction:NoteDirection):Bool + { + return getByDirection(direction).isConfirm(); + } + + public function playNoteSplash(direction:NoteDirection):Void + { + // TODO: Add a setting to disable note splashes. + // if (Settings.noSplash) return; + + var splash:NoteSplash = this.constructNoteSplash(); + + if (splash != null) + { + splash.play(direction); + + splash.x = this.x; + splash.x += getXPos(direction); + splash.x += INITIAL_OFFSET; + splash.y = this.y; + splash.y -= INITIAL_OFFSET; + splash.y += 0; + } + } + + public function buildNoteSprite(note:SongNoteData):Void + { + var noteSprite:NoteSprite = constructNoteSprite(); + + if (noteSprite != null) + { + noteSprite.strumTime = note.time; + noteSprite.direction = note.getDirection(); + noteSprite.noteData = note; + + noteSprite.x = this.x; + noteSprite.x += getXPos(DIRECTIONS[note.getDirection() % KEY_COUNT]); + noteSprite.x -= NUDGE; + // noteSprite.x += INITIAL_OFFSET; + noteSprite.y = -9999; + } + } + + public function buildHoldNoteSprite(note:SongNoteData):Void + { + var holdNoteSprite:SustainTrail = constructHoldNoteSprite(); + + if (holdNoteSprite != null) + { + holdNoteSprite.noteData = note; + holdNoteSprite.strumTime = note.time; + holdNoteSprite.noteDirection = note.getDirection(); + holdNoteSprite.fullSustainLength = note.length; + holdNoteSprite.sustainLength = note.length; + holdNoteSprite.missed = false; + + holdNoteSprite.x = this.x; + holdNoteSprite.x += getXPos(DIRECTIONS[note.getDirection() % KEY_COUNT]); + // holdNoteSprite.x += INITIAL_OFFSET; + holdNoteSprite.x += STRUMLINE_SIZE / 2; + holdNoteSprite.x -= holdNoteSprite.width / 2; + holdNoteSprite.y = -9999; + } + } + + /** + * Custom recycling behavior. + */ + function constructNoteSplash():NoteSplash + { + var result:NoteSplash = null; + + // If we haven't filled the pool yet... + if (noteSplashes.length < noteSplashes.maxSize) + { + // Create a new note splash. + result = new NoteSplash(); + this.noteSplashes.add(result); + } + else + { + // Else, find a note splash which is inactive so we can revive it. + result = this.noteSplashes.getFirstAvailable(); + + if (result != null) + { + result.revive(); + } + else + { + // The note splash pool is full and all note splashes are active, + // so we just pick one at random to destroy and restart. + result = FlxG.random.getObject(this.noteSplashes.members); + } + } + + return result; + } + + /** + * Custom recycling behavior. + */ + function constructNoteSprite():NoteSprite + { + var result:NoteSprite = null; + + // Else, find a note which is inactive so we can revive it. + result = this.notes.getFirstAvailable(); + + if (result != null) + { + // Revive and reuse the note. + result.revive(); + } + else + { + // The note sprite pool is full and all note splashes are active. + // We have to create a new note. + result = new NoteSprite(); + this.notes.add(result); + } + + return result; + } + + /** + * Custom recycling behavior. + */ + function constructHoldNoteSprite():SustainTrail + { + var result:SustainTrail = null; + + // Else, find a note which is inactive so we can revive it. + result = this.holdNotes.getFirstAvailable(); + + if (result != null) + { + // Revive and reuse the note. + result.revive(); + } + else + { + // The note sprite pool is full and all note splashes are active. + // We have to create a new note. + result = new SustainTrail(0, 100, Paths.image("NOTE_hold_assets")); + this.holdNotes.add(result); + } + + return result; + } + + function getXPos(direction:NoteDirection):Float + { + return switch (direction) + { + case NoteDirection.LEFT: 0; + case NoteDirection.DOWN: 0 + (1 * Strumline.NOTE_SPACING); + case NoteDirection.UP: 0 + (2 * Strumline.NOTE_SPACING); + case NoteDirection.RIGHT: 0 + (3 * Strumline.NOTE_SPACING); + default: 0; + } + } + + /** + * Apply a small animation which moves the arrow down and fades it in. + * Only plays at the start of Free Play songs. + * + * Note that modifying the offset of the whole strumline won't have the + * @param arrow The arrow to animate. + * @param index The index of the arrow in the strumline. + */ + function fadeInArrow(arrow:StrumlineNote):Void + { + arrow.y -= 10; + arrow.alpha = 0; + FlxTween.tween(arrow, {y: arrow.y + 10, alpha: 1}, 1, {ease: FlxEase.circOut, startDelay: 0.5 + (0.2 * arrow.ID)}); + } + + public function fadeInArrows():Void + { + for (arrow in this.strumlineNotes) + { + fadeInArrow(arrow); + } + } + + function compareNoteData(order:Int, a:SongNoteData, b:SongNoteData):Int + { + return FlxSort.byValues(order, a.time, b.time); + } + + function compareNoteSprites(order:Int, a:NoteSprite, b:NoteSprite):Int + { + return FlxSort.byValues(order, a?.strumTime, b?.strumTime); + } + + function compareHoldNoteSprites(order:Int, a:SustainTrail, b:SustainTrail):Int + { + return FlxSort.byValues(order, a?.strumTime, b?.strumTime); + } +} diff --git a/source/funkin/play/notes/StrumlineNote.hx b/source/funkin/play/notes/StrumlineNote.hx new file mode 100644 index 000000000..7fbb3a0f9 --- /dev/null +++ b/source/funkin/play/notes/StrumlineNote.hx @@ -0,0 +1,187 @@ +package funkin.play.notes; + +import flixel.graphics.frames.FlxAtlasFrames; +import flixel.FlxSprite; +import funkin.play.notes.NoteSprite; + +/** + * The actual receptor that you see on screen. + */ +class StrumlineNote extends FlxSprite +{ + public var isPlayer(default, null):Bool; + + public var direction(default, set):NoteDirection; + + public function updatePosition(parentNote:NoteSprite) + { + this.x = parentNote.x; + this.x += parentNote.width / 2; + this.x -= this.width / 2; + + this.y = parentNote.y; + this.y += parentNote.height / 2; + } + + function set_direction(value:NoteDirection):NoteDirection + { + this.direction = value; + setup(); + return this.direction; + } + + public function new(isPlayer:Bool, direction:NoteDirection) + { + super(0, 0); + + this.isPlayer = isPlayer; + + this.direction = direction; + + this.animation.callback = onAnimationFrame; + this.animation.finishCallback = onAnimationFinished; + + this.active = true; + } + + function onAnimationFrame(name:String, frameNumber:Int, frameIndex:Int):Void {} + + function onAnimationFinished(name:String):Void + { + if (!isPlayer && name.startsWith('confirm')) + { + playStatic(); + } + } + + override function update(elapsed:Float) + { + super.update(elapsed); + + centerOrigin(); + } + + function setup():Void + { + this.frames = Paths.getSparrowAtlas('StrumlineNotes'); + + switch (this.direction) + { + case NoteDirection.LEFT: + this.animation.addByIndices('static', 'left confirm', [6, 7], '', 24, false, false, false); + this.animation.addByPrefix('press', 'left press', 24, false, false, false); + this.animation.addByIndices('confirm', 'left confirm', [0, 1, 2, 3], '', 24, false, false, false); + this.animation.addByIndices('confirm-hold', 'left confirm', [2, 3, 4, 5], '', 24, true, false, false); + + case NoteDirection.DOWN: + this.animation.addByIndices('static', 'down confirm', [6, 7], '', 24, false, false, false); + this.animation.addByPrefix('press', 'down press', 24, false, false, false); + this.animation.addByIndices('confirm', 'down confirm', [0, 1, 2, 3], '', 24, false, false, false); + this.animation.addByIndices('confirm-hold', 'down confirm', [2, 3, 4, 5], '', 24, true, false, false); + + case NoteDirection.UP: + this.animation.addByIndices('static', 'up confirm', [6, 7], '', 24, false, false, false); + this.animation.addByPrefix('press', 'up press', 24, false, false, false); + this.animation.addByIndices('confirm', 'up confirm', [0, 1, 2, 3], '', 24, false, false, false); + this.animation.addByIndices('confirm-hold', 'up confirm', [2, 3, 4, 5], '', 24, true, false, false); + + case NoteDirection.RIGHT: + this.animation.addByIndices('static', 'right confirm', [6, 7], '', 24, false, false, false); + this.animation.addByPrefix('press', 'right press', 24, false, false, false); + this.animation.addByIndices('confirm', 'right confirm', [0, 1, 2, 3], '', 24, false, false, false); + this.animation.addByIndices('confirm-hold', 'right confirm', [2, 3, 4, 5], '', 24, true, false, false); + } + + this.antialiasing = true; + + this.setGraphicSize(Std.int(Strumline.STRUMLINE_SIZE * 1.55)); + this.updateHitbox(); + this.playStatic(); + } + + public function playAnimation(name:String = 'static', force:Bool = false, reversed:Bool = false, startFrame:Int = 0):Void + { + this.animation.play(name, force, reversed, startFrame); + + centerOffsets(); + centerOrigin(); + } + + public function playStatic():Void + { + this.active = false; + this.playAnimation('static', true); + } + + public function playPress():Void + { + this.active = true; + this.playAnimation('press', true); + } + + public function playConfirm():Void + { + this.active = true; + this.playAnimation('confirm', true); + } + + public function isConfirm():Bool + { + return getCurrentAnimation().startsWith('confirm'); + } + + public function holdConfirm():Void + { + this.active = true; + + if (getCurrentAnimation() == "confirm-hold") return; + if (getCurrentAnimation() == "confirm") + { + if (isAnimationFinished()) + { + this.playAnimation('confirm-hold', true, false); + } + return; + } + this.playAnimation('confirm', false, false); + } + + /** + * Returns the name of the animation that is currently playing. + * If no animation is playing (usually this means the sprite is BROKEN!), + * returns an empty string to prevent NPEs. + */ + public function getCurrentAnimation():String + { + if (this.animation == null || this.animation.curAnim == null) return ""; + return this.animation.curAnim.name; + } + + public function isAnimationFinished():Bool + { + return this.animation.finished; + } + + static final DEFAULT_OFFSET:Int = 13; + + /** + * Adjusts the position of the sprite's graphic relative to the hitbox. + */ + function fixOffsets():Void + { + // Automatically center the bounding box within the graphic. + this.centerOffsets(); + + if (getCurrentAnimation() == "confirm") + { + // Move the graphic down and to the right to compensate for + // the "glow" effect on the strumline note. + this.offset.x -= DEFAULT_OFFSET; + this.offset.y -= DEFAULT_OFFSET; + } + else + { + this.centerOrigin(); + } + } +} diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx new file mode 100644 index 000000000..0b84f2d64 --- /dev/null +++ b/source/funkin/play/notes/SustainTrail.hx @@ -0,0 +1,272 @@ +package funkin.play.notes; + +import funkin.play.notes.NoteDirection; +import funkin.play.song.SongData.SongNoteData; +import flixel.util.FlxDirectionFlags; +import flixel.FlxSprite; +import flixel.graphics.FlxGraphic; +import flixel.graphics.tile.FlxDrawTrianglesItem; +import flixel.math.FlxMath; +import funkin.ui.PreferencesMenu; + +/** + * This is based heavily on the `FlxStrip` class. It uses `drawTriangles()` to clip a sustain note + * trail at a certain time. + * The whole `FlxGraphic` is used as a texture map. See the `NOTE_hold_assets.fla` file for specifics + * on how it should be constructed. + * + * @author MtH + */ +class SustainTrail extends FlxSprite +{ + /** + * The triangles corresponding to the hold, followed by the endcap. + * `top left, top right, bottom left` + * `top left, bottom left, bottom right` + */ + static final TRIANGLE_VERTEX_INDICES:Array<Int> = [0, 1, 2, 1, 2, 3, 4, 5, 6, 5, 6, 7]; + + public var strumTime:Float = 0; // millis + public var noteDirection:NoteDirection = 0; + public var sustainLength(default, set):Float = 0; // millis + public var fullSustainLength:Float = 0; + public var noteData:SongNoteData; + + /** + * Set to `true` if the user missed the note. + * The trail should be made transparent, with clipping and effects disabled + */ + public var missed:Bool = false; // maybe BlendMode.MULTIPLY if missed somehow, drawTriangles does not support! + + /** + * A `Vector` of floats where each pair of numbers is treated as a coordinate location (an x, y pair). + */ + public var vertices:DrawData<Float> = new DrawData<Float>(); + + /** + * A `Vector` of integers or indexes, where every three indexes define a triangle. + */ + public var indices:DrawData<Int> = new DrawData<Int>(); + + /** + * A `Vector` of normalized coordinates used to apply texture mapping. + */ + public var uvtData:DrawData<Float> = new DrawData<Float>(); + + private var processedGraphic:FlxGraphic; + + private var zoom:Float = 1; + + /** + * What part of the trail's end actually represents the end of the note. + * This can be used to have a little bit sticking out. + */ + public var endOffset:Float = 0.5; // 0.73 is roughly the bottom of the sprite in the normal graphic! + + /** + * At what point the bottom for the trail's end should be clipped off. + * Used in cases where there's an extra bit of the graphic on the bottom to avoid antialiasing issues with overflow. + */ + public var bottomClip:Float = 0.9; + + /** + * Normally you would take strumTime:Float, noteData:Int, sustainLength:Float, parentNote:Note (?) + * @param NoteData + * @param SustainLength Length in milliseconds. + * @param fileName + */ + public function new(noteDirection:NoteDirection, sustainLength:Float, fileName:String) + { + super(0, 0, fileName); + + antialiasing = true; + if (fileName == "arrowEnds") + { + endOffset = bottomClip = 1; + antialiasing = false; + zoom = 6; + } + // BASIC SETUP + this.sustainLength = sustainLength; + this.fullSustainLength = sustainLength; + this.noteDirection = noteDirection; + + zoom *= 0.7; + + // CALCULATE SIZE + width = graphic.width / 8 * zoom; // amount of notes * 2 + height = sustainHeight(sustainLength, PlayState.instance.currentChart.scrollSpeed); + // instead of scrollSpeed, PlayState.SONG.speed + + flipY = PreferencesMenu.getPref('downscroll'); + + // alpha = 0.6; + alpha = 1.0; + // calls updateColorTransform(), which initializes processedGraphic! + updateColorTransform(); + + updateClipping(); + indices = new DrawData<Int>(12, true, TRIANGLE_VERTEX_INDICES); + } + + /** + * Calculates height of a sustain note for a given length (milliseconds) and scroll speed. + * @param susLength The length of the sustain note in milliseconds. + * @param scroll The current scroll speed. + */ + public static inline function sustainHeight(susLength:Float, scroll:Float) + { + return (susLength * 0.45 * scroll); + } + + function set_sustainLength(s:Float) + { + if (s < 0) s = 0; + + height = sustainHeight(s, PlayState.instance.currentChart.scrollSpeed); + updateColorTransform(); + updateClipping(); + return sustainLength = s; + } + + /** + * Sets up new vertex and UV data to clip the trail. + * If flipY is true, top and bottom bounds swap places. + * @param songTime The time to clip the note at, in milliseconds. + */ + public function updateClipping(songTime:Float = 0):Void + { + var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), PlayState.instance.currentChart.scrollSpeed), 0, height); + if (clipHeight == 0) + { + visible = false; + return; + } + else + visible = true; + + var bottomHeight:Float = graphic.height * zoom * endOffset; + var partHeight:Float = clipHeight - bottomHeight; + + // ===HOLD VERTICES== + // Top left + vertices[0 * 2] = 0.0; // Inline with left side + vertices[0 * 2 + 1] = flipY ? clipHeight : height - clipHeight; + + // Top right + vertices[1 * 2] = width; + vertices[1 * 2 + 1] = vertices[0 * 2 + 1]; // Inline with top left vertex + + // Bottom left + vertices[2 * 2] = 0.0; // Inline with left side + vertices[2 * 2 + 1] = if (partHeight > 0) + { + // flipY makes the sustain render upside down. + flipY ? 0.0 + bottomHeight : vertices[1] + partHeight; + } + else + { + vertices[0 * 2 + 1]; // Inline with top left vertex (no partHeight available) + } + + // Bottom right + vertices[3 * 2] = width; + vertices[3 * 2 + 1] = vertices[2 * 2 + 1]; // Inline with bottom left vertex + + // ===HOLD UVs=== + + // The UVs are a bit more complicated. + // UV coordinates are normalized, so they range from 0 to 1. + // We are expecting an image containing 8 horizontal segments, each representing a different colored hold note followed by its end cap. + + uvtData[0 * 2] = 1 / 4 * (noteDirection % 4); // 0%/25%/50%/75% of the way through the image + uvtData[0 * 2 + 1] = (-partHeight) / graphic.height / zoom; // top bound + // Top left + + // Top right + uvtData[1 * 2] = uvtData[0 * 2] + 1 / 8; // 12.5%/37.5%/62.5%/87.5% of the way through the image (1/8th past the top left) + uvtData[1 * 2 + 1] = uvtData[0 * 2 + 1]; // top bound + + // Bottom left + uvtData[2 * 2] = uvtData[0 * 2]; // 0%/25%/50%/75% of the way through the image + uvtData[2 * 2 + 1] = 0.0; // bottom bound + + // Bottom right + uvtData[3 * 2] = uvtData[1 * 2]; // 12.5%/37.5%/62.5%/87.5% of the way through the image (1/8th past the top left) + uvtData[3 * 2 + 1] = uvtData[2 * 2 + 1]; // bottom bound + + // === END CAP VERTICES === + // Top left + vertices[4 * 2] = vertices[2 * 2]; // Inline with bottom left vertex of hold + vertices[4 * 2 + 1] = vertices[2 * 2 + 1]; // Inline with bottom left vertex of hold + + // Top right + vertices[5 * 2] = vertices[3 * 2]; // Inline with bottom right vertex of hold + vertices[5 * 2 + 1] = vertices[3 * 2 + 1]; // Inline with bottom right vertex of hold + + // Bottom left + vertices[6 * 2] = vertices[2 * 2]; // Inline with left side + vertices[6 * 2 + 1] = flipY ? (graphic.height * (-bottomClip + endOffset) * zoom) : (height + graphic.height * (bottomClip - endOffset) * zoom); + + // Bottom right + vertices[7 * 2] = vertices[3 * 2]; // Inline with right side + vertices[7 * 2 + 1] = vertices[6 * 2 + 1]; // Inline with bottom of end cap + + // === END CAP UVs === + // Top left + uvtData[4 * 2] = uvtData[2 * 2] + 1 / 8; // 12.5%/37.5%/62.5%/87.5% of the way through the image (1/8th past the top left of hold) + uvtData[4 * 2 + 1] = if (partHeight > 0) + { + 0; + } + else + { + (bottomHeight - clipHeight) / zoom / graphic.height; + }; + + // Top right + uvtData[5 * 2] = uvtData[4 * 2] + 1 / 8; // 25%/50%/75%/100% of the way through the image (1/8th past the top left of cap) + uvtData[5 * 2 + 1] = uvtData[4 * 2 + 1]; // top bound + + // Bottom left + uvtData[6 * 2] = uvtData[4 * 2]; // 12.5%/37.5%/62.5%/87.5% of the way through the image (1/8th past the top left of hold) + uvtData[6 * 2 + 1] = bottomClip; // bottom bound + + // Bottom right + uvtData[7 * 2] = uvtData[5 * 2]; // 25%/50%/75%/100% of the way through the image (1/8th past the top left of cap) + uvtData[7 * 2 + 1] = uvtData[6 * 2 + 1]; // bottom bound + } + + @:access(flixel.FlxCamera) + override public function draw():Void + { + if (alpha == 0 || graphic == null || vertices == null) return; + + for (camera in cameras) + { + if (!camera.visible || !camera.exists) continue; + // if (!isOnScreen(camera)) continue; // TODO: Update this code to make it work properly. + + getScreenPosition(_point, camera).subtractPoint(offset); + camera.drawTriangles(processedGraphic, vertices, indices, uvtData, null, _point, blend, true, antialiasing); + } + } + + override public function destroy():Void + { + vertices = null; + indices = null; + uvtData = null; + processedGraphic.destroy(); + + super.destroy(); + } + + override function updateColorTransform():Void + { + super.updateColorTransform(); + if (processedGraphic != null) processedGraphic.destroy(); + processedGraphic = FlxGraphic.fromGraphic(graphic, true); + processedGraphic.bitmap.colorTransform(processedGraphic.bitmap.rect, colorTransform); + } +} diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index 7de005cb0..b42c8e7c4 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -298,9 +298,16 @@ class SongDifficulty return cast events; } - public inline function cacheInst():Void + public inline function cacheInst(?currentPlayerId:String = null):Void { - FlxG.sound.cache(Paths.inst(this.song.songId)); + if (currentPlayerId != null) + { + FlxG.sound.cache(Paths.inst(this.song.songId, getPlayableChar(currentPlayerId).inst)); + } + else + { + FlxG.sound.cache(Paths.inst(this.song.songId)); + } } public inline function playInst(volume:Float = 1.0, looped:Bool = false):Void diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx index a744c9a65..dc46ae365 100644 --- a/source/funkin/play/song/SongData.hx +++ b/source/funkin/play/song/SongData.hx @@ -427,6 +427,12 @@ abstract SongNoteData(RawSongNoteData) return Math.floor(this.d / strumlineSize); } + /** + * Returns true if the note is one that Boyfriend should try to hit (i.e. it's on his side). + * TODO: The name of this function is a little misleading; what about mines? + * @param strumlineSize Defaults to 4. + * @return True if it's Boyfriend's note. + */ public inline function getMustHitNote(strumlineSize:Int = 4):Bool { return getStrumlineIndex(strumlineSize) == 0; diff --git a/source/funkin/ui/ColorsMenu.hx b/source/funkin/ui/ColorsMenu.hx index 9ebccf1c9..68fc7e7e0 100644 --- a/source/funkin/ui/ColorsMenu.hx +++ b/source/funkin/ui/ColorsMenu.hx @@ -5,23 +5,24 @@ import flixel.addons.effects.chainable.FlxOutlineEffect; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.util.FlxColor; import funkin.ui.OptionsState.Page; +import funkin.play.notes.NoteSprite; class ColorsMenu extends Page { var curSelected:Int = 0; - var grpNotes:FlxTypedGroup<Note>; + var grpNotes:FlxTypedGroup<NoteSprite>; public function new() { super(); - grpNotes = new FlxTypedGroup<Note>(); + grpNotes = new FlxTypedGroup<NoteSprite>(); add(grpNotes); for (i in 0...4) { - var note:Note = new Note(0, i); + var note:NoteSprite = new NoteSprite(0, i); note.x = (100 * i) + i; note.screenCenter(Y); @@ -52,14 +53,14 @@ class ColorsMenu extends Page if (controls.UI_UP) { - grpNotes.members[curSelected].colorSwap.update(elapsed * 0.3); - Note.arrowColors[curSelected] += elapsed * 0.3; + // grpNotes.members[curSelected].colorSwap.update(elapsed * 0.3); + // Note.arrowColors[curSelected] += elapsed * 0.3; } if (controls.UI_DOWN) { - grpNotes.members[curSelected].colorSwap.update(-elapsed * 0.3); - Note.arrowColors[curSelected] += -elapsed * 0.3; + // grpNotes.members[curSelected].colorSwap.update(-elapsed * 0.3); + // Note.arrowColors[curSelected] += -elapsed * 0.3; } super.update(elapsed); diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index a23a04231..566e75706 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -22,6 +22,7 @@ import funkin.audio.VoicesGroup; import funkin.input.Cursor; import funkin.modding.events.ScriptEvent; import funkin.play.HealthIcon; +import funkin.play.notes.NoteSprite; import funkin.play.song.Song; import funkin.play.song.SongData.SongChartData; import funkin.play.song.SongData.SongDataParser; @@ -2803,11 +2804,9 @@ class ChartEditorState extends HaxeUIState // Character preview. - // Why does NOTESCRIPTEVENT TAKE A SPRITE AAAAA - var tempNote:Note = new Note(noteData.time, noteData.data, null, false, NORMAL); - tempNote.mustPress = noteData.getMustHitNote(); - tempNote.data.sustainLength = noteData.length; - tempNote.data.noteKind = noteData.kind; + // NoteScriptEvent takes a sprite, ehe. Need to rework that. + var tempNote:NoteSprite = new NoteSprite(); + tempNote.noteData = noteData; tempNote.scrollFactor.set(0, 0); var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, tempNote, 1, true); dispatchEvent(event); diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index c1bac76c4..bcf0f7359 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -88,9 +88,9 @@ class Constants public static final COLOR_HEALTH_BAR_GREEN:FlxColor = 0xFF66FF33; /** - * Default variation for charts. + * The base colors of the notes. */ - public static final DEFAULT_VARIATION:String = 'default'; + public static final COLOR_NOTES:Array<FlxColor> = [0xFFFF22AA, 0xFF00EEFF, 0xFF00CC00, 0xFFCC1111]; /** * STAGE DEFAULTS @@ -117,6 +117,11 @@ class Constants */ public static final DEFAULT_SONG:String = 'tutorial'; + /** + * Default variation for charts. + */ + public static final DEFAULT_VARIATION:String = 'default'; + /** * OTHER */ @@ -144,6 +149,9 @@ class Constants */ public static final COUNTDOWN_VOLUME:Float = 0.6; + public static final STRUMLINE_X_OFFSET:Float = 48; + public static final STRUMLINE_Y_OFFSET:Float = 24; + /** * The default intensity for camera zooms. */ diff --git a/source/funkin/util/SortUtil.hx b/source/funkin/util/SortUtil.hx index 60b522744..649923275 100644 --- a/source/funkin/util/SortUtil.hx +++ b/source/funkin/util/SortUtil.hx @@ -4,6 +4,7 @@ package funkin.util; import flixel.FlxBasic; import flixel.util.FlxSort; #end +import funkin.play.notes.NoteSprite; class SortUtil { @@ -22,8 +23,8 @@ class SortUtil * * @param order Either `FlxSort.ASCENDING` or `FlxSort.DESCENDING` */ - public static inline function byStrumtime(order:Int, a:Note, b:Note) + public static inline function byStrumtime(order:Int, a:NoteSprite, b:NoteSprite) { - return FlxSort.byValues(order, a.data.strumTime, b.data.strumTime); + return FlxSort.byValues(order, a.noteData.time, b.noteData.time); } } diff --git a/source/funkin/util/WindowUtil.hx b/source/funkin/util/WindowUtil.hx index f2f1dcf0a..42930570f 100644 --- a/source/funkin/util/WindowUtil.hx +++ b/source/funkin/util/WindowUtil.hx @@ -51,4 +51,13 @@ class WindowUtil // Do nothing. #end } + + /** + * Sets the title of the application window. + * @param value The title to use. + */ + public static function setWindowTitle(value:String):Void + { + lime.app.Application.current.window.title = value; + } } diff --git a/source/funkin/util/tools/ArraySortTools.hx b/source/funkin/util/tools/ArraySortTools.hx new file mode 100644 index 000000000..3af114b98 --- /dev/null +++ b/source/funkin/util/tools/ArraySortTools.hx @@ -0,0 +1,154 @@ +package funkin.util.tools; + +/** + * Contains code for sorting arrays using various algorithms. + * @see https://algs4.cs.princeton.edu/20sorting/ + */ +class ArraySortTools +{ + /** + * Sorts the input array using the merge sort algorithm. + * Stable and guaranteed to run in linearithmic time `O(n log n)`, + * but less efficient in "best-case" situations. + * + * @param input The array to sort in-place. + * @param compare The comparison function to use. + */ + public static function mergeSort<T>(input:Array<T>, compare:CompareFunction<T>):Void + { + if (input == null || input.length <= 1) return; + if (compare == null) throw 'No comparison function provided.'; + + // Haxe implements merge sort by default. + haxe.ds.ArraySort.sort(input, compare); + } + + /** + * Sorts the input array using the quick sort algorithm. + * More efficient on smaller arrays, but is inefficient `O(n^2)` in "worst-case" situations. + * Not stable; relative order of equal elements is not preserved. + * + * @see https://stackoverflow.com/questions/33884057/quick-sort-stackoverflow-error-for-large-arrays + * Fix for stack overflow issues. + * @param input The array to sort in-place. + * @param compare The comparison function to use. + */ + public static function quickSort<T>(input:Array<T>, compare:CompareFunction<T>):Void + { + if (input == null || input.length <= 1) return; + if (compare == null) throw 'No comparison function provided.'; + + quickSortInner(input, 0, input.length - 1, compare); + } + + /** + * Internal recursive function for the quick sort algorithm. + * Written with ChatGPT! + */ + static function quickSortInner<T>(input:Array<T>, low:Int, high:Int, compare:CompareFunction<T>):Void + { + // When low == high, the array is empty or too small to sort. + + // EDIT: Recurse on the smaller partition, and loop for the larger partition. + while (low < high) + { + // Designate the first element in the array as the pivot, then partition the array around it. + // Elements less than the pivot will be to the left, and elements greater than the pivot will be to the right. + // Return the index of the pivot. + var pivot:Int = quickSortPartition(input, low, high, compare); + + if ((pivot) - low <= high - (pivot + 1)) + { + quickSortInner(input, low, pivot, compare); + low = pivot + 1; + } + else + { + quickSortInner(input, pivot + 1, high, compare); + high = pivot; + } + } + } + + /** + * Internal function for sorting a partition of an array in the quick sort algorithm. + * Written with ChatGPT! + */ + static function quickSortPartition<T>(input:Array<T>, low:Int, high:Int, compare:CompareFunction<T>):Int + { + // Designate the first element in the array as the pivot. + var pivot:T = input[low]; + // Designate two pointers, used to divide the array into two partitions. + var i:Int = low - 1; + var j:Int = high + 1; + + while (true) + { + // Move the left pointer to the right until it finds an element greater than the pivot. + do + { + i++; + } + while (compare(input[i], pivot) < 0); + + // Move the right pointer to the left until it finds an element less than the pivot. + do + { + j--; + } + while (compare(input[j], pivot) > 0); + + // If i and j have crossed, the array has been partitioned, and the pivot will be at the index j. + if (i >= j) return j; + + // Else, swap the elements at i and j, and start over. + // This slowly moves the pivot towards the middle of the partition, + // while moving elements less than the pivot to the left and elements greater than the pivot to the right. + var temp:T = input[i]; + input[i] = input[j]; + input[j] = temp; + } + } + + /** + * Sorts the input array using the insertion sort algorithm. + * Stable and is very fast on nearly-sorted arrays, + * but is inefficient `O(n^2)` in "worst-case" situations. + * + * @param input The array to sort in-place. + * @param compare The comparison function to use. + */ + public static function insertionSort<T>(input:Array<T>, compare:CompareFunction<T>):Void + { + if (input == null || input.length <= 1) return; + if (compare == null) throw 'No comparison function provided.'; + + // Iterate through the array, starting at the second element. + for (i in 1...input.length) + { + // Store the current element. + var current:T = input[i]; + // Store the index of the previous element. + var j:Int = i - 1; + + // While the previous element is greater than the current element, + // move the previous element to the right and move the index to the left. + while (j >= 0 && compare(input[j], current) > 0) + { + input[j + 1] = input[j]; + j--; + } + + // Insert the current element into the array. + input[j + 1] = current; + } + } +} + +/** + * A comparison function. + * Returns a negative number if the first argument is less than the second, + * a positive number if the first argument is greater than the second, + * or zero if the two arguments are equal. + */ +typedef CompareFunction<T> = T->T->Int; diff --git a/source/funkin/util/tools/ArrayTools.hx b/source/funkin/util/tools/ArrayTools.hx index 02671a8e8..c27f1bf43 100644 --- a/source/funkin/util/tools/ArrayTools.hx +++ b/source/funkin/util/tools/ArrayTools.hx @@ -22,4 +22,19 @@ class ArrayTools } return result; } + + /** + * Return the first element of the array that satisfies the predicate, or null if none do. + * @param input The array to search + * @param predicate The predicate to call + * @return The result + */ + public static function find<T>(input:Array<T>, predicate:T->Bool):Null<T> + { + for (element in input) + { + if (predicate(element)) return element; + } + return null; + } } From 2cae781984a91505f79771bfe6b8e2eb7bda22c6 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Thu, 22 Jun 2023 19:28:39 -0400 Subject: [PATCH 02/30] Sustains are kinda working? --- source/funkin/play/PlayState.hx | 20 +++---------- source/funkin/play/notes/NoteSprite.hx | 12 ++++++++ source/funkin/play/notes/Strumline.hx | 37 +++++++++++++++--------- source/funkin/play/notes/SustainTrail.hx | 28 ++++++++++++++++-- 4 files changed, 64 insertions(+), 33 deletions(-) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 5583b7fed..bc1d4fb30 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1696,10 +1696,6 @@ class PlayState extends MusicBeatState // Judge the miss. // NOTE: This is what handles the scoring. onNoteMiss(note); - - // Kill the note. - // NOTE: This is what handles recycling the note graphic. - playerStrumline.killNote(note); } } } @@ -1932,17 +1928,9 @@ class PlayState extends MusicBeatState popUpScore(note, input); } - playerStrumline.playConfirm(note.noteData.getDirection()); + playerStrumline.hitNote(note); - note.hasBeenHit = true; vocals.playerVolume = 1; - - if (!note.isSustainNote) - { - note.kill(); - // activeNotes.remove(note, true); - note.destroy(); - } } } @@ -2017,9 +2005,9 @@ class PlayState extends MusicBeatState note.active = false; note.visible = false; - note.kill(); - // activeNotes.remove(note, true); - note.destroy(); + // Kill the note. + // NOTE: This is what handles recycling the note graphic. + playerStrumline.killNote(note); } /** diff --git a/source/funkin/play/notes/NoteSprite.hx b/source/funkin/play/notes/NoteSprite.hx index e4b866cc4..655f7e380 100644 --- a/source/funkin/play/notes/NoteSprite.hx +++ b/source/funkin/play/notes/NoteSprite.hx @@ -175,4 +175,16 @@ class NoteSprite extends FlxSprite this.tooLate = false; this.hasMissed = false; } + + public override function kill():Void + { + super.kill(); + } + + public override function destroy():Void + { + // This function should ONLY get called as you leave PlayState entirely. + // Otherwise, we want the game to keep reusing note sprites to save memory. + super.destroy(); + } } diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 7be17a4cb..3cd503b3b 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -161,10 +161,10 @@ class Strumline extends FlxSpriteGroup * @param strumTime * @return Float */ - static function calculateNoteYPos(strumTime:Float):Float + static function calculateNoteYPos(strumTime:Float, ?vwoosh:Bool = true):Float { // Make the note move faster visually as it moves offscreen. - var vwoosh:Float = (strumTime < Conductor.songPosition) ? 2.0 : 1.0; + var vwoosh:Float = (strumTime < Conductor.songPosition) && vwoosh ? 2.0 : 1.0; var scrollSpeed:Float = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0; return Conductor.PIXELS_PER_MS * (Conductor.songPosition - strumTime) * scrollSpeed * vwoosh * (PreferencesMenu.getPref('downscroll') ? 1 : -1); @@ -206,13 +206,13 @@ class Strumline extends FlxSpriteGroup { note.visible = false; note.hasMissed = true; - if (note.holdNoteSprite != null) note.holdNoteSprite.missed = true; + if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true; } else { note.visible = true; note.hasMissed = false; - if (note.holdNoteSprite != null) note.holdNoteSprite.missed = false; + if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = false; } } @@ -223,25 +223,25 @@ class Strumline extends FlxSpriteGroup var renderWindowEnd = holdNote.strumTime + holdNote.fullSustainLength + Conductor.HIT_WINDOW_MS + RENDER_DISTANCE_MS / 8; - if (Conductor.songPosition >= renderWindowEnd || holdNote.sustainLength <= 0) + if (holdNote.missedNote && Conductor.songPosition >= renderWindowEnd) { // Hold note is offscreen, kill it. holdNote.visible = false; holdNote.kill(); // Do not destroy! Recycling is faster. } - else if (holdNote.sustainLength <= 0) + else if (holdNote.hitNote && holdNote.sustainLength <= 0) { // Hold note is completed, kill it. playStatic(holdNote.noteDirection); holdNote.visible = false; holdNote.kill(); } - else if (holdNote.sustainLength <= 10) + else if (holdNote.hitNote && holdNote.sustainLength <= 10) { // TODO: Better handle the weird edge case where the hold note is almost completed. holdNote.visible = false; } - else if (Conductor.songPosition > holdNote.strumTime && !holdNote.missed) + else if (Conductor.songPosition > holdNote.strumTime && holdNote.hitNote) { // Hold note is currently being hit, clip it off. holdConfirm(holdNote.noteDirection); @@ -258,7 +258,7 @@ class Strumline extends FlxSpriteGroup holdNote.y = this.y - INITIAL_OFFSET + STRUMLINE_SIZE / 2; } } - else if (holdNote.missed && (holdNote.fullSustainLength > holdNote.sustainLength)) + else if (holdNote.missedNote && (holdNote.fullSustainLength > holdNote.sustainLength)) { // Hold note was dropped before completing, keep it in its clipped state. holdNote.visible = true; @@ -285,11 +285,11 @@ class Strumline extends FlxSpriteGroup if (PreferencesMenu.getPref('downscroll')) { - holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime) - holdNote.height + STRUMLINE_SIZE / 2; + holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, false) - holdNote.height + STRUMLINE_SIZE / 2; } else { - holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime) + STRUMLINE_SIZE / 2; + holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, false) + STRUMLINE_SIZE / 2; } } } @@ -316,7 +316,15 @@ class Strumline extends FlxSpriteGroup public function hitNote(note:NoteSprite):Void { playConfirm(note.direction); + note.hasBeenHit = true; killNote(note); + + if (note.holdNoteSprite != null) + { + note.holdNoteSprite.hitNote = true; + note.holdNoteSprite.missedNote = false; + note.holdNoteSprite.alpha = 1.0; + } } public function killNote(note:NoteSprite):Void @@ -327,8 +335,8 @@ class Strumline extends FlxSpriteGroup if (note.holdNoteSprite != null) { - holdNoteSprite.missed = true; - holdNoteSprite.alpha = 0.6; + note.holdNoteSprite.missedNote = true; + note.holdNoteSprite.alpha = 0.6; } } @@ -416,7 +424,8 @@ class Strumline extends FlxSpriteGroup holdNoteSprite.noteDirection = note.getDirection(); holdNoteSprite.fullSustainLength = note.length; holdNoteSprite.sustainLength = note.length; - holdNoteSprite.missed = false; + holdNoteSprite.missedNote = false; + holdNoteSprite.hitNote = false; holdNoteSprite.x = this.x; holdNoteSprite.x += getXPos(DIRECTIONS[note.getDirection() % KEY_COUNT]); diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx index 0b84f2d64..a9cc4100e 100644 --- a/source/funkin/play/notes/SustainTrail.hx +++ b/source/funkin/play/notes/SustainTrail.hx @@ -33,10 +33,18 @@ class SustainTrail extends FlxSprite public var noteData:SongNoteData; /** - * Set to `true` if the user missed the note. - * The trail should be made transparent, with clipping and effects disabled + * Set to `true` if the user hit the note and is currently holding the sustain. + * Should display associated effects. */ - public var missed:Bool = false; // maybe BlendMode.MULTIPLY if missed somehow, drawTriangles does not support! + public var hitNote:Bool = false; + + /** + * Set to `true` if the user missed the note or released the sustain. + * Should make the trail transparent. + */ + public var missedNote:Bool = false; + + // maybe BlendMode.MULTIPLY if missed somehow, drawTriangles does not support! /** * A `Vector` of floats where each pair of numbers is treated as a coordinate location (an x, y pair). @@ -252,6 +260,20 @@ class SustainTrail extends FlxSprite } } + public override function kill():Void + { + super.kill(); + + strumTime = 0; + noteDirection = 0; + sustainLength = 0; + fullSustainLength = 0; + noteData = null; + + hitNote = false; + missedNote = false; + } + override public function destroy():Void { vertices = null; From 0fac9184281d71b3cc6e1db46426b38a101f1c5e Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Sun, 25 Jun 2023 12:36:00 -0400 Subject: [PATCH 03/30] Reworked anti-aliasing code (sprites now default to true) --- .vscode/settings.json | 3 ++- source/funkin/Alphabet.hx | 2 -- source/funkin/ComboMilestone.hx | 2 -- source/funkin/FreeplayState.hx | 1 - source/funkin/InitState.hx | 14 +++++--------- source/funkin/LoadingState.hx | 1 - source/funkin/MainMenuState.hx | 2 -- source/funkin/NoteSplash.hx | 2 -- source/funkin/TitleState.hx | 4 ---- source/funkin/freeplayStuff/FreeplayScore.hx | 1 - source/funkin/freeplayStuff/SongMenuItem.hx | 1 - .../funkin/graphics/adobeanimate/FlxAtlasSprite.hx | 2 -- source/funkin/play/ResultState.hx | 7 ------- source/funkin/play/notes/NoteSplash.hx | 1 - source/funkin/play/notes/NoteSprite.hx | 1 - source/funkin/play/notes/StrumlineNote.hx | 2 -- source/funkin/play/notes/SustainTrail.hx | 2 ++ source/funkin/ui/AtlasText.hx | 1 - source/funkin/ui/ColorsMenu.hx | 1 - source/funkin/ui/MenuList.hx | 1 - source/funkin/ui/PopUpStuff.hx | 5 ++++- source/funkin/ui/PreferencesMenu.hx | 2 -- source/funkin/ui/StickerSubState.hx | 1 - source/funkin/ui/TallyCounter.hx | 1 - .../funkin/ui/animDebugShit/DebugBoundingState.hx | 1 - 25 files changed, 13 insertions(+), 48 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index dd4cd7aef..86ae2b643 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -116,5 +116,6 @@ "target": "html5", "args": ["-debug", "-watch"] } - ] + ], + "cmake.configureOnOpen": false } diff --git a/source/funkin/Alphabet.hx b/source/funkin/Alphabet.hx index a501707be..3835ae660 100644 --- a/source/funkin/Alphabet.hx +++ b/source/funkin/Alphabet.hx @@ -243,8 +243,6 @@ class AlphaCharacter extends FlxSprite super(x, y); var tex = Paths.getSparrowAtlas('alphabet'); frames = tex; - - antialiasing = true; } public function createBold(letter:String) diff --git a/source/funkin/ComboMilestone.hx b/source/funkin/ComboMilestone.hx index b72eda2fa..79e454c44 100644 --- a/source/funkin/ComboMilestone.hx +++ b/source/funkin/ComboMilestone.hx @@ -26,7 +26,6 @@ class ComboMilestone extends FlxTypedSpriteGroup<FlxSprite> effectStuff.frames = Paths.getSparrowAtlas('comboMilestone'); effectStuff.animation.addByPrefix('funny', 'NOTE COMBO animation', 24, false); effectStuff.animation.play('funny'); - effectStuff.antialiasing = true; effectStuff.animation.finishCallback = function(nameThing) { kill(); }; @@ -108,7 +107,6 @@ class ComboMilestoneNumber extends FlxSprite frames = Paths.getSparrowAtlas('comboMilestoneNumbers'); animation.addByPrefix(stringNum, stringNum, 24, false); animation.play(stringNum); - antialiasing = true; updateHitbox(); } diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx index 322e79e31..1c226dbb5 100644 --- a/source/funkin/FreeplayState.hx +++ b/source/funkin/FreeplayState.hx @@ -338,7 +338,6 @@ class FreeplayState extends MusicBeatSubState fnfHighscoreSpr.animation.addByPrefix("highscore", "highscore", 24, false); fnfHighscoreSpr.visible = false; fnfHighscoreSpr.setGraphicSize(0, Std.int(fnfHighscoreSpr.height * 1)); - fnfHighscoreSpr.antialiasing = true; fnfHighscoreSpr.updateHitbox(); add(fnfHighscoreSpr); diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 0ebe7871a..52bdb1015 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -33,15 +33,11 @@ class InitState extends FlxTransitionableState { override public function create():Void { - trace('This is a debug build, loading InitState...'); - #if android - FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK]; - #end - #if newgrounds - NGio.init(); - #end - #if discord_rpc - DiscordClient.initialize(); + // + // FLIXEL SETUP + // + // This ain't a pixel art game! (most of the time) + FlxSprite.defaultAntialiasing = true; Application.current.onExit.add(function(exitCode) { DiscordClient.shutdown(); diff --git a/source/funkin/LoadingState.hx b/source/funkin/LoadingState.hx index 604e78f79..3ec2e1005 100644 --- a/source/funkin/LoadingState.hx +++ b/source/funkin/LoadingState.hx @@ -42,7 +42,6 @@ class LoadingState extends MusicBeatState funkay.loadGraphic(Paths.image('funkay')); funkay.setGraphicSize(0, FlxG.height); funkay.updateHitbox(); - funkay.antialiasing = true; add(funkay); funkay.scrollFactor.set(); funkay.screenCenter(); diff --git a/source/funkin/MainMenuState.hx b/source/funkin/MainMenuState.hx index 82fcac77d..348bf8d17 100644 --- a/source/funkin/MainMenuState.hx +++ b/source/funkin/MainMenuState.hx @@ -68,7 +68,6 @@ class MainMenuState extends MusicBeatState bg.setGraphicSize(Std.int(bg.width * 1.2)); bg.updateHitbox(); bg.screenCenter(); - bg.antialiasing = true; add(bg); camFollow = new FlxObject(0, 0, 1, 1); @@ -82,7 +81,6 @@ class MainMenuState extends MusicBeatState magenta.x = bg.x; magenta.y = bg.y; magenta.visible = false; - magenta.antialiasing = true; magenta.color = 0xFFfd719b; if (PreferencesMenu.preferences.get('flashing-menu')) add(magenta); // magenta.scrollFactor.set(); diff --git a/source/funkin/NoteSplash.hx b/source/funkin/NoteSplash.hx index 7f3a8c5e4..a32a39c08 100644 --- a/source/funkin/NoteSplash.hx +++ b/source/funkin/NoteSplash.hx @@ -22,8 +22,6 @@ class NoteSplash extends FlxSprite setupNoteSplash(x, y, noteData); - antialiasing = true; - // alpha = 0.75; } diff --git a/source/funkin/TitleState.hx b/source/funkin/TitleState.hx index bc6ef571d..bd4e7084c 100644 --- a/source/funkin/TitleState.hx +++ b/source/funkin/TitleState.hx @@ -149,7 +149,6 @@ class TitleState extends MusicBeatState logoBl = new FlxSprite(-150, -100); logoBl.frames = Paths.getSparrowAtlas('logoBumpin'); - logoBl.antialiasing = true; logoBl.animation.addByPrefix('bump', 'logo bumpin', 24); logoBl.animation.play('bump'); @@ -161,7 +160,6 @@ class TitleState extends MusicBeatState gfDance.frames = Paths.getSparrowAtlas('gfDanceTitle'); gfDance.animation.addByIndices('danceLeft', 'gfDance', [30, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "", 24, false); gfDance.animation.addByIndices('danceRight', 'gfDance', [15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29], "", 24, false); - gfDance.antialiasing = true; add(gfDance); @@ -180,7 +178,6 @@ class TitleState extends MusicBeatState titleText.frames = Paths.getSparrowAtlas('titleEnter'); titleText.animation.addByPrefix('idle', "Press Enter to Begin", 24); titleText.animation.addByPrefix('press', "ENTER PRESSED", 24); - titleText.antialiasing = true; titleText.animation.play('idle'); titleText.updateHitbox(); // titleText.screenCenter(X); @@ -223,7 +220,6 @@ class TitleState extends MusicBeatState ngSpr.updateHitbox(); ngSpr.screenCenter(X); - ngSpr.antialiasing = true; FlxG.mouse.visible = false; diff --git a/source/funkin/freeplayStuff/FreeplayScore.hx b/source/funkin/freeplayStuff/FreeplayScore.hx index d22dd2276..ec8f4baa7 100644 --- a/source/funkin/freeplayStuff/FreeplayScore.hx +++ b/source/funkin/freeplayStuff/FreeplayScore.hx @@ -117,7 +117,6 @@ class ScoreNum extends FlxSprite this.digit = initDigit; animation.play(numToString[digit], true); - antialiasing = true; setGraphicSize(Std.int(width * 0.4)); updateHitbox(); diff --git a/source/funkin/freeplayStuff/SongMenuItem.hx b/source/funkin/freeplayStuff/SongMenuItem.hx index a32b387a3..3d9f9dd04 100644 --- a/source/funkin/freeplayStuff/SongMenuItem.hx +++ b/source/funkin/freeplayStuff/SongMenuItem.hx @@ -47,7 +47,6 @@ class SongMenuItem extends FlxSpriteGroup favIcon.frames = Paths.getSparrowAtlas('freeplay/favHeart'); favIcon.animation.addByPrefix('fav', "favorite heart", 24, false); favIcon.animation.play('fav'); - favIcon.antialiasing = true; favIcon.setGraphicSize(60, 60); add(favIcon); diff --git a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx index aad9cd851..ed2418930 100644 --- a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx +++ b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx @@ -42,8 +42,6 @@ class FlxAtlasSprite extends FlxAnimate throw 'FlxAtlasSprite not initialized properly. Are you sure the path (${path}) exists?'; } - this.antialiasing = true; - onAnimationFinish.add(cleanupAnimation); // This defaults the sprite to play the first animation in the atlas, diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx index 302858f2f..aaa2b6d1d 100644 --- a/source/funkin/play/ResultState.hx +++ b/source/funkin/play/ResultState.hx @@ -114,7 +114,6 @@ class ResultState extends MusicBeatSubState soundSystem.animation.play("idle"); soundSystem.visible = true; }); - soundSystem.antialiasing = true; add(soundSystem); difficulty = new FlxSprite(555); @@ -132,7 +131,6 @@ class ResultState extends MusicBeatSubState } difficulty.loadGraphic(Paths.image("resultScreen/" + diffSpr)); - difficulty.antialiasing = true; add(difficulty); var fontLetters:String = "AaBbCcDdEeFfGgHhiIJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:1234567890"; @@ -148,7 +146,6 @@ class ResultState extends MusicBeatSubState songName.text += PlayState.instance.currentSong.songId; } - songName.antialiasing = true; songName.letterSpacing = -15; songName.angle = -4.1; add(songName); @@ -164,22 +161,18 @@ class ResultState extends MusicBeatSubState var blackTopBar:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/topBarBlack")); blackTopBar.y = -blackTopBar.height; FlxTween.tween(blackTopBar, {y: 0}, 0.4, {ease: FlxEase.quartOut, startDelay: 0.5}); - blackTopBar.antialiasing = true; add(blackTopBar); var resultsAnim:FlxSprite = new FlxSprite(-200, -10); resultsAnim.frames = Paths.getSparrowAtlas("resultScreen/results"); resultsAnim.animation.addByPrefix("result", "results", 24, false); resultsAnim.animation.play("result"); - resultsAnim.antialiasing = true; add(resultsAnim); var ratingsPopin:FlxSprite = new FlxSprite(-150, 120); ratingsPopin.frames = Paths.getSparrowAtlas("resultScreen/ratingsPopin"); ratingsPopin.animation.addByPrefix("idle", "Categories", 24, false); - // ratingsPopin.animation.play("idle"); ratingsPopin.visible = false; - ratingsPopin.antialiasing = true; add(ratingsPopin); var scorePopin:FlxSprite = new FlxSprite(-180, 520); diff --git a/source/funkin/play/notes/NoteSplash.hx b/source/funkin/play/notes/NoteSplash.hx index 90c9825e9..bbe08546c 100644 --- a/source/funkin/play/notes/NoteSplash.hx +++ b/source/funkin/play/notes/NoteSplash.hx @@ -26,7 +26,6 @@ class NoteSplash extends FlxSprite setup(); this.alpha = ALPHA; - this.antialiasing = true; this.animation.finishCallback = this.onAnimationFinished; } diff --git a/source/funkin/play/notes/NoteSprite.hx b/source/funkin/play/notes/NoteSprite.hx index 655f7e380..697a29d80 100644 --- a/source/funkin/play/notes/NoteSprite.hx +++ b/source/funkin/play/notes/NoteSprite.hx @@ -162,7 +162,6 @@ class NoteSprite extends FlxSprite setGraphicSize(Strumline.STRUMLINE_SIZE); updateHitbox(); - antialiasing = true; } public override function revive():Void diff --git a/source/funkin/play/notes/StrumlineNote.hx b/source/funkin/play/notes/StrumlineNote.hx index 7fbb3a0f9..1d24759dc 100644 --- a/source/funkin/play/notes/StrumlineNote.hx +++ b/source/funkin/play/notes/StrumlineNote.hx @@ -92,8 +92,6 @@ class StrumlineNote extends FlxSprite this.animation.addByIndices('confirm-hold', 'right confirm', [2, 3, 4, 5], '', 24, true, false, false); } - this.antialiasing = true; - this.setGraphicSize(Std.int(Strumline.STRUMLINE_SIZE * 1.55)); this.updateHitbox(); this.playStatic(); diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx index a9cc4100e..c8f629c90 100644 --- a/source/funkin/play/notes/SustainTrail.hx +++ b/source/funkin/play/notes/SustainTrail.hx @@ -88,6 +88,8 @@ class SustainTrail extends FlxSprite super(0, 0, fileName); antialiasing = true; + + // TODO: Why does this reference pixel stuff? if (fileName == "arrowEnds") { endOffset = bottomClip = 1; diff --git a/source/funkin/ui/AtlasText.hx b/source/funkin/ui/AtlasText.hx index c311e387a..76837c7ed 100644 --- a/source/funkin/ui/AtlasText.hx +++ b/source/funkin/ui/AtlasText.hx @@ -171,7 +171,6 @@ class AtlasChar extends FlxSprite super(x, y); frames = atlas; this.char = char; - antialiasing = true; } function set_char(value:String) diff --git a/source/funkin/ui/ColorsMenu.hx b/source/funkin/ui/ColorsMenu.hx index 68fc7e7e0..dfa0cf067 100644 --- a/source/funkin/ui/ColorsMenu.hx +++ b/source/funkin/ui/ColorsMenu.hx @@ -31,7 +31,6 @@ class ColorsMenu extends Page add(_effectSpr); _effectSpr.y = 0; _effectSpr.x = i * 130; - _effectSpr.antialiasing = true; _effectSpr.scale.x = _effectSpr.scale.y = 0.7; // _effectSpr.setGraphicSize(); _effectSpr.height = note.height; diff --git a/source/funkin/ui/MenuList.hx b/source/funkin/ui/MenuList.hx index 39b53f998..f1de8d40e 100644 --- a/source/funkin/ui/MenuList.hx +++ b/source/funkin/ui/MenuList.hx @@ -225,7 +225,6 @@ class MenuItem extends FlxSprite { super(x, y); - antialiasing = true; setData(name, callback); idle(); } diff --git a/source/funkin/ui/PopUpStuff.hx b/source/funkin/ui/PopUpStuff.hx index 20380f50a..3e848b9e6 100644 --- a/source/funkin/ui/PopUpStuff.hx +++ b/source/funkin/ui/PopUpStuff.hx @@ -45,6 +45,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite> if (PlayState.instance.currentStageId.startsWith('school')) { rating.setGraphicSize(Std.int(rating.width * Constants.PIXEL_ART_SCALE * 0.7)); + rating.antialiasing = false; } else { @@ -95,6 +96,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite> if (PlayState.instance.currentStageId.startsWith('school')) { comboSpr.setGraphicSize(Std.int(comboSpr.width * Constants.PIXEL_ART_SCALE * 0.7)); + comboSpr.antialiasing = false; } else { @@ -134,11 +136,12 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite> if (PlayState.instance.currentStageId.startsWith('school')) { numScore.setGraphicSize(Std.int(numScore.width * Constants.PIXEL_ART_SCALE)); + numScore.antialiasing = false; } else { - numScore.antialiasing = true; numScore.setGraphicSize(Std.int(numScore.width * 0.5)); + numScore.antialiasing = true; } numScore.updateHitbox(); diff --git a/source/funkin/ui/PreferencesMenu.hx b/source/funkin/ui/PreferencesMenu.hx index 0bf83c125..4fa8f7f5b 100644 --- a/source/funkin/ui/PreferencesMenu.hx +++ b/source/funkin/ui/PreferencesMenu.hx @@ -177,8 +177,6 @@ class CheckboxThingie extends FlxSprite animation.addByPrefix('static', 'Check Box unselected', 24, false); animation.addByPrefix('checked', 'Check Box selecting animation', 24, false); - antialiasing = true; - setGraphicSize(Std.int(width * 0.7)); updateHitbox(); diff --git a/source/funkin/ui/StickerSubState.hx b/source/funkin/ui/StickerSubState.hx index 981c79dfa..e9d528773 100644 --- a/source/funkin/ui/StickerSubState.hx +++ b/source/funkin/ui/StickerSubState.hx @@ -266,7 +266,6 @@ class StickerSprite extends FlxSprite super(x, y); loadGraphic(Paths.image('transitionSwag/' + stickerSet + '/' + stickerName)); updateHitbox(); - antialiasing = true; scrollFactor.set(); } } diff --git a/source/funkin/ui/TallyCounter.hx b/source/funkin/ui/TallyCounter.hx index bcc39ca7b..72857671e 100644 --- a/source/funkin/ui/TallyCounter.hx +++ b/source/funkin/ui/TallyCounter.hx @@ -80,7 +80,6 @@ class TallyNumber extends FlxSprite animation.addByPrefix(Std.string(i), i + " small", 24, false); animation.play(Std.string(digit)); - antialiasing = true; updateHitbox(); } } diff --git a/source/funkin/ui/animDebugShit/DebugBoundingState.hx b/source/funkin/ui/animDebugShit/DebugBoundingState.hx index da7a4e3ff..5a7e555de 100644 --- a/source/funkin/ui/animDebugShit/DebugBoundingState.hx +++ b/source/funkin/ui/animDebugShit/DebugBoundingState.hx @@ -143,7 +143,6 @@ class DebugBoundingState extends FlxState addInfo('Width', bf.width); addInfo('Height', bf.height); - swagOutlines.antialiasing = true; spriteSheetView.add(swagOutlines); FlxG.stage.window.onDropFile.add(function(path:String) { From 6f737a1805987449622870ab2c5430ed78af51ab Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Sun, 25 Jun 2023 12:36:12 -0400 Subject: [PATCH 04/30] Update hxCodec to latest stable. --- hmm.json | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/hmm.json b/hmm.json index a1d78a29f..ac16be03d 100644 --- a/hmm.json +++ b/hmm.json @@ -66,10 +66,8 @@ }, { "name": "hxCodec", - "type": "git", - "dir": null, - "ref": "c42ab99", - "url": "https://github.com/polybiusproxy/hxCodec" + "type": "haxelib", + "version": "3.0.1" }, { "name": "hxcpp", @@ -123,4 +121,4 @@ "version": null } ] -} +} \ No newline at end of file From 4c85e590372d2c213432e4f7b135ec473c881b4c Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Sun, 25 Jun 2023 12:36:21 -0400 Subject: [PATCH 05/30] Rewrote documentation for InitState --- source/funkin/util/WindowUtil.hx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/source/funkin/util/WindowUtil.hx b/source/funkin/util/WindowUtil.hx index 42930570f..6e6a41641 100644 --- a/source/funkin/util/WindowUtil.hx +++ b/source/funkin/util/WindowUtil.hx @@ -11,17 +11,21 @@ import flixel.util.FlxSignal.FlxTypedSignal; #end class WindowUtil { + /** + * Runs platform-specific code to open a URL in a web browser. + * @param targetUrl The URL to open. + */ public static function openURL(targetUrl:String) { #if CAN_OPEN_LINKS #if linux - // Sys.command('/usr/bin/xdg-open', [, "&"]); Sys.command('/usr/bin/xdg-open', [targetUrl, "&"]); #else + // This should work on Windows and HTML5. FlxG.openURL(targetUrl); #end #else - trace('Cannot open'); + throw 'Cannot open URLs on this platform.'; #end } @@ -30,6 +34,10 @@ class WindowUtil */ public static final windowExit:FlxTypedSignal<Int->Void> = new FlxTypedSignal<Int->Void>(); + /** + * Wires up FlxSignals that happen based on window activity. + * For example, we can run a callback when the window is closed. + */ public static function initWindowEvents() { // onUpdate is called every frame just before rendering. From 6d5c5f5acb874d04534104ebcb8a479ce027ac8c Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Mon, 26 Jun 2023 20:39:47 -0400 Subject: [PATCH 06/30] Refactor InitState plus fix a couple crash bugs --- source/Main.hx | 11 - source/funkin/InitState.hx | 375 +++++++++++++---------- source/funkin/PlayerSettings.hx | 10 +- source/funkin/TitleState.hx | 33 +- source/funkin/ui/story/StoryMenuState.hx | 2 +- 5 files changed, 224 insertions(+), 207 deletions(-) diff --git a/source/Main.hx b/source/Main.hx index 006b54e18..9b70549ab 100644 --- a/source/Main.hx +++ b/source/Main.hx @@ -77,17 +77,6 @@ class Main extends Sprite * -Eric */ - #if !debug - /** - * Someone was like "hey let's make a state that only runs code on debug builds" - * then put essential initialization code in it. - * The easiest fix is to make it run in all builds. - * -Eric - */ - // TODO: Fix this properly. - // initialState = funkin.TitleState; - #end - initHaxeUI(); addChild(new FlxGame(gameWidth, gameHeight, initialState, framerate, framerate, skipSplash, startFullscreen)); diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 52bdb1015..4d74e1a05 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -6,52 +6,82 @@ import flixel.addons.transition.TransitionData; import flixel.graphics.FlxGraphic; import flixel.math.FlxPoint; import flixel.math.FlxRect; +import flixel.FlxSprite; import flixel.system.debug.log.LogStyle; import flixel.util.FlxColor; -import funkin.modding.module.ModuleHandler; -import funkin.play.character.CharacterData.CharacterDataParser; -import funkin.play.cutscene.dialogue.ConversationDataParser; -import funkin.play.cutscene.dialogue.DialogueBoxDataParser; -import funkin.play.cutscene.dialogue.SpeakerDataParser; -import funkin.play.event.SongEventData.SongEventParser; -import funkin.play.PlayState; -import funkin.play.song.SongData.SongDataParser; -import funkin.play.stage.StageData.StageDataParser; import funkin.ui.PreferencesMenu; import funkin.util.macro.MacroUtil; import funkin.util.WindowUtil; +import funkin.play.PlayStatePlaylist; import openfl.display.BitmapData; #if discord_rpc import Discord.DiscordClient; #end /** - * Initializes the game state using custom defines. - * Only used in Debug builds. + * The initialization state has several functions: + * - Calls code to set up the game, including loading saves and parsing game data. + * - Chooses whether to start via debug or via launching normally. */ class InitState extends FlxTransitionableState { - override public function create():Void + /** + * Perform a bunch of game setup, then immediately transition to the title screen. + */ + public override function create():Void + { + setupShit(); + + loadSaveData(); + + startGame(); + } + + /** + * Setup a bunch of important Flixel stuff. + */ + function setupShit() { // - // FLIXEL SETUP + // GAME SETUP // + + // Setup window events (like callbacks for onWindowClose) + WindowUtil.initWindowEvents(); + // Disable the thing on Windows where it tries to send a bug report to Microsoft because why do they care? + WindowUtil.disableCrashHandler(); + // This ain't a pixel art game! (most of the time) FlxSprite.defaultAntialiasing = true; - Application.current.onExit.add(function(exitCode) { - DiscordClient.shutdown(); - }); - #end + // Disable default keybinds for volume (we manually control volume in MusicBeatState with custom binds) + FlxG.sound.volumeUpKeys = []; + FlxG.sound.volumeDownKeys = []; + FlxG.sound.muteKeys = []; - // ==== flixel shit ==== // + // TODO: Make sure volume still saves/loads properly. + // if (FlxG.save.data.volume != null) FlxG.sound.volume = FlxG.save.data.volume; + // if (FlxG.save.data.mute != null) FlxG.sound.muted = FlxG.save.data.mute; + // Set the game to a lower frame rate while it is in the background. + FlxG.game.focusLostFramerate = 30; + + // + // FLIXEL DEBUG SETUP + // + #if debug + // Disable using ~ to open the console (we use that for the Editor menu) + FlxG.debugger.toggleKeys = [F2]; + + // Adds an additional Close Debugger button. // This big obnoxious white button is for MOBILE, so that you can press it // easily with your finger when debug bullshit pops up during testing lol! FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function() { FlxG.debugger.visible = false; }); + // Adds a red button to the debugger. + // This pauses the game AND the music! This ensures the Conductor stops. FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFFCC2233), function() { if (FlxG.vcr.paused) { @@ -77,7 +107,8 @@ class InitState extends FlxTransitionableState } }); - #if FLX_DEBUG + // Adds a blue button to the debugger. + // This skips forward in the song. FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFF2222CC), function() { FlxG.game.debugger.vcr.onStep(); @@ -90,175 +121,197 @@ class InitState extends FlxTransitionableState FlxG.sound.music.pause(); FlxG.sound.music.time += FlxG.elapsed * 1000; }); + + // Make errors and warnings less annoying. + // TODO: Disable this so we know to fix warnings. + if (false) + { + LogStyle.ERROR.openConsole = false; + LogStyle.ERROR.errorSound = null; + LogStyle.WARNING.openConsole = false; + LogStyle.WARNING.errorSound = null; + } #end - FlxG.sound.muteKeys = [ZERO]; - FlxG.game.focusLostFramerate = 60; - - // FlxG.stage.window.borderless = true; - // FlxG.stage.window.mouseLock = true; + // + // FLIXEL TRANSITIONS + // + // Diamond Transition var diamond:FlxGraphic = FlxGraphic.fromClass(GraphicTransTileDiamond); diamond.persist = true; diamond.destroyOnNoUse = false; - FlxTransitionableState.defaultTransIn = new TransitionData(FADE, FlxColor.BLACK, 1, new FlxPoint(0, -1), {asset: diamond, width: 32, height: 32}, - new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4)); - FlxTransitionableState.defaultTransOut = new TransitionData(FADE, FlxColor.BLACK, 0.7, new FlxPoint(0, 1), {asset: diamond, width: 32, height: 32}, - new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4)); - - // ===== save shit ===== // - - FlxG.save.bind('funkin', 'ninjamuffin99'); - - // https://github.com/HaxeFlixel/flixel/pull/2396 - // IF/WHEN MY PR GOES THRU AND IT GETS INTO MAIN FLIXEL, DELETE THIS CHUNKOF CODE, AND THEN UNCOMMENT THE LINE BELOW - // FlxG.sound.loadSavedPrefs(); - - if (FlxG.save.data.volume != null) FlxG.sound.volume = FlxG.save.data.volume; - if (FlxG.save.data.mute != null) FlxG.sound.muted = FlxG.save.data.mute; - - // Make errors and warnings less annoying. - LogStyle.ERROR.openConsole = false; - LogStyle.ERROR.errorSound = null; - LogStyle.WARNING.openConsole = false; - LogStyle.WARNING.errorSound = null; - - // FlxG.save.close(); - // FlxG.sound.loadSavedPrefs(); - WindowUtil.initWindowEvents(); - WindowUtil.disableCrashHandler(); - - PreferencesMenu.initPrefs(); - PlayerSettings.init(); - Highscore.load(); - - if (FlxG.save.data.weekUnlocked != null) - { - // FIX LATER!!! - // WEEK UNLOCK PROGRESSION!! - // StoryMenuState.weekUnlocked = FlxG.save.data.weekUnlocked; - - // if (StoryMenuState.weekUnlocked.length < 4) StoryMenuState.weekUnlocked.insert(0, true); - - // QUICK PATCH OOPS! - // if (!StoryMenuState.weekUnlocked[0]) StoryMenuState.weekUnlocked[0] = true; - } - - if (FlxG.save.data.seenVideo != null) VideoState.seenVideo = FlxG.save.data.seenVideo; - - // ===== fuck outta here ===== // - - // FlxTransitionableState.skipNextTransOut = true; + // FlxTransitionableState.defaultTransIn = new TransitionData(FADE, FlxColor.BLACK, 1, new FlxPoint(0, -1), {asset: diamond, width: 32, height: 32}, + // new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4)); + // FlxTransitionableState.defaultTransOut = new TransitionData(FADE, FlxColor.BLACK, 0.7, new FlxPoint(0, 1), {asset: diamond, width: 32, height: 32}, + // new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4)); + // Don't play transition in when entering the title state. FlxTransitionableState.skipNextTransIn = true; - // TODO: Register custom event callbacks here + // + // NEWGROUNDS API SETUP + // + #if newgrounds + NGio.init(); + #end + // + // DISCORD API SETUP + // + #if discord_rpc + DiscordClient.initialize(); + + Application.current.onExit.add(function(exitCode) { + DiscordClient.shutdown(); + }); + #end + + // + // ANDROID SETUP + // + #if android + FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK]; + #end + + // + // GAME DATA PARSING + // funkin.data.level.LevelRegistry.instance.loadEntries(); - SongEventParser.loadEventCache(); - ConversationDataParser.loadConversationCache(); - DialogueBoxDataParser.loadDialogueBoxCache(); - SpeakerDataParser.loadSpeakerCache(); - SongDataParser.loadSongCache(); - StageDataParser.loadStageCache(); - CharacterDataParser.loadCharacterCache(); - ModuleHandler.buildModuleCallbacks(); - ModuleHandler.loadModuleCache(); + funkin.play.event.SongEventData.SongEventParser.loadEventCache(); + funkin.play.cutscene.dialogue.ConversationDataParser.loadConversationCache(); + funkin.play.cutscene.dialogue.DialogueBoxDataParser.loadDialogueBoxCache(); + funkin.play.cutscene.dialogue.SpeakerDataParser.loadSpeakerCache(); + funkin.play.song.SongData.SongDataParser.loadSongCache(); + funkin.play.stage.StageData.StageDataParser.loadStageCache(); + funkin.play.character.CharacterData.CharacterDataParser.loadCharacterCache(); + funkin.modding.module.ModuleHandler.buildModuleCallbacks(); + funkin.modding.module.ModuleHandler.loadModuleCache(); - FlxG.debugger.toggleKeys = [F2]; + funkin.modding.module.ModuleHandler.callOnCreate(); + } - ModuleHandler.callOnCreate(); + /** + * Retrive and parse data from the user's save. + */ + function loadSaveData() + { + // Bind save data. + // TODO: Migrate save data to a better format. + FlxG.save.bind('funkin', 'ninjamuffin99'); - #if song - var song:String = getSong(); + // Load player options from save data. + PreferencesMenu.initPrefs(); + // Load controls from save data. + PlayerSettings.init(); + // Load highscores from save data. + Highscore.load(); + // TODO: Load level/character/cosmetic unlocks from save data. + } - var weeks:Array<Array<String>> = [ - ['bopeebo', 'fresh', 'dadbattle'], - ['spookeez', 'south', 'monster'], - ['spooky', 'spooky', 'monster'], - ['pico', 'philly', 'blammed'], - ['satin-panties', 'high', 'milf'], - ['cocoa', 'eggnog', 'winter-horrorland'], - ['senpai', 'roses', 'thorns'], - ['ugh', 'guns', 'stress'] - ]; - - var week:Int = 0; - for (i in 0...weeks.length) - { - if (weeks[i].contains(song)) - { - week = i + 1; - break; - } - } - - if (week == 0) throw 'Invalid -D song=$song'; - - startSong(week, song, false); - #elseif week - var week:Int = getWeek(); - - var songs:Array<String> = [ - 'bopeebo', - 'spookeez', - 'spooky', - 'pico', - 'satin-panties', - 'cocoa', - 'senpai', - 'ugh' - ]; - - if (week <= 0 || week >= songs.length) throw 'invalid -D week=' + week; - - startSong(week, songs[week - 1], true); - #elseif FREEPLAY + /** + * Start the game. + * + * By default, moves to the `TitleState`. + * But based on compile defines, the game can start immediately on a specific song, + * or immediately in a specific debug menu. + */ + function startGame():Void + { + #if SONG // -DSONG=bopeebo + startSong(defineSong(), defineDifficulty()); + #elseif LEVEL // -DLEVEL=week1 -DDIFFICULTY=hard + startLevel(defineLevel(), defineDifficulty()); + #elseif FREEPLAY // -DFREEPLAY FlxG.switchState(new FreeplayState()); - #elseif ANIMATE + #elseif ANIMATE // -DANIMATE FlxG.switchState(new funkin.ui.animDebugShit.FlxAnimateTest()); - #elseif CHARTING + #elseif CHARTING // -DCHARTING FlxG.switchState(new funkin.ui.debug.charting.ChartEditorState()); - #elseif STAGEBUILD - FlxG.switchState(new StageBuilderState()); - #elseif FIGHT - FlxG.switchState(new PicoFight()); - #elseif ANIMDEBUG + #elseif STAGEBUILD // -DSTAGEBUILD + FlxG.switchState(new funkin.ui.stageBullshit.StageBuilderState()); + #elseif ANIMDEBUG // -DANIMDEBUG FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState()); - #elseif LATENCY - FlxG.switchState(new LatencyState()); - #elseif NETTEST - FlxG.switchState(new netTest.NetTest()); + #elseif LATENCY // -DLATENCY + FlxG.switchState(new funkin.LatencyState()); #else - FlxG.sound.cache(Paths.music('freakyMenu')); - FlxG.switchState(new TitleState()); + startGameNormally(); #end } - function startSong(week, song, isStoryMode):Void + /** + * Start the game by moving to the title state and play the game as normal. + */ + function startGameNormally():Void { - var dif:Int = getDif(); + FlxG.sound.cache(Paths.music('freakyMenu')); + FlxG.switchState(new TitleState()); + } - var targetDifficulty = switch (dif) + /** + * Start the game by directly loading into a specific song. + * @param songId + * @param difficultyId + */ + function startSong(songId:String, difficultyId:String = 'normal'):Void + { + var songData:funkin.play.song.Song = funkin.play.song.SongData.SongDataParser.fetchSong(songId); + + if (songData == null) { - case 0: 'easy'; - case 1: 'normal'; - case 2: 'hard'; - default: 'normal'; - }; - LoadingState.loadAndSwitchState(new PlayState( + startGameNormally(); + return; + } + + LoadingState.loadAndSwitchState(new funkin.play.PlayState( { - targetSong: SongDataParser.fetchSong(song), - targetDifficulty: targetDifficulty, + targetSong: songData, + targetDifficulty: difficultyId, })); } + + /** + * Start the game by directly loading into a specific story mode level. + * @param levelId + * @param difficultyId + */ + function startLevel(levelId:String, difficultyId:String = 'normal'):Void + { + var currentLevel:funkin.ui.story.Level = funkin.data.level.LevelRegistry.instance.fetchEntry(levelId); + + if (currentLevel == null) + { + startGameNormally(); + return; + } + + PlayStatePlaylist.playlistSongIds = currentLevel.getSongs(); + PlayStatePlaylist.isStoryMode = true; + PlayStatePlaylist.campaignScore = 0; + + var targetSongId:String = PlayStatePlaylist.playlistSongIds.shift(); + + var targetSong:funkin.play.song.Song = funkin.play.song.SongData.SongDataParser.fetchSong(targetSongId); + + LoadingState.loadAndSwitchState(new funkin.play.PlayState( + { + targetSong: targetSong, + targetDifficulty: difficultyId, + })); + } + + function defineSong():String + { + return MacroUtil.getDefine('SONG'); + } + + function defineLevel():String + { + return MacroUtil.getDefine('LEVEL'); + } + + function defineDifficulty():String + { + return MacroUtil.getDefine('DIFFICULTY'); + } } - -function getWeek():Int - return Std.parseInt(MacroUtil.getDefine('week')); - -function getSong():String - return MacroUtil.getDefine('song'); - -function getDif():Int - return Std.parseInt(MacroUtil.getDefine('dif', '1')); diff --git a/source/funkin/PlayerSettings.hx b/source/funkin/PlayerSettings.hx index 1b64d26c2..54fd559fb 100644 --- a/source/funkin/PlayerSettings.hx +++ b/source/funkin/PlayerSettings.hx @@ -26,8 +26,10 @@ class PlayerSettings // public var avatar:Player; // public var camera(get, never):PlayCamera; - function new(id) + function new(id:Int) { + trace('loading player settings for id: $id'); + this.id = id; this.controls = new Controls('player$id', None); @@ -52,7 +54,11 @@ class PlayerSettings } } - if (useDefault) controls.setKeyboardScheme(Solo); + if (useDefault) + { + trace("falling back to default control scheme"); + controls.setKeyboardScheme(Solo); + } // Apply loaded settings. PreciseInputManager.instance.initializeKeys(controls); diff --git a/source/funkin/TitleState.hx b/source/funkin/TitleState.hx index bd4e7084c..59845ed40 100644 --- a/source/funkin/TitleState.hx +++ b/source/funkin/TitleState.hx @@ -44,6 +44,7 @@ class TitleState extends MusicBeatState override public function create():Void { + super.create(); swagShader = new ColorSwap(); curWacky = FlxG.random.getObject(getIntroTextShit()); @@ -51,38 +52,6 @@ class TitleState extends MusicBeatState // DEBUG BULLSHIT - super.create(); - - /* - #elseif web - - - if (!initialized) - { - - video = new Video(); - FlxG.stage.addChild(video); - - var netConnection = new NetConnection(); - netConnection.connect(null); - - netStream = new NetStream(netConnection); - netStream.client = {onMetaData: client_onMetaData}; - netStream.addEventListener(AsyncErrorEvent.ASYNC_ERROR, netStream_onAsyncError); - netConnection.addEventListener(NetStatusEvent.NET_STATUS, netConnection_onNetStatus); - // netStream.addEventListener(NetStatusEvent.NET_STATUS) // netStream.play(Paths.file('music/kickstarterTrailer.mp4')); - - overlay = new Sprite(); - overlay.graphics.beginFill(0, 0.5); - overlay.graphics.drawRect(0, 0, 1280, 720); - overlay.addEventListener(MouseEvent.MOUSE_DOWN, overlay_onMouseDown); - - overlay.buttonMode = true; - // FlxG.stage.addChild(overlay); - - } - */ - // netConnection.addEventListener(MouseEvent.MOUSE_DOWN, overlay_onMouseDown); new FlxTimer().start(1, function(tmr:FlxTimer) { startIntro(); diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index d7f6db00d..f62e064e1 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -110,7 +110,7 @@ class StoryMenuState extends MusicBeatState transIn = FlxTransitionableState.defaultTransIn; transOut = FlxTransitionableState.defaultTransOut; - if (!FlxG.sound.music.playing) + if (FlxG.sound.music == null || !FlxG.sound.music.playing) { FlxG.sound.playMusic(Paths.music('freakyMenu')); FlxG.sound.music.fadeIn(4, 0, 0.7); From 47d8d9e4db8bb54f21e08f7a5d236917d3b321f4 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Mon, 26 Jun 2023 20:40:26 -0400 Subject: [PATCH 07/30] Work in progress on redoing hold note rendering --- source/funkin/play/PlayState.hx | 17 +++-- source/funkin/play/notes/Strumline.hx | 106 ++++++++++++++++++-------- source/funkin/play/song/Song.hx | 8 +- 3 files changed, 91 insertions(+), 40 deletions(-) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index bc1d4fb30..7f3c137dc 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1014,8 +1014,9 @@ class PlayState extends MusicBeatState // super.stepHit() returns false if a module cancelled the event. if (!super.stepHit()) return false; - if (Math.abs(FlxG.sound.music.time - (Conductor.songPosition - Conductor.offset)) > 200 - || Math.abs(vocals.checkSyncError(Conductor.songPosition - Conductor.offset)) > 200) + if (FlxG.sound.music != null + && (Math.abs(FlxG.sound.music.time - (Conductor.songPosition - Conductor.offset)) > 200 + || Math.abs(vocals.checkSyncError(Conductor.songPosition - Conductor.offset)) > 200)) { trace("VOCALS NEED RESYNC"); if (vocals != null) trace(vocals.checkSyncError(Conductor.songPosition - Conductor.offset)); @@ -1473,7 +1474,6 @@ class PlayState extends MusicBeatState // } // Reset the notes on each strumline. - var noteData:Array<SongNoteData> = currentChart.notes; var playerNoteData:Array<SongNoteData> = []; var opponentNoteData:Array<SongNoteData> = []; @@ -1698,6 +1698,9 @@ class PlayState extends MusicBeatState onNoteMiss(note); } } + + // Process hold notes on the player's side. + // This handles scoring so we don't need it on the opponent's side. } /** @@ -1737,6 +1740,8 @@ class PlayState extends MusicBeatState { var input:PreciseInputEvent = inputPressQueue.shift(); + playerStrumline.pressKey(input.noteDirection); + var notesInDirection:Array<NoteSprite> = notesByDirection[input.noteDirection]; if (canMiss && notesInDirection.length == 0) @@ -1777,6 +1782,8 @@ class PlayState extends MusicBeatState // Play the strumline animation. playerStrumline.playStatic(input.noteDirection); + + playerStrumline.releaseKey(input.noteDirection); } } @@ -2004,10 +2011,6 @@ class PlayState extends MusicBeatState note.active = false; note.visible = false; - - // Kill the note. - // NOTE: This is what handles recycling the note graphic. - playerStrumline.killNote(note); } /** diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 3cd503b3b..2408025ce 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -53,6 +53,8 @@ class Strumline extends FlxSpriteGroup var noteData:Array<SongNoteData> = []; var nextNoteIndex:Int = -1; + var heldKeys:Array<Bool> = []; + public function new(isPlayer:Bool) { super(); @@ -60,19 +62,23 @@ class Strumline extends FlxSpriteGroup this.isPlayer = isPlayer; this.strumlineNotes = new FlxTypedSpriteGroup<StrumlineNote>(); + this.strumlineNotes.zIndex = 10; this.add(this.strumlineNotes); // Hold notes are added first so they render behind regular notes. this.holdNotes = new FlxTypedSpriteGroup<SustainTrail>(); + this.holdNotes.zIndex = 20; this.add(this.holdNotes); this.notes = new FlxTypedSpriteGroup<NoteSprite>(); + this.notes.zIndex = 30; this.add(this.notes); this.noteSplashes = new FlxTypedSpriteGroup<NoteSplash>(0, 0, NOTE_SPLASH_CAP); + this.noteSplashes.zIndex = 40; this.add(this.noteSplashes); - for (i in 0...DIRECTIONS.length) + for (i in 0...KEY_COUNT) { var child:StrumlineNote = new StrumlineNote(isPlayer, DIRECTIONS[i]); child.x = getXPos(DIRECTIONS[i]); @@ -81,13 +87,18 @@ class Strumline extends FlxSpriteGroup this.strumlineNotes.add(child); } + for (i in 0...KEY_COUNT) + { + heldKeys.push(false); + } + // This MUST be true for children to update! this.active = true; } override function get_width():Float { - return 4 * Strumline.NOTE_SPACING; + return KEY_COUNT * Strumline.NOTE_SPACING; } public override function update(elapsed:Float):Void @@ -183,11 +194,11 @@ class Strumline extends FlxSpriteGroup if (note == null) continue; if (note.time > renderWindowStart) break; - buildNoteSprite(note); + var noteSprite = buildNoteSprite(note); if (note.length > 0) { - buildHoldNoteSprite(note); + noteSprite.holdNoteSprite = buildHoldNoteSprite(note); } nextNoteIndex++; // Increment the nextNoteIndex rather than splicing the array, because splicing is slow. @@ -198,7 +209,8 @@ class Strumline extends FlxSpriteGroup { if (note == null || note.hasBeenHit) continue; - note.y = this.y - INITIAL_OFFSET + calculateNoteYPos(note.strumTime); + var vwoosh:Bool = note.holdNoteSprite == null; + note.y = this.y - INITIAL_OFFSET + calculateNoteYPos(note.strumTime, vwoosh); // Check if the note is outside the hit window, and if so, mark it as missed. // TODO: Check to make sure this doesn't happen when the note is on screen because it'll probably get deleted. @@ -221,6 +233,17 @@ class Strumline extends FlxSpriteGroup { if (holdNote == null || !holdNote.alive) continue; + if (Conductor.songPosition > holdNote.strumTime && holdNote.hitNote && !holdNote.missedNote) + { + if (isPlayer && !isKeyHeld(holdNote.noteDirection)) + { + // Stopped pressing the hold note. + playStatic(holdNote.noteDirection); + holdNote.missedNote = true; + holdNote.alpha = 0.6; + } + } + var renderWindowEnd = holdNote.strumTime + holdNote.fullSustainLength + Conductor.HIT_WINDOW_MS + RENDER_DISTANCE_MS / 8; if (holdNote.missedNote && Conductor.songPosition >= renderWindowEnd) @@ -241,6 +264,28 @@ class Strumline extends FlxSpriteGroup // TODO: Better handle the weird edge case where the hold note is almost completed. holdNote.visible = false; } + else if (holdNote.missedNote && (holdNote.fullSustainLength > holdNote.sustainLength)) + { + // Hold note was dropped before completing, keep it in its clipped state. + holdNote.visible = true; + + var yOffset:Float = (holdNote.fullSustainLength - holdNote.sustainLength) * Conductor.PIXELS_PER_MS; + + trace('yOffset: ' + yOffset); + trace('holdNote.fullSustainLength: ' + holdNote.fullSustainLength); + trace('holdNote.sustainLength: ' + holdNote.sustainLength); + + var vwoosh:Bool = false; + + if (PreferencesMenu.getPref('downscroll')) + { + holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2; + } + else + { + holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) + yOffset + STRUMLINE_SIZE / 2; + } + } else if (Conductor.songPosition > holdNote.strumTime && holdNote.hitNote) { // Hold note is currently being hit, clip it off. @@ -258,38 +303,19 @@ class Strumline extends FlxSpriteGroup holdNote.y = this.y - INITIAL_OFFSET + STRUMLINE_SIZE / 2; } } - else if (holdNote.missedNote && (holdNote.fullSustainLength > holdNote.sustainLength)) - { - // Hold note was dropped before completing, keep it in its clipped state. - holdNote.visible = true; - - var yOffset:Float = (holdNote.fullSustainLength - holdNote.sustainLength) * Conductor.PIXELS_PER_MS; - - trace('yOffset: ' + yOffset); - trace('holdNote.fullSustainLength: ' + holdNote.fullSustainLength); - trace('holdNote.sustainLength: ' + holdNote.sustainLength); - - if (PreferencesMenu.getPref('downscroll')) - { - holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime) - holdNote.height + STRUMLINE_SIZE / 2; - } - else - { - holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime) + yOffset + STRUMLINE_SIZE / 2; - } - } else { // Hold note is new, render it normally. holdNote.visible = true; + var vwoosh:Bool = false; if (PreferencesMenu.getPref('downscroll')) { - holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, false) - holdNote.height + STRUMLINE_SIZE / 2; + holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2; } else { - holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, false) + STRUMLINE_SIZE / 2; + holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) + STRUMLINE_SIZE / 2; } } } @@ -302,6 +328,21 @@ class Strumline extends FlxSpriteGroup if (holdNotes.members.length > 1) holdNotes.members.insertionSort(compareHoldNoteSprites.bind(FlxSort.ASCENDING)); } + public function pressKey(dir:NoteDirection):Void + { + heldKeys[dir] = true; + } + + public function releaseKey(dir:NoteDirection):Void + { + heldKeys[dir] = false; + } + + public function isKeyHeld(dir:NoteDirection):Bool + { + return heldKeys[dir]; + } + public function applyNoteData(data:Array<SongNoteData>):Void { this.notes.clear(); @@ -395,7 +436,7 @@ class Strumline extends FlxSpriteGroup } } - public function buildNoteSprite(note:SongNoteData):Void + public function buildNoteSprite(note:SongNoteData):NoteSprite { var noteSprite:NoteSprite = constructNoteSprite(); @@ -411,9 +452,11 @@ class Strumline extends FlxSpriteGroup // noteSprite.x += INITIAL_OFFSET; noteSprite.y = -9999; } + + return noteSprite; } - public function buildHoldNoteSprite(note:SongNoteData):Void + public function buildHoldNoteSprite(note:SongNoteData):SustainTrail { var holdNoteSprite:SustainTrail = constructHoldNoteSprite(); @@ -427,13 +470,16 @@ class Strumline extends FlxSpriteGroup holdNoteSprite.missedNote = false; holdNoteSprite.hitNote = false; + holdNoteSprite.alpha = 1.0; + holdNoteSprite.x = this.x; holdNoteSprite.x += getXPos(DIRECTIONS[note.getDirection() % KEY_COUNT]); - // holdNoteSprite.x += INITIAL_OFFSET; holdNoteSprite.x += STRUMLINE_SIZE / 2; holdNoteSprite.x -= holdNoteSprite.width / 2; holdNoteSprite.y = -9999; } + + return holdNoteSprite; } /** diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index b42c8e7c4..4cbf1ade3 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -283,8 +283,9 @@ class SongDifficulty return timeChanges[0].bpm; } - public function getPlayableChar(id:String):SongPlayableChar + public function getPlayableChar(id:String):Null<SongPlayableChar> { + if (id == null || id == '') return null; return chars.get(id); } @@ -300,9 +301,10 @@ class SongDifficulty public inline function cacheInst(?currentPlayerId:String = null):Void { - if (currentPlayerId != null) + var currentPlayer:Null<SongPlayableChar> = getPlayableChar(currentPlayerId); + if (currentPlayer != null) { - FlxG.sound.cache(Paths.inst(this.song.songId, getPlayableChar(currentPlayerId).inst)); + FlxG.sound.cache(Paths.inst(this.song.songId, currentPlayer.inst)); } else { From a672db7b0e316c6f9c4bc10e3ba6c839c5ee8858 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Tue, 27 Jun 2023 13:43:42 -0400 Subject: [PATCH 08/30] Messing around with logic for hold note misses/health gain --- source/funkin/play/PlayState.hx | 145 ++++++++++++------------- source/funkin/play/notes/NoteSprite.hx | 14 +-- source/funkin/play/notes/Strumline.hx | 26 ++--- source/funkin/util/Constants.hx | 74 +++++++++++++ 4 files changed, 166 insertions(+), 93 deletions(-) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 7f3c137dc..c76cf24d4 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -136,10 +136,8 @@ class PlayState extends MusicBeatState /** * The player's current health. - * The default maximum health is 2.0, and the default starting health is 1.0. - * TODO: Refactor this to [0.0, 1.0] */ - public var health:Float = 1; + public var health:Float = Constants.HEALTH_STARTING; /** * The player's current score. @@ -255,7 +253,7 @@ class PlayState extends MusicBeatState * The displayed value of the player's health. * Used to provide smooth animations based on linear interpolation of the player's health. */ - var healthLerp:Float = 1; + var healthLerp:Float = Constants.HEALTH_STARTING; /** * How long the user has held the "Skip Video Cutscene" button for. @@ -643,7 +641,7 @@ class PlayState extends MusicBeatState hudCameraZoomIntensity = Constants.DEFAULT_ZOOM_INTENSITY * 2.0; cameraZoomRate = Constants.DEFAULT_ZOOM_RATE; - health = 1; + health = Constants.HEALTH_STARTING; songScore = 0; Highscore.tallies.combo = 0; Countdown.performCountdown(currentStageId.startsWith('school')); @@ -735,8 +733,8 @@ class PlayState extends MusicBeatState } // Cap health. - if (health > 2.0) health = 2.0; - if (health < 0.0) health = 0.0; + if (health > Constants.HEALTH_MAX) health = Constants.HEALTH_MAX; + if (health < Constants.HEALTH_MIN) health = Constants.HEALTH_MIN; // Lerp the camera zoom towards the target level. if (subState == null) @@ -761,19 +759,19 @@ class PlayState extends MusicBeatState // RESET = Quick Game Over Screen if (controls.RESET) { - health = 0; + health = Constants.HEALTH_MIN; trace('RESET = True'); } #if CAN_CHEAT // brandon's a pussy if (controls.CHEAT) { - health += 1; + health += 0.25 * Constants.HEALTH_MAX; // +25% health. trace('User is cheating!'); } #end - if (health <= 0 && !isPracticeMode) + if (health <= Constants.HEALTH_MIN && !isPracticeMode) { vocals.pause(); FlxG.sound.music.pause(); @@ -934,7 +932,7 @@ class PlayState extends MusicBeatState */ public override function onFocus():Void { - if (health > 0 && !paused && FlxG.autoPause) + if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause) { if (Conductor.songPosition > 0.0) DiscordClient.changePresence(detailsText, currentSong.song + ' (' @@ -954,7 +952,8 @@ class PlayState extends MusicBeatState */ public override function onFocusLost():Void { - if (health > 0 && !paused && FlxG.autoPause) DiscordClient.changePresence(detailsPausedText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC); + if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause) DiscordClient.changePresence(detailsPausedText, + currentSong.song + ' (' + storyDifficultyText + ')', iconRPC); super.onFocusLost(); } @@ -1645,7 +1644,8 @@ class PlayState extends MusicBeatState { note.tooEarly = false; note.mayHit = false; - note.tooLate = true; + note.hasMissed = true; + if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true; } else if (Conductor.songPosition > hitWindowCenter) { @@ -1660,20 +1660,20 @@ class PlayState extends MusicBeatState // Command the opponent to hit the note on time. // NOTE: This is what handles the strumline and cleaning up the note itself! opponentStrumline.hitNote(note); - - // scoreNote(); } else if (Conductor.songPosition > hitWindowStart) { note.tooEarly = false; note.mayHit = true; - note.tooLate = false; + note.hasMissed = false; + if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = false; } else { note.tooEarly = true; note.mayHit = false; - note.tooLate = false; + note.hasMissed = false; + if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = false; } } @@ -1682,8 +1682,35 @@ class PlayState extends MusicBeatState { if (note == null || note.hasBeenHit) continue; - // If this is true, the note is already properly off the screen. - if (note.hasMissed) + var hitWindowStart = note.strumTime - Conductor.HIT_WINDOW_MS; + var hitWindowCenter = note.strumTime; + var hitWindowEnd = note.strumTime + Conductor.HIT_WINDOW_MS; + + if (Conductor.songPosition > hitWindowEnd) + { + note.tooEarly = false; + note.mayHit = false; + note.hasMissed = true; + if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true; + } + else if (Conductor.songPosition > hitWindowStart) + { + note.tooEarly = false; + note.mayHit = true; + note.hasMissed = false; + if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = false; + } + else + { + note.tooEarly = true; + note.mayHit = false; + note.hasMissed = false; + if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = false; + } + + // This becomes true when the note leaves the hit window. + // It might still be on screen. + if (note.hasMissed && !note.handledMiss) { // Call an event to allow canceling the note miss. // NOTE: This is what handles the character animations! @@ -1696,6 +1723,8 @@ class PlayState extends MusicBeatState // Judge the miss. // NOTE: This is what handles the scoring. onNoteMiss(note); + + note.handledMiss = true; } } @@ -1725,11 +1754,9 @@ class PlayState extends MusicBeatState } // Generate a list of notes within range. - var notesInRange:Array<NoteSprite> = playerStrumline.getNotesInRange(Conductor.songPosition, Conductor.HIT_WINDOW_MS); + var notesInRange:Array<NoteSprite> = playerStrumline.getNotesMayHit(); // If there are notes in range, pressing a key will cause a ghost miss. - // var canMiss:Bool = notesInRange.length > 0; - var canMiss:Bool = true; // Forced to true for consistency with other input systems. var notesByDirection:Array<Array<NoteSprite>> = [[], [], [], []]; @@ -1744,10 +1771,19 @@ class PlayState extends MusicBeatState var notesInDirection:Array<NoteSprite> = notesByDirection[input.noteDirection]; - if (canMiss && notesInDirection.length == 0) + if (!Constants.GHOST_TAPPING && notesInDirection.length == 0) { - // Pressed a wrong key with notes in range. - // Perform a ghost miss. + // Pressed a wrong key with no notes nearby. + // Perform a ghost miss (anti-spam). + ghostNoteMiss(input.noteDirection, notesInRange.length > 0); + + // Play the strumline animation. + playerStrumline.playPress(input.noteDirection); + } + else if (Constants.GHOST_TAPPING && notesInRange.length > 0 && notesInDirection.length == 0) + { + // Pressed a wrong key with no notes nearby AND with notes in a different direction available. + // Perform a ghost miss (anti-spam). ghostNoteMiss(input.noteDirection, notesInRange.length > 0); // Play the strumline animation. @@ -1837,37 +1873,6 @@ class PlayState extends MusicBeatState var directionList:Array<Int> = []; // directions that can be hit var dumbNotes:Array<NoteSprite> = []; // notes to kill later - /* - activeNotes.forEachAlive(function(daNote:Note) { - if (daNote.canBeHit && daNote.mustPress && !daNote.tooLate && !daNote.hasBeenHit) - { - if (directionList.contains(daNote.data.noteData)) - { - for (coolNote in possibleNotes) - { - if (coolNote.data.noteData == daNote.data.noteData && Math.abs(daNote.data.strumTime - coolNote.data.strumTime) < 10) - { // if it's the same note twice at < 10ms distance, just delete it - // EXCEPT u cant delete it in this loop cuz it fucks with the collection lol - dumbNotes.push(daNote); - break; - } - else if (coolNote.data.noteData == daNote.data.noteData && daNote.data.strumTime < coolNote.data.strumTime) - { // if daNote is earlier than existing note (coolNote), replace - possibleNotes.remove(coolNote); - possibleNotes.push(daNote); - break; - } - } - } - else - { - possibleNotes.push(daNote); - directionList.push(daNote.data.noteData); - } - } - }); - */ - for (note in dumbNotes) { FlxG.log.add('killing dumb ass note at ' + note.noteData.time); @@ -1955,7 +1960,7 @@ class PlayState extends MusicBeatState // Calling event.cancelEvent() skips all the other logic! Neat! if (event.eventCanceled) return; - health -= 0.0775; + health -= Constants.HEALTH_MISS_PENALTY; if (!isPracticeMode) { @@ -2008,9 +2013,6 @@ class PlayState extends MusicBeatState vocals.playerVolume = 0; FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2)); } - - note.active = false; - note.visible = false; } /** @@ -2025,7 +2027,7 @@ class PlayState extends MusicBeatState { var event:GhostMissNoteScriptEvent = new GhostMissNoteScriptEvent(direction, // Direction missed in. hasPossibleNotes, // Whether there was a note you could have hit. - - 0.035 * 2, // How much health to add (negative). + - 1 * Constants.HEALTH_MISS_PENALTY, // How much health to add (negative). - 10 // Amount of score to add (negative). ); dispatchEvent(event); @@ -2098,10 +2100,10 @@ class PlayState extends MusicBeatState if (FlxG.keys.justPressed.ONE) endSong(); // 2: Gain 10% health. - if (FlxG.keys.justPressed.TWO) health += 0.1 * 2.0; + if (FlxG.keys.justPressed.TWO) health += 0.1 * Constants.HEALTH_MAX; // 3: Lose 5% health. - if (FlxG.keys.justPressed.THREE) health -= 0.05 * 2.0; + if (FlxG.keys.justPressed.THREE) health -= 0.05 * Constants.HEALTH_MAX; #end // 7: Move to the charter. @@ -2146,36 +2148,33 @@ class PlayState extends MusicBeatState var score = Scoring.scoreNote(noteDiff, PBOT1); var daRating = Scoring.judgeNote(noteDiff, PBOT1); - var isSick:Bool = false; - var healthMulti:Float = 0; - switch (daRating) { case 'killer': Highscore.tallies.killer += 1; - healthMulti = 0.033; + health += Constants.HEALTH_KILLER_BONUS; case 'sick': Highscore.tallies.sick += 1; - healthMulti = 0.033; + health += Constants.HEALTH_SICK_BONUS; case 'good': Highscore.tallies.good += 1; - healthMulti = 0.033 * 0.78; + health += Constants.HEALTH_GOOD_BONUS; case 'bad': Highscore.tallies.bad += 1; - healthMulti = 0.033 * 0.2; + health += Constants.HEALTH_BAD_BONUS; case 'shit': Highscore.tallies.shit += 1; - healthMulti = 0; + health += Constants.HEALTH_SHIT_BONUS; case 'miss': Highscore.tallies.missed += 1; - healthMulti = 0; + health -= Constants.HEALTH_MISS_PENALTY; } - health += healthMulti; if (daRating == "sick" || daRating == "killer") { playerStrumline.playNoteSplash(daNote.noteData.getDirection()); } + // Only add the score if you're not on practice mode if (!isPracticeMode) { diff --git a/source/funkin/play/notes/NoteSprite.hx b/source/funkin/play/notes/NoteSprite.hx index 697a29d80..ed6417f90 100644 --- a/source/funkin/play/notes/NoteSprite.hx +++ b/source/funkin/play/notes/NoteSprite.hx @@ -87,8 +87,10 @@ class NoteSprite extends FlxSprite public var lowPriority:Bool = false; /** - * This is true if the note has been fully missed by the player. - * It will be destroyed immediately. + * This is true if the note is later than 10 frames within the strumline, + * and thus can't be hit by the player. + * It will be destroyed after it moves offscreen. + * Managed by PlayState. */ public var hasMissed:Bool; @@ -107,11 +109,10 @@ class NoteSprite extends FlxSprite public var mayHit:Bool; /** - * This is true if the note is earlier than 10 frames after the strumline, - * and thus can't be hit by the player. - * Managed by PlayState. + * This is true if the PlayState has performed the logic for missing this note. + * Subtracting score, subtracting health, etc. */ - public var tooLate:Bool; + public var handledMiss:Bool; public function new(strumTime:Float = 0, direction:Int = 0) { @@ -171,7 +172,6 @@ class NoteSprite extends FlxSprite this.tooEarly = false; this.hasBeenHit = false; this.mayHit = false; - this.tooLate = false; this.hasMissed = false; } diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 2408025ce..f01031345 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -123,6 +123,13 @@ class Strumline extends FlxSpriteGroup }); } + public function getNotesMayHit():Array<NoteSprite> + { + return notes.members.filter(function(note:NoteSprite) { + return note != null && note.alive && !note.hasBeenHit && note.mayHit; + }); + } + public function getHoldNotesInRange(strumTime:Float, hitWindow:Float):Array<SustainTrail> { var hitWindowStart:Float = strumTime - hitWindow; @@ -207,24 +214,17 @@ class Strumline extends FlxSpriteGroup // Update rendering of notes. for (note in notes.members) { - if (note == null || note.hasBeenHit) continue; + if (note == null || !note.alive || note.hasBeenHit) continue; var vwoosh:Bool = note.holdNoteSprite == null; + // Set the note's position. note.y = this.y - INITIAL_OFFSET + calculateNoteYPos(note.strumTime, vwoosh); - // Check if the note is outside the hit window, and if so, mark it as missed. - // TODO: Check to make sure this doesn't happen when the note is on screen because it'll probably get deleted. - if (Conductor.songPosition > (note.noteData.time + Conductor.HIT_WINDOW_MS)) + // If the note is miss + var isOffscreen = PreferencesMenu.getPref('downscroll') ? note.y > FlxG.height : note.y < -note.height; + if (note.handledMiss && isOffscreen) { - note.visible = false; - note.hasMissed = true; - if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true; - } - else - { - note.visible = true; - note.hasMissed = false; - if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = false; + killNote(note); } } diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index bcf0f7359..b014047bc 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -122,6 +122,80 @@ class Constants */ public static final DEFAULT_VARIATION:String = 'default'; + /** + * HEALTH VALUES + */ + // ============================== + + /** + * The player's maximum health. + * If the player is at this value, they can't gain any more health. + */ + public static final HEALTH_MAX:Float = 2.0; + + /** + * The player's starting health. + */ + public static final HEALTH_STARTING = HEALTH_MAX / 2.0; + + /** + * The player's minimum health. + * If the player is at or below this value, they lose. + */ + public static final HEALTH_MIN:Float = 0.0; + + /** + * The amount of health the player gains when hitting a note with the KILLER rating. + */ + public static final HEALTH_KILLER_BONUS:Float = 2.0 / 100.0 / HEALTH_MAX; // +2.0% + + /** + * The amount of health the player gains when hitting a note with the SICK rating. + */ + public static final HEALTH_SICK_BONUS:Float = 1.5 / 100.0 / HEALTH_MAX; // +1.0% + + /** + * The amount of health the player gains when hitting a note with the GOOD rating. + */ + public static final HEALTH_GOOD_BONUS:Float = 0.75 / 100.0 / HEALTH_MAX; // +0.75% + + /** + * The amount of health the player gains when hitting a note with the BAD rating. + */ + public static final HEALTH_BAD_BONUS:Float = 0.0 / 100.0 / HEALTH_MAX; // +0.0% + + /** + * The amount of health the player gains when hitting a note with the SHIT rating. + * If negative, the player will actually lose health. + */ + public static final HEALTH_SHIT_BONUS:Float = -1.0 / 100.0 / HEALTH_MAX; // -1.0% + + /** + * The amount of health the player loses upon missing a note. + */ + public static final HEALTH_MISS_PENALTY:Float = 4.0 / 100.0 / HEALTH_MAX; // 4.0% + + /** + * The amount of health the player loses upon pressing a key when no note is there. + */ + public static final HEALTH_GHOST_MISS_PENALTY:Float = 2.0 / 100.0 / HEALTH_MAX; // 2.0% + + /** + * The amount of health the player loses upon letting go of a hold note while it is still going. + */ + public static final HEALTH_HOLD_DROP_PENALTY:Float = 0.0; // 0.0% + + /** + * The amount of health the player loses upon hitting a mine. + */ + public static final HEALTH_MINE_PENALTY:Float = 15.0 / 100.0 / HEALTH_MAX; // 15.0% + + /** + * If true, the player will not receive the ghost miss penalty if there are no notes within the hit window. + * This is the thing people have been begging for forever lolol. + */ + public static final GHOST_TAPPING:Bool = true; + /** * OTHER */ From 2f7708c10623bb29fcf8b0e7e770ce3c508697d1 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Tue, 27 Jun 2023 17:22:51 -0400 Subject: [PATCH 09/30] Hold notes pretty much done! yay! --- source/funkin/Alphabet.hx | 2 - source/funkin/NoteSplash.hx | 21 +- .../funkin/graphics/rendering/SustainTrail.hx | 233 ------------------ source/funkin/play/PlayState.hx | 17 +- source/funkin/play/notes/Strumline.hx | 10 +- source/funkin/play/notes/StrumlineNote.hx | 1 + source/funkin/util/Constants.hx | 21 +- 7 files changed, 47 insertions(+), 258 deletions(-) delete mode 100644 source/funkin/graphics/rendering/SustainTrail.hx diff --git a/source/funkin/Alphabet.hx b/source/funkin/Alphabet.hx index 3835ae660..670496727 100644 --- a/source/funkin/Alphabet.hx +++ b/source/funkin/Alphabet.hx @@ -264,8 +264,6 @@ class AlphaCharacter extends FlxSprite animation.play(letter); updateHitbox(); - FlxG.log.add('the row' + row); - y = (110 - height); y += row * 60; } diff --git a/source/funkin/NoteSplash.hx b/source/funkin/NoteSplash.hx index a32a39c08..318ef8f9f 100644 --- a/source/funkin/NoteSplash.hx +++ b/source/funkin/NoteSplash.hx @@ -2,6 +2,7 @@ package funkin; import flixel.FlxSprite; import haxe.io.Path; +import flixel.graphics.frames.FlxAtlasFrames; class NoteSplash extends FlxSprite { @@ -9,15 +10,13 @@ class NoteSplash extends FlxSprite { super(x, y); - frames = Paths.getSparrowAtlas('noteSplashes'); - + animation.addByPrefix('note0-0', 'note impact 1 purple', 24, false); animation.addByPrefix('note1-0', 'note impact 1 blue', 24, false); animation.addByPrefix('note2-0', 'note impact 1 green', 24, false); - animation.addByPrefix('note0-0', 'note impact 1 purple', 24, false); animation.addByPrefix('note3-0', 'note impact 1 red', 24, false); + animation.addByPrefix('note0-1', 'note impact 2 purple', 24, false); animation.addByPrefix('note1-1', 'note impact 2 blue', 24, false); animation.addByPrefix('note2-1', 'note impact 2 green', 24, false); - animation.addByPrefix('note0-1', 'note impact 2 purple', 24, false); animation.addByPrefix('note3-1', 'note impact 2 red', 24, false); setupNoteSplash(x, y, noteData); @@ -25,6 +24,20 @@ class NoteSplash extends FlxSprite // alpha = 0.75; } + public static function buildSplashFrames(force:Bool = false):FlxAtlasFrames + { + // static variables inside functions are a cool of Haxe 4.3.0. + static var splashFrames:FlxAtlasFrames = null; + + if (splashFrames != null && !force) return splashFrames; + + splashFrames = Paths.getSparrowAtlas('noteSplashes'); + + splashFrames.parent.persist = true; + + return splashFrames; + } + public function setupNoteSplash(x:Float, y:Float, noteData:Int = 0) { setPosition(x, y); diff --git a/source/funkin/graphics/rendering/SustainTrail.hx b/source/funkin/graphics/rendering/SustainTrail.hx deleted file mode 100644 index d9f43584e..000000000 --- a/source/funkin/graphics/rendering/SustainTrail.hx +++ /dev/null @@ -1,233 +0,0 @@ -package funkin.graphics.rendering; - -import flixel.FlxSprite; -import flixel.graphics.FlxGraphic; -import flixel.graphics.tile.FlxDrawTrianglesItem; -import flixel.math.FlxMath; - -/** - * This is based heavily on the `FlxStrip` class. It uses `drawTriangles()` to clip a sustain note - * trail at a certain time. - * The whole `FlxGraphic` is used as a texture map. See the `NOTE_hold_assets.fla` file for specifics - * on how it should be constructed. - * - * @author MtH - */ -class SustainTrail extends FlxSprite -{ - /** - * Used to determine which note color/direction to draw for the sustain. - */ - public var noteData:Int = 0; - - /** - * The zoom level to render the sustain at. - * Defaults to 1.0, increased to 6.0 for pixel notes. - */ - public var zoom(default, set):Float = 1; - - /** - * The strumtime of the note, in milliseconds. - */ - public var strumTime:Float = 0; // millis - - /** - * The sustain length of the note, in milliseconds. - */ - public var sustainLength(default, set):Float = 0; // millis - - /** - * The scroll speed of the note, as a multiplier. - */ - public var scrollSpeed(default, set):Float = 1.0; // stand-in for PlayState scroll speed - - /** - * Whether the note was missed. - */ - public var missed:Bool = false; // maybe BlendMode.MULTIPLY if missed somehow, drawTriangles does not support! - - /** - * A `Vector` of floats where each pair of numbers is treated as a coordinate location (an x, y pair). - */ - var vertices:DrawData<Float> = new DrawData<Float>(); - - /** - * A `Vector` of integers or indexes, where every three indexes define a triangle. - */ - var indices:DrawData<Int> = new DrawData<Int>(); - - /** - * A `Vector` of normalized coordinates used to apply texture mapping. - */ - var uvtData:DrawData<Float> = new DrawData<Float>(); - - var processedGraphic:FlxGraphic; - - /** - * What part of the trail's end actually represents the end of the note. - * This can be used to have a little bit sticking out. - */ - public var endOffset:Float = 0.5; // 0.73 is roughly the bottom of the sprite in the normal graphic! - - /** - * At what point the bottom for the trail's end should be clipped off. - * Used in cases where there's an extra bit of the graphic on the bottom to avoid antialiasing issues with overflow. - */ - public var bottomClip:Float = 0.9; - - /** - * Normally you would take strumTime:Float, noteData:Int, sustainLength:Float, parentNote:Note (?) - * @param NoteData - * @param SustainLength - * @param FileName - */ - public function new(NoteData:Int, SustainLength:Float, Path:String, ?Alpha:Float = 0.6, ?Pixel:Bool = false) - { - super(0, 0, Path); - - // BASIC SETUP - this.sustainLength = SustainLength; - this.noteData = NoteData; - - // CALCULATE SIZE - if (Pixel) - { - this.endOffset = bottomClip = 1; - this.antialiasing = false; - this.zoom = 6.0; - } - else - { - this.antialiasing = true; - this.zoom = 1.0; - } - // width = graphic.width / 8 * zoom; // amount of notes * 2 - height = sustainHeight(sustainLength, scrollSpeed); - // instead of scrollSpeed, PlayState.SONG.speed - - alpha = Alpha; // setting alpha calls updateColorTransform(), which initializes processedGraphic! - - updateClipping(); - indices = new DrawData<Int>(12, true, [0, 1, 2, 2, 3, 0, 4, 5, 6, 6, 7, 4]); - } - - /** - * Calculates height of a sustain note for a given length (milliseconds) and scroll speed. - * @param susLength The length of the sustain note in milliseconds. - * @param scroll The current scroll speed. - */ - public static inline function sustainHeight(susLength:Float, scroll:Float) - { - return (susLength * 0.45 * scroll); - } - - function set_zoom(z:Float) - { - this.zoom = z; - width = graphic.width / 8 * z; - updateClipping(); - return this.zoom; - } - - function set_sustainLength(s:Float) - { - height = sustainHeight(s, scrollSpeed); - return sustainLength = s; - } - - function set_scrollSpeed(s:Float) - { - height = sustainHeight(sustainLength, s); - return scrollSpeed = s; - } - - /** - * Sets up new vertex and UV data to clip the trail. - * If flipY is true, top and bottom bounds swap places. - * @param songTime The time to clip the note at, in milliseconds. - */ - public function updateClipping(songTime:Float = 0):Void - { - var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), scrollSpeed), 0, height); - if (clipHeight == 0) - { - visible = false; - return; - } - else - visible = true; - var bottomHeight:Float = graphic.height * zoom * endOffset; - var partHeight:Float = clipHeight - bottomHeight; - // == HOLD == // - // left bound - vertices[6] = vertices[0] = 0.0; - // top bound - vertices[3] = vertices[1] = flipY ? clipHeight : height - clipHeight; - // right bound - vertices[4] = vertices[2] = width; - // bottom bound (also top bound for hold ends) - if (partHeight > 0) vertices[7] = vertices[5] = flipY ? 0.0 + bottomHeight : vertices[1] + partHeight; - else - vertices[7] = vertices[5] = vertices[1]; - - // same shit with da bounds, just in relation to the texture - uvtData[6] = uvtData[0] = 1 / 4 * (noteData % 4); - // height overflows past image bounds so wraps around, looping the texture - // flipY bounds are not swapped for UV data, so the graphic is actually flipped - // top bound - uvtData[3] = uvtData[1] = (-partHeight) / graphic.height / zoom; - uvtData[4] = uvtData[2] = uvtData[0] + 1 / 8; // 1 - // bottom bound - uvtData[7] = uvtData[5] = 0.0; - - // == HOLD ENDS == // - // left bound - vertices[14] = vertices[8] = vertices[0]; - // top bound - vertices[11] = vertices[9] = vertices[5]; - // right bound - vertices[12] = vertices[10] = vertices[2]; - // bottom bound, mind the bottomClip because it clips off bottom of graphic!! - vertices[15] = vertices[13] = flipY ? graphic.height * (-bottomClip + endOffset) : height + graphic.height * (bottomClip - endOffset); - - uvtData[14] = uvtData[8] = uvtData[2]; - if (partHeight > 0) uvtData[11] = uvtData[9] = 0.0; - else - uvtData[11] = uvtData[9] = (bottomHeight - clipHeight) / zoom / graphic.height; - uvtData[12] = uvtData[10] = uvtData[8] + 1 / 8; - // again, clips off bottom !! - uvtData[15] = uvtData[13] = bottomClip; - } - - @:access(flixel.FlxCamera) - override public function draw():Void - { - if (alpha == 0 || graphic == null || vertices == null) return; - - for (camera in cameras) - { - if (!camera.visible || !camera.exists || !isOnScreen(camera)) continue; - - getScreenPosition(_point, camera).subtractPoint(offset); - camera.drawTriangles(processedGraphic, vertices, indices, uvtData, null, _point, blend, true, antialiasing); - } - } - - override public function destroy():Void - { - vertices = null; - indices = null; - uvtData = null; - processedGraphic.destroy(); - - super.destroy(); - } - - override function updateColorTransform():Void - { - super.updateColorTransform(); - if (processedGraphic != null) processedGraphic.destroy(); - processedGraphic = FlxGraphic.fromGraphic(graphic, true); - processedGraphic.bitmap.colorTransform(processedGraphic.bitmap.rect, colorTransform); - } -} diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index c76cf24d4..0c4c1abfb 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -836,7 +836,7 @@ class PlayState extends MusicBeatState if (isInCutscene && !disableKeys) handleCutsceneKeys(elapsed); // Moving notes into position is now done by Strumline.update(). - processNotes(); + processNotes(elapsed); // Dispatch the onUpdate event to scripted elements. dispatchEvent(new UpdateScriptEvent(elapsed)); @@ -1378,10 +1378,6 @@ class PlayState extends MusicBeatState if (!PlayStatePlaylist.isStoryMode) { playerStrumline.fadeInArrows(); - } - - if (!PlayStatePlaylist.isStoryMode) - { opponentStrumline.fadeInArrows(); } @@ -1629,7 +1625,7 @@ class PlayState extends MusicBeatState /** * Handles opponent note hits and player note misses. */ - function processNotes():Void + function processNotes(elapsed:Float):Void { // Process notes on the opponent's side. for (note in opponentStrumline.notes.members) @@ -1730,6 +1726,15 @@ class PlayState extends MusicBeatState // Process hold notes on the player's side. // This handles scoring so we don't need it on the opponent's side. + for (holdNote in playerStrumline.holdNotes.members) + { + // While the hold note is being hit, and there is length on the hold note... + if (holdNote.hitNote && holdNote.sustainLength > 0) + { + // Grant the player health. + health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed; + } + } } /** diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index f01031345..77b039712 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -588,18 +588,18 @@ class Strumline extends FlxSpriteGroup * @param arrow The arrow to animate. * @param index The index of the arrow in the strumline. */ - function fadeInArrow(arrow:StrumlineNote):Void + function fadeInArrow(index:Int, arrow:StrumlineNote):Void { arrow.y -= 10; - arrow.alpha = 0; - FlxTween.tween(arrow, {y: arrow.y + 10, alpha: 1}, 1, {ease: FlxEase.circOut, startDelay: 0.5 + (0.2 * arrow.ID)}); + arrow.alpha = 0.0; + FlxTween.tween(arrow, {y: arrow.y + 10, alpha: 1}, 1, {ease: FlxEase.circOut, startDelay: 0.5 + (0.2 * index)}); } public function fadeInArrows():Void { - for (arrow in this.strumlineNotes) + for (index => arrow in this.strumlineNotes.members.keyValueIterator()) { - fadeInArrow(arrow); + fadeInArrow(index, arrow); } } diff --git a/source/funkin/play/notes/StrumlineNote.hx b/source/funkin/play/notes/StrumlineNote.hx index 1d24759dc..2f2b41374 100644 --- a/source/funkin/play/notes/StrumlineNote.hx +++ b/source/funkin/play/notes/StrumlineNote.hx @@ -41,6 +41,7 @@ class StrumlineNote extends FlxSprite this.animation.callback = onAnimationFrame; this.animation.finishCallback = onAnimationFinished; + // Must be true for animations to play. this.active = true; } diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index b014047bc..c6a6d0265 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -147,38 +147,43 @@ class Constants /** * The amount of health the player gains when hitting a note with the KILLER rating. */ - public static final HEALTH_KILLER_BONUS:Float = 2.0 / 100.0 / HEALTH_MAX; // +2.0% + public static final HEALTH_KILLER_BONUS:Float = 2.0 / 100.0 * HEALTH_MAX; // +2.0% /** * The amount of health the player gains when hitting a note with the SICK rating. */ - public static final HEALTH_SICK_BONUS:Float = 1.5 / 100.0 / HEALTH_MAX; // +1.0% + public static final HEALTH_SICK_BONUS:Float = 1.5 / 100.0 * HEALTH_MAX; // +1.0% /** * The amount of health the player gains when hitting a note with the GOOD rating. */ - public static final HEALTH_GOOD_BONUS:Float = 0.75 / 100.0 / HEALTH_MAX; // +0.75% + public static final HEALTH_GOOD_BONUS:Float = 0.75 / 100.0 * HEALTH_MAX; // +0.75% /** * The amount of health the player gains when hitting a note with the BAD rating. */ - public static final HEALTH_BAD_BONUS:Float = 0.0 / 100.0 / HEALTH_MAX; // +0.0% + public static final HEALTH_BAD_BONUS:Float = 0.0 / 100.0 * HEALTH_MAX; // +0.0% /** * The amount of health the player gains when hitting a note with the SHIT rating. * If negative, the player will actually lose health. */ - public static final HEALTH_SHIT_BONUS:Float = -1.0 / 100.0 / HEALTH_MAX; // -1.0% + public static final HEALTH_SHIT_BONUS:Float = -1.0 / 100.0 * HEALTH_MAX; // -1.0% + + /** + * The amount of health the player gains, while holding a hold note, per second. + */ + public static final HEALTH_HOLD_BONUS_PER_SECOND:Float = 7.5 / 100.0 * HEALTH_MAX; // +7.5% / second /** * The amount of health the player loses upon missing a note. */ - public static final HEALTH_MISS_PENALTY:Float = 4.0 / 100.0 / HEALTH_MAX; // 4.0% + public static final HEALTH_MISS_PENALTY:Float = 4.0 / 100.0 * HEALTH_MAX; // 4.0% /** * The amount of health the player loses upon pressing a key when no note is there. */ - public static final HEALTH_GHOST_MISS_PENALTY:Float = 2.0 / 100.0 / HEALTH_MAX; // 2.0% + public static final HEALTH_GHOST_MISS_PENALTY:Float = 2.0 / 100.0 * HEALTH_MAX; // 2.0% /** * The amount of health the player loses upon letting go of a hold note while it is still going. @@ -188,7 +193,7 @@ class Constants /** * The amount of health the player loses upon hitting a mine. */ - public static final HEALTH_MINE_PENALTY:Float = 15.0 / 100.0 / HEALTH_MAX; // 15.0% + public static final HEALTH_MINE_PENALTY:Float = 15.0 / 100.0 * HEALTH_MAX; // 15.0% /** * If true, the player will not receive the ghost miss penalty if there are no notes within the hit window. From d91c341bd2a3e6cd17b8a2c4167d2a7bf9715c3d Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Tue, 27 Jun 2023 18:06:33 -0400 Subject: [PATCH 10/30] Fixed a bug where you could hold notes forever --- source/funkin/NoteSplash.hx | 10 ++++++++++ source/funkin/play/PlayState.hx | 11 +++++++++-- source/funkin/play/notes/Strumline.hx | 26 ++++++++++++++++++++------ 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/source/funkin/NoteSplash.hx b/source/funkin/NoteSplash.hx index 318ef8f9f..81b35b36d 100644 --- a/source/funkin/NoteSplash.hx +++ b/source/funkin/NoteSplash.hx @@ -24,6 +24,16 @@ class NoteSplash extends FlxSprite // alpha = 0.75; } + public override function update(elapsed:Float):Void + { + super.update(elapsed); + + if (animation.finished) + { + kill(); + } + } + public static function buildSplashFrames(force:Bool = false):FlxAtlasFrames { // static variables inside functions are a cool of Haxe 4.3.0. diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 0c4c1abfb..c407203f6 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -33,6 +33,7 @@ import funkin.play.event.SongEventData.SongEventParser; import funkin.play.notes.NoteSprite; import funkin.play.notes.NoteDirection; import funkin.play.notes.Strumline; +import funkin.play.notes.SustainTrail; import funkin.play.scoring.Scoring; import funkin.play.song.Song; import funkin.play.song.SongData.SongDataParser; @@ -1728,10 +1729,15 @@ class PlayState extends MusicBeatState // This handles scoring so we don't need it on the opponent's side. for (holdNote in playerStrumline.holdNotes.members) { + if (holdNote == null || !holdNote.alive) continue; + // While the hold note is being hit, and there is length on the hold note... - if (holdNote.hitNote && holdNote.sustainLength > 0) + if (holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0) { // Grant the player health. + trace(holdNote); + trace(holdNote.noteData); + trace(holdNote.sustainLength); health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed; } } @@ -1760,6 +1766,7 @@ class PlayState extends MusicBeatState // Generate a list of notes within range. var notesInRange:Array<NoteSprite> = playerStrumline.getNotesMayHit(); + var holdNotesInRange:Array<SustainTrail> = playerStrumline.getHoldNotesHitOrMissed(); // If there are notes in range, pressing a key will cause a ghost miss. @@ -1785,7 +1792,7 @@ class PlayState extends MusicBeatState // Play the strumline animation. playerStrumline.playPress(input.noteDirection); } - else if (Constants.GHOST_TAPPING && notesInRange.length > 0 && notesInDirection.length == 0) + else if (Constants.GHOST_TAPPING && (holdNotesInRange.length + notesInRange.length > 0) && notesInDirection.length == 0) { // Pressed a wrong key with no notes nearby AND with notes in a different direction available. // Perform a ghost miss (anti-spam). diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 77b039712..0edc4435a 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -130,6 +130,13 @@ class Strumline extends FlxSpriteGroup }); } + public function getHoldNotesHitOrMissed():Array<SustainTrail> + { + return holdNotes.members.filter(function(holdNote:SustainTrail) { + return holdNote != null && holdNote.alive && (holdNote.hitNote || holdNote.missedNote); + }); + } + public function getHoldNotesInRange(strumTime:Float, hitWindow:Float):Array<SustainTrail> { var hitWindowStart:Float = strumTime - hitWindow; @@ -255,15 +262,17 @@ class Strumline extends FlxSpriteGroup else if (holdNote.hitNote && holdNote.sustainLength <= 0) { // Hold note is completed, kill it. - playStatic(holdNote.noteDirection); + if (isKeyHeld(holdNote.noteDirection)) + { + playPress(holdNote.noteDirection); + } + else + { + playStatic(holdNote.noteDirection); + } holdNote.visible = false; holdNote.kill(); } - else if (holdNote.hitNote && holdNote.sustainLength <= 10) - { - // TODO: Better handle the weird edge case where the hold note is almost completed. - holdNote.visible = false; - } else if (holdNote.missedNote && (holdNote.fullSustainLength > holdNote.sustainLength)) { // Hold note was dropped before completing, keep it in its clipped state. @@ -294,6 +303,11 @@ class Strumline extends FlxSpriteGroup holdNote.sustainLength = (holdNote.strumTime + holdNote.fullSustainLength) - Conductor.songPosition; + if (holdNote.sustainLength <= 10) + { + holdNote.visible = false; + } + if (PreferencesMenu.getPref('downscroll')) { holdNote.y = this.y - holdNote.height + STRUMLINE_SIZE / 2; From 085d7aaa9d1918e1a8a1207d3426725a3f94ec16 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Tue, 27 Jun 2023 21:29:50 -0400 Subject: [PATCH 11/30] Fix for note splashes --- source/funkin/play/PlayState.hx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index c407203f6..6ebffd0c7 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -35,6 +35,7 @@ import funkin.play.notes.NoteDirection; import funkin.play.notes.Strumline; import funkin.play.notes.SustainTrail; import funkin.play.scoring.Scoring; +import funkin.NoteSplash; import funkin.play.song.Song; import funkin.play.song.SongData.SongDataParser; import funkin.play.song.SongData.SongEventData; @@ -453,6 +454,8 @@ class PlayState extends MusicBeatState } instance = this; + NoteSplash.buildSplashFrames(); + if (currentSong != null) { // Load and cache the song's charts. From 100565f9fcce2b7d3cd9d98797d0b09c5a36bb85 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Tue, 27 Jun 2023 21:29:58 -0400 Subject: [PATCH 12/30] Messing with transitions. --- source/funkin/InitState.hx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 4d74e1a05..686453603 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -142,10 +142,13 @@ class InitState extends FlxTransitionableState diamond.persist = true; diamond.destroyOnNoUse = false; - // FlxTransitionableState.defaultTransIn = new TransitionData(FADE, FlxColor.BLACK, 1, new FlxPoint(0, -1), {asset: diamond, width: 32, height: 32}, - // new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4)); - // FlxTransitionableState.defaultTransOut = new TransitionData(FADE, FlxColor.BLACK, 0.7, new FlxPoint(0, 1), {asset: diamond, width: 32, height: 32}, - // new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4)); + // NOTE: tileData is ignored if TransitionData.type is FADE instead of TILES. + var tileData:TransitionTileData = {asset: diamond, width: 32, height: 32}; + + FlxTransitionableState.defaultTransIn = new TransitionData(FADE, FlxColor.BLACK, 1, new FlxPoint(1, 1), tileData, + new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4)); + FlxTransitionableState.defaultTransOut = new TransitionData(FADE, FlxColor.BLACK, 0.7, new FlxPoint(1, 1), tileData, + new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4)); // Don't play transition in when entering the title state. FlxTransitionableState.skipNextTransIn = true; From b615fbf82af84dbd6012da98195cafebba423efa Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Tue, 4 Jul 2023 16:38:10 -0400 Subject: [PATCH 13/30] WIP on hold covers --- source/funkin/InitState.hx | 4 +- source/funkin/play/PlayState.hx | 5 ++ source/funkin/play/notes/NoteHoldCover.hx | 75 +++++++++++++++++++++ source/funkin/play/notes/NoteSprite.hx | 13 ---- source/funkin/play/notes/Strumline.hx | 81 ++++++++++++++++++++--- source/funkin/play/song/SongData.hx | 7 ++ 6 files changed, 160 insertions(+), 25 deletions(-) create mode 100644 source/funkin/play/notes/NoteHoldCover.hx diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 686453603..97e451320 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -145,9 +145,9 @@ class InitState extends FlxTransitionableState // NOTE: tileData is ignored if TransitionData.type is FADE instead of TILES. var tileData:TransitionTileData = {asset: diamond, width: 32, height: 32}; - FlxTransitionableState.defaultTransIn = new TransitionData(FADE, FlxColor.BLACK, 1, new FlxPoint(1, 1), tileData, + FlxTransitionableState.defaultTransIn = new TransitionData(FADE, FlxColor.BLACK, 1, new FlxPoint(0, -1), tileData, new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4)); - FlxTransitionableState.defaultTransOut = new TransitionData(FADE, FlxColor.BLACK, 0.7, new FlxPoint(1, 1), tileData, + FlxTransitionableState.defaultTransOut = new TransitionData(FADE, FlxColor.BLACK, 0.7, new FlxPoint(0, 1), tileData, new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4)); // Don't play transition in when entering the title state. FlxTransitionableState.skipNextTransIn = true; diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 6ebffd0c7..fffc9a549 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -2190,6 +2190,11 @@ class PlayState extends MusicBeatState playerStrumline.playNoteSplash(daNote.noteData.getDirection()); } + if (daNote.noteData.isHoldNote) + { + playerStrumline.playNoteHoldCover(daNote.noteData.getDirection()); + } + // Only add the score if you're not on practice mode if (!isPracticeMode) { diff --git a/source/funkin/play/notes/NoteHoldCover.hx b/source/funkin/play/notes/NoteHoldCover.hx new file mode 100644 index 000000000..e64238681 --- /dev/null +++ b/source/funkin/play/notes/NoteHoldCover.hx @@ -0,0 +1,75 @@ +package funkin.play.notes; + +import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; +import funkin.play.notes.NoteDirection; +import flixel.graphics.frames.FlxFramesCollection; +import flixel.FlxG; +import flixel.graphics.frames.FlxAtlasFrames; +import flixel.FlxSprite; + +class NoteHoldCover extends FlxTypedSpriteGroup<FlxSprite> +{ + static final FRAMERATE_DEFAULT:Int = 24; + + static var glowFrames:FlxAtlasFrames; + + var glow:FlxSprite; + var sparks:FlxSprite; + + public static function preloadFrames():Void + { + glowFrames = Paths.getSparrowAtlas('holdCoverRed'); + } + + public function new() + { + super(0, 0); + + setup(); + } + + /** + * Add ALL the animations to this sprite. We will recycle and reuse the FlxSprite multiple times. + */ + function setup():Void + { + glow = new FlxSprite(); + add(glow); + if (glowFrames == null) preloadFrames(); + glow.frames = glowFrames; + + glow.animation.addByPrefix('holdCoverRed', 'holdCoverRed0', FRAMERATE_DEFAULT, true, false, false); + glow.animation.addByPrefix('holdCoverEndRed', 'holdCoverEndRed0', FRAMERATE_DEFAULT, true, false, false); + + glow.animation.finishCallback = this.onAnimationFinished; + + if (glow.animation.getAnimationList().length < 2) + { + trace('WARNING: NoteHoldCover failed to initialize all animations.'); + } + } + + public function playStart(direction:NoteDirection):Void + { + glow.animation.play('holdCoverRed'); + } + + public function playContinue(direction:NoteDirection):Void + { + glow.animation.play('holdCoverRed'); + } + + public function playEnd(direction:NoteDirection):Void + { + glow.animation.play('holdCoverEndRed'); + } + + public function onAnimationFinished(animationName:String):Void + { + if (animationName.startsWith('holdCoverEnd')) + { + // *lightning* *zap* *crackle* + this.kill(); + } + } +} diff --git a/source/funkin/play/notes/NoteSprite.hx b/source/funkin/play/notes/NoteSprite.hx index ed6417f90..dd378bf02 100644 --- a/source/funkin/play/notes/NoteSprite.hx +++ b/source/funkin/play/notes/NoteSprite.hx @@ -21,19 +21,6 @@ class NoteSprite extends FlxSprite return this.strumTime; } - /** - * The length of the note's sustain, in milliseconds. - * If 0, the note is a tap note. - */ - public var length(default, set):Float; - - function set_length(value:Float):Float - { - this.length = value; - this.isSustainNote = (this.length > 0); - return this.length; - } - /** * The time at which the note should be hit, in steps. */ diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 0edc4435a..298c429c0 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -1,16 +1,18 @@ package funkin.play.notes; -import flixel.tweens.FlxEase; -import flixel.tweens.FlxTween; -import funkin.ui.PreferencesMenu; -import funkin.play.notes.NoteSprite; -import flixel.util.FlxSort; -import funkin.play.notes.SustainTrail; -import funkin.util.SortUtil; -import funkin.play.song.SongData.SongNoteData; import flixel.FlxG; import flixel.group.FlxSpriteGroup; import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; +import flixel.tweens.FlxEase; +import flixel.tweens.FlxTween; +import flixel.util.FlxSort; +import funkin.play.notes.NoteHoldCover; +import funkin.play.notes.NoteSplash; +import funkin.play.notes.NoteSprite; +import funkin.play.notes.SustainTrail; +import funkin.play.song.SongData.SongNoteData; +import funkin.ui.PreferencesMenu; +import funkin.util.SortUtil; /** * A group of sprites which handles the receptor, the note splashes, and the notes (with sustains) for a given player. @@ -48,7 +50,7 @@ class Strumline extends FlxSpriteGroup var strumlineNotes:FlxTypedSpriteGroup<StrumlineNote>; var noteSplashes:FlxTypedSpriteGroup<NoteSplash>; - var sustainSplashes:FlxTypedSpriteGroup<NoteSplash>; + var noteHoldCovers:FlxTypedSpriteGroup<NoteHoldCover>; var noteData:Array<SongNoteData> = []; var nextNoteIndex:Int = -1; @@ -74,8 +76,12 @@ class Strumline extends FlxSpriteGroup this.notes.zIndex = 30; this.add(this.notes); + this.noteHoldCovers = new FlxTypedSpriteGroup<NoteHoldCover>(0, 0, 4); + this.noteHoldCovers.zIndex = 40; + this.add(this.noteHoldCovers); + this.noteSplashes = new FlxTypedSpriteGroup<NoteSplash>(0, 0, NOTE_SPLASH_CAP); - this.noteSplashes.zIndex = 40; + this.noteSplashes.zIndex = 50; this.add(this.noteSplashes); for (i in 0...KEY_COUNT) @@ -450,6 +456,27 @@ class Strumline extends FlxSpriteGroup } } + public function playNoteHoldCover(direction:NoteDirection):Void + { + // TODO: Add a setting to disable note splashes. + // if (Settings.noSplash) return; + + var cover:NoteHoldCover = this.constructNoteHoldCover(); + + if (cover != null) + { + cover.playStart(direction); + + cover.x = this.x; + cover.x += getXPos(direction); + cover.x -= cover.width / 8; + cover.x += INITIAL_OFFSET; + cover.y = this.y; + cover.y -= cover.height / 4; + // + } + } + public function buildNoteSprite(note:SongNoteData):NoteSprite { var noteSprite:NoteSprite = constructNoteSprite(); @@ -530,6 +557,40 @@ class Strumline extends FlxSpriteGroup return result; } + /** + * Custom recycling behavior. + */ + function constructNoteHoldCover():NoteHoldCover + { + var result:NoteHoldCover = null; + + // If we haven't filled the pool yet... + if (noteHoldCovers.length < noteHoldCovers.maxSize) + { + // Create a new note hold cover. + result = new NoteHoldCover(); + this.noteHoldCovers.add(result); + } + else + { + // Else, find a note splash which is inactive so we can revive it. + result = this.noteHoldCovers.getFirstAvailable(); + + if (result != null) + { + result.revive(); + } + else + { + // The note hold cover pool is full and all note hold covers are active, + // so we just pick one at random to destroy and restart. + result = FlxG.random.getObject(this.noteHoldCovers.members); + } + } + + return result; + } + /** * Custom recycling behavior. */ diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx index dc46ae365..7122d130e 100644 --- a/source/funkin/play/song/SongData.hx +++ b/source/funkin/play/song/SongData.hx @@ -450,6 +450,13 @@ abstract SongNoteData(RawSongNoteData) return this.l = value; } + public var isHoldNote(get, never):Bool; + + public function get_isHoldNote():Bool + { + return this.l > 0; + } + public var kind(get, set):String; public function get_kind():String From 8e071221ff92482064647fd87be4205c09f7c121 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Wed, 5 Jul 2023 22:11:58 -0400 Subject: [PATCH 14/30] Work in progress on hold covers --- source/funkin/play/PlayState.hx | 17 +++++--- source/funkin/play/notes/NoteHoldCover.hx | 52 +++++++++++++++++------ source/funkin/play/notes/NoteSprite.hx | 7 ++- source/funkin/play/notes/Strumline.hx | 29 ++++++++++--- source/funkin/play/notes/SustainTrail.hx | 2 + source/funkin/ui/AtlasText.hx | 11 ++++- 6 files changed, 89 insertions(+), 29 deletions(-) diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index fffc9a549..65a750e66 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1660,6 +1660,11 @@ class PlayState extends MusicBeatState // Command the opponent to hit the note on time. // NOTE: This is what handles the strumline and cleaning up the note itself! opponentStrumline.hitNote(note); + + if (note.holdNoteSprite != null) + { + opponentStrumline.playNoteHoldCover(note.holdNoteSprite); + } } else if (Conductor.songPosition > hitWindowStart) { @@ -1945,7 +1950,7 @@ class PlayState extends MusicBeatState // Calling event.cancelEvent() skips all the other logic! Neat! if (event.eventCanceled) return; - if (!note.isSustainNote) + if (!note.isHoldNote) { Highscore.tallies.combo++; Highscore.tallies.totalNotesHit++; @@ -1957,6 +1962,11 @@ class PlayState extends MusicBeatState playerStrumline.hitNote(note); + if (note.holdNoteSprite != null) + { + playerStrumline.playNoteHoldCover(note.holdNoteSprite); + } + vocals.playerVolume = 1; } } @@ -2190,11 +2200,6 @@ class PlayState extends MusicBeatState playerStrumline.playNoteSplash(daNote.noteData.getDirection()); } - if (daNote.noteData.isHoldNote) - { - playerStrumline.playNoteHoldCover(daNote.noteData.getDirection()); - } - // Only add the score if you're not on practice mode if (!isPracticeMode) { diff --git a/source/funkin/play/notes/NoteHoldCover.hx b/source/funkin/play/notes/NoteHoldCover.hx index e64238681..b68de3946 100644 --- a/source/funkin/play/notes/NoteHoldCover.hx +++ b/source/funkin/play/notes/NoteHoldCover.hx @@ -13,14 +13,11 @@ class NoteHoldCover extends FlxTypedSpriteGroup<FlxSprite> static var glowFrames:FlxAtlasFrames; + public var holdNote:SustainTrail; + var glow:FlxSprite; var sparks:FlxSprite; - public static function preloadFrames():Void - { - glowFrames = Paths.getSparrowAtlas('holdCoverRed'); - } - public function new() { super(0, 0); @@ -28,6 +25,12 @@ class NoteHoldCover extends FlxTypedSpriteGroup<FlxSprite> setup(); } + public static function preloadFrames():Void + { + glowFrames = Paths.getSparrowAtlas('holdCoverRed'); + glowFrames.parent.persist = true; + } + /** * Add ALL the animations to this sprite. We will recycle and reuse the FlxSprite multiple times. */ @@ -38,8 +41,9 @@ class NoteHoldCover extends FlxTypedSpriteGroup<FlxSprite> if (glowFrames == null) preloadFrames(); glow.frames = glowFrames; + glow.animation.addByPrefix('holdCoverStartRed', 'holdCoverStartRed0', FRAMERATE_DEFAULT, false, false, false); glow.animation.addByPrefix('holdCoverRed', 'holdCoverRed0', FRAMERATE_DEFAULT, true, false, false); - glow.animation.addByPrefix('holdCoverEndRed', 'holdCoverEndRed0', FRAMERATE_DEFAULT, true, false, false); + glow.animation.addByPrefix('holdCoverEndRed', 'holdCoverEndRed0', FRAMERATE_DEFAULT, false, false, false); glow.animation.finishCallback = this.onAnimationFinished; @@ -49,26 +53,48 @@ class NoteHoldCover extends FlxTypedSpriteGroup<FlxSprite> } } - public function playStart(direction:NoteDirection):Void + public override function update(elapsed):Void { + super.update(elapsed); + if (!holdNote.alive && !glow.animation.curAnim.name.startsWith('holdCoverEnd')) + { + this.visible = false; + this.kill(); + } + else + { + this.visible = true; + } + } + + public function playStart():Void + { + // glow.animation.play('holdCoverStart${noteDirection.colorName.toTitleCase()}');\ + glow.animation.play('holdCoverStartRed'); + } + + public function playContinue():Void + { + // glow.animation.play('holdCover${noteDirection.colorName.toTitleCase()}');\ glow.animation.play('holdCoverRed'); } - public function playContinue(direction:NoteDirection):Void - { - glow.animation.play('holdCoverRed'); - } - - public function playEnd(direction:NoteDirection):Void + public function playEnd():Void { + // glow.animation.play('holdCoverEnd${noteDirection.colorName.toTitleCase()}');\ glow.animation.play('holdCoverEndRed'); } public function onAnimationFinished(animationName:String):Void { + if (animationName.startsWith('holdCoverStart')) + { + playContinue(); + } if (animationName.startsWith('holdCoverEnd')) { // *lightning* *zap* *crackle* + this.visible = false; this.kill(); } } diff --git a/source/funkin/play/notes/NoteSprite.hx b/source/funkin/play/notes/NoteSprite.hx index dd378bf02..8955f9d42 100644 --- a/source/funkin/play/notes/NoteSprite.hx +++ b/source/funkin/play/notes/NoteSprite.hx @@ -61,7 +61,12 @@ class NoteSprite extends FlxSprite public var noteData:SongNoteData; - public var isSustainNote:Bool = false; + public var isHoldNote(get, never):Bool; + + function get_isHoldNote():Bool + { + return noteData.length > 0; + } /** * Set this flag to true when hitting the note to avoid scoring it multiple times. diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 298c429c0..60df77e69 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -264,6 +264,8 @@ class Strumline extends FlxSpriteGroup // Hold note is offscreen, kill it. holdNote.visible = false; holdNote.kill(); // Do not destroy! Recycling is faster. + + // The cover will see this and clean itself up. } else if (holdNote.hitNote && holdNote.sustainLength <= 0) { @@ -276,6 +278,12 @@ class Strumline extends FlxSpriteGroup { playStatic(holdNote.noteDirection); } + + if (holdNote.cover != null) + { + holdNote.cover.playEnd(); + } + holdNote.visible = false; holdNote.kill(); } @@ -456,7 +464,7 @@ class Strumline extends FlxSpriteGroup } } - public function playNoteHoldCover(direction:NoteDirection):Void + public function playNoteHoldCover(holdNote:SustainTrail):Void { // TODO: Add a setting to disable note splashes. // if (Settings.noSplash) return; @@ -465,15 +473,22 @@ class Strumline extends FlxSpriteGroup if (cover != null) { - cover.playStart(direction); + cover.holdNote = holdNote; + holdNote.cover = cover; + cover.visible = true; + + cover.playStart(); cover.x = this.x; - cover.x += getXPos(direction); - cover.x -= cover.width / 8; - cover.x += INITIAL_OFFSET; + cover.x += getXPos(holdNote.noteDirection); + cover.x += STRUMLINE_SIZE / 2; + cover.x -= cover.width / 2; + // cover.x += INITIAL_OFFSET * 2; cover.y = this.y; - cover.y -= cover.height / 4; - // + cover.y += INITIAL_OFFSET; + cover.y += STRUMLINE_SIZE / 2; + // cover.y -= cover.height / 2; + // cover.y += STRUMLINE_SIZE / 2; } } diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx index c8f629c90..addc312f4 100644 --- a/source/funkin/play/notes/SustainTrail.hx +++ b/source/funkin/play/notes/SustainTrail.hx @@ -32,6 +32,8 @@ class SustainTrail extends FlxSprite public var fullSustainLength:Float = 0; public var noteData:SongNoteData; + public var cover:NoteHoldCover = null; + /** * Set to `true` if the user hit the note and is currently holding the sustain. * Should display associated effects. diff --git a/source/funkin/ui/AtlasText.hx b/source/funkin/ui/AtlasText.hx index 76837c7ed..fea09de54 100644 --- a/source/funkin/ui/AtlasText.hx +++ b/source/funkin/ui/AtlasText.hx @@ -178,8 +178,15 @@ class AtlasChar extends FlxSprite if (this.char != value) { var prefix = getAnimPrefix(value); - animation.addByPrefix("anim", prefix, 24); - animation.play("anim"); + animation.addByPrefix('anim', prefix, 24); + if (animation.exists('anim')) + { + animation.play('anim'); + } + else + { + trace('Could not find animation for char "' + value + '"'); + } updateHitbox(); } From 796a51325f92d303c281386056899173ffa50cd8 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Sat, 8 Jul 2023 01:03:46 -0400 Subject: [PATCH 15/30] Hold note covers are in and properly positioned --- source/funkin/MusicBeatState.hx | 5 ++ source/funkin/play/PlayState.hx | 83 ++++++++++++------- source/funkin/play/character/BaseCharacter.hx | 11 ++- source/funkin/play/notes/NoteHoldCover.hx | 41 ++++++--- source/funkin/play/notes/NoteSprite.hx | 20 ++--- source/funkin/play/notes/Strumline.hx | 48 +++++++++-- source/funkin/play/notes/StrumlineNote.hx | 73 ++++++++++------ source/funkin/util/Constants.hx | 2 +- 8 files changed, 193 insertions(+), 90 deletions(-) diff --git a/source/funkin/MusicBeatState.hx b/source/funkin/MusicBeatState.hx index 4b86d801c..9aad66773 100644 --- a/source/funkin/MusicBeatState.hx +++ b/source/funkin/MusicBeatState.hx @@ -60,6 +60,11 @@ class MusicBeatState extends FlxUIState implements IEventHandler { super.update(elapsed); + // Rebindable volume keys. + if (controls.VOLUME_MUTE) FlxG.sound.toggleMuted(); + else if (controls.VOLUME_UP) FlxG.sound.changeVolume(0.1); + else if (controls.VOLUME_DOWN) FlxG.sound.changeVolume(-0.1); + // Emergency exit button. if (FlxG.keys.justPressed.F4) FlxG.switchState(new MainMenuState()); diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 65a750e66..7d5dc48b9 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1367,13 +1367,14 @@ class PlayState extends MusicBeatState add(playerStrumline); add(opponentStrumline); - // Position the player strumline on the right - playerStrumline.x = FlxG.width - playerStrumline.width - Constants.STRUMLINE_X_OFFSET; + // Position the player strumline on the right half of the screen + playerStrumline.x = FlxG.width / 2 + Constants.STRUMLINE_X_OFFSET; // Classic style + // playerStrumline.x = FlxG.width - playerStrumline.width - Constants.STRUMLINE_X_OFFSET; // Centered style playerStrumline.y = PreferencesMenu.getPref('downscroll') ? FlxG.height - playerStrumline.height - Constants.STRUMLINE_Y_OFFSET : Constants.STRUMLINE_Y_OFFSET; playerStrumline.zIndex = 200; playerStrumline.cameras = [camHUD]; - // Position the opponent strumline on the left + // Position the opponent strumline on the left half of the screen opponentStrumline.x = Constants.STRUMLINE_X_OFFSET; opponentStrumline.y = PreferencesMenu.getPref('downscroll') ? FlxG.height - opponentStrumline.height - Constants.STRUMLINE_Y_OFFSET : Constants.STRUMLINE_Y_OFFSET; opponentStrumline.zIndex = 100; @@ -1642,13 +1643,18 @@ class PlayState extends MusicBeatState if (Conductor.songPosition > hitWindowEnd) { + if (note.hasMissed) continue; + note.tooEarly = false; note.mayHit = false; note.hasMissed = true; + if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true; } else if (Conductor.songPosition > hitWindowCenter) { + if (note.hasBeenHit) continue; + // Call an event to allow canceling the note hit. // NOTE: This is what handles the character animations! var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, note, 0, true); @@ -1668,6 +1674,8 @@ class PlayState extends MusicBeatState } else if (Conductor.songPosition > hitWindowStart) { + if (note.hasBeenHit || note.hasMissed) continue; + note.tooEarly = false; note.mayHit = true; note.hasMissed = false; @@ -1682,6 +1690,25 @@ class PlayState extends MusicBeatState } } + // Process hold notes on the opponent's side. + for (holdNote in opponentStrumline.holdNotes.members) + { + if (holdNote == null || !holdNote.alive) continue; + + // While the hold note is being hit, and there is length on the hold note... + if (holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0) + { + // Make sure the opponent keeps singing while the note is held. + if (currentStage != null && currentStage.getDad() != null && currentStage.getDad().isSinging()) + { + currentStage.getDad().holdTimer = 0; + } + } + + // TODO: Potential penalty for dropping a hold note? + // if (holdNote.missedNote && !holdNote.handledMiss) { holdNote.handledMiss = true; } + } + // Process notes on the player's side. for (note in playerStrumline.notes.members) { @@ -1743,11 +1770,11 @@ class PlayState extends MusicBeatState if (holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0) { // Grant the player health. - trace(holdNote); - trace(holdNote.noteData); - trace(holdNote.sustainLength); health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed; } + + // TODO: Potential penalty for dropping a hold note? + // if (holdNote.missedNote && !holdNote.handledMiss) { holdNote.handledMiss = true; } } } @@ -1821,6 +1848,7 @@ class PlayState extends MusicBeatState targetNote.visible = false; targetNote.kill(); + notesInDirection.remove(targetNote); // Play the strumline animation. playerStrumline.playConfirm(input.noteDirection); @@ -1942,33 +1970,30 @@ class PlayState extends MusicBeatState function goodNoteHit(note:NoteSprite, input:PreciseInputEvent):Void { - if (!note.hasBeenHit) + var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, note, Highscore.tallies.combo + 1, true); + dispatchEvent(event); + + // Calling event.cancelEvent() skips all the other logic! Neat! + if (event.eventCanceled) return; + + if (!note.isHoldNote) { - var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, note, Highscore.tallies.combo + 1, true); - dispatchEvent(event); + Highscore.tallies.combo++; + Highscore.tallies.totalNotesHit++; - // Calling event.cancelEvent() skips all the other logic! Neat! - if (event.eventCanceled) return; + if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo; - if (!note.isHoldNote) - { - Highscore.tallies.combo++; - Highscore.tallies.totalNotesHit++; - - if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo; - - popUpScore(note, input); - } - - playerStrumline.hitNote(note); - - if (note.holdNoteSprite != null) - { - playerStrumline.playNoteHoldCover(note.holdNoteSprite); - } - - vocals.playerVolume = 1; + popUpScore(note, input); } + + playerStrumline.hitNote(note); + + if (note.holdNoteSprite != null) + { + playerStrumline.playNoteHoldCover(note.holdNoteSprite); + } + + vocals.playerVolume = 1; } /** diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index b27a46a0f..fa0a502fb 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -359,7 +359,7 @@ class BaseCharacter extends Bopper } // Handle character note hold time. - if (getCurrentAnimation().startsWith('sing')) + if (isSinging()) { // TODO: Rework this code (and all character animations ugh) // such that the hold time is handled by padding frames, @@ -405,6 +405,11 @@ class BaseCharacter extends Bopper } } + public function isSinging():Bool + { + return getCurrentAnimation().startsWith('sing'); + } + override function dance(force:Bool = false):Void { // Prevent default dancing behavior. @@ -412,13 +417,13 @@ class BaseCharacter extends Bopper if (!force) { - if (getCurrentAnimation().startsWith('sing')) return; + if (isSinging()) return; if (['hey', 'cheer'].contains(getCurrentAnimation()) && !isAnimationFinished()) return; } // Prevent dancing while another animation is playing. - if (!force && getCurrentAnimation().startsWith('sing')) return; + if (!force && isSinging()) return; // Otherwise, fallback to the super dance() method, which handles playing the idle animation. super.dance(); diff --git a/source/funkin/play/notes/NoteHoldCover.hx b/source/funkin/play/notes/NoteHoldCover.hx index b68de3946..a2041fb83 100644 --- a/source/funkin/play/notes/NoteHoldCover.hx +++ b/source/funkin/play/notes/NoteHoldCover.hx @@ -47,7 +47,7 @@ class NoteHoldCover extends FlxTypedSpriteGroup<FlxSprite> glow.animation.finishCallback = this.onAnimationFinished; - if (glow.animation.getAnimationList().length < 2) + if (glow.animation.getAnimationList().length < 3) { trace('WARNING: NoteHoldCover failed to initialize all animations.'); } @@ -56,35 +56,54 @@ class NoteHoldCover extends FlxTypedSpriteGroup<FlxSprite> public override function update(elapsed):Void { super.update(elapsed); - if (!holdNote.alive && !glow.animation.curAnim.name.startsWith('holdCoverEnd')) + if ((!holdNote.alive || holdNote.missedNote) && !glow.animation.curAnim.name.startsWith('holdCoverEnd')) { - this.visible = false; - this.kill(); - } - else - { - this.visible = true; + // If alive is false, the hold note was held to completion. + // If missedNote is true, the hold note was "dropped". + + playEnd(); } } public function playStart():Void { - // glow.animation.play('holdCoverStart${noteDirection.colorName.toTitleCase()}');\ + // glow.animation.play('holdCoverStart${noteDirection.colorName.toTitleCase()}'); glow.animation.play('holdCoverStartRed'); } public function playContinue():Void { - // glow.animation.play('holdCover${noteDirection.colorName.toTitleCase()}');\ + // glow.animation.play('holdCover${noteDirection.colorName.toTitleCase()}'); glow.animation.play('holdCoverRed'); } public function playEnd():Void { - // glow.animation.play('holdCoverEnd${noteDirection.colorName.toTitleCase()}');\ + // glow.animation.play('holdCoverEnd${noteDirection.colorName.toTitleCase()}'); glow.animation.play('holdCoverEndRed'); } + public override function kill():Void + { + super.kill(); + + this.visible = false; + + if (glow != null) glow.visible = false; + if (sparks != null) sparks.visible = false; + } + + public override function revive():Void + { + super.revive(); + + this.visible = true; + this.alpha = 1.0; + + if (glow != null) glow.visible = true; + if (sparks != null) sparks.visible = true; + } + public function onAnimationFinished(animationName:String):Void { if (animationName.startsWith('holdCoverStart')) diff --git a/source/funkin/play/notes/NoteSprite.hx b/source/funkin/play/notes/NoteSprite.hx index 8955f9d42..b407e7f74 100644 --- a/source/funkin/play/notes/NoteSprite.hx +++ b/source/funkin/play/notes/NoteSprite.hx @@ -127,7 +127,7 @@ class NoteSprite extends FlxSprite if (noteFrames != null && !force) return noteFrames; - noteFrames = Paths.getSparrowAtlas('NOTE_assets'); + noteFrames = Paths.getSparrowAtlas('notes'); noteFrames.parent.persist = true; @@ -138,20 +138,10 @@ class NoteSprite extends FlxSprite { this.frames = buildNoteFrames(); - animation.addByPrefix('greenScroll', 'green instance'); - animation.addByPrefix('redScroll', 'red instance'); - animation.addByPrefix('blueScroll', 'blue instance'); - animation.addByPrefix('purpleScroll', 'purple instance'); - - animation.addByPrefix('purpleholdend', 'pruple end hold'); - animation.addByPrefix('greenholdend', 'green hold end'); - animation.addByPrefix('redholdend', 'red hold end'); - animation.addByPrefix('blueholdend', 'blue hold end'); - - animation.addByPrefix('purplehold', 'purple hold piece'); - animation.addByPrefix('greenhold', 'green hold piece'); - animation.addByPrefix('redhold', 'red hold piece'); - animation.addByPrefix('bluehold', 'blue hold piece'); + animation.addByPrefix('greenScroll', 'noteUp'); + animation.addByPrefix('redScroll', 'noteRight'); + animation.addByPrefix('blueScroll', 'noteDown'); + animation.addByPrefix('purpleScroll', 'noteLeft'); setGraphicSize(Strumline.STRUMLINE_SIZE); updateHitbox(); diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 60df77e69..7730073f8 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -20,7 +20,7 @@ import funkin.util.SortUtil; class Strumline extends FlxSpriteGroup { public static final DIRECTIONS:Array<NoteDirection> = [NoteDirection.LEFT, NoteDirection.DOWN, NoteDirection.UP, NoteDirection.RIGHT]; - public static final STRUMLINE_SIZE:Int = 112; + public static final STRUMLINE_SIZE:Int = 104; public static final NOTE_SPACING:Int = STRUMLINE_SIZE + 8; // Positional fixes for new strumline graphics. @@ -84,6 +84,8 @@ class Strumline extends FlxSpriteGroup this.noteSplashes.zIndex = 50; this.add(this.noteSplashes); + this.refresh(); + for (i in 0...KEY_COUNT) { var child:StrumlineNote = new StrumlineNote(isPlayer, DIRECTIONS[i]); @@ -102,6 +104,11 @@ class Strumline extends FlxSpriteGroup this.active = true; } + public function refresh():Void + { + sort(SortUtil.byZIndex, FlxSort.ASCENDING); + } + override function get_width():Float { return KEY_COUNT * Strumline.NOTE_SPACING; @@ -112,8 +119,25 @@ class Strumline extends FlxSpriteGroup super.update(elapsed); updateNotes(); + + #if debug + if (!isPlayer) + { + FlxG.watch.addQuick("strumlineAnim", strumlineNotes.members[3]?.animation?.curAnim?.name); + var curFrame = strumlineNotes.members[3]?.animation?.curAnim?.curFrame; + frameMax = (curFrame > frameMax) ? curFrame : frameMax; + FlxG.watch.addQuick("strumlineFrame", strumlineNotes.members[3]?.animation?.curAnim?.curFrame); + FlxG.watch.addQuick("strumlineFrameMax", frameMax); + animFinishedEver = animFinishedEver || strumlineNotes.members[3]?.animation?.curAnim?.finished; + FlxG.watch.addQuick("strumlineFinished", strumlineNotes.members[3]?.animation?.curAnim?.finished); + FlxG.watch.addQuick("strumlineFinishedEver", animFinishedEver); + } + #end } + var frameMax:Int; + var animFinishedEver:Bool; + /** * Get a list of notes within + or - the given strumtime. * @param strumTime The current time. @@ -253,7 +277,8 @@ class Strumline extends FlxSpriteGroup // Stopped pressing the hold note. playStatic(holdNote.noteDirection); holdNote.missedNote = true; - holdNote.alpha = 0.6; + holdNote.visible = true; + holdNote.alpha = 0.0; } } @@ -347,6 +372,15 @@ class Strumline extends FlxSpriteGroup } } } + + // Update rendering of pressed keys. + for (dir in DIRECTIONS) + { + if (isKeyHeld(dir) && getByDirection(dir).getCurrentAnimation() == "static") + { + playPress(dir); + } + } } public function onBeatHit():Void @@ -405,7 +439,7 @@ class Strumline extends FlxSpriteGroup if (note.holdNoteSprite != null) { note.holdNoteSprite.missedNote = true; - note.holdNoteSprite.alpha = 0.6; + note.holdNoteSprite.visible = false; } } @@ -483,12 +517,12 @@ class Strumline extends FlxSpriteGroup cover.x += getXPos(holdNote.noteDirection); cover.x += STRUMLINE_SIZE / 2; cover.x -= cover.width / 2; - // cover.x += INITIAL_OFFSET * 2; + cover.x += -12; // Manual tweaking because fuck. + cover.y = this.y; cover.y += INITIAL_OFFSET; cover.y += STRUMLINE_SIZE / 2; - // cover.y -= cover.height / 2; - // cover.y += STRUMLINE_SIZE / 2; + cover.y += -96; // Manual tweaking because fuck. } } @@ -525,7 +559,7 @@ class Strumline extends FlxSpriteGroup holdNoteSprite.sustainLength = note.length; holdNoteSprite.missedNote = false; holdNoteSprite.hitNote = false; - + holdNoteSprite.visible = true; holdNoteSprite.alpha = 1.0; holdNoteSprite.x = this.x; diff --git a/source/funkin/play/notes/StrumlineNote.hx b/source/funkin/play/notes/StrumlineNote.hx index 2f2b41374..6361f607e 100644 --- a/source/funkin/play/notes/StrumlineNote.hx +++ b/source/funkin/play/notes/StrumlineNote.hx @@ -13,6 +13,10 @@ class StrumlineNote extends FlxSprite public var direction(default, set):NoteDirection; + var confirmHoldTimer:Float = -1; + + static final CONFIRM_HOLD_TIME:Float = 0.1; + public function updatePosition(parentNote:NoteSprite) { this.x = parentNote.x; @@ -49,9 +53,12 @@ class StrumlineNote extends FlxSprite function onAnimationFinished(name:String):Void { - if (!isPlayer && name.startsWith('confirm')) + // Run a timer before we stop playing the confirm animation. + // On opponent, this prevent issues with hold notes. + // On player, this allows holding the confirm key to fall back to press. + if (name == 'confirm') { - playStatic(); + confirmHoldTimer = 0; } } @@ -60,37 +67,49 @@ class StrumlineNote extends FlxSprite super.update(elapsed); centerOrigin(); + + if (confirmHoldTimer >= 0) + { + confirmHoldTimer += elapsed; + + // Ensure the opponent stops holding the key after a certain amount of time. + if (confirmHoldTimer >= CONFIRM_HOLD_TIME) + { + confirmHoldTimer = -1; + playStatic(); + } + } } function setup():Void { - this.frames = Paths.getSparrowAtlas('StrumlineNotes'); + this.frames = Paths.getSparrowAtlas('noteStrumline'); switch (this.direction) { case NoteDirection.LEFT: - this.animation.addByIndices('static', 'left confirm', [6, 7], '', 24, false, false, false); - this.animation.addByPrefix('press', 'left press', 24, false, false, false); - this.animation.addByIndices('confirm', 'left confirm', [0, 1, 2, 3], '', 24, false, false, false); - this.animation.addByIndices('confirm-hold', 'left confirm', [2, 3, 4, 5], '', 24, true, false, false); + this.animation.addByPrefix('static', 'staticLeft0', 24, false, false, false); + this.animation.addByPrefix('press', 'pressLeft0', 24, false, false, false); + this.animation.addByPrefix('confirm', 'confirmLeft0', 24, false, false, false); + this.animation.addByPrefix('confirm-hold', 'confirmHoldLeft0', 24, true, false, false); case NoteDirection.DOWN: - this.animation.addByIndices('static', 'down confirm', [6, 7], '', 24, false, false, false); - this.animation.addByPrefix('press', 'down press', 24, false, false, false); - this.animation.addByIndices('confirm', 'down confirm', [0, 1, 2, 3], '', 24, false, false, false); - this.animation.addByIndices('confirm-hold', 'down confirm', [2, 3, 4, 5], '', 24, true, false, false); + this.animation.addByPrefix('static', 'staticDown0', 24, false, false, false); + this.animation.addByPrefix('press', 'pressDown0', 24, false, false, false); + this.animation.addByPrefix('confirm', 'confirmDown0', 24, false, false, false); + this.animation.addByPrefix('confirm-hold', 'confirmHoldDown0', 24, true, false, false); case NoteDirection.UP: - this.animation.addByIndices('static', 'up confirm', [6, 7], '', 24, false, false, false); - this.animation.addByPrefix('press', 'up press', 24, false, false, false); - this.animation.addByIndices('confirm', 'up confirm', [0, 1, 2, 3], '', 24, false, false, false); - this.animation.addByIndices('confirm-hold', 'up confirm', [2, 3, 4, 5], '', 24, true, false, false); + this.animation.addByPrefix('static', 'staticUp0', 24, false, false, false); + this.animation.addByPrefix('press', 'pressUp0', 24, false, false, false); + this.animation.addByPrefix('confirm', 'confirmUp0', 24, false, false, false); + this.animation.addByPrefix('confirm-hold', 'confirmHoldUp0', 24, true, false, false); case NoteDirection.RIGHT: - this.animation.addByIndices('static', 'right confirm', [6, 7], '', 24, false, false, false); - this.animation.addByPrefix('press', 'right press', 24, false, false, false); - this.animation.addByIndices('confirm', 'right confirm', [0, 1, 2, 3], '', 24, false, false, false); - this.animation.addByIndices('confirm-hold', 'right confirm', [2, 3, 4, 5], '', 24, true, false, false); + this.animation.addByPrefix('static', 'staticRight0', 24, false, false, false); + this.animation.addByPrefix('press', 'pressRight0', 24, false, false, false); + this.animation.addByPrefix('confirm', 'confirmRight0', 24, false, false, false); + this.animation.addByPrefix('confirm-hold', 'confirmHoldRight0', 24, true, false, false); } this.setGraphicSize(Std.int(Strumline.STRUMLINE_SIZE * 1.55)); @@ -133,16 +152,22 @@ class StrumlineNote extends FlxSprite { this.active = true; - if (getCurrentAnimation() == "confirm-hold") return; - if (getCurrentAnimation() == "confirm") + if (getCurrentAnimation() == "confirm-hold") + { + return; + } + else if (getCurrentAnimation() == "confirm") { if (isAnimationFinished()) { - this.playAnimation('confirm-hold', true, false); + this.confirmHoldTimer = -1; + this.playAnimation('confirm-hold', false, false); } - return; } - this.playAnimation('confirm', false, false); + else + { + this.playAnimation('confirm', false, false); + } } /** diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index c6a6d0265..bc280f176 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -199,7 +199,7 @@ class Constants * If true, the player will not receive the ghost miss penalty if there are no notes within the hit window. * This is the thing people have been begging for forever lolol. */ - public static final GHOST_TAPPING:Bool = true; + public static final GHOST_TAPPING:Bool = false; /** * OTHER From 17a8610640c2c418834229c6392e9b0cbe82ca90 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Mon, 10 Jul 2023 18:14:34 -0400 Subject: [PATCH 16/30] Added colors to hold note covers --- source/funkin/play/notes/NoteHoldCover.hx | 47 ++++++++++++++++------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/source/funkin/play/notes/NoteHoldCover.hx b/source/funkin/play/notes/NoteHoldCover.hx index a2041fb83..52ae97d4f 100644 --- a/source/funkin/play/notes/NoteHoldCover.hx +++ b/source/funkin/play/notes/NoteHoldCover.hx @@ -3,6 +3,7 @@ package funkin.play.notes; import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; import funkin.play.notes.NoteDirection; import flixel.graphics.frames.FlxFramesCollection; +import funkin.util.assets.FlxAnimationUtil; import flixel.FlxG; import flixel.graphics.frames.FlxAtlasFrames; import flixel.FlxSprite; @@ -11,7 +12,7 @@ class NoteHoldCover extends FlxTypedSpriteGroup<FlxSprite> { static final FRAMERATE_DEFAULT:Int = 24; - static var glowFrames:FlxAtlasFrames; + static var glowFrames:FlxFramesCollection; public var holdNote:SustainTrail; @@ -27,8 +28,23 @@ class NoteHoldCover extends FlxTypedSpriteGroup<FlxSprite> public static function preloadFrames():Void { - glowFrames = Paths.getSparrowAtlas('holdCoverRed'); - glowFrames.parent.persist = true; + glowFrames = null; + for (direction in Strumline.DIRECTIONS) + { + var directionName = direction.colorName.toTitleCase(); + + var atlas:FlxFramesCollection = Paths.getSparrowAtlas('holdCover${directionName}'); + atlas.parent.persist = true; + + if (glowFrames != null) + { + glowFrames = FlxAnimationUtil.combineFramesCollections(glowFrames, atlas); + } + else + { + glowFrames = atlas; + } + } } /** @@ -41,13 +57,18 @@ class NoteHoldCover extends FlxTypedSpriteGroup<FlxSprite> if (glowFrames == null) preloadFrames(); glow.frames = glowFrames; - glow.animation.addByPrefix('holdCoverStartRed', 'holdCoverStartRed0', FRAMERATE_DEFAULT, false, false, false); - glow.animation.addByPrefix('holdCoverRed', 'holdCoverRed0', FRAMERATE_DEFAULT, true, false, false); - glow.animation.addByPrefix('holdCoverEndRed', 'holdCoverEndRed0', FRAMERATE_DEFAULT, false, false, false); + for (direction in Strumline.DIRECTIONS) + { + var directionName = direction.colorName.toTitleCase(); + + glow.animation.addByPrefix('holdCoverStart$directionName', 'holdCoverStart${directionName}0', FRAMERATE_DEFAULT, false, false, false); + glow.animation.addByPrefix('holdCover$directionName', 'holdCover${directionName}0', FRAMERATE_DEFAULT, true, false, false); + glow.animation.addByPrefix('holdCoverEnd$directionName', 'holdCoverEnd${directionName}0', FRAMERATE_DEFAULT, false, false, false); + } glow.animation.finishCallback = this.onAnimationFinished; - if (glow.animation.getAnimationList().length < 3) + if (glow.animation.getAnimationList().length < 3 * 4) { trace('WARNING: NoteHoldCover failed to initialize all animations.'); } @@ -67,20 +88,20 @@ class NoteHoldCover extends FlxTypedSpriteGroup<FlxSprite> public function playStart():Void { - // glow.animation.play('holdCoverStart${noteDirection.colorName.toTitleCase()}'); - glow.animation.play('holdCoverStartRed'); + var direction:NoteDirection = holdNote.noteDirection; + glow.animation.play('holdCoverStart${direction.colorName.toTitleCase()}'); } public function playContinue():Void { - // glow.animation.play('holdCover${noteDirection.colorName.toTitleCase()}'); - glow.animation.play('holdCoverRed'); + var direction:NoteDirection = holdNote.noteDirection; + glow.animation.play('holdCover${direction.colorName.toTitleCase()}'); } public function playEnd():Void { - // glow.animation.play('holdCoverEnd${noteDirection.colorName.toTitleCase()}'); - glow.animation.play('holdCoverEndRed'); + var direction:NoteDirection = holdNote.noteDirection; + glow.animation.play('holdCoverEnd${direction.colorName.toTitleCase()}'); } public override function kill():Void From 364753286f73dcbdc5db7f2365ac4f90675cafbd Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Mon, 10 Jul 2023 20:10:17 -0400 Subject: [PATCH 17/30] Attempt at fixing custom Lime in build --- .github/workflows/build-shit.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml index 35d436b2c..ac5ce00f0 100644 --- a/.github/workflows/build-shit.yml +++ b/.github/workflows/build-shit.yml @@ -45,6 +45,16 @@ jobs: steps: - uses: actions/checkout@v3 - uses: ./.github/actions/setup-haxeshit + - name: Build Lime + # TODO: Remove the step that builds Lime later. + run: | + LIME_PATH=$(haxelib libpath lime) + echo "Moving to $LIME_PATH" + cd $LIME_PATH + git submodule sync --recursive + git submodule update --recursive + git status + lime rebuild windows --clean - name: Build game run: | haxelib run lime build windows -debug From 6c9ec918afdcda075db406b20cf5b2533a8d50d2 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Mon, 10 Jul 2023 20:49:28 -0400 Subject: [PATCH 18/30] Attempt 2, with powershell --- .github/workflows/build-shit.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml index ac5ce00f0..77b8e9895 100644 --- a/.github/workflows/build-shit.yml +++ b/.github/workflows/build-shit.yml @@ -47,8 +47,9 @@ jobs: - uses: ./.github/actions/setup-haxeshit - name: Build Lime # TODO: Remove the step that builds Lime later. + # Powershell method run: | - LIME_PATH=$(haxelib libpath lime) + $LIME_PATH = &"haxelib libpath lime" echo "Moving to $LIME_PATH" cd $LIME_PATH git submodule sync --recursive From b0b8b4fba0d26efa80ba031b27461b5bfa72de4e Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Mon, 10 Jul 2023 23:23:18 -0400 Subject: [PATCH 19/30] Shut up StickerState --- source/funkin/ui/StickerSubState.hx | 49 ----------------------------- 1 file changed, 49 deletions(-) diff --git a/source/funkin/ui/StickerSubState.hx b/source/funkin/ui/StickerSubState.hx index e9d528773..93eabdf14 100644 --- a/source/funkin/ui/StickerSubState.hx +++ b/source/funkin/ui/StickerSubState.hx @@ -44,7 +44,6 @@ class StickerSubState extends MusicBeatSubState for (sticker in oldStickers) { grpStickers.add(sticker); - trace(sticker); } degenStickers(); @@ -89,31 +88,6 @@ class StickerSubState extends MusicBeatSubState for (stickerSets in stickerInfo.getPack("all")) { stickers.set(stickerSets, stickerInfo.getStickers(stickerSets)); - - trace(stickers); - - // for (stickerShit in stickerInfo.getStickers(stickerSets)) - // { - // // for loop jus to repeat it easy easy easy - // for (i in 0...FlxG.random.int(1, 5)) - // { - // var sticky:StickerSprite = new StickerSprite(0, 0, stickerInfo.name, stickerShit); - // sticky.x -= sticky.width / 2; - // sticky.y -= sticky.height * 0.9; - - // // random location by default - // sticky.x += FlxG.random.float(0, FlxG.width); - // sticky.y += FlxG.random.float(0, FlxG.height); - - // sticky.visible = false; - // sticky.scrollFactor.set(); - // sticky.angle = FlxG.random.int(-60, 70); - // // sticky.flipX = FlxG.random.bool(); - // grpStickers.add(sticky); - - // sticky.timing = FlxG.random.float(0, 0.8); - // } - // } } var xPos:Float = -100; @@ -281,7 +255,6 @@ class StickerInfo { var path = Paths.file('images/transitionSwag/' + stickerSet + '/stickers.json'); var json = Json.parse(Assets.getText(path)); - trace(json); // doin this dipshit nonsense cuz i dunno how to deal with casting a json object with // a dash in its name (sticker-packs) @@ -298,13 +271,8 @@ class StickerInfo var stickerStuff = Reflect.field(stickerFunny, field); stickerPacks.set(field, cast stickerStuff); - - trace(field); - trace(Reflect.field(stickerFunny, field)); } - trace(stickerPacks); - // creates a similar for loop as before but for the stickers stickers = new Map<String, Array<String>>(); @@ -314,24 +282,7 @@ class StickerInfo var stickerStuff = Reflect.field(stickerFunny, field); stickers.set(field, cast stickerStuff); - - trace(field); - trace(Reflect.field(stickerFunny, field)); } - - trace(stickers); - - // this.stickerPacks = cast jsonInfo.stickerPacks; - // this.stickers = cast jsonInfo.stickers; - - // trace(stickerPacks); - // trace(stickers); - - // for (packs in stickers) - // { - // // this.stickers.set(packs, Reflect.field(json, "sticker-packs")); - // trace(packs); - // } } public function getStickers(stickerName:String):Array<String> From 736eecfd9a78846cc5b2750813b622e1e5c3853f Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Thu, 13 Jul 2023 00:37:54 -0400 Subject: [PATCH 20/30] Fixed some random crash bug? --- source/funkin/ui/StickerSubState.hx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/source/funkin/ui/StickerSubState.hx b/source/funkin/ui/StickerSubState.hx index 93eabdf14..067f50c31 100644 --- a/source/funkin/ui/StickerSubState.hx +++ b/source/funkin/ui/StickerSubState.hx @@ -159,6 +159,8 @@ class StickerSubState extends MusicBeatSubState if (ind == grpStickers.members.length - 1) frameTimer = 2; new FlxTimer().start((1 / 24) * frameTimer, _ -> { + if (sticker == null) return; + sticker.scale.x = sticker.scale.y = FlxG.random.float(0.97, 1.02); if (ind == grpStickers.members.length - 1) From 70584c6e88e98549efaa1e7a0bff7b969a8cefc3 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Thu, 13 Jul 2023 01:13:26 -0400 Subject: [PATCH 21/30] Make funkin.util.Constants auto-imported --- source/funkin/DialogueBox.hx | 1 - source/funkin/MainMenuState.hx | 1 - source/funkin/TitleState.hx | 1 - source/funkin/import.hx | 1 + source/funkin/play/Countdown.hx | 1 - source/funkin/play/PlayState.hx | 1 - source/funkin/play/PlayStatePlaylist.hx | 4 +--- .../funkin/play/cutscene/dialogue/ConversationDebugState.hx | 1 - source/funkin/play/event/SetCameraBopSongEvent.hx | 5 ++--- source/funkin/play/notes/NoteDirection.hx | 1 - source/funkin/play/song/SongValidator.hx | 1 - source/funkin/ui/OptionsState.hx | 1 - source/funkin/ui/PopUpStuff.hx | 1 - source/funkin/ui/debug/charting/ChartEditorState.hx | 1 - source/funkin/ui/story/StoryMenuState.hx | 1 - source/funkin/util/Constants.hx | 1 + 16 files changed, 5 insertions(+), 18 deletions(-) diff --git a/source/funkin/DialogueBox.hx b/source/funkin/DialogueBox.hx index 4258f71ce..342fcba10 100644 --- a/source/funkin/DialogueBox.hx +++ b/source/funkin/DialogueBox.hx @@ -1,6 +1,5 @@ package funkin; -import funkin.util.Constants; import flixel.FlxSprite; import flixel.addons.text.FlxTypeText; import flixel.group.FlxSpriteGroup; diff --git a/source/funkin/MainMenuState.hx b/source/funkin/MainMenuState.hx index 348bf8d17..020a121c0 100644 --- a/source/funkin/MainMenuState.hx +++ b/source/funkin/MainMenuState.hx @@ -26,7 +26,6 @@ import funkin.ui.story.StoryMenuState; import funkin.ui.OptionsState; import funkin.ui.PreferencesMenu; import funkin.ui.Prompt; -import funkin.util.Constants; import funkin.util.WindowUtil; import lime.app.Application; import openfl.filters.ShaderFilter; diff --git a/source/funkin/TitleState.hx b/source/funkin/TitleState.hx index 59845ed40..30e8d67a5 100644 --- a/source/funkin/TitleState.hx +++ b/source/funkin/TitleState.hx @@ -14,7 +14,6 @@ import funkin.shaderslmfao.ColorSwap; import funkin.shaderslmfao.LeftMaskShader; import funkin.shaderslmfao.TitleOutline; import funkin.ui.AtlasText; -import funkin.util.Constants; import openfl.Assets; import openfl.display.Sprite; import openfl.events.AsyncErrorEvent; diff --git a/source/funkin/import.hx b/source/funkin/import.hx index 9aa99fade..4ba062b8f 100644 --- a/source/funkin/import.hx +++ b/source/funkin/import.hx @@ -2,6 +2,7 @@ package; #if !macro // Only import these when we aren't in a macro. +import funkin.util.Constants; import funkin.Paths; import flixel.FlxG; // This one in particular causes a compile error if you're using macros. diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx index 169bda24b..51d72693e 100644 --- a/source/funkin/play/Countdown.hx +++ b/source/funkin/play/Countdown.hx @@ -1,6 +1,5 @@ package funkin.play; -import funkin.util.Constants; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import flixel.FlxSprite; diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 7d5dc48b9..f651f5ac6 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -47,7 +47,6 @@ import funkin.ui.PopUpStuff; import funkin.ui.PreferencesMenu; import funkin.ui.stageBuildShit.StageOffsetSubState; import funkin.ui.story.StoryMenuState; -import funkin.util.Constants; import funkin.util.SerializerUtil; import funkin.util.SortUtil; import lime.ui.Haptic; diff --git a/source/funkin/play/PlayStatePlaylist.hx b/source/funkin/play/PlayStatePlaylist.hx index acfd26752..6b754878c 100644 --- a/source/funkin/play/PlayStatePlaylist.hx +++ b/source/funkin/play/PlayStatePlaylist.hx @@ -1,10 +1,8 @@ package funkin.play; -import funkin.util.Constants; - /** * Manages playback of multiple songs in a row. - * + * * TODO: Add getters/setters for all these properties to validate them. */ class PlayStatePlaylist diff --git a/source/funkin/play/cutscene/dialogue/ConversationDebugState.hx b/source/funkin/play/cutscene/dialogue/ConversationDebugState.hx index 5f2b98f8b..4d7f74a58 100644 --- a/source/funkin/play/cutscene/dialogue/ConversationDebugState.hx +++ b/source/funkin/play/cutscene/dialogue/ConversationDebugState.hx @@ -4,7 +4,6 @@ import flixel.FlxState; import funkin.modding.events.ScriptEventDispatcher; import funkin.modding.events.ScriptEvent; import flixel.util.FlxColor; -import funkin.Paths; /** * A state with displays a conversation with no background. diff --git a/source/funkin/play/event/SetCameraBopSongEvent.hx b/source/funkin/play/event/SetCameraBopSongEvent.hx index 6f8a0645d..b17d4511c 100644 --- a/source/funkin/play/event/SetCameraBopSongEvent.hx +++ b/source/funkin/play/event/SetCameraBopSongEvent.hx @@ -1,6 +1,5 @@ package funkin.play.event; -import funkin.util.Constants; import flixel.tweens.FlxTween; import flixel.FlxCamera; import flixel.tweens.FlxEase; @@ -11,7 +10,7 @@ import funkin.play.event.SongEventData.SongEventFieldType; /** * This class represents a handler for configuring camera bop intensity and rate. - * + * * Example: Bop the camera twice as hard, once per beat (rather than once every four beats). * ``` * { @@ -22,7 +21,7 @@ import funkin.play.event.SongEventData.SongEventFieldType; * } * } * ``` - * + * * Example: Reset the camera bop to default values. * ``` * { diff --git a/source/funkin/play/notes/NoteDirection.hx b/source/funkin/play/notes/NoteDirection.hx index 8a0fb5ecc..c937916f1 100644 --- a/source/funkin/play/notes/NoteDirection.hx +++ b/source/funkin/play/notes/NoteDirection.hx @@ -1,6 +1,5 @@ package funkin.play.notes; -import funkin.util.Constants; import flixel.util.FlxColor; /** diff --git a/source/funkin/play/song/SongValidator.hx b/source/funkin/play/song/SongValidator.hx index d393c11eb..936ad46f7 100644 --- a/source/funkin/play/song/SongValidator.hx +++ b/source/funkin/play/song/SongValidator.hx @@ -5,7 +5,6 @@ import funkin.play.song.SongData.SongMetadata; import funkin.play.song.SongData.SongPlayData; import funkin.play.song.SongData.SongTimeChange; import funkin.play.song.SongData.SongTimeFormat; -import funkin.util.Constants; /** * For SongMetadata and SongChartData objects, diff --git a/source/funkin/ui/OptionsState.hx b/source/funkin/ui/OptionsState.hx index 291dafdf1..6c32c7f4c 100644 --- a/source/funkin/ui/OptionsState.hx +++ b/source/funkin/ui/OptionsState.hx @@ -5,7 +5,6 @@ import flixel.FlxSubState; import flixel.addons.transition.FlxTransitionableState; import flixel.group.FlxGroup; import flixel.util.FlxSignal; -import funkin.util.Constants; import funkin.util.WindowUtil; class OptionsState extends MusicBeatState diff --git a/source/funkin/ui/PopUpStuff.hx b/source/funkin/ui/PopUpStuff.hx index 3e848b9e6..75fc87c8b 100644 --- a/source/funkin/ui/PopUpStuff.hx +++ b/source/funkin/ui/PopUpStuff.hx @@ -4,7 +4,6 @@ import flixel.FlxSprite; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.tweens.FlxTween; import funkin.play.PlayState; -import funkin.util.Constants; class PopUpStuff extends FlxTypedGroup<FlxSprite> { diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 566e75706..aad43f93f 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -34,7 +34,6 @@ import funkin.ui.debug.charting.ChartEditorThemeHandler.ChartEditorTheme; import funkin.ui.debug.charting.ChartEditorToolboxHandler.ChartEditorToolMode; import funkin.ui.haxeui.components.CharacterPlayer; import funkin.ui.haxeui.HaxeUIState; -import funkin.util.Constants; import funkin.util.FileUtil; import funkin.util.DateUtil; import funkin.util.SerializerUtil; diff --git a/source/funkin/ui/story/StoryMenuState.hx b/source/funkin/ui/story/StoryMenuState.hx index 37aa94d23..8badbbded 100644 --- a/source/funkin/ui/story/StoryMenuState.hx +++ b/source/funkin/ui/story/StoryMenuState.hx @@ -16,7 +16,6 @@ import funkin.play.PlayState; import funkin.play.PlayStatePlaylist; import funkin.play.song.Song; import funkin.play.song.SongData.SongDataParser; -import funkin.util.Constants; class StoryMenuState extends MusicBeatState { diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index bc280f176..0278d59b9 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -205,6 +205,7 @@ class Constants * OTHER */ // ============================== + public static final LIBRARY_SEPARATOR:String = ':'; /** * All MP3 decoders introduce a playback delay of `528` samples, From 883ab879318e52e903c323c080a59483e945c393 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Thu, 13 Jul 2023 20:25:01 -0400 Subject: [PATCH 22/30] Moved AnimationData into the data package --- .../{play => data/animation}/AnimationData.hx | 48 ++++++++++++++++++- source/funkin/data/level/LevelData.hx | 2 +- source/funkin/play/character/CharacterData.hx | 1 + .../play/cutscene/dialogue/DialogueBoxData.hx | 1 + .../play/cutscene/dialogue/SpeakerData.hx | 2 + source/funkin/play/stage/StageData.hx | 1 + source/funkin/util/assets/FlxAnimationUtil.hx | 6 +-- 7 files changed, 56 insertions(+), 5 deletions(-) rename source/funkin/{play => data/animation}/AnimationData.hx (60%) diff --git a/source/funkin/play/AnimationData.hx b/source/funkin/data/animation/AnimationData.hx similarity index 60% rename from source/funkin/play/AnimationData.hx rename to source/funkin/data/animation/AnimationData.hx index e512bb757..2116109db 100644 --- a/source/funkin/play/AnimationData.hx +++ b/source/funkin/data/animation/AnimationData.hx @@ -1,14 +1,60 @@ -package funkin.play; +package funkin.data.animation; +class AnimationDataUtil +{ + public static function toNamed(data:UnnamedAnimationData, ?name:String = ""):AnimationData + { + return { + name: name, + prefix: data.prefix, + assetPath: data.assetPath, + offsets: data.offsets, + looped: data.looped, + flipX: data.flipX, + flipY: data.flipY, + frameRate: data.frameRate, + frameIndices: data.frameIndices + }; + } + + public static function toUnnamed(data:AnimationData):UnnamedAnimationData + { + return { + prefix: data.prefix, + assetPath: data.assetPath, + offsets: data.offsets, + looped: data.looped, + flipX: data.flipX, + flipY: data.flipY, + frameRate: data.frameRate, + frameIndices: data.frameIndices + }; + } +} + +/** + * A data structure representing an animation in a spritesheet. + * This is a generic data structure used by characters, stage props, and more! + * BE CAREFUL when changing it. + */ typedef AnimationData = { + > UnnamedAnimationData, + /** * The name for the animation. * This should match the animation name queried by the game; * for example, characters need animations with names `idle`, `singDOWN`, `singUPmiss`, etc. */ var name:String; +} +/** + * A data structure representing an animation in a spritesheet. + * This animation doesn't specify a name, that's presumably specified by the parent data structure. + */ +typedef UnnamedAnimationData = +{ /** * The prefix for the frames of the animation as defined by the XML file. * This will may or may not differ from the `name` of the animation, diff --git a/source/funkin/data/level/LevelData.hx b/source/funkin/data/level/LevelData.hx index 0342c3d39..0ba26354a 100644 --- a/source/funkin/data/level/LevelData.hx +++ b/source/funkin/data/level/LevelData.hx @@ -1,6 +1,6 @@ package funkin.data.level; -import funkin.play.AnimationData; +import funkin.data.animation.AnimationData; /** * A type definition for the data in a story mode level JSON file. diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx index 16bf3a8a6..18a537a40 100644 --- a/source/funkin/play/character/CharacterData.hx +++ b/source/funkin/play/character/CharacterData.hx @@ -1,5 +1,6 @@ package funkin.play.character; +import funkin.data.animation.AnimationData; import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEventDispatcher; import funkin.play.character.ScriptedCharacter.ScriptedAnimateAtlasCharacter; diff --git a/source/funkin/play/cutscene/dialogue/DialogueBoxData.hx b/source/funkin/play/cutscene/dialogue/DialogueBoxData.hx index 2ae79f8d8..537a27129 100644 --- a/source/funkin/play/cutscene/dialogue/DialogueBoxData.hx +++ b/source/funkin/play/cutscene/dialogue/DialogueBoxData.hx @@ -1,5 +1,6 @@ package funkin.play.cutscene.dialogue; +import funkin.data.animation.AnimationData; import funkin.util.SerializerUtil; /** diff --git a/source/funkin/play/cutscene/dialogue/SpeakerData.hx b/source/funkin/play/cutscene/dialogue/SpeakerData.hx index 44e13b025..a0f9a3300 100644 --- a/source/funkin/play/cutscene/dialogue/SpeakerData.hx +++ b/source/funkin/play/cutscene/dialogue/SpeakerData.hx @@ -1,5 +1,7 @@ package funkin.play.cutscene.dialogue; +import funkin.data.animation.AnimationData; + /** * Data about a conversation. * Includes what speakers are in the conversation, and what phrases they say. diff --git a/source/funkin/play/stage/StageData.hx b/source/funkin/play/stage/StageData.hx index 6164c3cde..de8804cfb 100644 --- a/source/funkin/play/stage/StageData.hx +++ b/source/funkin/play/stage/StageData.hx @@ -1,5 +1,6 @@ package funkin.play.stage; +import funkin.data.animation.AnimationData; import flixel.util.typeLimit.OneOfTwo; import funkin.play.stage.ScriptedStage; import funkin.play.stage.Stage; diff --git a/source/funkin/util/assets/FlxAnimationUtil.hx b/source/funkin/util/assets/FlxAnimationUtil.hx index bab1e090b..0e32a1918 100644 --- a/source/funkin/util/assets/FlxAnimationUtil.hx +++ b/source/funkin/util/assets/FlxAnimationUtil.hx @@ -2,12 +2,12 @@ package funkin.util.assets; import flixel.FlxSprite; import flixel.graphics.frames.FlxFramesCollection; -import funkin.play.AnimationData; +import funkin.data.animation.AnimationData; class FlxAnimationUtil { /** - * Properly adds an animation to a sprite based on JSON data. + * Properly adds an animation to a sprite based on the provided animation data. */ public static function addAtlasAnimation(target:FlxSprite, anim:AnimationData) { @@ -31,7 +31,7 @@ class FlxAnimationUtil } /** - * Properly adds multiple animations to a sprite based on JSON data. + * Properly adds multiple animations to a sprite based on the provided animation data. */ public static function addAtlasAnimations(target:FlxSprite, animations:Array<AnimationData>) { From 00cfeeff72710a8dacb885f5c0c821e238baacd6 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Thu, 13 Jul 2023 20:25:44 -0400 Subject: [PATCH 23/30] Created the Note Style class and data registry --- source/funkin/data/BaseRegistry.hx | 14 + source/funkin/data/notestyle/NoteStyleData.hx | 171 ++++++++++ .../data/notestyle/NoteStyleRegistry.hx | 65 ++++ .../funkin/play/notes/notestyle/NoteStyle.hx | 304 ++++++++++++++++++ .../play/notes/notestyle/ScriptedNoteStyle.hx | 9 + 5 files changed, 563 insertions(+) create mode 100644 source/funkin/data/notestyle/NoteStyleData.hx create mode 100644 source/funkin/data/notestyle/NoteStyleRegistry.hx create mode 100644 source/funkin/play/notes/notestyle/NoteStyle.hx create mode 100644 source/funkin/play/notes/notestyle/ScriptedNoteStyle.hx diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx index b30c311a3..36b1d26e3 100644 --- a/source/funkin/data/BaseRegistry.hx +++ b/source/funkin/data/BaseRegistry.hx @@ -84,6 +84,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo } catch (e:Dynamic) { + // Print the error. trace(' Failed to load entry data: ${entryId}'); trace(e); continue; @@ -91,16 +92,29 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo } } + /** + * Retrieve a list of all entry IDs in this registry. + * @return The list of entry IDs. + */ public function listEntryIds():Array<String> { return entries.keys().array(); } + /** + * Count the number of entries in this registry. + * @return The number of entries. + */ public function countEntries():Int { return entries.size(); } + /** + * Fetch an entry by its ID. + * @param id The ID of the entry to fetch. + * @return The entry, or `null` if it does not exist. + */ public function fetchEntry(id:String):Null<T> { return entries.get(id); diff --git a/source/funkin/data/notestyle/NoteStyleData.hx b/source/funkin/data/notestyle/NoteStyleData.hx new file mode 100644 index 000000000..04fda67ca --- /dev/null +++ b/source/funkin/data/notestyle/NoteStyleData.hx @@ -0,0 +1,171 @@ +package funkin.data.notestyle; + +import haxe.DynamicAccess; +import funkin.data.animation.AnimationData; + +/** + * A type definition for the data in a note style JSON file. + * @see https://lib.haxe.org/p/json2object/ + */ +typedef NoteStyleData = +{ + /** + * The version number of the note style data schema. + * When making changes to the note style data format, this should be incremented, + * and a migration function should be added to NoteStyleDataParser to handle old versions. + */ + @:default(funkin.data.notestyle.NoteStyleRegistry.NOTE_STYLE_DATA_VERSION) + var version:String; + + /** + * The readable title of the note style. + */ + var name:String; + + /** + * The author of the note style. + */ + var author:String; + + /** + * The note style to use as a fallback/parent. + * @default null + */ + @:optional + var fallback:Null<String>; + + /** + * Data for each of the assets in the note style. + */ + var assets:NoteStyleAssetsData; +} + +typedef NoteStyleAssetsData = +{ + /** + * The sprites for the notes. + * @default The sprites from the fallback note style. + */ + @:optional + var note:NoteStyleAssetData<NoteStyleData_Note>; + + /** + * The sprites for the hold notes. + * @default The sprites from the fallback note style. + */ + @:optional + var holdNote:NoteStyleAssetData<NoteStyleData_HoldNote>; + + /** + * The sprites for the strumline. + * @default The sprites from the fallback note style. + */ + @:optional + var noteStrumline:NoteStyleAssetData<NoteStyleData_NoteStrumline>; + + /** + * The sprites for the note splashes. + */ + @:optional + var noteSplash:NoteStyleAssetData<NoteStyleData_NoteSplash>; + + /** + * The sprites for the hold note covers. + */ + @:optional + var holdNoteCover:NoteStyleAssetData<NoteStyleData_HoldNoteCover>; +} + +/** + * Data shared by all note style assets. + */ +typedef NoteStyleAssetData<T> = +{ + /** + * The image to use for the asset. May be a Sparrow sprite sheet. + */ + var assetPath:String; + + /** + * The scale to render the prop at. + * @default 1.0 + */ + @:default(1.0) + @:optional + var scale:Float; + + /** + * Offset the sprite's position by this amount. + * @default [0, 0] + */ + @:default([0, 0]) + @:optional + var offsets:Null<Array<Float>>; + + /** + * If true, the prop is a pixel sprite, and will be rendered without anti-aliasing. + */ + @:default(false) + @:optional + var isPixel:Bool; + + /** + * The structure of this data depends on the asset. + */ + var data:T; +} + +typedef NoteStyleData_Note = +{ + var left:UnnamedAnimationData; + var down:UnnamedAnimationData; + var up:UnnamedAnimationData; + var right:UnnamedAnimationData; +} + +typedef NoteStyleData_HoldNote = {} + +/** + * Data on animations for each direction of the strumline. + */ +typedef NoteStyleData_NoteStrumline = +{ + var leftStatic:UnnamedAnimationData; + var leftPress:UnnamedAnimationData; + var leftConfirm:UnnamedAnimationData; + var leftConfirmHold:UnnamedAnimationData; + var downStatic:UnnamedAnimationData; + var downPress:UnnamedAnimationData; + var downConfirm:UnnamedAnimationData; + var downConfirmHold:UnnamedAnimationData; + var upStatic:UnnamedAnimationData; + var upPress:UnnamedAnimationData; + var upConfirm:UnnamedAnimationData; + var upConfirmHold:UnnamedAnimationData; + var rightStatic:UnnamedAnimationData; + var rightPress:UnnamedAnimationData; + var rightConfirm:UnnamedAnimationData; + var rightConfirmHold:UnnamedAnimationData; +} + +typedef NoteStyleData_NoteSplash = +{ + /** + * If false, note splashes are entirely hidden on this note style. + * @default Note splashes are enabled. + */ + @:optional + @:default(true) + var enabled:Bool; +}; + +typedef NoteStyleData_HoldNoteCover = +{ + /** + * If false, hold note covers are entirely hidden on this note style. + * @default Hold note covers are enabled. + */ + @:optional + @:default(true) + var enabled:Bool; +}; diff --git a/source/funkin/data/notestyle/NoteStyleRegistry.hx b/source/funkin/data/notestyle/NoteStyleRegistry.hx new file mode 100644 index 000000000..d666a037c --- /dev/null +++ b/source/funkin/data/notestyle/NoteStyleRegistry.hx @@ -0,0 +1,65 @@ +package funkin.data.notestyle; + +import funkin.play.notes.notestyle.NoteStyle; +import funkin.play.notes.notestyle.ScriptedNoteStyle; +import funkin.data.notestyle.NoteStyleData; + +class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData> +{ + /** + * The current version string for the note style data format. + * Handle breaking changes by incrementing this value + * and adding migration to the `migrateNoteStyleData()` function. + */ + public static final NOTE_STYLE_DATA_VERSION:String = "1.0.0"; + + public static final DEFAULT_NOTE_STYLE_ID:String = "funkin"; + + public static final instance:NoteStyleRegistry = new NoteStyleRegistry(); + + public function new() + { + super('NOTESTYLE', 'notestyles'); + } + + public function fetchDefault():NoteStyle + { + return fetchEntry(DEFAULT_NOTE_STYLE_ID); + } + + /** + * Read, parse, and validate the JSON data and produce the corresponding data object. + */ + public function parseEntryData(id:String):Null<NoteStyleData> + { + if (id == null) id = DEFAULT_NOTE_STYLE_ID; + + // JsonParser does not take type parameters, + // otherwise this function would be in BaseRegistry. + var parser = new json2object.JsonParser<NoteStyleData>(); + var jsonStr:String = loadEntryFile(id); + + parser.fromJson(jsonStr); + + if (parser.errors.length > 0) + { + trace('Failed to parse entry data: ${id}'); + for (error in parser.errors) + { + trace(error); + } + return null; + } + return parser.value; + } + + function createScriptedEntry(clsName:String):NoteStyle + { + return ScriptedNoteStyle.init(clsName, "unknown"); + } + + function getScriptedClassNames():Array<String> + { + return ScriptedNoteStyle.listScriptClasses(); + } +} diff --git a/source/funkin/play/notes/notestyle/NoteStyle.hx b/source/funkin/play/notes/notestyle/NoteStyle.hx new file mode 100644 index 000000000..6cb9e7cdc --- /dev/null +++ b/source/funkin/play/notes/notestyle/NoteStyle.hx @@ -0,0 +1,304 @@ +package funkin.play.notes.notestyle; + +import flixel.graphics.frames.FlxAtlasFrames; +import flixel.graphics.frames.FlxFramesCollection; +import funkin.data.animation.AnimationData; +import funkin.data.IRegistryEntry; +import funkin.data.notestyle.NoteStyleData; +import funkin.data.notestyle.NoteStyleRegistry; +import funkin.data.notestyle.NoteStyleRegistry; +import funkin.util.assets.FlxAnimationUtil; + +using funkin.data.animation.AnimationData.AnimationDataUtil; + +/** + * Holds the data for what assets to use for a note style, + * and provides convenience methods for building sprites based on them. + */ +class NoteStyle implements IRegistryEntry<NoteStyleData> +{ + /** + * The ID of the note style. + */ + public final id:String; + + /** + * Note style data as parsed from the JSON file. + */ + public final _data:NoteStyleData; + + /** + * The note style to use if this one doesn't have a certain asset. + * This can be recursive, ehe. + */ + final fallback:Null<NoteStyle>; + + /** + * @param id The ID of the JSON file to parse. + */ + public function new(id:String) + { + this.id = id; + _data = _fetchData(id); + + if (_data == null) + { + throw 'Could not parse note style data for id: $id'; + } + + this.fallback = NoteStyleRegistry.instance.fetchEntry(getFallbackID()); + } + + /** + * Get the readable name of the note style. + * @return String + */ + public function getName():String + { + return _data.name; + } + + /** + * Get the author of the note style. + * @return String + */ + public function getAuthor():String + { + return _data.author; + } + + /** + * Get the note style ID of the parent note style. + * @return The string ID, or `null` if there is no parent. + */ + function getFallbackID():Null<String> + { + return _data.fallback; + } + + public function buildNoteSprite(target:NoteSprite):Void + { + // Apply the note sprite frames. + var atlas:FlxAtlasFrames = buildNoteFrames(false); + + if (atlas == null) + { + throw 'Could not load spritesheet for note style: $id'; + } + + target.frames = atlas; + + target.scale.x = _data.assets.note.scale; + target.scale.y = _data.assets.note.scale; + target.antialiasing = !_data.assets.note.isPixel; + + // Apply the animations. + buildNoteAnimations(target); + } + + var noteFrames:FlxAtlasFrames = null; + + function buildNoteFrames(force:Bool = false):FlxAtlasFrames + { + if (noteFrames != null && !force) return noteFrames; + + noteFrames = Paths.getSparrowAtlas(getNoteAssetPath(), getNoteAssetLibrary()); + + noteFrames.parent.persist = true; + + return noteFrames; + } + + function getNoteAssetPath(?raw:Bool = false):String + { + if (raw) + { + var rawPath:Null<String> = _data?.assets?.note?.assetPath; + if (rawPath == null) return fallback.getNoteAssetPath(true); + return rawPath; + } + + // library:path + var parts = getNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR); + if (parts.length == 1) return getNoteAssetPath(true); + return parts[1]; + } + + function getNoteAssetLibrary():Null<String> + { + // library:path + var parts = getNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR); + if (parts.length == 1) return null; + return parts[0]; + } + + function buildNoteAnimations(target:NoteSprite):Void + { + var leftData:AnimationData = fetchNoteAnimationData(LEFT); + target.animation.addByPrefix('purpleScroll', leftData.prefix); + var downData:AnimationData = fetchNoteAnimationData(DOWN); + target.animation.addByPrefix('blueScroll', downData.prefix); + var upData:AnimationData = fetchNoteAnimationData(UP); + target.animation.addByPrefix('greenScroll', upData.prefix); + var rightData:AnimationData = fetchNoteAnimationData(RIGHT); + target.animation.addByPrefix('redScroll', rightData.prefix); + } + + function fetchNoteAnimationData(dir:NoteDirection):AnimationData + { + var result:Null<AnimationData> = switch (dir) + { + case LEFT: _data.assets.note.data.left.toNamed(); + case DOWN: _data.assets.note.data.down.toNamed(); + case UP: _data.assets.note.data.up.toNamed(); + case RIGHT: _data.assets.note.data.right.toNamed(); + }; + + return (result == null) ? fallback.fetchNoteAnimationData(dir) : result; + } + + public function getHoldNoteAssetPath(?raw:Bool = false):String + { + if (raw) + { + var rawPath:Null<String> = _data?.assets?.holdNote?.assetPath; + return (rawPath == null) ? fallback.getHoldNoteAssetPath(true) : rawPath; + } + + // library:path + var parts = getHoldNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR); + if (parts.length == 1) return Paths.image(parts[0]); + return Paths.image(parts[1], parts[0]); + } + + public function isHoldNotePixel():Bool + { + var data = _data?.assets?.holdNote; + if (data == null) return fallback.isHoldNotePixel(); + return data.isPixel; + } + + public function fetchHoldNoteScale():Float + { + var data = _data?.assets?.holdNote; + if (data == null) return fallback.fetchHoldNoteScale(); + return data.scale; + } + + public function applyStrumlineFrames(target:StrumlineNote):Void + { + // TODO: Add support for multi-Sparrow. + // Will be less annoying after this is merged: https://github.com/HaxeFlixel/flixel/pull/2772 + + var atlas:FlxAtlasFrames = Paths.getSparrowAtlas(getStrumlineAssetPath(), getStrumlineAssetLibrary()); + + if (atlas == null) + { + throw 'Could not load spritesheet for note style: $id'; + } + + target.frames = atlas; + + target.scale.x = _data.assets.noteStrumline.scale; + target.scale.y = _data.assets.noteStrumline.scale; + target.antialiasing = !_data.assets.noteStrumline.isPixel; + } + + function getStrumlineAssetPath(?raw:Bool = false):String + { + if (raw) + { + var rawPath:Null<String> = _data?.assets?.noteStrumline?.assetPath; + if (rawPath == null) return fallback.getStrumlineAssetPath(true); + return rawPath; + } + + // library:path + var parts = getStrumlineAssetPath(true).split(Constants.LIBRARY_SEPARATOR); + if (parts.length == 1) return getStrumlineAssetPath(true); + return parts[1]; + } + + function getStrumlineAssetLibrary():Null<String> + { + // library:path + var parts = getStrumlineAssetPath(true).split(Constants.LIBRARY_SEPARATOR); + if (parts.length == 1) return null; + return parts[0]; + } + + public function applyStrumlineAnimations(target:StrumlineNote, dir:NoteDirection):Void + { + FlxAnimationUtil.addAtlasAnimations(target, getStrumlineAnimationData(dir)); + } + + function getStrumlineAnimationData(dir:NoteDirection):Array<AnimationData> + { + var result:Array<AnimationData> = switch (dir) + { + case NoteDirection.LEFT: [ + _data.assets.noteStrumline.data.leftStatic.toNamed('static'), + _data.assets.noteStrumline.data.leftPress.toNamed('press'), + _data.assets.noteStrumline.data.leftConfirm.toNamed('confirm'), + _data.assets.noteStrumline.data.leftConfirmHold.toNamed('confirm-hold'), + ]; + case NoteDirection.DOWN: [ + _data.assets.noteStrumline.data.downStatic.toNamed('static'), + _data.assets.noteStrumline.data.downPress.toNamed('press'), + _data.assets.noteStrumline.data.downConfirm.toNamed('confirm'), + _data.assets.noteStrumline.data.downConfirmHold.toNamed('confirm-hold'), + ]; + case NoteDirection.UP: [ + _data.assets.noteStrumline.data.upStatic.toNamed('static'), + _data.assets.noteStrumline.data.upPress.toNamed('press'), + _data.assets.noteStrumline.data.upConfirm.toNamed('confirm'), + _data.assets.noteStrumline.data.upConfirmHold.toNamed('confirm-hold'), + ]; + case NoteDirection.RIGHT: [ + _data.assets.noteStrumline.data.rightStatic.toNamed('static'), + _data.assets.noteStrumline.data.rightPress.toNamed('press'), + _data.assets.noteStrumline.data.rightConfirm.toNamed('confirm'), + _data.assets.noteStrumline.data.rightConfirmHold.toNamed('confirm-hold'), + ]; + }; + + return result; + } + + public function applyStrumlineOffsets(target:StrumlineNote) + { + target.x += _data.assets.noteStrumline.offsets[0]; + target.y += _data.assets.noteStrumline.offsets[1]; + } + + public function getStrumlineScale():Float + { + return _data.assets.noteStrumline.scale; + } + + public function isNoteSplashEnabled():Bool + { + var data = _data?.assets?.noteSplash?.data; + if (data == null) return fallback.isNoteSplashEnabled(); + return data.enabled; + } + + public function isHoldNoteCoverEnabled():Bool + { + var data = _data?.assets?.holdNoteCover?.data; + if (data == null) return fallback.isHoldNoteCoverEnabled(); + return data.enabled; + } + + public function destroy():Void {} + + public function toString():String + { + return 'NoteStyle($id)'; + } + + public function _fetchData(id:String):Null<NoteStyleData> + { + return NoteStyleRegistry.instance.parseEntryData(id); + } +} diff --git a/source/funkin/play/notes/notestyle/ScriptedNoteStyle.hx b/source/funkin/play/notes/notestyle/ScriptedNoteStyle.hx new file mode 100644 index 000000000..cae0e60ec --- /dev/null +++ b/source/funkin/play/notes/notestyle/ScriptedNoteStyle.hx @@ -0,0 +1,9 @@ +package funkin.play.notes.notestyle; + +/** + * A script that can be tied to a NoteStyle. + * Create a scripted class that extends NoteStyle to use this. + * This allows you to customize how a specific note style appears. + */ +@:hscriptClass +class ScriptedNoteStyle extends funkin.play.notes.notestyle.NoteStyle implements polymod.hscript.HScriptedClass {} From ba53957191c9f9d37562bcbc68ab0d86f596bb98 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Thu, 13 Jul 2023 20:26:56 -0400 Subject: [PATCH 24/30] Fixed an issue where dialogue scripts were not being reloaded --- source/funkin/InitState.hx | 36 +++++++++++++------ source/funkin/modding/PolymodHandler.hx | 17 ++++++--- .../dialogue/DialogueBoxDataParser.hx | 4 +-- .../play/cutscene/dialogue/ScriptedSpeaker.hx | 7 +++- .../cutscene/dialogue/SpeakerDataParser.hx | 4 +-- 5 files changed, 47 insertions(+), 21 deletions(-) diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 97e451320..ce863bd0b 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -14,6 +14,16 @@ import funkin.util.macro.MacroUtil; import funkin.util.WindowUtil; import funkin.play.PlayStatePlaylist; import openfl.display.BitmapData; +import funkin.data.level.LevelRegistry; +import funkin.data.notestyle.NoteStyleRegistry; +import funkin.play.event.SongEventData.SongEventParser; +import funkin.play.cutscene.dialogue.ConversationDataParser; +import funkin.play.cutscene.dialogue.DialogueBoxDataParser; +import funkin.play.cutscene.dialogue.SpeakerDataParser; +import funkin.play.song.SongData.SongDataParser; +import funkin.play.stage.StageData.StageDataParser; +import funkin.play.character.CharacterData.CharacterDataParser; +import funkin.modding.module.ModuleHandler; #if discord_rpc import Discord.DiscordClient; #end @@ -180,18 +190,22 @@ class InitState extends FlxTransitionableState // // GAME DATA PARSING // - funkin.data.level.LevelRegistry.instance.loadEntries(); - funkin.play.event.SongEventData.SongEventParser.loadEventCache(); - funkin.play.cutscene.dialogue.ConversationDataParser.loadConversationCache(); - funkin.play.cutscene.dialogue.DialogueBoxDataParser.loadDialogueBoxCache(); - funkin.play.cutscene.dialogue.SpeakerDataParser.loadSpeakerCache(); - funkin.play.song.SongData.SongDataParser.loadSongCache(); - funkin.play.stage.StageData.StageDataParser.loadStageCache(); - funkin.play.character.CharacterData.CharacterDataParser.loadCharacterCache(); - funkin.modding.module.ModuleHandler.buildModuleCallbacks(); - funkin.modding.module.ModuleHandler.loadModuleCache(); - funkin.modding.module.ModuleHandler.callOnCreate(); + // NOTE: Registries and data parsers must be imported and not referenced with fully qualified names, + // to ensure build macros work properly. + LevelRegistry.instance.loadEntries(); + NoteStyleRegistry.instance.loadEntries(); + SongEventParser.loadEventCache(); + ConversationDataParser.loadConversationCache(); + DialogueBoxDataParser.loadDialogueBoxCache(); + SpeakerDataParser.loadSpeakerCache(); + SongDataParser.loadSongCache(); + StageDataParser.loadStageCache(); + CharacterDataParser.loadCharacterCache(); + ModuleHandler.buildModuleCallbacks(); + ModuleHandler.loadModuleCache(); + + ModuleHandler.callOnCreate(); } /** diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index d90c1386d..bed63d1d8 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -10,6 +10,11 @@ import polymod.backends.PolymodAssets.PolymodAssetType; import polymod.format.ParseRules.TextFileFormat; import funkin.play.event.SongEventData.SongEventParser; import funkin.util.FileUtil; +import funkin.data.level.LevelRegistry; +import funkin.data.notestyle.NoteStyleRegistry; +import funkin.play.cutscene.dialogue.ConversationDataParser; +import funkin.play.cutscene.dialogue.DialogueBoxDataParser; +import funkin.play.cutscene.dialogue.SpeakerDataParser; class PolymodHandler { @@ -279,12 +284,14 @@ class PolymodHandler // TODO: Reload event callbacks - funkin.data.level.LevelRegistry.instance.loadEntries(); + // These MUST be imported at the top of the file and not referred to by fully qualified name, + // to ensure build macros work properly. + LevelRegistry.instance.loadEntries(); + NoteStyleRegistry.instance.loadEntries(); SongEventParser.loadEventCache(); - // TODO: Uncomment this once conversation data is implemented. - // ConversationDataParser.loadConversationCache(); - // DialogueBoxDataParser.loadDialogueBoxCache(); - // SpeakerDataParser.loadSpeakerCache(); + ConversationDataParser.loadConversationCache(); + DialogueBoxDataParser.loadDialogueBoxCache(); + SpeakerDataParser.loadSpeakerCache(); SongDataParser.loadSongCache(); StageDataParser.loadStageCache(); CharacterDataParser.loadCharacterCache(); diff --git a/source/funkin/play/cutscene/dialogue/DialogueBoxDataParser.hx b/source/funkin/play/cutscene/dialogue/DialogueBoxDataParser.hx index 7bac9cf38..cb00dd80d 100644 --- a/source/funkin/play/cutscene/dialogue/DialogueBoxDataParser.hx +++ b/source/funkin/play/cutscene/dialogue/DialogueBoxDataParser.hx @@ -21,7 +21,7 @@ class DialogueBoxDataParser /** * Parses and preloads the game's dialogueBox data and scripts when the game starts. - * + * * If you want to force dialogue boxes to be reloaded, you can just call this function again. */ public static function loadDialogueBoxCache():Void @@ -123,7 +123,7 @@ class DialogueBoxDataParser /** * Load a dialogueBox's JSON file, parse its data, and return it. - * + * * @param dialogueBoxId The dialogueBox to load. * @return The dialogueBox data, or null if validation failed. */ diff --git a/source/funkin/play/cutscene/dialogue/ScriptedSpeaker.hx b/source/funkin/play/cutscene/dialogue/ScriptedSpeaker.hx index 03846eb42..a244ff6f1 100644 --- a/source/funkin/play/cutscene/dialogue/ScriptedSpeaker.hx +++ b/source/funkin/play/cutscene/dialogue/ScriptedSpeaker.hx @@ -1,4 +1,9 @@ package funkin.play.cutscene.dialogue; +/** + * A script that can be tied to a Speaker. + * Create a scripted class that extends Speaker to use this. + * This allows you to customize how a specific conversation speaker appears. + */ @:hscriptClass -class ScriptedSpeaker extends Speaker implements polymod.hscript.HScriptedClass {} +class ScriptedSpeaker extends funkin.play.cutscene.dialogue.Speaker implements polymod.hscript.HScriptedClass {} diff --git a/source/funkin/play/cutscene/dialogue/SpeakerDataParser.hx b/source/funkin/play/cutscene/dialogue/SpeakerDataParser.hx index 62a8a105b..f7ddb099f 100644 --- a/source/funkin/play/cutscene/dialogue/SpeakerDataParser.hx +++ b/source/funkin/play/cutscene/dialogue/SpeakerDataParser.hx @@ -21,7 +21,7 @@ class SpeakerDataParser /** * Parses and preloads the game's speaker data and scripts when the game starts. - * + * * If you want to force speakers to be reloaded, you can just call this function again. */ public static function loadSpeakerCache():Void @@ -123,7 +123,7 @@ class SpeakerDataParser /** * Load a speaker's JSON file, parse its data, and return it. - * + * * @param speakerId The speaker to load. * @return The speaker data, or null if validation failed. */ From 16bcf2c76770e228423e4a200c1a9dd4b79243ef Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Thu, 13 Jul 2023 20:27:45 -0400 Subject: [PATCH 25/30] Rework note sprites to pull note style data --- source/funkin/LatencyState.hx | 3 +- source/funkin/MusicBeatState.hx | 4 +- source/funkin/audio/VoicesGroup.hx | 7 +++ source/funkin/play/PlayState.hx | 62 ++++++++++--------- source/funkin/play/notes/NoteSprite.hx | 28 ++------- source/funkin/play/notes/Strumline.hx | 29 ++++----- source/funkin/play/notes/StrumlineNote.hx | 52 +++------------- source/funkin/play/notes/SustainTrail.hx | 12 ++-- source/funkin/ui/ColorsMenu.hx | 3 +- .../ui/debug/charting/ChartEditorState.hx | 3 +- 10 files changed, 83 insertions(+), 120 deletions(-) diff --git a/source/funkin/LatencyState.hx b/source/funkin/LatencyState.hx index bd78a4298..4a8ed2d2e 100644 --- a/source/funkin/LatencyState.hx +++ b/source/funkin/LatencyState.hx @@ -1,5 +1,6 @@ package funkin; +import funkin.data.notestyle.NoteStyleRegistry; import flixel.FlxSprite; import flixel.FlxSubState; import flixel.group.FlxGroup; @@ -128,7 +129,7 @@ class LatencyState extends MusicBeatSubState for (i in 0...32) { - var note:NoteSprite = new NoteSprite(Conductor.beatLengthMs * i); + var note:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault(), Conductor.beatLengthMs * i); noteGrp.add(note); } diff --git a/source/funkin/MusicBeatState.hx b/source/funkin/MusicBeatState.hx index 9aad66773..4499b0946 100644 --- a/source/funkin/MusicBeatState.hx +++ b/source/funkin/MusicBeatState.hx @@ -107,7 +107,9 @@ class MusicBeatState extends FlxUIState implements IEventHandler { PolymodHandler.forceReloadAssets(); - // Restart the current state, so old data is cleared. + this.destroy(); + + // Create a new instance of the current state, so old data is cleared. FlxG.resetState(); } diff --git a/source/funkin/audio/VoicesGroup.hx b/source/funkin/audio/VoicesGroup.hx index 9f688eb48..6d61e6481 100644 --- a/source/funkin/audio/VoicesGroup.hx +++ b/source/funkin/audio/VoicesGroup.hx @@ -65,4 +65,11 @@ class VoicesGroup extends SoundGroup opponentVoices.clear(); super.clear(); } + + public override function destroy():Void + { + playerVoices.destroy(); + opponentVoices.destroy(); + super.destroy(); + } } diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index f651f5ac6..ff5b924f8 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -1,6 +1,9 @@ package funkin.play; import haxe.Int64; +import funkin.play.notes.notestyle.NoteStyle; +import funkin.data.notestyle.NoteStyleData; +import funkin.data.notestyle.NoteStyleRegistry; import flixel.addons.display.FlxPieDial; import flixel.addons.transition.FlxTransitionableState; import flixel.FlxCamera; @@ -989,6 +992,9 @@ class PlayState extends MusicBeatState */ override function debug_refreshModules():Void { + // Prevent further gameplay updates, which will try to reference dead objects. + criticalFailure = true; + // Remove the current stage. If the stage gets deleted while it's still in use, // it'll probably crash the game or something. if (this.currentStage != null) @@ -999,8 +1005,14 @@ class PlayState extends MusicBeatState currentStage = null; } + // Stop the instrumental. + if (FlxG.sound.music != null) + { + FlxG.sound.music.stop(); + } + // Stop the vocals. - if (vocals != null) + if (vocals != null && vocals.exists) { vocals.stop(); } @@ -1013,6 +1025,8 @@ class PlayState extends MusicBeatState override function stepHit():Bool { + if (criticalFailure) return false; + // super.stepHit() returns false if a module cancelled the event. if (!super.stepHit()) return false; @@ -1034,6 +1048,8 @@ class PlayState extends MusicBeatState override function beatHit():Bool { + if (criticalFailure) return false; + // super.beatHit() returns false if a module cancelled the event. if (!super.beatHit()) return false; @@ -1279,9 +1295,9 @@ class PlayState extends MusicBeatState // // OPPONENT HEALTH ICON // - iconP2 = new HealthIcon(currentCharData.opponent, 1); + iconP2 = new HealthIcon('dad', 1); iconP2.y = healthBar.y - (iconP2.height / 2); - dad.initHealthIcon(true); + dad.initHealthIcon(true); // Apply the character ID here add(iconP2); iconP2.cameras = [camHUD]; @@ -1298,9 +1314,9 @@ class PlayState extends MusicBeatState // // PLAYER HEALTH ICON // - iconP1 = new HealthIcon(currentPlayerId, 0); + iconP1 = new HealthIcon('bf', 0); iconP1.y = healthBar.y - (iconP1.height / 2); - boyfriend.initHealthIcon(false); + boyfriend.initHealthIcon(false); // Apply the character ID here add(iconP1); iconP1.cameras = [camHUD]; @@ -1350,19 +1366,17 @@ class PlayState extends MusicBeatState */ function initStrumlines():Void { - // var strumlineStyle:StrumlineStyle = NORMAL; - // - //// TODO: Put this in the chart or something? - // switch (currentStageId) - // { - // case 'school': - // strumlineStyle = PIXEL; - // case 'schoolEvil': - // strumlineStyle = PIXEL; - // } + var noteStyleId:String = switch (currentStageId) + { + case 'school': 'pixel'; + case 'schoolEvil': 'pixel'; + default: 'funkin'; + } + var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId); + if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault(); - playerStrumline = new Strumline(true); - opponentStrumline = new Strumline(false); + playerStrumline = new Strumline(noteStyle, true); + opponentStrumline = new Strumline(noteStyle, false); add(playerStrumline); add(opponentStrumline); @@ -1460,18 +1474,6 @@ class PlayState extends MusicBeatState songEvents = currentChart.getEvents(); SongEventParser.resetEvents(songEvents); - // TODO: Put this in the chart or something? - // var strumlineStyle:StrumlineStyle = null; - // switch (currentStageId) - // { - // case 'school': - // strumlineStyle = PIXEL; - // case 'schoolEvil': - // strumlineStyle = PIXEL; - // default: - // strumlineStyle = NORMAL; - // } - // Reset the notes on each strumline. var playerNoteData:Array<SongNoteData> = []; var opponentNoteData:Array<SongNoteData> = []; @@ -1631,6 +1633,8 @@ class PlayState extends MusicBeatState */ function processNotes(elapsed:Float):Void { + if (playerStrumline?.notes?.members == null || opponentStrumline?.notes?.members == null) return; + // Process notes on the opponent's side. for (note in opponentStrumline.notes.members) { diff --git a/source/funkin/play/notes/NoteSprite.hx b/source/funkin/play/notes/NoteSprite.hx index b407e7f74..25b23eee7 100644 --- a/source/funkin/play/notes/NoteSprite.hx +++ b/source/funkin/play/notes/NoteSprite.hx @@ -1,6 +1,7 @@ package funkin.play.notes; import funkin.play.song.SongData.SongNoteData; +import funkin.play.notes.notestyle.NoteStyle; import flixel.graphics.frames.FlxAtlasFrames; import flixel.FlxSprite; @@ -106,7 +107,7 @@ class NoteSprite extends FlxSprite */ public var handledMiss:Bool; - public function new(strumTime:Float = 0, direction:Int = 0) + public function new(noteStyle:NoteStyle, strumTime:Float = 0, direction:Int = 0) { super(0, -9999); this.strumTime = strumTime; @@ -114,34 +115,15 @@ class NoteSprite extends FlxSprite if (this.strumTime < 0) this.strumTime = 0; - setupNoteGraphic(); + setupNoteGraphic(noteStyle); // Disables the update() function for performance. this.active = false; } - public static function buildNoteFrames(force:Bool = false):FlxAtlasFrames + function setupNoteGraphic(noteStyle:NoteStyle):Void { - // static variables inside functions are a cool of Haxe 4.3.0. - static var noteFrames:FlxAtlasFrames = null; - - if (noteFrames != null && !force) return noteFrames; - - noteFrames = Paths.getSparrowAtlas('notes'); - - noteFrames.parent.persist = true; - - return noteFrames; - } - - function setupNoteGraphic():Void - { - this.frames = buildNoteFrames(); - - animation.addByPrefix('greenScroll', 'noteUp'); - animation.addByPrefix('redScroll', 'noteRight'); - animation.addByPrefix('blueScroll', 'noteDown'); - animation.addByPrefix('purpleScroll', 'noteLeft'); + noteStyle.buildNoteSprite(this); setGraphicSize(Strumline.STRUMLINE_SIZE); updateHitbox(); diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx index 7730073f8..4fdf5afe3 100644 --- a/source/funkin/play/notes/Strumline.hx +++ b/source/funkin/play/notes/Strumline.hx @@ -1,6 +1,7 @@ package funkin.play.notes; import flixel.FlxG; +import funkin.play.notes.notestyle.NoteStyle; import flixel.group.FlxSpriteGroup; import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; import flixel.tweens.FlxEase; @@ -52,16 +53,19 @@ class Strumline extends FlxSpriteGroup var noteSplashes:FlxTypedSpriteGroup<NoteSplash>; var noteHoldCovers:FlxTypedSpriteGroup<NoteHoldCover>; + final noteStyle:NoteStyle; + var noteData:Array<SongNoteData> = []; var nextNoteIndex:Int = -1; var heldKeys:Array<Bool> = []; - public function new(isPlayer:Bool) + public function new(noteStyle:NoteStyle, isPlayer:Bool) { super(); this.isPlayer = isPlayer; + this.noteStyle = noteStyle; this.strumlineNotes = new FlxTypedSpriteGroup<StrumlineNote>(); this.strumlineNotes.zIndex = 10; @@ -88,10 +92,11 @@ class Strumline extends FlxSpriteGroup for (i in 0...KEY_COUNT) { - var child:StrumlineNote = new StrumlineNote(isPlayer, DIRECTIONS[i]); + var child:StrumlineNote = new StrumlineNote(noteStyle, isPlayer, DIRECTIONS[i]); child.x = getXPos(DIRECTIONS[i]); child.x += INITIAL_OFFSET; child.y = 0; + noteStyle.applyStrumlineOffsets(child); this.strumlineNotes.add(child); } @@ -119,20 +124,6 @@ class Strumline extends FlxSpriteGroup super.update(elapsed); updateNotes(); - - #if debug - if (!isPlayer) - { - FlxG.watch.addQuick("strumlineAnim", strumlineNotes.members[3]?.animation?.curAnim?.name); - var curFrame = strumlineNotes.members[3]?.animation?.curAnim?.curFrame; - frameMax = (curFrame > frameMax) ? curFrame : frameMax; - FlxG.watch.addQuick("strumlineFrame", strumlineNotes.members[3]?.animation?.curAnim?.curFrame); - FlxG.watch.addQuick("strumlineFrameMax", frameMax); - animFinishedEver = animFinishedEver || strumlineNotes.members[3]?.animation?.curAnim?.finished; - FlxG.watch.addQuick("strumlineFinished", strumlineNotes.members[3]?.animation?.curAnim?.finished); - FlxG.watch.addQuick("strumlineFinishedEver", animFinishedEver); - } - #end } var frameMax:Int; @@ -482,6 +473,7 @@ class Strumline extends FlxSpriteGroup { // TODO: Add a setting to disable note splashes. // if (Settings.noSplash) return; + if (!noteStyle.isNoteSplashEnabled()) return; var splash:NoteSplash = this.constructNoteSplash(); @@ -502,6 +494,7 @@ class Strumline extends FlxSpriteGroup { // TODO: Add a setting to disable note splashes. // if (Settings.noSplash) return; + if (!noteStyle.isHoldNoteCoverEnabled()) return; var cover:NoteHoldCover = this.constructNoteHoldCover(); @@ -659,7 +652,7 @@ class Strumline extends FlxSpriteGroup { // The note sprite pool is full and all note splashes are active. // We have to create a new note. - result = new NoteSprite(); + result = new NoteSprite(noteStyle); this.notes.add(result); } @@ -685,7 +678,7 @@ class Strumline extends FlxSpriteGroup { // The note sprite pool is full and all note splashes are active. // We have to create a new note. - result = new SustainTrail(0, 100, Paths.image("NOTE_hold_assets")); + result = new SustainTrail(0, 100, noteStyle.getHoldNoteAssetPath(), noteStyle); this.holdNotes.add(result); } diff --git a/source/funkin/play/notes/StrumlineNote.hx b/source/funkin/play/notes/StrumlineNote.hx index 6361f607e..40d893255 100644 --- a/source/funkin/play/notes/StrumlineNote.hx +++ b/source/funkin/play/notes/StrumlineNote.hx @@ -1,5 +1,6 @@ package funkin.play.notes; +import funkin.play.notes.notestyle.NoteStyle; import flixel.graphics.frames.FlxAtlasFrames; import flixel.FlxSprite; import funkin.play.notes.NoteSprite; @@ -17,24 +18,13 @@ class StrumlineNote extends FlxSprite static final CONFIRM_HOLD_TIME:Float = 0.1; - public function updatePosition(parentNote:NoteSprite) - { - this.x = parentNote.x; - this.x += parentNote.width / 2; - this.x -= this.width / 2; - - this.y = parentNote.y; - this.y += parentNote.height / 2; - } - function set_direction(value:NoteDirection):NoteDirection { this.direction = value; - setup(); return this.direction; } - public function new(isPlayer:Bool, direction:NoteDirection) + public function new(noteStyle:NoteStyle, isPlayer:Bool, direction:NoteDirection) { super(0, 0); @@ -42,6 +32,8 @@ class StrumlineNote extends FlxSprite this.direction = direction; + setup(noteStyle); + this.animation.callback = onAnimationFrame; this.animation.finishCallback = onAnimationFinished; @@ -81,39 +73,15 @@ class StrumlineNote extends FlxSprite } } - function setup():Void + function setup(noteStyle:NoteStyle):Void { - this.frames = Paths.getSparrowAtlas('noteStrumline'); + noteStyle.applyStrumlineFrames(this); + noteStyle.applyStrumlineAnimations(this, this.direction); - switch (this.direction) - { - case NoteDirection.LEFT: - this.animation.addByPrefix('static', 'staticLeft0', 24, false, false, false); - this.animation.addByPrefix('press', 'pressLeft0', 24, false, false, false); - this.animation.addByPrefix('confirm', 'confirmLeft0', 24, false, false, false); - this.animation.addByPrefix('confirm-hold', 'confirmHoldLeft0', 24, true, false, false); - - case NoteDirection.DOWN: - this.animation.addByPrefix('static', 'staticDown0', 24, false, false, false); - this.animation.addByPrefix('press', 'pressDown0', 24, false, false, false); - this.animation.addByPrefix('confirm', 'confirmDown0', 24, false, false, false); - this.animation.addByPrefix('confirm-hold', 'confirmHoldDown0', 24, true, false, false); - - case NoteDirection.UP: - this.animation.addByPrefix('static', 'staticUp0', 24, false, false, false); - this.animation.addByPrefix('press', 'pressUp0', 24, false, false, false); - this.animation.addByPrefix('confirm', 'confirmUp0', 24, false, false, false); - this.animation.addByPrefix('confirm-hold', 'confirmHoldUp0', 24, true, false, false); - - case NoteDirection.RIGHT: - this.animation.addByPrefix('static', 'staticRight0', 24, false, false, false); - this.animation.addByPrefix('press', 'pressRight0', 24, false, false, false); - this.animation.addByPrefix('confirm', 'confirmRight0', 24, false, false, false); - this.animation.addByPrefix('confirm-hold', 'confirmHoldRight0', 24, true, false, false); - } - - this.setGraphicSize(Std.int(Strumline.STRUMLINE_SIZE * 1.55)); + this.setGraphicSize(Std.int(Strumline.STRUMLINE_SIZE * noteStyle.getStrumlineScale())); this.updateHitbox(); + noteStyle.applyStrumlineOffsets(this); + this.playStatic(); } diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx index addc312f4..d9f1aab6e 100644 --- a/source/funkin/play/notes/SustainTrail.hx +++ b/source/funkin/play/notes/SustainTrail.hx @@ -1,5 +1,6 @@ package funkin.play.notes; +import funkin.play.notes.notestyle.NoteStyle; import funkin.play.notes.NoteDirection; import funkin.play.song.SongData.SongNoteData; import flixel.util.FlxDirectionFlags; @@ -79,25 +80,28 @@ class SustainTrail extends FlxSprite */ public var bottomClip:Float = 0.9; + public var isPixel:Bool; + /** * Normally you would take strumTime:Float, noteData:Int, sustainLength:Float, parentNote:Note (?) * @param NoteData * @param SustainLength Length in milliseconds. * @param fileName */ - public function new(noteDirection:NoteDirection, sustainLength:Float, fileName:String) + public function new(noteDirection:NoteDirection, sustainLength:Float, fileName:String, noteStyle:NoteStyle) { super(0, 0, fileName); antialiasing = true; - // TODO: Why does this reference pixel stuff? - if (fileName == "arrowEnds") + this.isPixel = noteStyle.isHoldNotePixel(); + if (isPixel) { endOffset = bottomClip = 1; antialiasing = false; - zoom = 6; } + zoom *= noteStyle.fetchHoldNoteScale(); + // BASIC SETUP this.sustainLength = sustainLength; this.fullSustainLength = sustainLength; diff --git a/source/funkin/ui/ColorsMenu.hx b/source/funkin/ui/ColorsMenu.hx index dfa0cf067..6a844eef3 100644 --- a/source/funkin/ui/ColorsMenu.hx +++ b/source/funkin/ui/ColorsMenu.hx @@ -1,5 +1,6 @@ package funkin.ui; +import funkin.data.notestyle.NoteStyleRegistry; import flixel.addons.effects.chainable.FlxEffectSprite; import flixel.addons.effects.chainable.FlxOutlineEffect; import flixel.group.FlxGroup.FlxTypedGroup; @@ -22,7 +23,7 @@ class ColorsMenu extends Page for (i in 0...4) { - var note:NoteSprite = new NoteSprite(0, i); + var note:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault(), 0, i); note.x = (100 * i) + i; note.screenCenter(Y); diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index aad43f93f..ecb9db5b6 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1,5 +1,6 @@ package funkin.ui.debug.charting; +import funkin.data.notestyle.NoteStyleRegistry; import funkin.ui.debug.charting.ChartEditorCommand; import flixel.input.keyboard.FlxKey; import funkin.input.TurboKeyHandler; @@ -2804,7 +2805,7 @@ class ChartEditorState extends HaxeUIState // Character preview. // NoteScriptEvent takes a sprite, ehe. Need to rework that. - var tempNote:NoteSprite = new NoteSprite(); + var tempNote:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault()); tempNote.noteData = noteData; tempNote.scrollFactor.set(0, 0); var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, tempNote, 1, true); From 8fe837d76d469b4ab31af60cb7891be34391abab Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Fri, 14 Jul 2023 19:51:45 -0400 Subject: [PATCH 26/30] Attempt to fix github actions --- .github/workflows/build-shit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml index 77b8e9895..6d818f6d1 100644 --- a/.github/workflows/build-shit.yml +++ b/.github/workflows/build-shit.yml @@ -49,7 +49,7 @@ jobs: # TODO: Remove the step that builds Lime later. # Powershell method run: | - $LIME_PATH = &"haxelib libpath lime" + $LIME_PATH = haxelib libpath lime echo "Moving to $LIME_PATH" cd $LIME_PATH git submodule sync --recursive From f675a5c578005815982d745585abc922da37c9ae Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Fri, 14 Jul 2023 22:25:51 -0400 Subject: [PATCH 27/30] Fix actions more --- .github/workflows/build-shit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml index 6d818f6d1..11cd28343 100644 --- a/.github/workflows/build-shit.yml +++ b/.github/workflows/build-shit.yml @@ -55,7 +55,7 @@ jobs: git submodule sync --recursive git submodule update --recursive git status - lime rebuild windows --clean + haxelib run lime rebuild windows --clean - name: Build game run: | haxelib run lime build windows -debug From 5a70a50a56cd580d121cb58b0b613d437e60e0a6 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Fri, 14 Jul 2023 22:53:11 -0400 Subject: [PATCH 28/30] Remember to build Lime on HTML5 too. --- .github/workflows/build-shit.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml index 11cd28343..c18db7093 100644 --- a/.github/workflows/build-shit.yml +++ b/.github/workflows/build-shit.yml @@ -25,7 +25,18 @@ jobs: steps: - uses: actions/checkout@v3 - uses: ./.github/actions/setup-haxeshit - - name: Build game? + - name: Build Lime + # TODO: Remove the step that builds Lime later. + # Powershell method + run: | + $LIME_PATH = haxelib libpath lime + echo "Moving to $LIME_PATH" + cd $LIME_PATH + git submodule sync --recursive + git submodule update --recursive + git status + haxelib run lime rebuild windows --clean + - name: Build game run: | sudo apt-get install -y libx11-dev libxinerama-dev libxrandr-dev libgl1-mesa-dev libgl-dev libxi-dev libxext-dev libasound2-dev haxelib run lime build html5 -debug --times From ddcb0474a3ef49b2f58ea2fb7607297e8f64b9c8 Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Fri, 14 Jul 2023 23:07:17 -0400 Subject: [PATCH 29/30] Convert command to Bash --- .github/workflows/build-shit.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml index c18db7093..07de07075 100644 --- a/.github/workflows/build-shit.yml +++ b/.github/workflows/build-shit.yml @@ -27,9 +27,9 @@ jobs: - uses: ./.github/actions/setup-haxeshit - name: Build Lime # TODO: Remove the step that builds Lime later. - # Powershell method + # Bash method run: | - $LIME_PATH = haxelib libpath lime + LIME_PATH=`haxelib libpath lime` echo "Moving to $LIME_PATH" cd $LIME_PATH git submodule sync --recursive From 3d8459fe6f56aa9918f24331d7024c2dbb6b165e Mon Sep 17 00:00:00 2001 From: EliteMasterEric <ericmyllyoja@gmail.com> Date: Sat, 15 Jul 2023 04:22:02 -0400 Subject: [PATCH 30/30] Added dependency to HTML5 lime build --- .github/workflows/build-shit.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml index 07de07075..32c2a0ede 100644 --- a/.github/workflows/build-shit.yml +++ b/.github/workflows/build-shit.yml @@ -35,7 +35,8 @@ jobs: git submodule sync --recursive git submodule update --recursive git status - haxelib run lime rebuild windows --clean + sudo apt-get install -y libxinerama-dev + haxelib run lime rebuild linux --clean - name: Build game run: | sudo apt-get install -y libx11-dev libxinerama-dev libxrandr-dev libgl1-mesa-dev libgl-dev libxi-dev libxext-dev libasound2-dev