diff --git a/Project.xml b/Project.xml
index 81f017853..8b7f37f19 100644
--- a/Project.xml
+++ b/Project.xml
@@ -54,7 +54,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