diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx index 660ef8adf..590e066a4 100644 --- a/source/funkin/Conductor.hx +++ b/source/funkin/Conductor.hx @@ -1,11 +1,10 @@ package funkin; import funkin.SongLoad.SwagSong; +import funkin.play.song.Song.SongDifficulty; +import funkin.play.song.SongData.ConductorTimeChange; +import funkin.play.song.SongData.SongTimeChange; -/** - * ... - * @author - */ typedef BPMChangeEvent = { var stepTime:Int; @@ -16,12 +15,40 @@ typedef BPMChangeEvent = class Conductor { /** - * Beats per minute of the song. + * The list of time changes in the song. + * There should be at least one time change (at the beginning of the song) to define the BPM. */ - public static var bpm:Float = 100; + private static var timeChanges:Array = []; /** - * Duration of a beat in millisecond. + * The current time change. + */ + private static var currentTimeChange:ConductorTimeChange; + + /** + * The current position in the song in milliseconds. + * Updated every frame based on the audio position. + */ + public static var songPosition:Float; + + /** + * Beats per minute of the current song at the current time. + */ + public static var bpm(get, null):Float = 100; + + static function get_bpm():Float + { + if (currentTimeChange == null) + return 100; + + return currentTimeChange.bpm; + } + + // OLD, replaced with timeChanges. + public static var bpmChangeMap:Array = []; + + /** + * Duration of a beat in millisecond. Calculated based on bpm. */ public static var crochet(get, null):Float; @@ -31,7 +58,7 @@ class Conductor } /** - * Duration of a step in milliseconds. + * Duration of a step in milliseconds. Calculated based on bpm. */ public static var stepCrochet(get, null):Float; @@ -40,19 +67,62 @@ class Conductor return crochet / 4; } - /** - * The current position in the song in milliseconds. - */ - public static var songPosition:Float; + public static var currentBeat(get, null):Float; + + static function get_currentBeat():Float + { + return currentBeat; + } + + public static var currentStep(get, null):Int; + + static function get_currentStep():Int + { + return currentStep; + } public static var lastSongPos:Float; public static var visualOffset:Float = 0; public static var audioOffset:Float = 0; public static var offset:Float = 0; - public static var bpmChangeMap:Array = []; + public function new() + { + } - public function new() {} + public static function getLastBPMChange() + { + var lastChange:BPMChangeEvent = { + stepTime: 0, + songTime: 0, + bpm: 0 + } + for (i in 0...Conductor.bpmChangeMap.length) + { + if (Conductor.songPosition >= Conductor.bpmChangeMap[i].songTime) + lastChange = Conductor.bpmChangeMap[i]; + + if (Conductor.songPosition < Conductor.bpmChangeMap[i].songTime) + break; + } + return lastChange; + } + + public static function forceBPM(bpm:Float) + { + // TODO: Get rid of this and use song metadata instead. + Conductor.bpm = bpm; + } + + /** + * Update the conductor with the current song position. + * BPM, current step, etc. will be re-calculated based on the song position. + */ + public static function update(songPosition:Float) + { + Conductor.songPosition = songPosition; + Conductor.bpm = Conductor.getLastBPMChange().bpm; + } public static function mapBPMChanges(song:SwagSong) { @@ -80,4 +150,34 @@ class Conductor } trace("new BPM map BUDDY " + bpmChangeMap); } + + public static function mapTimeChanges(currentChart:SongDifficulty) + { + var songTimeChanges:Array = currentChart.timeChanges; + + timeChanges = []; + + for (songTimeChange in timeChanges) + { + var prevTimeChange:ConductorTimeChange = timeChanges.length == 0 ? null : timeChanges[timeChanges.length - 1]; + var currentTimeChange:ConductorTimeChange = cast songTimeChange; + + if (prevTimeChange != null) + { + var deltaTime:Float = currentTimeChange.timeStamp - prevTimeChange.timeStamp; + var deltaSteps:Int = Math.round(deltaTime / (60 / prevTimeChange.bpm) * 1000 / 4); + + currentTimeChange.stepTime = prevTimeChange.stepTime + deltaSteps; + } + else + { + // We know the time and steps of this time change is 0, since this is the first time change. + currentTimeChange.stepTime = 0; + } + + timeChanges.push(currentTimeChange); + } + + // Done. + } } diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx index f9f0681ae..fbb90ed98 100644 --- a/source/funkin/FreeplayState.hx +++ b/source/funkin/FreeplayState.hx @@ -27,6 +27,7 @@ import funkin.freeplayStuff.FreeplayScore; import funkin.freeplayStuff.SongMenuItem; import funkin.play.HealthIcon; import funkin.play.PlayState; +import funkin.play.song.SongData.SongDataParser; import funkin.shaderslmfao.AngleMask; import funkin.shaderslmfao.PureColor; import funkin.shaderslmfao.StrokeShader; @@ -97,7 +98,7 @@ class FreeplayState extends MusicBeatSubstate } if (StoryMenuState.weekUnlocked[2] || isDebug) - addWeek(['Bopeebo', 'Fresh', 'Dadbattle'], 1, ['dad']); + addWeek(['Bopeebo', 'Bopeebo_new', 'Fresh', 'Dadbattle'], 1, ['dad']); if (StoryMenuState.weekUnlocked[2] || isDebug) addWeek(['Spookeez', 'South', 'Monster'], 2, ['spooky', 'spooky', 'monster']); @@ -520,8 +521,10 @@ class FreeplayState extends MusicBeatSubstate }*/ PlayState.currentSong = SongLoad.loadFromJson(poop, songs[curSelected].songName.toLowerCase()); + PlayState.currentSong_NEW = SongDataParser.fetchSong(songs[curSelected].songName.toLowerCase()); PlayState.isStoryMode = false; PlayState.storyDifficulty = curDifficulty; + PlayState.storyDifficulty_NEW = 'easy'; // SongLoad.curDiff = Highscore.formatSong() SongLoad.curDiff = switch (curDifficulty) @@ -562,6 +565,7 @@ class FreeplayState extends MusicBeatSubstate intendedScore = FlxG.random.int(0, 100000); PlayState.storyDifficulty = curDifficulty; + PlayState.storyDifficulty_NEW = 'easy'; grpDifficulties.group.forEach(function(spr) { diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index 36deb4e2b..558127482 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -191,8 +191,10 @@ class InitState extends FlxTransitionableState var dif = getDif(); PlayState.currentSong = SongLoad.loadFromJson(song, song); + PlayState.currentSong_NEW = SongDataParser.fetchSong(song); PlayState.isStoryMode = isStoryMode; PlayState.storyDifficulty = dif; + PlayState.storyDifficulty_NEW = 'easy'; SongLoad.curDiff = switch (dif) { case 0: 'easy'; diff --git a/source/funkin/LatencyState.hx b/source/funkin/LatencyState.hx index d3a790104..79f3f217a 100644 --- a/source/funkin/LatencyState.hx +++ b/source/funkin/LatencyState.hx @@ -70,7 +70,7 @@ class LatencyState extends MusicBeatSubstate // funnyStatsGraph.hi - Conductor.bpm = 60; + Conductor.forceBPM(60); noteGrp = new FlxTypedGroup(); add(noteGrp); diff --git a/source/funkin/Note.hx b/source/funkin/Note.hx index 90d01666f..9db44775e 100644 --- a/source/funkin/Note.hx +++ b/source/funkin/Note.hx @@ -2,6 +2,8 @@ package funkin; 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; diff --git a/source/funkin/PauseSubState.hx b/source/funkin/PauseSubState.hx index 8a9648795..8f8a2752f 100644 --- a/source/funkin/PauseSubState.hx +++ b/source/funkin/PauseSubState.hx @@ -1,17 +1,15 @@ package funkin; import flixel.FlxSprite; -import flixel.FlxSubState; import flixel.addons.transition.FlxTransitionableState; import flixel.group.FlxGroup.FlxTypedGroup; -import flixel.input.keyboard.FlxKey; import flixel.system.FlxSound; import flixel.text.FlxText; import flixel.tweens.FlxEase; import flixel.tweens.FlxTween; import flixel.util.FlxColor; -import funkin.Controls.Control; import funkin.play.PlayState; +import funkin.play.song.SongData.SongDataParser; class PauseSubState extends MusicBeatSubstate { @@ -61,7 +59,14 @@ class PauseSubState extends MusicBeatSubstate add(metaDataGrp); var levelInfo:FlxText = new FlxText(20, 15, 0, "", 32); - levelInfo.text += PlayState.currentSong.song; + if (PlayState.instance.currentChart != null) + { + levelInfo.text += '${PlayState.instance.currentChart.songName} - ${PlayState.instance.currentChart.songArtist}'; + } + else + { + levelInfo.text += PlayState.currentSong.song; + } levelInfo.scrollFactor.set(); levelInfo.setFormat(Paths.font("vcr.ttf"), 32); levelInfo.updateHitbox(); @@ -180,9 +185,11 @@ class PauseSubState extends MusicBeatSubstate close(); case "EASY" | 'NORMAL' | "HARD": PlayState.currentSong = SongLoad.loadFromJson(PlayState.currentSong.song.toLowerCase(), PlayState.currentSong.song.toLowerCase()); + PlayState.currentSong_NEW = SongDataParser.fetchSong(PlayState.currentSong.song.toLowerCase()); SongLoad.curDiff = daSelected.toLowerCase(); PlayState.storyDifficulty = curSelected; + PlayState.storyDifficulty_NEW = 'easy'; PlayState.needsReset = true; diff --git a/source/funkin/SongLoad.hx b/source/funkin/SongLoad.hx index b2baaf17c..acbeaab09 100644 --- a/source/funkin/SongLoad.hx +++ b/source/funkin/SongLoad.hx @@ -2,6 +2,7 @@ package funkin; import funkin.Section.SwagSection; import funkin.noteStuff.NoteBasic.NoteData; +import funkin.play.PlayState; import haxe.Json; import lime.utils.Assets; @@ -47,7 +48,21 @@ class SongLoad public static function loadFromJson(jsonInput:String, ?folder:String):SwagSong { - var rawJson = Assets.getText(Paths.json('songs/${folder.toLowerCase()}/${jsonInput.toLowerCase()}')).trim(); + var rawJson:Dynamic = 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("}")) { @@ -112,6 +127,11 @@ class SongLoad 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; @@ -137,6 +157,14 @@ class SongLoad 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 { diff --git a/source/funkin/StoryMenuState.hx b/source/funkin/StoryMenuState.hx index d3023b4a3..b5e4a6bc7 100644 --- a/source/funkin/StoryMenuState.hx +++ b/source/funkin/StoryMenuState.hx @@ -12,6 +12,7 @@ import flixel.util.FlxColor; import flixel.util.FlxTimer; import funkin.MenuItem.WeekType; import funkin.play.PlayState; +import funkin.play.song.SongData.SongDataParser; import lime.net.curl.CURLCode; import openfl.Assets; @@ -372,10 +373,12 @@ class StoryMenuState extends MusicBeatState selectedWeek = true; PlayState.currentSong = SongLoad.loadFromJson(PlayState.storyPlaylist[0].toLowerCase(), PlayState.storyPlaylist[0].toLowerCase()); + PlayState.currentSong_NEW = SongDataParser.fetchSong(PlayState.storyPlaylist[0].toLowerCase()); PlayState.storyWeek = curWeek; PlayState.campaignScore = 0; PlayState.storyDifficulty = curDifficulty; + PlayState.storyDifficulty_NEW = 'easy'; SongLoad.curDiff = switch (curDifficulty) { case 0: diff --git a/source/funkin/TitleState.hx b/source/funkin/TitleState.hx index 3ae12dd43..200198f5c 100644 --- a/source/funkin/TitleState.hx +++ b/source/funkin/TitleState.hx @@ -140,7 +140,7 @@ class TitleState extends MusicBeatState { FlxG.sound.playMusic(Paths.music('freakyMenu'), 0); FlxG.sound.music.fadeIn(4, 0, 0.7); - Conductor.bpm = Constants.FREAKY_MENU_BPM; + Conductor.forceBPM(Constants.FREAKY_MENU_BPM); } persistentUpdate = true; @@ -474,7 +474,7 @@ class TitleState extends MusicBeatState var spec:SpectogramSprite = new SpectogramSprite(FlxG.sound.music); add(spec); - Conductor.bpm = 190; + Conductor.forceBPM(190); FlxG.camera.flash(FlxColor.WHITE, 1); FlxG.sound.play(Paths.sound('confirmMenu'), 0.7); } diff --git a/source/funkin/charting/ChartingState.hx b/source/funkin/charting/ChartingState.hx index 90b0de853..cb0faa74e 100644 --- a/source/funkin/charting/ChartingState.hx +++ b/source/funkin/charting/ChartingState.hx @@ -148,7 +148,7 @@ class ChartingState extends MusicBeatState updateGrid(); loadSong(_song.song); - Conductor.bpm = _song.bpm; + // Conductor.bpm = _song.bpm; Conductor.mapBPMChanges(_song); bpmTxt = new FlxText(1000, 50, 0, "", 16); @@ -549,7 +549,7 @@ class ChartingState extends MusicBeatState { tempBpm = nums.value; Conductor.mapBPMChanges(_song); - Conductor.bpm = nums.value; + Conductor.forceBPM(nums.value); } else if (wname == 'note_susLength') { @@ -1223,7 +1223,7 @@ class ChartingState extends MusicBeatState if (SongLoad.getSong()[curSection].changeBPM && SongLoad.getSong()[curSection].bpm > 0) { - Conductor.bpm = SongLoad.getSong()[curSection].bpm; + Conductor.forceBPM(SongLoad.getSong()[curSection].bpm); FlxG.log.add('CHANGED BPM!'); } else @@ -1233,7 +1233,7 @@ class ChartingState extends MusicBeatState for (i in 0...curSection) if (SongLoad.getSong()[i].changeBPM) daBPM = SongLoad.getSong()[i].bpm; - Conductor.bpm = daBPM; + Conductor.forceBPM(daBPM); } /* // PORT BULLSHIT, INCASE THERE'S NO SUSTAIN DATA FOR A NOTE diff --git a/source/funkin/play/PicoFight.hx b/source/funkin/play/PicoFight.hx index 286d13550..b6adb811c 100644 --- a/source/funkin/play/PicoFight.hx +++ b/source/funkin/play/PicoFight.hx @@ -5,6 +5,7 @@ import flixel.addons.effects.FlxTrail; import flixel.group.FlxGroup.FlxTypedGroup; import flixel.math.FlxMath; import flixel.util.FlxColor; +import flixel.util.FlxDirectionFlags; import funkin.audiovis.PolygonSpectogram; import funkin.noteStuff.NoteBasic.NoteData; @@ -35,7 +36,7 @@ class PicoFight extends MusicBeatState FlxG.sound.playMusic(Paths.inst("blazin")); SongLoad.loadFromJson('blazin', "blazin"); - Conductor.bpm = SongLoad.songData.bpm; + Conductor.forceBPM(SongLoad.songData.bpm); for (dumbassSection in SongLoad.songData.noteMap['hard']) { diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index b10206cd7..5fbfccde1 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -28,6 +28,10 @@ import funkin.play.Strumline.StrumlineStyle; import funkin.play.character.BaseCharacter; import funkin.play.character.CharacterData; import funkin.play.scoring.Scoring; +import funkin.play.song.Song; +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; import funkin.ui.PopUpStuff; @@ -62,6 +66,8 @@ class PlayState extends MusicBeatState */ public static var currentSong:SwagSong = null; + public static var currentSong_NEW:Song = null; + /** * Whether the game is currently in Story Mode. If false, we are in Free Play Mode. */ @@ -116,6 +122,8 @@ class PlayState extends MusicBeatState */ public var currentStage:Stage = null; + public var currentChart(get, null):SongDifficulty; + /** * The internal ID of the currently active Stage. * Used to retrieve the data required to build the `currentStage`. @@ -166,6 +174,12 @@ class PlayState extends MusicBeatState */ private var healthLerp:Float = 1; + /** + * Forcibly disables all update logic while the game moves back to the Menu state. + * This is used only when a critical error occurs and the game cannot continue. + */ + private var criticalFailure:Bool = false; + /** * RENDER OBJECTS */ @@ -244,6 +258,7 @@ class PlayState extends MusicBeatState public static var storyWeek:Int = 0; public static var storyPlaylist:Array = []; public static var storyDifficulty:Int = 1; + public static var storyDifficulty_NEW:String = "normal"; public static var seenCutscene:Bool = false; public static var campaignScore:Int = 0; @@ -279,8 +294,10 @@ class PlayState extends MusicBeatState { super.create(); - if (currentSong == null) + if (currentSong == null && currentSong_NEW == null) { + criticalFailure = true; + lime.app.Application.current.window.alert("There was a critical error while accessing the selected song. Click OK to return to the main menu.", "Error loading PlayState"); FlxG.switchState(new MainMenuState()); @@ -308,28 +325,48 @@ class PlayState extends MusicBeatState FlxG.sound.music.stop(); // Prepare the current song to be played. - FlxG.sound.cache(Paths.inst(currentSong.song)); - FlxG.sound.cache(Paths.voices(currentSong.song)); + if (currentChart != null) + { + currentChart.cacheInst(); + currentChart.cacheVocals(); + } + else + { + FlxG.sound.cache(Paths.inst(currentSong.song)); + FlxG.sound.cache(Paths.voices(currentSong.song)); + } - Conductor.songPosition = -5000; + Conductor.update(-5000); // Initialize stage stuff. initCameras(); - if (currentSong == null) - currentSong = SongLoad.loadFromJson('tutorial'); - - Conductor.mapBPMChanges(currentSong); - Conductor.bpm = currentSong.bpm; - - switch (currentSong.song.toLowerCase()) + if (currentSong == null && currentSong_NEW == null) { - case 'senpai': - dialogue = CoolUtil.coolTextFile(Paths.txt('songs/senpai/senpaiDialogue')); - case 'roses': - dialogue = CoolUtil.coolTextFile(Paths.txt('songs/roses/rosesDialogue')); - case 'thorns': - dialogue = CoolUtil.coolTextFile(Paths.txt('songs/thorns/thornsDialogue')); + currentSong = SongLoad.loadFromJson('tutorial'); + } + + if (currentSong_NEW != null) + { + Conductor.mapTimeChanges(currentChart); + // Conductor.bpm = currentChart.getStartingBPM(); + + // TODO: Support for dialog. + } + else + { + Conductor.mapBPMChanges(currentSong); + // Conductor.bpm = currentSong.bpm; + + switch (currentSong.song.toLowerCase()) + { + case 'senpai': + dialogue = CoolUtil.coolTextFile(Paths.txt('songs/senpai/senpaiDialogue')); + case 'roses': + dialogue = CoolUtil.coolTextFile(Paths.txt('songs/roses/rosesDialogue')); + case 'thorns': + dialogue = CoolUtil.coolTextFile(Paths.txt('songs/thorns/thornsDialogue')); + } } if (dialogue != null) @@ -379,7 +416,14 @@ class PlayState extends MusicBeatState add(grpNoteSplashes); - generateSong(); + if (currentSong_NEW != null) + { + generateSong_NEW(); + } + else + { + generateSong(); + } resetCamera(); @@ -442,6 +486,13 @@ class PlayState extends MusicBeatState #end } + function get_currentChart():SongDifficulty + { + if (currentSong_NEW == null || storyDifficulty_NEW == null) + return null; + return currentSong_NEW.getDifficulty(storyDifficulty_NEW); + } + /** * Initializes the game and HUD cameras. */ @@ -460,6 +511,12 @@ class PlayState extends MusicBeatState function initStage() { + if (currentSong_NEW != null) + { + initStage_NEW(); + return; + } + // TODO: Move stageId to the song file. switch (currentSong.song.toLowerCase()) { @@ -487,9 +544,6 @@ class PlayState extends MusicBeatState currentStageId = 'schoolEvil'; case 'guns' | 'stress' | 'ugh': currentStageId = 'tankmanBattlefield'; - case 'experimental-phase' | 'perfection': - // SERIOUSLY REVAMP THE CHART FORMAT ALREADY - currentStageId = "breakout"; default: currentStageId = "mainStage"; } @@ -497,8 +551,33 @@ class PlayState extends MusicBeatState loadStage(currentStageId); } + function initStage_NEW() + { + if (currentChart == null) + { + trace('Song difficulty could not be loaded.'); + } + + if (currentChart.stage != null && currentChart.stage != '') + { + currentStageId = currentChart.stage; + } + else + { + currentStageId = SongValidator.DEFAULT_STAGE; + } + + loadStage(currentStageId); + } + function initCharacters() { + if (currentSong_NEW != null) + { + initCharacters_NEW(); + return; + } + iconP1 = new HealthIcon(currentSong.player1, 0); iconP1.y = healthBar.y - (iconP1.height / 2); add(iconP1); @@ -615,6 +694,111 @@ class PlayState extends MusicBeatState } } + function initCharacters_NEW() + { + if (currentSong_NEW == 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 currentPlayer = 'bf'; + + var currentCharData:SongPlayableChar = currentChart.getPlayableChar(currentPlayer); + + // + // 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; + } + + // TODO: Cut out this code/make it generic. + switch (currentCharData.opponent) + { + case 'gf': + if (isStoryMode) + { + cameraFollowPoint.x += 600; + tweenCamIn(); + } + } + + // + // OPPONENT HEALTH ICON + // + iconP2 = new HealthIcon(currentCharData.opponent, 1); + iconP2.y = healthBar.y - (iconP2.height / 2); + add(iconP2); + + // + // BOYFRIEND + // + var boyfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentPlayer); + + if (boyfriend != null) + { + boyfriend.characterType = CharacterType.BF; + } + + // + // PLAYER HEALTH ICON + // + iconP1 = new HealthIcon(currentPlayer, 0); + iconP1.y = healthBar.y - (iconP1.height / 2); + 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 (boyfriend != null) + { + currentStage.addCharacter(boyfriend, BF); + } + + if (dad != null) + { + currentStage.addCharacter(dad, DAD); + // Camera starts at dad. + cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y); + } + + // Rearrange by z-indexes. + currentStage.refresh(); + } + } + /** * Removes any references to the current stage, then clears the stage cache, * then reloads all the stages. @@ -794,7 +978,14 @@ class PlayState extends MusicBeatState // if (FlxG.sound.music != null) // FlxG.sound.music.play(true); // else - FlxG.sound.playMusic(Paths.inst(currentSong.song), 1, false); + if (currentChart != null) + { + currentChart.playInst(1.0, false); + } + else + { + FlxG.sound.playMusic(Paths.inst(currentSong.song), 1, false); + } } FlxG.sound.music.onComplete = endSong; @@ -813,7 +1004,7 @@ class PlayState extends MusicBeatState { // FlxG.log.add(ChartParser.parse()); - Conductor.bpm = currentSong.bpm; + Conductor.forceBPM(currentSong.bpm); currentSong.song = currentSong.song; @@ -836,6 +1027,32 @@ class PlayState extends MusicBeatState generatedMusic = true; } + private function generateSong_NEW():Void + { + if (currentChart == null) + { + trace('Song difficulty could not be loaded.'); + } + + Conductor.forceBPM(currentChart.getStartingBPM()); + + // TODO: Fix grouped vocals + vocals = currentChart.buildVocals(); + vocals.members[0].onComplete = function() + { + vocalsFinished = true; + } + + // Create the rendered note group. + activeNotes = new FlxTypedGroup(); + activeNotes.zIndex = 1000; + add(activeNotes); + + regenNoteData_NEW(); + + generatedMusic = true; + } + function regenNoteData():Void { // make unspawn notes shit def empty @@ -950,6 +1167,133 @@ class PlayState extends MusicBeatState }); } + function regenNoteData_NEW():Void + { + // 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 = 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) + { + if (playerStrumline != null) + { + // Align with the strumline arrow. + newNote.x = playerStrumline.getArrow(songNote.getDirection()).x; + } + else + { + // Assume strumline position. + newNote.x += FlxG.width / 2; + } + } + else + { + if (enemyStrumline != null) + { + newNote.x = enemyStrumline.getArrow(songNote.getDirection()).x; + } + else + { + // newNote.x += 0; + } + } + + inactiveNotes.push(newNote); + + oldNote = newNote; + + // Generate X sustain notes. + var sustainSections = Math.round(songNote.length / Conductor.stepCrochet); + for (noteIndex in 0...sustainSections) + { + var noteTimeOffset:Float = Conductor.stepCrochet + (Conductor.stepCrochet * 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) + { + if (playerStrumline != null) + { + // Align with the strumline arrow. + sustainNote.x = playerStrumline.getArrow(songNote.getDirection()).x; + } + else + { + // Assume strumline position. + sustainNote.x += FlxG.width / 2; + } + } + else + { + if (enemyStrumline != null) + { + sustainNote.x = enemyStrumline.getArrow(songNote.getDirection()).x; + } + else + { + // newNote.x += 0; + } + } + + inactiveNotes.push(sustainNote); + + oldNote = sustainNote; + } + } + + // Sorting is an expensive operation. + // Assume it was done in the chart file. + /** + inactiveNotes.sort(function(a:Note, b:Note):Int + { + return SortUtil.byStrumtime(FlxSort.ASCENDING, a, b); + }); + **/ + } + function tweenCamIn():Void { FlxTween.tween(FlxG.camera, {zoom: 1.3 * FlxCamera.defaultZoom}, (Conductor.stepCrochet * 4 / 1000), {ease: FlxEase.elasticInOut}); @@ -986,7 +1330,7 @@ class PlayState extends MusicBeatState vocals.pause(); FlxG.sound.music.play(); - Conductor.songPosition = FlxG.sound.music.time + Conductor.offset; + Conductor.update(FlxG.sound.music.time + Conductor.offset); if (vocalsFinished) return; @@ -999,6 +1343,9 @@ class PlayState extends MusicBeatState { super.update(elapsed); + if (criticalFailure) + return; + if (FlxG.keys.justPressed.U) { // hack for HaxeUI generation, doesn't work unless persistentUpdate is false at state creation!! @@ -1027,7 +1374,16 @@ class PlayState extends MusicBeatState currentStage.resetStage(); - regenNoteData(); // loads the note data from start + // Delete all notes and reset the arrays. + if (currentChart != null) + { + regenNoteData_NEW(); + } + else + { + regenNoteData(); + } + health = 1; songScore = 0; combo = 0; @@ -1058,7 +1414,7 @@ class PlayState extends MusicBeatState if (Paths.SOUND_EXT == 'mp3') Conductor.offset = -13; // DO NOT FORGET TO REMOVE THE HARDCODE! WHEN I MAKE BETTER OFFSET SYSTEM! - Conductor.songPosition = FlxG.sound.music.time + Conductor.offset; // 20 is THE MILLISECONDS?? + Conductor.update(FlxG.sound.music.time + Conductor.offset); if (!isGamePaused) { @@ -1177,7 +1533,7 @@ class PlayState extends MusicBeatState } FlxG.watch.addQuick("songPos", Conductor.songPosition); - if (currentSong.song == 'Fresh') + if (currentSong != null && currentSong.song == 'Fresh') { switch (curBeat) { @@ -1307,7 +1663,7 @@ class PlayState extends MusicBeatState if (!daNote.mustPress && daNote.wasGoodHit && !daNote.tooLate) { - if (currentSong.song != 'Tutorial') + if (currentSong != null && currentSong.song != 'Tutorial') camZooming = true; var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, daNote, combo, true); @@ -1324,7 +1680,7 @@ class PlayState extends MusicBeatState else { // Volume of DAD. - if (currentSong.needsVoices) + if (currentSong != null && currentSong.needsVoices) vocals.volume = 1; } } @@ -1412,8 +1768,9 @@ class PlayState extends MusicBeatState } daPos += 4 * (1000 * 60 / daBPM); } - Conductor.songPosition = FlxG.sound.music.time = daPos; - Conductor.songPosition += Conductor.offset; + + FlxG.sound.music.time = daPos; + Conductor.update(FlxG.sound.music.time + Conductor.offset); updateCurStep(); resyncVocals(); } @@ -1857,7 +2214,7 @@ class PlayState extends MusicBeatState { if (SongLoad.getSong()[Math.floor(curStep / 16)].changeBPM) { - Conductor.bpm = SongLoad.getSong()[Math.floor(curStep / 16)].bpm; + Conductor.forceBPM(SongLoad.getSong()[Math.floor(curStep / 16)].bpm); FlxG.log.add('CHANGED BPM!'); } } @@ -2118,8 +2475,14 @@ class PlayState extends MusicBeatState function performCleanup() { // Uncache the song. - openfl.utils.Assets.cache.clear(Paths.inst(currentSong.song)); - openfl.utils.Assets.cache.clear(Paths.voices(currentSong.song)); + if (currentChart != null) + { + } + else + { + openfl.utils.Assets.cache.clear(Paths.inst(currentSong.song)); + openfl.utils.Assets.cache.clear(Paths.voices(currentSong.song)); + } // Remove reference to stage and remove sprites from it to save memory. if (currentStage != null) diff --git a/source/funkin/play/Strumline.hx b/source/funkin/play/Strumline.hx index a93be5c94..5efe21863 100644 --- a/source/funkin/play/Strumline.hx +++ b/source/funkin/play/Strumline.hx @@ -4,9 +4,11 @@ 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; /** diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index 7b739c138..ca708da8d 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -273,6 +273,10 @@ class BaseCharacter extends Bopper { if (!isOpponent) { + if (PlayState.instance.iconP1 == null) + { + trace('[WARN] Player 1 health icon not found!'); + } PlayState.instance.iconP1.characterId = _data.healthIcon.id; PlayState.instance.iconP1.size.set(_data.healthIcon.scale, _data.healthIcon.scale); PlayState.instance.iconP1.offset.x = _data.healthIcon.offsets[0]; @@ -281,6 +285,10 @@ class BaseCharacter extends Bopper } else { + if (PlayState.instance.iconP2 == null) + { + trace('[WARN] Player 2 health icon not found!'); + } PlayState.instance.iconP2.characterId = _data.healthIcon.id; PlayState.instance.iconP2.size.set(_data.healthIcon.scale, _data.healthIcon.scale); PlayState.instance.iconP2.offset.x = _data.healthIcon.offsets[0]; diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index aa9095f8c..56053bcf8 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -1,5 +1,7 @@ package funkin.play.song; +import funkin.VoicesGroup; +import funkin.play.song.SongData.SongChartData; import funkin.play.song.SongData.SongDataParser; import funkin.play.song.SongData.SongEventData; import funkin.play.song.SongData.SongMetadata; @@ -45,14 +47,18 @@ class Song // implements IPlayStateScriptedClass cacheCharts(); } - function populateFromMetadata() + /** + * Populate the song data from the provided metadata, + * including data from individual difficulties. Does not load chart data. + */ + function populateFromMetadata():Void { // Variations may have different artist, time format, generatedBy, etc. for (metadata in _metadata) { for (diffId in metadata.playData.difficulties) { - var difficulty = new SongDifficulty(diffId, metadata.variation); + var difficulty:SongDifficulty = new SongDifficulty(this, diffId, metadata.variation); variations.push(metadata.variation); @@ -83,25 +89,27 @@ class Song // implements IPlayStateScriptedClass /** * Parse and cache the chart for all difficulties of this song. */ - public function cacheCharts() + public function cacheCharts():Void { trace('Caching ${variations.length} chart files for song $songId'); for (variation in variations) { - var chartData = SongDataParser.parseSongChartData(songId, variation); + var chartData:SongChartData = SongDataParser.parseSongChartData(songId, variation); + var chartNotes = chartData.notes; - for (diffId in chartData.notes.keys()) + for (diffId in chartNotes.keys()) { - trace(' Difficulty $diffId'); - var difficulty = difficulties.get(diffId); + // Retrieve the cached difficulty data. + var difficulty:Null = difficulties.get(diffId); if (difficulty == null) { trace('Could not find difficulty $diffId for song $songId'); continue; } - + // Add the chart data to the difficulty. difficulty.notes = chartData.notes.get(diffId); - difficulty.scrollSpeed = chartData.scrollSpeed.get(diffId); + difficulty.scrollSpeed = chartData.getScrollSpeed(diffId); + difficulty.events = chartData.events; } } @@ -111,7 +119,7 @@ class Song // implements IPlayStateScriptedClass /** * Retrieve the metadata for a specific difficulty, including the chart if it is loaded. */ - public function getDifficulty(diffId:String):SongDifficulty + public inline function getDifficulty(diffId:String):SongDifficulty { return difficulties.get(diffId); } @@ -119,7 +127,7 @@ class Song // implements IPlayStateScriptedClass /** * Purge the cached chart data for each difficulty of this song. */ - public function clearCharts() + public function clearCharts():Void { for (diff in difficulties) { @@ -135,6 +143,11 @@ class Song // implements IPlayStateScriptedClass class SongDifficulty { + /** + * The parent song for this difficulty. + */ + public final song:Song; + /** * The difficulty ID, such as `easy` or `hard`. */ @@ -162,8 +175,9 @@ class SongDifficulty public var notes:Array; public var events:Array; - public function new(diffId:String, variation:String) + public function new(song:Song, diffId:String, variation:String) { + this.song = song; this.difficulty = diffId; this.variation = variation; } @@ -172,4 +186,48 @@ class SongDifficulty { notes = null; } + + public function getStartingBPM():Float + { + if (timeChanges.length == 0) + { + return 0; + } + + return timeChanges[0].bpm; + } + + public function getPlayableChar(id:String):SongPlayableChar + { + return chars.get(id); + } + + public inline function cacheInst() + { + // DEBUG: Remove this. + // FlxG.sound.cache(Paths.inst(this.song.songId)); + FlxG.sound.cache(Paths.inst('bopeebo')); + } + + public inline function playInst(volume:Float = 1.0, looped:Bool = false) + { + // DEBUG: Remove this. + // FlxG.sound.playMusic(Paths.inst(this.song.songId), volume, looped); + FlxG.sound.playMusic(Paths.inst('bopeebo'), volume, looped); + } + + public inline function cacheVocals() + { + // DEBUG: Remove this. + // FlxG.sound.cache(Paths.voices(this.song.songId)); + FlxG.sound.cache(Paths.voices('bopeebo')); + } + + public inline function buildVocals(charId:String = "bf"):VoicesGroup + { + // DEBUG: Remove this. + // var result:VoicesGroup = new VoicesGroup(this.song.songId, null, false); + var result:VoicesGroup = new VoicesGroup('bopeebo', null, false); + return result; + } } diff --git a/source/funkin/play/song/SongData.hx b/source/funkin/play/song/SongData.hx index bdf9d9043..04d3d2305 100644 --- a/source/funkin/play/song/SongData.hx +++ b/source/funkin/play/song/SongData.hx @@ -339,6 +339,11 @@ abstract SongNoteData(RawSongNoteData) return Math.floor(this.d / strumlineSize); } + public inline function getMustHitNote(strumlineSize:Int = 4):Bool + { + return getStrumlineIndex(strumlineSize) == 0; + } + public var length(get, set):Float; public function get_length():Float @@ -522,7 +527,7 @@ abstract SongPlayableChar(RawSongPlayableChar) } } -typedef SongChartData = +typedef RawSongChartData = { var version:Version; @@ -532,6 +537,32 @@ typedef SongChartData = var generatedBy:String; }; +@:forward +abstract SongChartData(RawSongChartData) +{ + public function new(scrollSpeed:DynamicAccess, events:Array, notes:DynamicAccess>) + { + this = { + version: SongMigrator.CHART_VERSION, + + events: events, + notes: notes, + scrollSpeed: scrollSpeed, + generatedBy: SongValidator.DEFAULT_GENERATEDBY + } + } + + public function getScrollSpeed(diff:String = 'default'):Float + { + var result:Float = this.scrollSpeed.get(diff); + + if (result == 0.0 && diff != 'default') + return getScrollSpeed('default'); + + return (result == 0.0) ? 1.0 : result; + } +} + typedef RawSongTimeChange = { /** @@ -569,6 +600,17 @@ typedef RawSongTimeChange = var bt:OneOfTwo>; } +typedef RawConductorTimeChange = +{ + > RawSongTimeChange, + + /** + * The time in the song (in steps) that this change occurs at. + * This time is somewhat weird because the rate it increases is dependent on the BPM at that point in the song. + */ + public var st:Float; +} + /** * Add aliases to the minimalized property names of the typedef, * to improve readability. @@ -667,6 +709,113 @@ abstract SongTimeChange(RawSongTimeChange) } } +abstract ConductorTimeChange(RawConductorTimeChange) +{ + public function new(timeStamp:Float, beatTime:Int, bpm:Float, timeSignatureNum:Int = 4, timeSignatureDen:Int = 4, beatTuplets:Array) + { + this = { + t: timeStamp, + b: beatTime, + bpm: bpm, + n: timeSignatureNum, + d: timeSignatureDen, + bt: beatTuplets, + st: 0.0 + } + } + + public var timeStamp(get, set):Float; + + public function get_timeStamp():Float + { + return this.t; + } + + public function set_timeStamp(value:Float):Float + { + return this.t = value; + } + + public var beatTime(get, set):Int; + + public function get_beatTime():Int + { + return this.b; + } + + public function set_beatTime(value:Int):Int + { + return this.b = value; + } + + public var bpm(get, set):Float; + + public function get_bpm():Float + { + return this.bpm; + } + + public function set_bpm(value:Float):Float + { + return this.bpm = value; + } + + public var timeSignatureNum(get, set):Int; + + public function get_timeSignatureNum():Int + { + return this.n; + } + + public function set_timeSignatureNum(value:Int):Int + { + return this.n = value; + } + + public var timeSignatureDen(get, set):Int; + + public function get_timeSignatureDen():Int + { + return this.d; + } + + public function set_timeSignatureDen(value:Int):Int + { + return this.d = value; + } + + public var beatTuplets(get, set):Array; + + public function get_beatTuplets():Array + { + if (Std.isOfType(this.bt, Int)) + { + return [this.bt]; + } + else + { + return this.bt; + } + } + + public function set_beatTuplets(value:Array):Array + { + return this.bt = value; + } + + public var stepTime(get, set):Float; + + public function get_stepTime():Float + { + return this.st; + } + + public function set_stepTime(value:Float):Float + { + return this.st = value; + } +} + enum abstract SongTimeFormat(String) from String to String { var TICKS = "ticks"; diff --git a/source/funkin/play/song/SongValidator.hx b/source/funkin/play/song/SongValidator.hx index 8e52bd33e..592ca818d 100644 --- a/source/funkin/play/song/SongValidator.hx +++ b/source/funkin/play/song/SongValidator.hx @@ -5,6 +5,7 @@ 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, @@ -17,10 +18,16 @@ class SongValidator public static final DEFAULT_TIMEFORMAT:SongTimeFormat = SongTimeFormat.MILLISECONDS; public static final DEFAULT_DIVISIONS:Int = -1; public static final DEFAULT_LOOP:Bool = false; - public static final DEFAULT_GENERATEDBY:String = "Unknown"; public static final DEFAULT_STAGE:String = "mainStage"; public static final DEFAULT_SCROLLSPEED:Float = 1.0; + public static var DEFAULT_GENERATEDBY(get, null):String; + + static function get_DEFAULT_GENERATEDBY():String + { + return '${Constants.TITLE} - ${Constants.VERSION}'; + } + /** * Validates the fields of a SongMetadata object (excluding the version field). *