diff --git a/Project.xml b/Project.xml index 81f017853..8b7f37f19 100644 --- a/Project.xml +++ b/Project.xml @@ -54,7 +54,7 @@ - +
@@ -68,7 +68,7 @@ - +
@@ -91,8 +91,8 @@ - - + + @@ -128,6 +128,7 @@ + diff --git a/hmm.json b/hmm.json index 0f03b9155..985375c42 100644 --- a/hmm.json +++ b/hmm.json @@ -24,6 +24,13 @@ "type": "haxelib", "version": "2.4.0" }, + { + "name": "flxanimate", + "type": "git", + "dir": null, + "ref": "master", + "url": "https://github.com/Dot-Stuff/flxanimate" + }, { "name": "hscript", "type": "git", diff --git a/source/funkin/FreeplayState.hx b/source/funkin/FreeplayState.hx index 7f37eff18..c9e498647 100644 --- a/source/funkin/FreeplayState.hx +++ b/source/funkin/FreeplayState.hx @@ -118,7 +118,6 @@ class FreeplayState extends MusicBeatSubstate addWeek(['Ugh', 'Guns', 'Stress'], 7, ['tankman']); addWeek(["Darnell", "lit-up", "2hot"], 8, ['darnell']); - addWeek(["bro"], 1, ['gf']); // LOAD MUSIC diff --git a/source/funkin/GameOverSubstate.hx b/source/funkin/GameOverSubstate.hx index 113e64b63..45fa0134d 100644 --- a/source/funkin/GameOverSubstate.hx +++ b/source/funkin/GameOverSubstate.hx @@ -1,5 +1,6 @@ package funkin; +import flixel.FlxSprite; import flixel.FlxObject; import flixel.system.FlxSound; import flixel.util.FlxColor; @@ -50,7 +51,11 @@ class GameOverSubstate extends MusicBeatSubstate public function new() { super(); + } + override public function create() + { + super.create(); FlxG.sound.list.add(gameOverMusic); gameOverMusic.stop(); @@ -73,13 +78,22 @@ class GameOverSubstate extends MusicBeatSubstate } } + // By adding a background we can make it transparent for testing. + var bg = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK); + bg.alpha = 0.25; + bg.scrollFactor.set(); + add(bg); + // We have to remove boyfriend from the stage. Then we can add him back at the end. boyfriend = PlayState.instance.currentStage.getBoyfriend(true); boyfriend.isDead = true; - boyfriend.playAnimation('firstDeath'); add(boyfriend); + boyfriend.resetCharacter(); + boyfriend.playAnimation('firstDeath'); cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1); + cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x; + cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y; add(cameraFollowPoint); // FlxG.camera.scroll.set(); @@ -124,11 +138,10 @@ class GameOverSubstate extends MusicBeatSubstate // Start panning the camera to BF after 12 frames. // TODO: Should this be de-hardcoded? - if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.animation.curAnim.curFrame == 12) - { - cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x; - cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y; - } + //if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.animation.curAnim.curFrame == 12) + //{ +// + //} if (gameOverMusic.playing) { diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx index bfde8c327..cba1afb6f 100644 --- a/source/funkin/InitState.hx +++ b/source/funkin/InitState.hx @@ -171,7 +171,7 @@ class InitState extends FlxTransitionableState #elseif FREEPLAY FlxG.switchState(new FreeplayState()); #elseif ANIMATE - FlxG.switchState(new animate.AnimTestStage()); + FlxG.switchState(new funkin.animate.dotstuff.DotStuffTestStage()); #elseif CHARTING FlxG.switchState(new ChartingState()); #elseif STAGEBUILD @@ -179,11 +179,7 @@ class InitState extends FlxTransitionableState #elseif FIGHT FlxG.switchState(new PicoFight()); #elseif ANIMDEBUG -<<<<<<< HEAD FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState()); -======= - FlxG.switchState(new DebugBoundingState()); ->>>>>>> origin/feature/scripted-modules #elseif NETTEST FlxG.switchState(new netTest.NetTest()); #else diff --git a/source/funkin/LoadingState.hx b/source/funkin/LoadingState.hx index ad649bb03..3c60d34df 100644 --- a/source/funkin/LoadingState.hx +++ b/source/funkin/LoadingState.hx @@ -188,8 +188,10 @@ class LoadingState extends MusicBeatState { Paths.setCurrentLevel('tutorial'); } - else - { + else if (PlayState.storyWeek == 8) { + // TODO: Refactor this code. + Paths.setCurrentLevel("weekend1"); + } else { Paths.setCurrentLevel("week" + PlayState.storyWeek); } #if NO_PRELOAD_ALL diff --git a/source/funkin/MusicBeatSubstate.hx b/source/funkin/MusicBeatSubstate.hx index db20bce34..c3a9bed0e 100644 --- a/source/funkin/MusicBeatSubstate.hx +++ b/source/funkin/MusicBeatSubstate.hx @@ -1,5 +1,6 @@ package funkin; +import flixel.util.FlxColor; import flixel.FlxSubState; import funkin.Conductor.BPMChangeEvent; import funkin.modding.events.ScriptEvent; @@ -10,9 +11,9 @@ import funkin.modding.module.ModuleHandler; */ class MusicBeatSubstate extends FlxSubState { - public function new() + public function new(bgColor:FlxColor = FlxColor.TRANSPARENT) { - super(); + super(bgColor); } private var curStep:Int = 0; diff --git a/source/funkin/Note.hx b/source/funkin/Note.hx index 566b4ea49..cace04407 100644 --- a/source/funkin/Note.hx +++ b/source/funkin/Note.hx @@ -283,7 +283,9 @@ class Note extends FlxSprite static public function fromData(data:NoteData, prevNote:Note, isSustainNote = false) { - return new Note(data.strumTime, data.noteData, prevNote, isSustainNote); + var result = new Note(data.strumTime, data.noteData, prevNote, isSustainNote); + result.data = data; + return result; } } @@ -292,20 +294,18 @@ 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) + public function new(strumTime = 0.0, noteData:NoteType = 0, sustainLength = 0.0, noteKind = NORMAL) { this = { strumTime: strumTime, noteData: noteData, sustainLength: sustainLength, - altNote: altNote, noteKind: noteKind } } @@ -470,11 +470,5 @@ 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"; + var ALT = "alt"; } diff --git a/source/funkin/SongLoad.hx b/source/funkin/SongLoad.hx index f786d96f5..d4e414ae4 100644 --- a/source/funkin/SongLoad.hx +++ b/source/funkin/SongLoad.hx @@ -191,11 +191,7 @@ class SongLoad noteStuff[sectionIndex].sectionNotes[noteIndex].sustainLength = arrayDipshit[2]; if (arrayDipshit.length > 3) { - noteStuff[sectionIndex].sectionNotes[noteIndex].altNote = arrayDipshit[3]; - } - if (arrayDipshit.length > 4) - { - noteStuff[sectionIndex].sectionNotes[noteIndex].noteKind = arrayDipshit[4]; + noteStuff[sectionIndex].sectionNotes[noteIndex].noteKind = arrayDipshit[3]; } } else if (noteDataArray != null) @@ -227,7 +223,7 @@ class SongLoad noteTypeDefShit.strumTime, noteTypeDefShit.noteData, noteTypeDefShit.sustainLength, - noteTypeDefShit.altNote + noteTypeDefShit.noteKind ]; noteStuff[sectionIndex].sectionNotes[noteIndex] = cast dipshitArray; @@ -252,7 +248,15 @@ class SongLoad public static function parseJSONshit(rawJson:String):SwagSong { - var songParsed:Dynamic = Json.parse(rawJson); + 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(); diff --git a/source/funkin/charting/ChartingState.hx b/source/funkin/charting/ChartingState.hx index 3f19edc07..382c55743 100644 --- a/source/funkin/charting/ChartingState.hx +++ b/source/funkin/charting/ChartingState.hx @@ -1016,8 +1016,8 @@ class ChartingState extends MusicBeatState if (curSelectedNote != null) { trace('ALT NOTE SHIT'); - curSelectedNote.altNote = (curSelectedNote.altNote == "alt") ? "" : "alt"; - trace(curSelectedNote.altNote); + curSelectedNote.noteKind = (curSelectedNote.noteKind == "alt") ? "" : "alt"; + trace(curSelectedNote.noteKind); } } @@ -1358,7 +1358,7 @@ class ChartingState extends MusicBeatState var noteStrum = getStrumTime(dummyArrow.y) + sectionStartTime(); var noteData = Math.floor(FlxG.mouse.x / GRID_SIZE); var noteSus = 0; - var noteAlt = ""; + var noteKind = ""; justPlacedNote = true; @@ -1399,7 +1399,7 @@ class ChartingState extends MusicBeatState var daNewNote:Note = new Note(noteStrum, noteData); daNewNote.data.sustainLength = noteSus; - daNewNote.data.altNote = noteAlt; + daNewNote.data.noteKind = noteKind; SongLoad.getSong()[curSection].sectionNotes.push(daNewNote.data); curSelectedNote = SongLoad.getSong()[curSection].sectionNotes[SongLoad.getSong()[curSection].sectionNotes.length - 1]; diff --git a/source/funkin/modding/IScriptedClass.hx b/source/funkin/modding/IScriptedClass.hx index 4261032a2..0855ee604 100644 --- a/source/funkin/modding/IScriptedClass.hx +++ b/source/funkin/modding/IScriptedClass.hx @@ -44,7 +44,7 @@ interface INoteScriptedClass extends IScriptedClass * * I previously considered adding events for onKeyDown, onKeyUp, mouse events, etc. * However, I realized that you can simply call something like the following within a module: - * `FlxG.stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);` + * `FlxG.state.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);` * This is more efficient than adding an entire event handler for every key press. * * -Eric diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index 1fe2b0682..d0e0bce4d 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -1,5 +1,6 @@ package funkin.modding; +import funkin.play.character.CharacterData.CharacterDataParser; import funkin.modding.module.ModuleHandler; import funkin.play.stage.StageData; import polymod.Polymod; @@ -157,7 +158,7 @@ class PolymodHandler return { assetLibraryPaths: [ "songs" => "songs", "shared" => "", "tutorial" => "tutorial", "scripts" => "scripts", "week1" => "week1", "week2" => "week2", - "week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "week8" => "week8", + "week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1", ] } } @@ -227,9 +228,10 @@ class PolymodHandler // Reload scripted classes so stages and modules will update. polymod.hscript.PolymodScriptClass.registerAllScriptClasses(); - // Reload the stages in cache. - // TODO: Currently this causes lag since you're reading a lot of files, how to fix? + // Reload everything that is cached. + // Currently this freezes the game for a second but I guess that's tolerable? StageDataParser.loadStageCache(); + CharacterDataParser.loadCharacterCache(); ModuleHandler.loadModuleCache(); } } diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx index ca671d8e4..e10471173 100644 --- a/source/funkin/modding/events/ScriptEvent.hx +++ b/source/funkin/modding/events/ScriptEvent.hx @@ -142,7 +142,7 @@ class ScriptEvent public static inline final GAME_OVER:ScriptEventType = "GAME_OVER"; /** - * Called when the player presses a key to restart the game. + * Called after the player presses a key to restart the game. * This can happen from the pause menu or the game over screen. * * This event IS cancelable! Canceling this event will prevent the game from restarting. diff --git a/source/funkin/play/HealthIcon.hx b/source/funkin/play/HealthIcon.hx index 083d26783..e37050088 100644 --- a/source/funkin/play/HealthIcon.hx +++ b/source/funkin/play/HealthIcon.hx @@ -110,8 +110,6 @@ class HealthIcon extends FlxSprite this.antialiasing = !isPixel; - this.flipX = playerId == 0; - initTargetSize(); } diff --git a/source/funkin/play/PicoFight.hx b/source/funkin/play/PicoFight.hx index 0cc72932d..cafed7598 100644 --- a/source/funkin/play/PicoFight.hx +++ b/source/funkin/play/PicoFight.hx @@ -28,7 +28,7 @@ class PicoFight extends MusicBeatState override function create() { - Paths.setCurrentLevel("week8"); + Paths.setCurrentLevel("weekend1"); var bg:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, FlxG.height); bg.scrollFactor.set(); diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 52d123a65..719bb52b9 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -28,6 +28,7 @@ import funkin.play.Strumline.StrumlineArrow; import funkin.play.Strumline.StrumlineStyle; import funkin.play.character.BaseCharacter; import funkin.play.character.CharacterData; +import funkin.play.scoring.Scoring; import funkin.play.stage.Stage; import funkin.play.stage.StageData; import funkin.ui.PopUpStuff; @@ -279,6 +280,14 @@ class PlayState extends MusicBeatState implements IHook { super.create(); + if (currentSong == null) { + 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()); + } + instance = this; // Displays the camera follow point as a sprite for debug purposes. @@ -465,12 +474,13 @@ class PlayState extends MusicBeatState implements IHook currentStageId = 'mallXmas'; case 'winter-horrorland': currentStageId = 'mallEvil'; + case 'senpai' | 'roses': + currentStageId = 'school'; + case "darnell" | "lit-up" | "2hot": + // currentStageId = 'phillyStreets'; + currentStageId = 'pyro'; case 'pyro': currentStageId = 'pyro'; - case 'senpai' | 'roses': - currentStageId = 'school'; - case "darnell": - currentStageId = 'phillyStreets'; case 'thorns': currentStageId = 'schoolEvil'; case 'guns' | 'stress' | 'ugh': @@ -504,6 +514,8 @@ class PlayState extends MusicBeatState implements IHook switch (currentStageId) { + case 'pyro' | 'phillyStreets': + gfVersion = 'nene'; case 'limoRide': gfVersion = 'gf-car'; case 'mallXmas' | 'mallEvil': @@ -580,12 +592,19 @@ class PlayState extends MusicBeatState implements IHook { // We're using Eric's stage handler. // Characters get added to the stage, not the main scene. - currentStage.addCharacter(girlfriend, GF); - currentStage.addCharacter(boyfriend, BF); - currentStage.addCharacter(dad, DAD); + if (girlfriend != null) { + currentStage.addCharacter(girlfriend, GF); + } - // Camera starts at dad. - cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y); + 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); + } // Redo z-indexes. currentStage.refresh(); @@ -867,7 +886,7 @@ class PlayState extends MusicBeatState implements IHook var swagNote:Note = new Note(daStrumTime, daNoteData, oldNote, false, strumlineStyle); // swagNote.data = songNotes; swagNote.data.sustainLength = songNotes.sustainLength; - swagNote.data.altNote = songNotes.altNote; + swagNote.data.noteKind = songNotes.noteKind; swagNote.scrollFactor.set(0, 0); var susLength:Float = swagNote.data.sustainLength; @@ -881,6 +900,7 @@ class PlayState extends MusicBeatState implements IHook var sustainNote:Note = new Note(daStrumTime + (Conductor.stepCrochet * susNote) + Conductor.stepCrochet, daNoteData, oldNote, true, strumlineStyle); + sustainNote.data.noteKind = songNotes.noteKind; sustainNote.scrollFactor.set(); inactiveNotes.push(sustainNote); @@ -976,6 +996,8 @@ class PlayState extends MusicBeatState implements IHook FlxG.sound.music.time = 0; + currentStage.resetStage(); + regenNoteData(); // loads the note data from start health = 1; songScore = 0; @@ -1037,7 +1059,9 @@ class PlayState extends MusicBeatState implements IHook if (!event.eventCanceled) { + // Pause updates while the substate is open, preventing the game state from advancing. persistentUpdate = false; + // Enable drawing while the substate is open, allowing the game state to be shown behind the pause menu. persistentDraw = true; // There is a 1/1000 change to use a special pause menu. @@ -1062,6 +1086,21 @@ class PlayState extends MusicBeatState implements IHook } } + #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) { FlxG.switchState(new ChartingState()); @@ -1071,25 +1110,26 @@ class PlayState extends MusicBeatState implements IHook #end } + // 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 (health > 2) - health = 2; - #if debug - if (FlxG.keys.justPressed.ONE) - endSong(); - if (FlxG.keys.justPressed.PAGEUP) changeSection(1); if (FlxG.keys.justPressed.PAGEDOWN) changeSection(-1); #end + if (health > 2.0) + health = 2.0; + if (health < 0.0) + health = 0.0; + if (camZooming && subState == null) { FlxG.camera.zoom = FlxMath.lerp(defaultCameraZoom, FlxG.camera.zoom, 0.95); @@ -1139,9 +1179,6 @@ class PlayState extends MusicBeatState implements IHook if (health <= 0 && !isPracticeMode) { - persistentUpdate = false; - persistentDraw = false; - vocals.pause(); FlxG.sound.music.pause(); @@ -1149,7 +1186,22 @@ class PlayState extends MusicBeatState implements IHook dispatchEvent(new ScriptEvent(ScriptEvent.GAME_OVER)); - openSubState(new GameOverSubstate()); + // Disable updates, preventing animations in the background from playing. + persistentUpdate = false; + #if debug + if (FlxG.keys.pressed.THREE) { + // TODO: Change the key or delete this? + // In debug builds, pressing 3 to kill the player makes the background transparent. + persistentDraw = true; + } else { + #end + persistentDraw = false; + #if debug + } + #end + + var gameOverSubstate = new GameOverSubstate(); + openSubState(gameOverSubstate); #if discord_rpc // Game Over doesn't get his own variable because it's only used here @@ -1307,6 +1359,11 @@ class PlayState extends MusicBeatState implements IHook } #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 sec + */ function changeSection(sec:Int):Void { FlxG.sound.music.pause(); @@ -1432,30 +1489,22 @@ class PlayState extends MusicBeatState implements IHook // boyfriend.playAnimation('hey'); vocals.volume = 1; - var score:Int = 350; - var daRating:String = "sick"; var isSick:Bool = false; - var healthMulti:Float = 1; - - healthMulti *= daNote.lowStakes ? 0.002 : 0.033; + 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'; - score = 50; } else if (noteDiff > Note.HIT_WINDOW * Note.GOOD_THRESHOLD) { healthMulti *= 0.2; - daRating = 'bad'; - score = 100; } else if (noteDiff > Note.HIT_WINDOW * Note.SICK_THRESHOLD) { healthMulti *= 0.78; - daRating = 'good'; - score = 200; } else isSick = true; @@ -1795,6 +1844,7 @@ class PlayState extends MusicBeatState implements IHook // bruh this var is bonkers i thot it was a function lmfaooo + var shouldShowComboText:Bool = (curBeat % 8 == 7) // End of measure. TODO: Is this always the correct time? && (SongLoad.getSong()[Std.int(curStep / 16)].mustHitSection) // Current section is BF's. && (combo > 5) // Don't want to show on small combos. @@ -1832,11 +1882,13 @@ class PlayState extends MusicBeatState implements IHook if (currentStage == null) return; + // TODO: Move this to a song event. if (curBeat % 8 == 7 && currentSong.song == 'Bopeebo') { currentStage.getBoyfriend().playAnimation('hey', true); } + // TODO: Move this to a song event. if (curBeat % 16 == 15 && currentSong.song == 'Tutorial' && currentStage.getDad().characterId == 'gf' diff --git a/source/funkin/play/Scoring.hx b/source/funkin/play/Scoring.hx deleted file mode 100644 index 7806cd8e4..000000000 --- a/source/funkin/play/Scoring.hx +++ /dev/null @@ -1,6 +0,0 @@ -package funkin.play; - -/** - * A static class which holds any functions related to scoring. - */ -class Scoring {} diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index 713733804..7bcd1f71d 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -92,7 +92,7 @@ class BaseCharacter extends Bopper override function set_animOffsets(value:Array) { if (animOffsets == null) - animOffsets = [0, 0]; + value = [0, 0]; if (animOffsets == value) return value; @@ -157,6 +157,15 @@ class BaseCharacter extends Bopper shouldBop = false; } + /** + * Gets the value of flipX from the character data. + * `!getFlipX()` is the direction Boyfriend should face. + */ + public function getDataFlipX():Bool + { + return _data.flipX; + } + function findCountAnimations(prefix:String):Array { var animNames:Array = this.animation.getNameList(); @@ -176,6 +185,27 @@ class BaseCharacter extends Bopper return result; } + /** + * Reset the character so it can be used at the start of the level. + * Call this when restarting the level. + */ + public function resetCharacter(resetCamera:Bool = true):Void { + // Reset the animation offsets. This will modify x and y to be the absolute position of the character. + this.animOffsets = [0, 0]; + + // Now we can set the x and y to be their original values without having to account for animOffsets. + this.resetPosition(); + + // Make sure we are playing the idle animation (to reapply animOffsets)... + this.dance(); + // ...then update the hitbox so that this.width and this.height are correct. + this.updateHitbox(); + + // Reset the camera focus point while we're at it. + if (resetCamera) + this.resetCameraFocusPoint(); + } + /** * Set the sprite scale to the appropriate value. * @param scale @@ -206,10 +236,14 @@ class BaseCharacter extends Bopper override function onCreate(event:ScriptEvent):Void { - // Camera focus point - var charCenterX = this.x + this.width / 2; - var charCenterY = this.y + this.height / 2; - this.cameraFocusPoint = new FlxPoint(charCenterX + _data.cameraOffsets[0], charCenterY + _data.cameraOffsets[1]); + // Make sure we are playing the idle animation... + this.dance(); + // ...then update the hitbox so that this.width and this.height are correct. + this.updateHitbox(); + // Without the above code, width and height (and therefore character position) + // will be based on the first animation in the sheet rather than the default animation. + + this.resetCameraFocusPoint(); // Child class should have created animations by now, // so we can query which ones are available. @@ -218,10 +252,18 @@ class BaseCharacter extends Bopper trace('${this.animation.getNameList()}'); trace('Combo note counts: ' + this.comboNoteCounts); trace('Drop note counts: ' + this.dropNoteCounts); - + super.onCreate(event); } + function resetCameraFocusPoint():Void + { + // Calculate the camera focus point + var charCenterX = this.x + this.width / 2; + var charCenterY = this.y + this.height / 2; + this.cameraFocusPoint = new FlxPoint(charCenterX + _data.cameraOffsets[0], charCenterY + _data.cameraOffsets[1]); + } + public function initHealthIcon(isOpponent:Bool):Void { if (!isOpponent) @@ -230,6 +272,7 @@ class BaseCharacter extends Bopper PlayState.instance.iconP1.size.set(_data.healthIcon.scale, _data.healthIcon.scale); PlayState.instance.iconP1.offset.x = _data.healthIcon.offsets[0]; PlayState.instance.iconP1.offset.y = _data.healthIcon.offsets[1]; + PlayState.instance.iconP1.flipX = !_data.healthIcon.flipX; } else { @@ -237,6 +280,7 @@ class BaseCharacter extends Bopper PlayState.instance.iconP2.size.set(_data.healthIcon.scale, _data.healthIcon.scale); PlayState.instance.iconP2.offset.x = _data.healthIcon.offsets[0]; PlayState.instance.iconP2.offset.y = _data.healthIcon.offsets[1]; + PlayState.instance.iconP1.flipX = _data.healthIcon.flipX; } } @@ -255,16 +299,16 @@ class BaseCharacter extends Bopper playDeathAnimation(); } - if (hasAnimation('idle-end') && getCurrentAnimation() == "idle" && isAnimationFinished()) - playAnimation('idle-end'); - if (hasAnimation('singLEFT-end') && getCurrentAnimation() == "singLEFT" && isAnimationFinished()) - playAnimation('singLEFT-end'); - if (hasAnimation('singDOWN-end') && getCurrentAnimation() == "singDOWN" && isAnimationFinished()) - playAnimation('singDOWN-end'); - if (hasAnimation('singUP-end') && getCurrentAnimation() == "singUP" && isAnimationFinished()) - playAnimation('singUP-end'); - if (hasAnimation('singRIGHT-end') && getCurrentAnimation() == "singRIGHT" && isAnimationFinished()) - playAnimation('singRIGHT-end'); + if (hasAnimation('idle-hold') && getCurrentAnimation() == "idle" && isAnimationFinished()) + playAnimation('idle-hold'); + if (hasAnimation('singLEFT-hold') && getCurrentAnimation() == "singLEFT" && isAnimationFinished()) + playAnimation('singLEFT-hold'); + if (hasAnimation('singDOWN-hold') && getCurrentAnimation() == "singDOWN" && isAnimationFinished()) + playAnimation('singDOWN-hold'); + if (hasAnimation('singUP-hold') && getCurrentAnimation() == "singUP" && isAnimationFinished()) + playAnimation('singUP-hold'); + if (hasAnimation('singRIGHT-hold') && getCurrentAnimation() == "singRIGHT" && isAnimationFinished()) + playAnimation('singRIGHT-hold'); // Handle character note hold time. if (getCurrentAnimation().startsWith("sing")) @@ -276,9 +320,8 @@ class BaseCharacter extends Bopper var shouldStopSinging:Bool = (this.characterType == BF) ? !isHoldingNote() : true; FlxG.watch.addQuick('singTimeMs-${characterId}', singTimeMs); - if (holdTimer > singTimeMs && shouldStopSinging && !getCurrentAnimation().endsWith("miss")) + if (holdTimer > singTimeMs && shouldStopSinging) // && !getCurrentAnimation().endsWith("miss") { - trace(getCurrentAnimation()); // trace('holdTimer reached ${holdTimer}sec (> ${singTimeMs}), stopping sing animation'); holdTimer = 0; dance(true); @@ -401,14 +444,14 @@ class BaseCharacter extends Bopper if (event.note.mustPress && characterType == BF) { // If the note is from the same strumline, play the sing animation. - this.playSingAnimation(event.note.data.dir, false, event.note.data.altNote); + this.playSingAnimation(event.note.data.dir, false); } else if (!event.note.mustPress && characterType == DAD) { // If the note is from the same strumline, play the sing animation. - this.playSingAnimation(event.note.data.dir, false, event.note.data.altNote); + this.playSingAnimation(event.note.data.dir, false); } else if (characterType == GF) { - if (this.comboNoteCounts.contains(event.comboCount)) { + if (event.note.mustPress && this.comboNoteCounts.contains(event.comboCount)) { trace('Playing GF combo animation: combo${event.comboCount}'); this.playAnimation('combo${event.comboCount}', true, true); } @@ -426,12 +469,12 @@ class BaseCharacter extends Bopper if (event.note.mustPress && characterType == BF) { // If the note is from the same strumline, play the sing animation. - this.playSingAnimation(event.note.data.dir, true, event.note.data.altNote); + this.playSingAnimation(event.note.data.dir, true); } else if (!event.note.mustPress && characterType == DAD) { // If the note is from the same strumline, play the sing animation. - this.playSingAnimation(event.note.data.dir, true, event.note.data.altNote); + this.playSingAnimation(event.note.data.dir, true); } else if (event.note.mustPress && characterType == GF) { var dropAnim = ''; @@ -466,9 +509,9 @@ class BaseCharacter extends Bopper if (characterType == BF) { - trace('Playing ghost miss animation...'); // If the note is from the same strumline, play the sing animation. - this.playSingAnimation(event.dir, true, null); + // trace('Playing ghost miss animation...'); + this.playSingAnimation(event.dir, true); } } diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx index 5fa55ada4..885b22eb0 100644 --- a/source/funkin/play/character/CharacterData.hx +++ b/source/funkin/play/character/CharacterData.hx @@ -109,7 +109,7 @@ class CharacterDataParser trace(' Instantiating ${scriptedCharClassNames3.length} (Multi-Sparrow) scripted characters...'); for (charCls in scriptedCharClassNames3) { - var character = ScriptedBaseCharacter.init(charCls, DEFAULT_CHAR_ID); + var character = ScriptedMultiSparrowCharacter.init(charCls, DEFAULT_CHAR_ID); if (character == null) { trace(' Failed to instantiate scripted character: ${charCls}'); @@ -362,6 +362,7 @@ class CharacterDataParser input.healthIcon = { id: null, scale: null, + flipX: null, offsets: null }; } @@ -376,6 +377,11 @@ class CharacterDataParser input.healthIcon.scale = DEFAULT_SCALE; } + if (input.healthIcon.flipX == null) + { + input.healthIcon.flipX = DEFAULT_FLIPX; + } + if (input.healthIcon.offsets == null) { input.healthIcon.offsets = DEFAULT_OFFSETS; @@ -583,6 +589,12 @@ typedef HealthIconData = */ var scale:Null; + /** + * Whether to flip the health icon horizontally. + * @default false + */ + var flipX:Null; + /** * The offset of the health icon, in pixels. * @default [0, 25] diff --git a/source/funkin/play/character/MultiSparrowCharacter.hx b/source/funkin/play/character/MultiSparrowCharacter.hx index eaadc3080..5554f1f66 100644 --- a/source/funkin/play/character/MultiSparrowCharacter.hx +++ b/source/funkin/play/character/MultiSparrowCharacter.hx @@ -50,8 +50,6 @@ class MultiSparrowCharacter extends BaseCharacter buildSpritesheets(); buildAnimations(); - playAnimation(_data.startingAnimation); - if (_data.isPixel) { this.antialiasing = false; @@ -124,7 +122,7 @@ class MultiSparrowCharacter extends BaseCharacter if (members.exists(assetPath)) { // Switch to a new set of sprites. - trace('Loading frames from asset path: ${assetPath}'); + // trace('Loading frames from asset path: ${assetPath}'); this.frames = members.get(assetPath); this.activeMember = assetPath; this.setScale(_data.scale); @@ -176,7 +174,9 @@ class MultiSparrowCharacter extends BaseCharacter public override function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false):Void { - if (!this.canPlayOtherAnims) + // Make sure we ignore other animations if we're currently playing a forced one, + // unless we're forcing a new animation. + if (!this.canPlayOtherAnims && !ignoreOther) return; loadFramesByAnimName(name); diff --git a/source/funkin/play/character/PackerCharacter.hx b/source/funkin/play/character/PackerCharacter.hx index b7282423e..91e44e9f2 100644 --- a/source/funkin/play/character/PackerCharacter.hx +++ b/source/funkin/play/character/PackerCharacter.hx @@ -23,8 +23,6 @@ class PackerCharacter extends BaseCharacter loadSpritesheet(); loadAnimations(); - playAnimation(_data.startingAnimation); - super.onCreate(event); } diff --git a/source/funkin/play/character/SparrowCharacter.hx b/source/funkin/play/character/SparrowCharacter.hx index e8191940c..927a4c764 100644 --- a/source/funkin/play/character/SparrowCharacter.hx +++ b/source/funkin/play/character/SparrowCharacter.hx @@ -25,8 +25,6 @@ class SparrowCharacter extends BaseCharacter loadSpritesheet(); loadAnimations(); - playAnimation(_data.startingAnimation); - super.onCreate(event); } diff --git a/source/funkin/play/scoring/Scoring.hx b/source/funkin/play/scoring/Scoring.hx new file mode 100644 index 000000000..88f52ec57 --- /dev/null +++ b/source/funkin/play/scoring/Scoring.hx @@ -0,0 +1,198 @@ +package funkin.play.scoring; + +enum abstract ScoringSystem(String) { + /** + * The scoring system used in versions of the game Week 6 and older. + * Scores the player based on judgement, represented by a step function. + */ + var LEGACY; + /** + * The scoring system used in Week 7. It has tighter scoring windows than Legacy. + * Scores the player based on judgement, represented by a step function. + */ + var WEEK7; + /** + * Points Based On Timing scoring system, version 1 + * Scores the player based on the offset based on timing, represented by a sigmoid function. + */ + var PBOT1; + // WIFE1 + // WIFE3 +} + +/** + * A static class which holds any functions related to scoring. + */ +class Scoring { + /** + * Determine the score a note receives under a given scoring system. + * @param msTiming The difference between the note's time and when it was hit. + * @param scoringSystem The scoring system to use. + * @return The score the note receives. + */ + public static function scoreNote(msTiming:Float, scoringSystem:ScoringSystem = PBOT1) { + switch (scoringSystem) { + case LEGACY: + return scoreNote_LEGACY(msTiming); + case WEEK7: + return scoreNote_WEEK7(msTiming); + case PBOT1: + return scoreNote_PBOT1(msTiming); + default: + trace('ERROR: Unknown scoring system: ' + scoringSystem); + return 0; + } + } + + /** + * Determine the judgement a note receives under a given scoring system. + * @param msTiming The difference between the note's time and when it was hit. + * @return The judgement the note receives. + */ + public static function judgeNote(msTiming:Float, scoringSystem:ScoringSystem = PBOT1):String { + switch (scoringSystem) { + case LEGACY: + return judgeNote_LEGACY(msTiming); + case WEEK7: + return judgeNote_WEEK7(msTiming); + case PBOT1: + return judgeNote_PBOT1(msTiming); + default: + trace('ERROR: Unknown scoring system: ' + scoringSystem); + return 'miss'; + } + } + + /** + * The maximum score received. + */ + public static var PBOT1_MAX_SCORE = 350; + /** + * The minimum score received. + */ + public static var PBOT1_MIN_SCORE = 0; + /** + * The threshold at which a note hit is considered perfect and always given the max score. + **/ + public static var PBOT1_PERFECT_THRESHOLD = 5.0; // 5ms. + /** + * The threshold at which a note hit is considered missed and always given the min score. + **/ + public static var PBOT1_MISS_THRESHOLD = (10/60) * 1000; // ~166ms + + // Magic numbers used to tweak the shape of the scoring function. + public static var PBOT1_SCORING_SLOPE:Float = 0.052; + public static var PBOT1_SCORING_OFFSET:Float = 80.0; + + static function scoreNote_PBOT1(msTiming:Float):Int { + // Absolute value because otherwise late hits are always given the max score. + var absTiming = Math.abs(msTiming); + if (absTiming > PBOT1_MISS_THRESHOLD) { + return PBOT1_MIN_SCORE; + } else if (absTiming < PBOT1_PERFECT_THRESHOLD) { + return PBOT1_MAX_SCORE; + } else { + // Calculate the score based on the timing using a sigmoid function. + var factor:Float = 1.0 - (1.0 / (1.0 + Math.exp(-PBOT1_SCORING_SLOPE * (absTiming - PBOT1_SCORING_OFFSET)))); + + var score = Std.int(PBOT1_MAX_SCORE * factor); + + return score; + } + } + + static function judgeNote_PBOT1(msTiming:Float):String { + return judgeNote_WEEK7(msTiming); + } + + /** + * The window of time in which a note is considered to be hit, on the Funkin Legacy scoring system. + * Currently equal to 10 frames at 60fps, or ~166ms. + */ + public static var LEGACY_HIT_WINDOW:Float = (10 / 60) * 1000; // 166.67 ms hit window (10 frames at 60fps) + /** + * The threshold at which a note is considered a "Bad" hit rather than a "Shit" hit. + * Represented as a percentage of the total hit window. + */ + public static var LEGACY_BAD_THRESHOLD:Float = 0.9; + public static var LEGACY_GOOD_THRESHOLD:Float = 0.75; + public static var LEGACY_SICK_THRESHOLD:Float = 0.2; + public static var LEGACY_SHIT_SCORE = 50; + public static var LEGACY_BAD_SCORE = 100; + public static var LEGACY_GOOD_SCORE = 200; + public static var LEGACY_SICK_SCORE = 350; + + static function scoreNote_LEGACY(msTiming:Float):Int { + var absTiming = Math.abs(msTiming); + if (absTiming < LEGACY_HIT_WINDOW * LEGACY_SICK_THRESHOLD) { + return LEGACY_SICK_SCORE; + } else if (absTiming < LEGACY_HIT_WINDOW * LEGACY_GOOD_THRESHOLD) { + return LEGACY_GOOD_SCORE; + } else if (absTiming < LEGACY_HIT_WINDOW * LEGACY_BAD_THRESHOLD) { + return LEGACY_BAD_SCORE; + } else if (absTiming < LEGACY_HIT_WINDOW) { + return LEGACY_SHIT_SCORE; + } else { + return 0; + } + } + + static function judgeNote_LEGACY(msTiming:Float):String { + var absTiming = Math.abs(msTiming); + if (absTiming < LEGACY_HIT_WINDOW * LEGACY_SICK_THRESHOLD) { + return 'sick'; + } else if (absTiming < LEGACY_HIT_WINDOW * LEGACY_GOOD_THRESHOLD) { + return 'good'; + } else if (absTiming < LEGACY_HIT_WINDOW * LEGACY_BAD_THRESHOLD) { + return 'bad'; + } else if (absTiming < LEGACY_HIT_WINDOW) { + return 'shit'; + } else { + return 'miss'; + } + } + + /** + * The window of time in which a note is considered to be hit, on the Funkin Classic scoring system. + * Same as L 10 frames at 60fps, or ~166ms. + */ + public static var WEEK7_HIT_WINDOW = LEGACY_HIT_WINDOW; + public static var WEEK7_BAD_THRESHOLD = 0.8; // 80% of the hit window, or ~125ms + public static var WEEK7_GOOD_THRESHOLD = 0.55; // 55% of the hit window, or ~91ms + public static var WEEK7_SICK_THRESHOLD = 0.2; // 20% of the hit window, or ~33ms + public static var WEEK7_SHIT_SCORE = 50; + public static var WEEK7_BAD_SCORE = 100; + public static var WEEK7_GOOD_SCORE = 200; + public static var WEEK7_SICK_SCORE = 350; + + static function scoreNote_WEEK7(msTiming:Float):Int { + var absTiming = Math.abs(msTiming); + if (absTiming < WEEK7_HIT_WINDOW * WEEK7_SICK_THRESHOLD) { + return WEEK7_SICK_SCORE; + } else if (absTiming < WEEK7_HIT_WINDOW * WEEK7_GOOD_THRESHOLD) { + return WEEK7_GOOD_SCORE; + } else if (absTiming < WEEK7_HIT_WINDOW * WEEK7_BAD_THRESHOLD) { + return WEEK7_BAD_SCORE; + } else if (absTiming < WEEK7_HIT_WINDOW) { + return WEEK7_SHIT_SCORE; + } else { + return 0; + } + } + + static function judgeNote_WEEK7(msTiming:Float):String { + var absTiming = Math.abs(msTiming); + if (absTiming < WEEK7_HIT_WINDOW * WEEK7_SICK_THRESHOLD) { + return 'sick'; + } else if (absTiming < WEEK7_HIT_WINDOW * WEEK7_GOOD_THRESHOLD) { + return 'good'; + } else if (absTiming < WEEK7_HIT_WINDOW * WEEK7_BAD_THRESHOLD) { + return 'bad'; + } else if (absTiming < WEEK7_HIT_WINDOW) { + return 'shit'; + } else { + return 'miss'; + } + } +} + diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx index 331469275..f417781e3 100644 --- a/source/funkin/play/stage/Bopper.hx +++ b/source/funkin/play/stage/Bopper.hx @@ -104,11 +104,36 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass { super(); this.danceEvery = danceEvery; - this.animation.finishCallback = function(name) - { - if (finishCallbackMap[name] != null) - finishCallbackMap[name](); - }; + + this.animation.callback = this.onAnimationFrame; + this.animation.finishCallback = this.onAnimationFinished; + } + + /** + * Called when an animation finishes. + * @param name The name of the animation that just finished. + */ + function onAnimationFinished(name:String) { + if (!canPlayOtherAnims) { + canPlayOtherAnims = true; + } + } + + /** + * Called when the current animation's frame changes. + * @param name The name of the current animation. + * @param frameNumber The number of the current frame. + * @param frameIndex The index of the current frame. + * + * For example, if an animation was defined as having the indexes [3, 0, 1, 2], + * then the first callback would have frameNumber = 0 and frameIndex = 3. + */ + function onAnimationFrame(name:String = "", frameNumber:Int = -1, frameIndex:Int = -1) { + // Do nothing by default. + // This can be overridden by, for example, scripted characters. + // Try not to do anything expensive here, it runs many times a second. + + // Sometimes this gets called with empty values? IDK why but adding defaults keeps it from crashing. } /** @@ -236,13 +261,6 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass if (ignoreOther) { canPlayOtherAnims = false; - - // doing it with this funny map, since overriding the animation.finishCallback is a bit messier IMO - finishCallbackMap[name] = function() - { - canPlayOtherAnims = true; - finishCallbackMap[name] = null; - }; } applyAnimationOffsets(correctName); diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx index 172b0dc9c..52e0c4c9f 100644 --- a/source/funkin/play/stage/Stage.hx +++ b/source/funkin/play/stage/Stage.hx @@ -64,6 +64,36 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte this.refresh(); } + public function resetStage():Void { + // Reset positions of characters. + if (getBoyfriend() != null) { + getBoyfriend().resetCharacter(false); + } else { + trace('STAGE RESET: No boyfriend found.'); + } + if (getGirlfriend() != null) { + getGirlfriend().resetCharacter(false); + } + if (getDad() != null) { + getDad().resetCharacter(false); + } + + // Reset positions of named props. + for (dataProp in _data.props) { + // Fetch the prop. + var prop:FlxSprite = getNamedProp(dataProp.name); + + if (prop != null) { + // Reset the position. + prop.x = dataProp.position[0]; + prop.y = dataProp.position[1]; + prop.zIndex = dataProp.zIndex; + } + } + + // We can assume unnamed props are not moving. + } + /** * The default stage construction routine. Called when the stage is going to be played in. * Instantiates each prop and adds it to the stage, while setting its parameters. @@ -253,9 +283,13 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte // Should display at the stage position of the character (before any offsets). // TODO: Make this a toggle? It's useful to turn on from time to time. var debugIcon:FlxSprite = new FlxSprite(0, 0); + var debugIcon2:FlxSprite = new FlxSprite(0, 0); debugIcon.makeGraphic(8, 8, 0xffff00ff); - debugIcon.visible = false; + debugIcon2.makeGraphic(8, 8, 0xff00ffff); + debugIcon.visible = true; + debugIcon2.visible = true; debugIcon.zIndex = 1000000; + debugIcon2.zIndex = 1000000; #end // Apply position and z-index. @@ -265,20 +299,25 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte case BF: this.characters.set("bf", character); charData = _data.characters.bf; - character.flipX = !character.flipX; - // flip offsets if flipX + character.flipX = !character.getDataFlipX(); character.initHealthIcon(false); case GF: this.characters.set("gf", character); charData = _data.characters.gf; + character.flipX = character.getDataFlipX(); case DAD: this.characters.set("dad", character); charData = _data.characters.dad; - // flip offsets if flipX + character.flipX = character.getDataFlipX(); character.initHealthIcon(true); default: this.characters.set(character.characterId, character); } + + // Reset the character before adding it to the stage. + // This ensures positioning is based on the idle animation. + character.resetCharacter(true); + if (charData != null) { character.zIndex = charData.zIndex; @@ -289,17 +328,28 @@ class Stage extends FlxSpriteGroup implements IHook implements IPlayStateScripte character.x = charData.position[0] - character.characterOrigin.x + character.globalOffsets[0]; character.y = charData.position[1] - character.characterOrigin.y + character.globalOffsets[1]; + character.originalPosition.x = character.x; + character.originalPosition.y = character.y; + character.cameraFocusPoint.x += charData.cameraOffsets[0]; character.cameraFocusPoint.y += charData.cameraOffsets[1]; + #if debug // Draw the debug icon at the character's feet. - debugIcon.x = charData.position[0]; - debugIcon.y = charData.position[1]; + if (charType == BF || charType == DAD) + { + debugIcon.x = charData.position[0]; + debugIcon.y = charData.position[1]; + debugIcon2.x = character.x; + debugIcon2.y = character.y; + } + #end } // Add the character to the scene. this.add(character); this.add(debugIcon); + this.add(debugIcon2); } public inline function getGirlfriendPosition():FlxPoint diff --git a/source/funkin/util/BezierUtil.hx b/source/funkin/util/BezierUtil.hx new file mode 100644 index 000000000..a1bd45762 --- /dev/null +++ b/source/funkin/util/BezierUtil.hx @@ -0,0 +1,77 @@ +package funkin.util; + +import flixel.math.FlxPoint; + +class BezierUtil { + /** + * Linearly interpolate between two values. + * Depending on p, 0 = a, 1 = b, 0.5 = halfway between a and b. + */ + static inline function mix2(p:Float, a:Float, b:Float):Float { + return a * (1 - p) + (b * p); + } + + /** + * Linearly interpolate between three values. + * Depending on p, 0 = a, 0.5 = b, 1 = c, 0.25 = halfway between a and c, etc. + */ + static inline function mix3(p:Float, a:Float, b:Float, c:Float):Float { + return mix2(p, mix2(p, a, b), mix2(p, b, c)); + } + + static inline function mix4(p:Float, a:Float, b:Float, c:Float, d:Float):Float { + return mix2(p, mix3(p, a, b, c), mix3(p, b, c, d)); + } + + static inline function mix5(p:Float, a:Float, b:Float, c:Float, d:Float, e:Float):Float { + return mix2(p, mix4(p, a, b, c, d), mix4(p, b, c, d, e)); + } + + static inline function mix6(p:Float, a:Float, b:Float, c:Float, d:Float, e:Float, f:Float):Float { + return mix2(p, mix5(p, a, b, c, d, e), mix5(p, b, c, d, e, f)); + } + + /** + * A bezier curve with two points. + * This is really just linear interpolation but whatever. + */ + public static function bezier2(p:Float, a:FlxPoint, b:FlxPoint):FlxPoint { + return new FlxPoint(mix2(p, a.x, b.x), mix2(p, a.y, b.y)); + } + + /** + * A bezier curve with three points. + * @param p The percentage of the way through the curve. + * @param a The start point. + * @param b The control point. + * @param c The end point. + */ + public static function bezier3(p:Float, a:FlxPoint, b:FlxPoint, c:FlxPoint):FlxPoint { + return new FlxPoint(mix3(p, a.x, b.x, c.x), mix3(p, a.y, b.y, c.y)); + } + + /** + * A bezier curve with four points. + * @param p The percentage of the way through the curve. + * @param a The start point. + * @param b The first control point. + * @param c The second control point. + * @param d The end point. + */ + public static function bezier4(p:Float, a:FlxPoint, b:FlxPoint, c:FlxPoint, d:FlxPoint):FlxPoint { + return new FlxPoint(mix4(p, a.x, b.x, c.x, d.x), mix4(p, a.y, b.y, c.y, d.y)); + } + + /** + * A bezier curve with four points. + * @param p The percentage of the way through the curve. + * @param a The start point. + * @param b The first control point. + * @param c The second control point. + * @param c The third control point. + * @param d The end point. + */ + public static function bezier5(p:Float, a:FlxPoint, b:FlxPoint, c:FlxPoint, d:FlxPoint, e:FlxPoint):FlxPoint { + return new FlxPoint(mix5(p, a.x, b.x, c.x, d.x, e.x), mix5(p, a.y, b.y, c.y, d.y, e.y)); + } +} \ No newline at end of file