1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-03-24 02:49:33 +00:00

Merge branch 'feature/new-input-system-yay' into feature/chart-editor-bpm

This commit is contained in:
EliteMasterEric 2023-07-19 21:19:21 -04:00
commit aaad06c97d
83 changed files with 5014 additions and 3451 deletions

View file

@ -25,7 +25,19 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup-haxeshit
- name: Build game?
- name: Build Lime
# TODO: Remove the step that builds Lime later.
# Bash method
run: |
LIME_PATH=`haxelib libpath lime`
echo "Moving to $LIME_PATH"
cd $LIME_PATH
git submodule sync --recursive
git submodule update --recursive
git status
sudo apt-get install -y libxinerama-dev
haxelib run lime rebuild linux --clean
- name: Build game
run: |
sudo apt-get install -y libx11-dev libxinerama-dev libxrandr-dev libgl1-mesa-dev libgl-dev libxi-dev libxext-dev libasound2-dev
haxelib run lime build html5 -debug --times
@ -45,6 +57,17 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup-haxeshit
- name: Build Lime
# TODO: Remove the step that builds Lime later.
# Powershell method
run: |
$LIME_PATH = haxelib libpath lime
echo "Moving to $LIME_PATH"
cd $LIME_PATH
git submodule sync --recursive
git submodule update --recursive
git status
haxelib run lime rebuild windows --clean
- name: Build game
run: |
haxelib run lime build windows -debug

View file

@ -116,5 +116,6 @@
"target": "html5",
"args": ["-debug", "-watch"]
}
]
],
"cmake.configureOnOpen": false
}

View file

@ -66,10 +66,8 @@
},
{
"name": "hxCodec",
"type": "git",
"dir": null,
"ref": "a56f4b4",
"url": "https://github.com/FunkinCrew/hxCodec"
"type": "haxelib",
"version": "3.0.1"
},
{
"name": "hxcpp",
@ -95,8 +93,8 @@
"name": "lime",
"type": "git",
"dir": null,
"ref": "5634ad7",
"url": "https://github.com/openfl/lime"
"ref": "2447ae6",
"url": "https://github.com/elitemastereric/lime"
},
{
"name": "openfl",
@ -123,4 +121,4 @@
"version": null
}
]
}
}

View file

@ -77,17 +77,6 @@ class Main extends Sprite
* -Eric
*/
#if !debug
/**
* Someone was like "hey let's make a state that only runs code on debug builds"
* then put essential initialization code in it.
* The easiest fix is to make it run in all builds.
* -Eric
*/
// TODO: Fix this properly.
// initialState = funkin.TitleState;
#end
initHaxeUI();
addChild(new FlxGame(gameWidth, gameHeight, initialState, framerate, framerate, skipSplash, startFullscreen));

View file

@ -243,8 +243,6 @@ class AlphaCharacter extends FlxSprite
super(x, y);
var tex = Paths.getSparrowAtlas('alphabet');
frames = tex;
antialiasing = true;
}
public function createBold(letter:String)
@ -266,8 +264,6 @@ class AlphaCharacter extends FlxSprite
animation.play(letter);
updateHitbox();
FlxG.log.add('the row' + row);
y = (110 - height);
y += row * 60;
}

View file

@ -26,7 +26,6 @@ class ComboMilestone extends FlxTypedSpriteGroup<FlxSprite>
effectStuff.frames = Paths.getSparrowAtlas('comboMilestone');
effectStuff.animation.addByPrefix('funny', 'NOTE COMBO animation', 24, false);
effectStuff.animation.play('funny');
effectStuff.antialiasing = true;
effectStuff.animation.finishCallback = function(nameThing) {
kill();
};
@ -108,7 +107,6 @@ class ComboMilestoneNumber extends FlxSprite
frames = Paths.getSparrowAtlas('comboMilestoneNumbers');
animation.addByPrefix(stringNum, stringNum, 24, false);
animation.play(stringNum);
antialiasing = true;
updateHitbox();
}

View file

@ -13,6 +13,12 @@ import funkin.play.song.SongData.SongTimeChange;
*/
class Conductor
{
public static final PIXELS_PER_MS:Float = 0.45;
public static final HIT_WINDOW_MS:Float = 160;
public static final SECONDS_PER_MINUTE:Float = 60;
public static final MILLIS_PER_SECOND:Float = 1000;
public static final STEPS_PER_BEAT:Int = 4;
// onBeatHit is called every quarter note
// onStepHit is called every sixteenth note
// 4/4 = 4 beats per measure = 16 steps per measure
@ -82,7 +88,8 @@ class Conductor
static function get_beatLengthMs():Float
{
return ((Constants.SECS_PER_MIN / bpm) * Constants.MS_PER_SEC);
// Tied directly to BPM.
return ((SECONDS_PER_MINUTE / bpm) * MILLIS_PER_SECOND);
}
/**

View file

@ -391,6 +391,26 @@ class Controls extends FlxActionSet
return byName[name].check();
}
public function getKeysForAction(name:Action):Array<FlxKey> {
#if debug
if (!byName.exists(name))
throw 'Invalid name: $name';
#end
return byName[name].inputs.map(function(input) return (input.device == KEYBOARD) ? input.inputID : null)
.filter(function(key) return key != null);
}
public function getButtonsForAction(name:Action):Array<FlxGamepadInputID> {
#if debug
if (!byName.exists(name))
throw 'Invalid name: $name';
#end
return byName[name].inputs.map(function(input) return (input.device == GAMEPAD) ? input.inputID : null)
.filter(function(key) return key != null);
}
public function getDialogueName(action:FlxActionDigital):String
{
var input = action.inputs[0];

View file

@ -1,6 +1,5 @@
package funkin;
import funkin.util.Constants;
import flixel.FlxSprite;
import flixel.addons.text.FlxTypeText;
import flixel.group.FlxSpriteGroup;

View file

@ -338,7 +338,6 @@ class FreeplayState extends MusicBeatSubState
fnfHighscoreSpr.animation.addByPrefix("highscore", "highscore", 24, false);
fnfHighscoreSpr.visible = false;
fnfHighscoreSpr.setGraphicSize(0, Std.int(fnfHighscoreSpr.height * 1));
fnfHighscoreSpr.antialiasing = true;
fnfHighscoreSpr.updateHitbox();
add(fnfHighscoreSpr);

View file

@ -192,6 +192,7 @@ abstract Tallies(RawTallies)
bad: 0,
good: 0,
sick: 0,
killer: 0,
totalNotes: 0,
totalNotesHit: 0,
maxCombo: 0,
@ -213,6 +214,7 @@ typedef RawTallies =
var bad:Int;
var good:Int;
var sick:Int;
var killer:Int;
var maxCombo:Int;
var isNewHighscore:Bool;

View file

@ -6,56 +6,92 @@ import flixel.addons.transition.TransitionData;
import flixel.graphics.FlxGraphic;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import flixel.FlxSprite;
import flixel.system.debug.log.LogStyle;
import flixel.util.FlxColor;
import funkin.modding.module.ModuleHandler;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.cutscene.dialogue.ConversationDataParser;
import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
import funkin.play.cutscene.dialogue.SpeakerDataParser;
import funkin.play.event.SongEventData.SongEventParser;
import funkin.play.PlayState;
import funkin.play.song.SongData.SongDataParser;
import funkin.play.stage.StageData.StageDataParser;
import funkin.ui.PreferencesMenu;
import funkin.util.macro.MacroUtil;
import funkin.util.WindowUtil;
import funkin.play.PlayStatePlaylist;
import openfl.display.BitmapData;
import funkin.data.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.event.SongEventData.SongEventParser;
import funkin.play.cutscene.dialogue.ConversationDataParser;
import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
import funkin.play.cutscene.dialogue.SpeakerDataParser;
import funkin.play.song.SongData.SongDataParser;
import funkin.play.stage.StageData.StageDataParser;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.modding.module.ModuleHandler;
#if discord_rpc
import Discord.DiscordClient;
#end
/**
* Initializes the game state using custom defines.
* Only used in Debug builds.
* The initialization state has several functions:
* - Calls code to set up the game, including loading saves and parsing game data.
* - Chooses whether to start via debug or via launching normally.
*/
class InitState extends FlxTransitionableState
{
override public function create():Void
/**
* Perform a bunch of game setup, then immediately transition to the title screen.
*/
public override function create():Void
{
trace('This is a debug build, loading InitState...');
#if android
FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK];
#end
#if newgrounds
NGio.init();
#end
#if discord_rpc
DiscordClient.initialize();
setupShit();
Application.current.onExit.add(function(exitCode) {
DiscordClient.shutdown();
});
#end
loadSaveData();
// ==== flixel shit ==== //
startGame();
}
/**
* Setup a bunch of important Flixel stuff.
*/
function setupShit()
{
//
// GAME SETUP
//
// Setup window events (like callbacks for onWindowClose)
WindowUtil.initWindowEvents();
// Disable the thing on Windows where it tries to send a bug report to Microsoft because why do they care?
WindowUtil.disableCrashHandler();
// This ain't a pixel art game! (most of the time)
FlxSprite.defaultAntialiasing = true;
// Disable default keybinds for volume (we manually control volume in MusicBeatState with custom binds)
FlxG.sound.volumeUpKeys = [];
FlxG.sound.volumeDownKeys = [];
FlxG.sound.muteKeys = [];
// TODO: Make sure volume still saves/loads properly.
// if (FlxG.save.data.volume != null) FlxG.sound.volume = FlxG.save.data.volume;
// if (FlxG.save.data.mute != null) FlxG.sound.muted = FlxG.save.data.mute;
// Set the game to a lower frame rate while it is in the background.
FlxG.game.focusLostFramerate = 30;
//
// FLIXEL DEBUG SETUP
//
#if debug
// Disable using ~ to open the console (we use that for the Editor menu)
FlxG.debugger.toggleKeys = [F2];
// Adds an additional Close Debugger button.
// This big obnoxious white button is for MOBILE, so that you can press it
// easily with your finger when debug bullshit pops up during testing lol!
FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function() {
FlxG.debugger.visible = false;
});
// Adds a red button to the debugger.
// This pauses the game AND the music! This ensures the Conductor stops.
FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFFCC2233), function() {
if (FlxG.vcr.paused)
{
@ -81,7 +117,8 @@ class InitState extends FlxTransitionableState
}
});
#if FLX_DEBUG
// Adds a blue button to the debugger.
// This skips forward in the song.
FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFF2222CC), function() {
FlxG.game.debugger.vcr.onStep();
@ -94,71 +131,70 @@ class InitState extends FlxTransitionableState
FlxG.sound.music.pause();
FlxG.sound.music.time += FlxG.elapsed * 1000;
});
// Make errors and warnings less annoying.
// TODO: Disable this so we know to fix warnings.
if (false)
{
LogStyle.ERROR.openConsole = false;
LogStyle.ERROR.errorSound = null;
LogStyle.WARNING.openConsole = false;
LogStyle.WARNING.errorSound = null;
}
#end
FlxG.sound.muteKeys = [ZERO];
FlxG.game.focusLostFramerate = 60;
// FlxG.stage.window.borderless = true;
// FlxG.stage.window.mouseLock = true;
//
// FLIXEL TRANSITIONS
//
// Diamond Transition
var diamond:FlxGraphic = FlxGraphic.fromClass(GraphicTransTileDiamond);
diamond.persist = true;
diamond.destroyOnNoUse = false;
FlxTransitionableState.defaultTransIn = new TransitionData(FADE, FlxColor.BLACK, 1, new FlxPoint(0, -1), {asset: diamond, width: 32, height: 32},
// NOTE: tileData is ignored if TransitionData.type is FADE instead of TILES.
var tileData:TransitionTileData = {asset: diamond, width: 32, height: 32};
FlxTransitionableState.defaultTransIn = new TransitionData(FADE, FlxColor.BLACK, 1, new FlxPoint(0, -1), tileData,
new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4));
FlxTransitionableState.defaultTransOut = new TransitionData(FADE, FlxColor.BLACK, 0.7, new FlxPoint(0, 1), {asset: diamond, width: 32, height: 32},
FlxTransitionableState.defaultTransOut = new TransitionData(FADE, FlxColor.BLACK, 0.7, new FlxPoint(0, 1), tileData,
new FlxRect(-200, -200, FlxG.width * 1.4, FlxG.height * 1.4));
// ===== save shit ===== //
FlxG.save.bind('funkin', 'ninjamuffin99');
// https://github.com/HaxeFlixel/flixel/pull/2396
// IF/WHEN MY PR GOES THRU AND IT GETS INTO MAIN FLIXEL, DELETE THIS CHUNKOF CODE, AND THEN UNCOMMENT THE LINE BELOW
// FlxG.sound.loadSavedPrefs();
if (FlxG.save.data.volume != null) FlxG.sound.volume = FlxG.save.data.volume;
if (FlxG.save.data.mute != null) FlxG.sound.muted = FlxG.save.data.mute;
// Make errors and warnings less annoying.
LogStyle.ERROR.openConsole = false;
LogStyle.ERROR.errorSound = null;
LogStyle.WARNING.openConsole = false;
LogStyle.WARNING.errorSound = null;
// FlxG.save.close();
// FlxG.sound.loadSavedPrefs();
WindowUtil.initWindowEvents();
WindowUtil.disableCrashHandler();
PreferencesMenu.initPrefs();
PlayerSettings.init();
Highscore.load();
if (FlxG.save.data.weekUnlocked != null)
{
// FIX LATER!!!
// WEEK UNLOCK PROGRESSION!!
// StoryMenuState.weekUnlocked = FlxG.save.data.weekUnlocked;
// if (StoryMenuState.weekUnlocked.length < 4) StoryMenuState.weekUnlocked.insert(0, true);
// QUICK PATCH OOPS!
// if (!StoryMenuState.weekUnlocked[0]) StoryMenuState.weekUnlocked[0] = true;
}
if (FlxG.save.data.seenVideo != null) VideoState.seenVideo = FlxG.save.data.seenVideo;
// ===== fuck outta here ===== //
// FlxTransitionableState.skipNextTransOut = true;
// Don't play transition in when entering the title state.
FlxTransitionableState.skipNextTransIn = true;
// TODO: Register custom event callbacks here
//
// NEWGROUNDS API SETUP
//
#if newgrounds
NGio.init();
#end
funkin.data.level.LevelRegistry.instance.loadEntries();
//
// DISCORD API SETUP
//
#if discord_rpc
DiscordClient.initialize();
Application.current.onExit.add(function(exitCode) {
DiscordClient.shutdown();
});
#end
//
// ANDROID SETUP
//
#if android
FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK];
#end
//
// GAME DATA PARSING
//
// NOTE: Registries and data parsers must be imported and not referenced with fully qualified names,
// to ensure build macros work properly.
LevelRegistry.instance.loadEntries();
NoteStyleRegistry.instance.loadEntries();
SongEventParser.loadEventCache();
ConversationDataParser.loadConversationCache();
DialogueBoxDataParser.loadDialogueBoxCache();
@ -169,100 +205,130 @@ class InitState extends FlxTransitionableState
ModuleHandler.buildModuleCallbacks();
ModuleHandler.loadModuleCache();
FlxG.debugger.toggleKeys = [F2];
ModuleHandler.callOnCreate();
}
#if song
var song:String = getSong();
/**
* Retrive and parse data from the user's save.
*/
function loadSaveData()
{
// Bind save data.
// TODO: Migrate save data to a better format.
FlxG.save.bind('funkin', 'ninjamuffin99');
var weeks:Array<Array<String>> = [
['bopeebo', 'fresh', 'dadbattle'],
['spookeez', 'south', 'monster'],
['spooky', 'spooky', 'monster'],
['pico', 'philly', 'blammed'],
['satin-panties', 'high', 'milf'],
['cocoa', 'eggnog', 'winter-horrorland'],
['senpai', 'roses', 'thorns'],
['ugh', 'guns', 'stress']
];
// Load player options from save data.
PreferencesMenu.initPrefs();
// Load controls from save data.
PlayerSettings.init();
// Load highscores from save data.
Highscore.load();
// TODO: Load level/character/cosmetic unlocks from save data.
}
var week:Int = 0;
for (i in 0...weeks.length)
{
if (weeks[i].contains(song))
{
week = i + 1;
break;
}
}
if (week == 0) throw 'Invalid -D song=$song';
startSong(week, song, false);
#elseif week
var week:Int = getWeek();
var songs:Array<String> = [
'bopeebo',
'spookeez',
'spooky',
'pico',
'satin-panties',
'cocoa',
'senpai',
'ugh'
];
if (week <= 0 || week >= songs.length) throw 'invalid -D week=' + week;
startSong(week, songs[week - 1], true);
#elseif FREEPLAY
/**
* Start the game.
*
* By default, moves to the `TitleState`.
* But based on compile defines, the game can start immediately on a specific song,
* or immediately in a specific debug menu.
*/
function startGame():Void
{
#if SONG // -DSONG=bopeebo
startSong(defineSong(), defineDifficulty());
#elseif LEVEL // -DLEVEL=week1 -DDIFFICULTY=hard
startLevel(defineLevel(), defineDifficulty());
#elseif FREEPLAY // -DFREEPLAY
FlxG.switchState(new FreeplayState());
#elseif ANIMATE
#elseif ANIMATE // -DANIMATE
FlxG.switchState(new funkin.ui.animDebugShit.FlxAnimateTest());
#elseif CHARTING
#elseif CHARTING // -DCHARTING
FlxG.switchState(new funkin.ui.debug.charting.ChartEditorState());
#elseif STAGEBUILD
FlxG.switchState(new StageBuilderState());
#elseif FIGHT
FlxG.switchState(new PicoFight());
#elseif ANIMDEBUG
#elseif STAGEBUILD // -DSTAGEBUILD
FlxG.switchState(new funkin.ui.stageBullshit.StageBuilderState());
#elseif ANIMDEBUG // -DANIMDEBUG
FlxG.switchState(new funkin.ui.animDebugShit.DebugBoundingState());
#elseif LATENCY
FlxG.switchState(new LatencyState());
#elseif NETTEST
FlxG.switchState(new netTest.NetTest());
#elseif LATENCY // -DLATENCY
FlxG.switchState(new funkin.LatencyState());
#else
FlxG.sound.cache(Paths.music('freakyMenu'));
FlxG.switchState(new TitleState());
startGameNormally();
#end
}
function startSong(week, song, isStoryMode):Void
/**
* Start the game by moving to the title state and play the game as normal.
*/
function startGameNormally():Void
{
var dif:Int = getDif();
FlxG.sound.cache(Paths.music('freakyMenu'));
FlxG.switchState(new TitleState());
}
var targetDifficulty = switch (dif)
/**
* Start the game by directly loading into a specific song.
* @param songId
* @param difficultyId
*/
function startSong(songId:String, difficultyId:String = 'normal'):Void
{
var songData:funkin.play.song.Song = funkin.play.song.SongData.SongDataParser.fetchSong(songId);
if (songData == null)
{
case 0: 'easy';
case 1: 'normal';
case 2: 'hard';
default: 'normal';
};
LoadingState.loadAndSwitchState(new PlayState(
startGameNormally();
return;
}
LoadingState.loadAndSwitchState(new funkin.play.PlayState(
{
targetSong: SongDataParser.fetchSong(song),
targetDifficulty: targetDifficulty,
targetSong: songData,
targetDifficulty: difficultyId,
}));
}
/**
* Start the game by directly loading into a specific story mode level.
* @param levelId
* @param difficultyId
*/
function startLevel(levelId:String, difficultyId:String = 'normal'):Void
{
var currentLevel:funkin.ui.story.Level = funkin.data.level.LevelRegistry.instance.fetchEntry(levelId);
if (currentLevel == null)
{
startGameNormally();
return;
}
PlayStatePlaylist.playlistSongIds = currentLevel.getSongs();
PlayStatePlaylist.isStoryMode = true;
PlayStatePlaylist.campaignScore = 0;
var targetSongId:String = PlayStatePlaylist.playlistSongIds.shift();
var targetSong:funkin.play.song.Song = funkin.play.song.SongData.SongDataParser.fetchSong(targetSongId);
LoadingState.loadAndSwitchState(new funkin.play.PlayState(
{
targetSong: targetSong,
targetDifficulty: difficultyId,
}));
}
function defineSong():String
{
return MacroUtil.getDefine('SONG');
}
function defineLevel():String
{
return MacroUtil.getDefine('LEVEL');
}
function defineDifficulty():String
{
return MacroUtil.getDefine('DIFFICULTY');
}
}
function getWeek():Int
return Std.parseInt(MacroUtil.getDefine('week'));
function getSong():String
return MacroUtil.getDefine('song');
function getDif():Int
return Std.parseInt(MacroUtil.getDefine('dif', '1'));

View file

@ -1,15 +1,17 @@
package funkin;
import funkin.data.notestyle.NoteStyleRegistry;
import flixel.FlxSprite;
import flixel.FlxSubState;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.group.FlxGroup;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.math.FlxMath;
import flixel.sound.FlxSound;
import flixel.system.debug.stats.StatsGraph;
import flixel.text.FlxText;
import flixel.util.FlxColor;
import funkin.audio.visualize.PolygonSpectogram;
import funkin.play.notes.NoteSprite;
import funkin.ui.CoolStatsGraph;
import haxe.Timer;
import openfl.events.KeyboardEvent;
@ -17,7 +19,7 @@ import openfl.events.KeyboardEvent;
class LatencyState extends MusicBeatSubState
{
var offsetText:FlxText;
var noteGrp:FlxTypedGroup<Note>;
var noteGrp:FlxTypedGroup<NoteSprite>;
var strumLine:FlxSprite;
var blocks:FlxTypedGroup<FlxSprite>;
@ -74,7 +76,7 @@ class LatencyState extends MusicBeatSubState
Conductor.forceBPM(60);
noteGrp = new FlxTypedGroup<Note>();
noteGrp = new FlxTypedGroup<NoteSprite>();
add(noteGrp);
diffGrp = new FlxTypedGroup<FlxText>();
@ -127,7 +129,7 @@ class LatencyState extends MusicBeatSubState
for (i in 0...32)
{
var note:Note = new Note(Conductor.beatLengthMs * i, 1);
var note:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault(), Conductor.beatLengthMs * i);
noteGrp.add(note);
}
@ -246,8 +248,8 @@ class LatencyState extends MusicBeatSubState
FlxG.resetState();
}*/
noteGrp.forEach(function(daNote:Note) {
daNote.y = (strumLine.y - ((Conductor.songPosition - Conductor.audioOffset) - daNote.data.strumTime) * 0.45);
noteGrp.forEach(function(daNote:NoteSprite) {
daNote.y = (strumLine.y - ((Conductor.songPosition - Conductor.audioOffset) - daNote.noteData.time) * 0.45);
daNote.x = strumLine.x + 30;
if (daNote.y < strumLine.y) daNote.alpha = 0.5;

View file

@ -42,7 +42,6 @@ class LoadingState extends MusicBeatState
funkay.loadGraphic(Paths.image('funkay'));
funkay.setGraphicSize(0, FlxG.height);
funkay.updateHitbox();
funkay.antialiasing = true;
add(funkay);
funkay.scrollFactor.set();
funkay.screenCenter();

View file

@ -26,7 +26,6 @@ import funkin.ui.story.StoryMenuState;
import funkin.ui.OptionsState;
import funkin.ui.PreferencesMenu;
import funkin.ui.Prompt;
import funkin.util.Constants;
import funkin.util.WindowUtil;
import lime.app.Application;
import openfl.filters.ShaderFilter;
@ -68,7 +67,6 @@ class MainMenuState extends MusicBeatState
bg.setGraphicSize(Std.int(bg.width * 1.2));
bg.updateHitbox();
bg.screenCenter();
bg.antialiasing = true;
add(bg);
camFollow = new FlxObject(0, 0, 1, 1);
@ -82,7 +80,6 @@ class MainMenuState extends MusicBeatState
magenta.x = bg.x;
magenta.y = bg.y;
magenta.visible = false;
magenta.antialiasing = true;
magenta.color = 0xFFfd719b;
if (PreferencesMenu.preferences.get('flashing-menu')) add(magenta);
// magenta.scrollFactor.set();

View file

@ -60,6 +60,11 @@ class MusicBeatState extends FlxUIState implements IEventHandler
{
super.update(elapsed);
// Rebindable volume keys.
if (controls.VOLUME_MUTE) FlxG.sound.toggleMuted();
else if (controls.VOLUME_UP) FlxG.sound.changeVolume(0.1);
else if (controls.VOLUME_DOWN) FlxG.sound.changeVolume(-0.1);
// Emergency exit button.
if (FlxG.keys.justPressed.F4) FlxG.switchState(new MainMenuState());
@ -104,7 +109,9 @@ class MusicBeatState extends FlxUIState implements IEventHandler
{
PolymodHandler.forceReloadAssets();
// Restart the current state, so old data is cleared.
this.destroy();
// Create a new instance of the current state, so old data is cleared.
FlxG.resetState();
}

View file

@ -1,301 +0,0 @@
package funkin;
import funkin.play.Strumline.StrumlineArrow;
import flixel.FlxSprite;
import flixel.math.FlxMath;
import funkin.noteStuff.NoteBasic.NoteData;
import funkin.noteStuff.NoteBasic.NoteType;
import funkin.play.PlayState;
import funkin.play.Strumline.StrumlineStyle;
import funkin.shaderslmfao.ColorSwap;
import funkin.ui.PreferencesMenu;
import funkin.util.Constants;
class Note extends FlxSprite
{
public var data = new NoteData();
/**
* code colors for.... code....
* i think goes in order of left to right
*
* left 0
* down 1
* up 2
* right 3
*/
public static var codeColors:Array<Int> = [0xFFFF22AA, 0xFF00EEFF, 0xFF00CC00, 0xFFCC1111];
public var mustPress:Bool = false;
public var followsTime:Bool = true; // used if you want the note to follow the time shit!
public var canBeHit:Bool = false;
public var tooLate:Bool = false;
public var wasGoodHit:Bool = false;
public var prevNote:Note;
var willMiss:Bool = false;
public var invisNote:Bool = false;
public var isSustainNote:Bool = false;
public var colorSwap:ColorSwap;
/** the lowercase name of the note, for anim control, i.e. left right up down */
public var dirName(get, never):String;
inline function get_dirName()
return data.dirName;
/** the uppercase name of the note, for anim control, i.e. left right up down */
public var dirNameUpper(get, never):String;
inline function get_dirNameUpper()
return data.dirNameUpper;
/** the lowercase name of the note's color, for anim control, i.e. purple blue green red */
public var colorName(get, never):String;
inline function get_colorName()
return data.colorName;
/** the lowercase name of the note's color, for anim control, i.e. purple blue green red */
public var colorNameUpper(get, never):String;
inline function get_colorNameUpper()
return data.colorNameUpper;
public var highStakes(get, never):Bool;
inline function get_highStakes()
return data.highStakes;
public var lowStakes(get, never):Bool;
inline function get_lowStakes()
return data.lowStakes;
public static var swagWidth:Float = 160 * 0.7;
public static var PURP_NOTE:Int = 0;
public static var GREEN_NOTE:Int = 2;
public static var BLUE_NOTE:Int = 1;
public static var RED_NOTE:Int = 3;
// SCORING STUFF
public static var HIT_WINDOW:Float = (10 / 60) * 1000; // 166.67 ms hit window (10 frames at 60fps)
// thresholds are fractions of HIT_WINDOW ^^
// anything above bad threshold is shit
public static var BAD_THRESHOLD:Float = 0.8; // 125ms , 8 frames
public static var GOOD_THRESHOLD:Float = 0.55; // 91.67ms , 5.5 frames
public static var SICK_THRESHOLD:Float = 0.2; // 33.33ms , 2 frames
public var noteSpeedMulti:Float = 1;
public var pastHalfWay:Bool = false;
// anything below sick threshold is sick
public static var arrowColors:Array<Float> = [1, 1, 1, 1];
// Which note asset to load?
public var style:StrumlineStyle = NORMAL;
public function new(strumTime:Float = 0, noteData:NoteType, ?prevNote:Note, ?sustainNote:Bool = false, ?style:StrumlineStyle = NORMAL)
{
super();
if (prevNote == null) prevNote = this;
this.prevNote = prevNote;
isSustainNote = sustainNote;
x += 50;
// MAKE SURE ITS DEFINITELY OFF SCREEN?
y -= 2000;
data.strumTime = strumTime;
data.noteData = noteData;
this.style = style;
if (this.style == null) this.style = StrumlineStyle.NORMAL;
// TODO: Make this logic more generic
switch (this.style)
{
case PIXEL:
loadGraphic(Paths.image('weeb/pixelUI/arrows-pixels'), true, 17, 17);
animation.add('greenScroll', [6]);
animation.add('redScroll', [7]);
animation.add('blueScroll', [5]);
animation.add('purpleScroll', [4]);
if (isSustainNote)
{
loadGraphic(Paths.image('weeb/pixelUI/arrowEnds'), true, 7, 6);
animation.add('purpleholdend', [4]);
animation.add('greenholdend', [6]);
animation.add('redholdend', [7]);
animation.add('blueholdend', [5]);
animation.add('purplehold', [0]);
animation.add('greenhold', [2]);
animation.add('redhold', [3]);
animation.add('bluehold', [1]);
}
setGraphicSize(Std.int(width * Constants.PIXEL_ART_SCALE));
updateHitbox();
default:
frames = Paths.getSparrowAtlas('NOTE_assets');
animation.addByPrefix('purpleScroll', 'purple instance');
animation.addByPrefix('blueScroll', 'blue instance');
animation.addByPrefix('greenScroll', 'green instance');
animation.addByPrefix('redScroll', 'red instance');
animation.addByPrefix('purpleholdend', 'pruple end hold');
animation.addByPrefix('greenholdend', 'green hold end');
animation.addByPrefix('redholdend', 'red hold end');
animation.addByPrefix('blueholdend', 'blue hold end');
animation.addByPrefix('purplehold', 'purple hold piece');
animation.addByPrefix('greenhold', 'green hold piece');
animation.addByPrefix('redhold', 'red hold piece');
animation.addByPrefix('bluehold', 'blue hold piece');
setGraphicSize(Std.int(width * 0.7));
updateHitbox();
antialiasing = true;
// colorSwap.colorToReplace = 0xFFF9393F;
// colorSwap.newColor = 0xFF00FF00;
// color = FlxG.random.color();
// color.saturation *= 4;
// replaceColor(0xFFC1C1C1, FlxColor.RED);
}
colorSwap = new ColorSwap();
shader = colorSwap.shader;
updateColors();
x += swagWidth * data.int;
animation.play(data.colorName + 'Scroll');
// trace(prevNote);
if (isSustainNote && prevNote != null)
{
alpha = 0.6;
if (PreferencesMenu.getPref('downscroll')) angle = 180;
x += width / 2;
animation.play(data.colorName + 'holdend');
updateHitbox();
x -= width / 2;
if (PlayState.instance.currentStageId.startsWith('school')) x += 30;
if (prevNote.isSustainNote)
{
prevNote.animation.play(prevNote.colorName + 'hold');
prevNote.updateHitbox();
var scaleThing:Float = Math.round((Conductor.stepLengthMs) * (0.45 * FlxMath.roundDecimal(PlayState.instance.currentChart.scrollSpeed, 2)));
// get them a LIL closer together cuz the antialiasing blurs the edges
if (antialiasing) scaleThing *= 1.0 + (1.0 / prevNote.frameHeight);
prevNote.scale.y = scaleThing / prevNote.frameHeight;
prevNote.updateHitbox();
}
}
}
public function alignToSturmlineArrow(arrow:StrumlineArrow):Void
{
x = arrow.x;
if (isSustainNote && prevNote != null)
{
if (prevNote.isSustainNote)
{
x = prevNote.x;
}
else
{
x += prevNote.width / 2;
x -= width / 2;
}
}
}
override function destroy()
{
prevNote = null;
super.destroy();
}
public function updateColors():Void
{
colorSwap.update(arrowColors[data.noteData]);
}
override function update(elapsed:Float)
{
super.update(elapsed);
// mustPress indicates the player is the one pressing the key
if (mustPress)
{
// miss on the NEXT frame so lag doesnt make u miss notes
if (willMiss && !wasGoodHit)
{
tooLate = true;
canBeHit = false;
}
else
{
if (!pastHalfWay && data.strumTime <= Conductor.songPosition)
{
pastHalfWay = true;
noteSpeedMulti *= 2;
}
if (data.strumTime > Conductor.songPosition - HIT_WINDOW)
{
// * 0.5 if sustain note, so u have to keep holding it closer to all the way thru!
if (data.strumTime < Conductor.songPosition + (HIT_WINDOW * (isSustainNote ? 0.5 : 1))) canBeHit = true;
}
else
{
canBeHit = true;
willMiss = true;
}
}
}
else
{
canBeHit = false;
if (data.strumTime <= Conductor.songPosition) wasGoodHit = true;
}
if (tooLate)
{
if (alpha > 0.3) alpha = 0.3;
}
}
static public function fromData(data:NoteData, prevNote:Note, isSustainNote = false)
{
var result = new Note(data.strumTime, data.noteData, prevNote, isSustainNote);
result.data = data;
return result;
}
}

View file

@ -2,6 +2,7 @@ package funkin;
import flixel.FlxSprite;
import haxe.io.Path;
import flixel.graphics.frames.FlxAtlasFrames;
class NoteSplash extends FlxSprite
{
@ -9,24 +10,44 @@ class NoteSplash extends FlxSprite
{
super(x, y);
frames = Paths.getSparrowAtlas('noteSplashes');
animation.addByPrefix('note0-0', 'note impact 1 purple', 24, false);
animation.addByPrefix('note1-0', 'note impact 1 blue', 24, false);
animation.addByPrefix('note2-0', 'note impact 1 green', 24, false);
animation.addByPrefix('note0-0', 'note impact 1 purple', 24, false);
animation.addByPrefix('note3-0', 'note impact 1 red', 24, false);
animation.addByPrefix('note0-1', 'note impact 2 purple', 24, false);
animation.addByPrefix('note1-1', 'note impact 2 blue', 24, false);
animation.addByPrefix('note2-1', 'note impact 2 green', 24, false);
animation.addByPrefix('note0-1', 'note impact 2 purple', 24, false);
animation.addByPrefix('note3-1', 'note impact 2 red', 24, false);
setupNoteSplash(x, y, noteData);
antialiasing = true;
// alpha = 0.75;
}
public override function update(elapsed:Float):Void
{
super.update(elapsed);
if (animation.finished)
{
kill();
}
}
public static function buildSplashFrames(force:Bool = false):FlxAtlasFrames
{
// static variables inside functions are a cool of Haxe 4.3.0.
static var splashFrames:FlxAtlasFrames = null;
if (splashFrames != null && !force) return splashFrames;
splashFrames = Paths.getSparrowAtlas('noteSplashes');
splashFrames.parent.persist = true;
return splashFrames;
}
public function setupNoteSplash(x:Float, y:Float, noteData:Int = 0)
{
setPosition(x, y);

View file

@ -2,6 +2,7 @@ package funkin;
import funkin.Controls;
import flixel.FlxCamera;
import funkin.input.PreciseInputManager;
import flixel.input.actions.FlxActionInput;
import flixel.input.gamepad.FlxGamepad;
import flixel.util.FlxSignal;
@ -25,8 +26,10 @@ class PlayerSettings
// public var avatar:Player;
// public var camera(get, never):PlayCamera;
function new(id)
function new(id:Int)
{
trace('loading player settings for id: $id');
this.id = id;
this.controls = new Controls('player$id', None);
@ -51,7 +54,14 @@ class PlayerSettings
}
}
if (useDefault) controls.setKeyboardScheme(Solo);
if (useDefault)
{
trace("falling back to default control scheme");
controls.setKeyboardScheme(Solo);
}
// Apply loaded settings.
PreciseInputManager.instance.initializeKeys(controls);
}
function addGamepad(gamepad:FlxGamepad)

View file

@ -1,33 +0,0 @@
package funkin;
import funkin.noteStuff.NoteBasic.NoteData;
typedef SwagSection =
{
var sectionNotes:Array<NoteData>;
var lengthInSteps:Int;
var typeOfSection:Int;
var mustHitSection:Bool;
var bpm:Float;
var changeBPM:Bool;
var altAnim:Bool;
}
class Section
{
public var sectionNotes:Array<Dynamic> = [];
public var lengthInSteps:Int = 16;
public var typeOfSection:Int = 0;
public var mustHitSection:Bool = true;
/**
* Copies the first section into the second section!
*/
public static var COPYCAT:Int = 0;
public function new(lengthInSteps:Int = 16)
{
this.lengthInSteps = lengthInSteps;
}
}

View file

@ -1,325 +0,0 @@
package funkin;
import funkin.Section.SwagSection;
import funkin.noteStuff.NoteBasic.NoteData;
import funkin.play.PlayState;
import haxe.Json;
import lime.utils.Assets;
typedef SwagSong =
{
var song:String;
var notes:FunnyNotes;
var difficulties:Array<String>;
var noteMap:Map<String, Array<SwagSection>>;
var bpm:Float;
var needsVoices:Bool;
var voiceList:Array<String>;
var speed:FunnySpeed;
var speedMap:Map<String, Float>;
var player1:String;
var player2:String;
var validScore:Bool;
var extraNotes:Map<String, Array<SwagSection>>;
}
typedef FunnySpeed =
{
var ?easy:Float;
var ?normal:Float;
var ?hard:Float;
}
typedef FunnyNotes =
{
var ?easy:Array<SwagSection>;
var ?normal:Array<SwagSection>;
var ?hard:Array<SwagSection>;
}
class SongLoad
{
public static var curDiff:String = 'normal';
public static var curNotes:Array<SwagSection>;
public static var songData:SwagSong;
public static function loadFromJson(jsonInput:String, ?folder:String):SwagSong
{
var rawJson:String = null;
try
{
rawJson = Assets.getText(Paths.json('songs/${folder.toLowerCase()}/${jsonInput.toLowerCase()}')).trim();
}
catch (e)
{
trace('Failed to load song data: ${e}');
rawJson = null;
}
if (rawJson == null)
{
return null;
}
while (!rawJson.endsWith("}"))
{
rawJson = rawJson.substr(0, rawJson.length - 1);
}
return parseJSONshit(rawJson);
}
public static function getSong(?diff:String):Array<SwagSection>
{
if (diff == null) diff = SongLoad.curDiff;
var songShit:Array<SwagSection> = [];
// THIS IS OVERWRITTEN, WILL BE DEPRECTATED AND REPLACED SOOOOON
if (songData != null)
{
switch (diff)
{
case 'easy':
songShit = songData.notes.easy;
case 'normal':
songShit = songData.notes.normal;
case 'hard':
songShit = songData.notes.hard;
}
}
checkAndCreateNotemap(curDiff);
songShit = songData.noteMap[diff];
return songShit;
}
public static function checkAndCreateNotemap(diff:String):Void
{
if (songData == null || songData.noteMap == null) return;
if (songData.noteMap[diff] == null) songData.noteMap[diff] = [];
}
public static function getSpeed(?diff:String):Float
{
if (PlayState.instance != null && PlayState.instance.currentChart != null)
{
return getSpeed_NEW(diff);
}
if (diff == null) diff = SongLoad.curDiff;
var speedShit:Float = 1;
// all this shit is overridden by the thing that loads it from speedMap Map object!!!
// replace and delete later!
switch (diff)
{
case 'easy':
speedShit = songData?.speed?.easy ?? 1.0;
case 'normal':
speedShit = songData?.speed?.normal ?? 1.0;
case 'hard':
speedShit = songData?.speed?.hard ?? 1.0;
}
if (songData?.speedMap == null || songData?.speedMap[diff] == null)
{
speedShit = 1;
}
else
{
speedShit = songData.speedMap[diff];
}
return speedShit;
}
public static function getSpeed_NEW(?diff:String):Float
{
if (PlayState.instance == null
|| PlayState.instance.currentChart == null
|| PlayState.instance.currentChart.scrollSpeed == 0.0) return 1.0;
return PlayState.instance.currentChart.scrollSpeed;
}
public static function getDefaultSwagSong():SwagSong
{
return {
song: 'Test',
notes: {easy: [], normal: [], hard: []},
difficulties: ["easy", "normal", "hard"],
noteMap: new Map(),
speedMap: new Map(),
bpm: 150,
needsVoices: true,
player1: 'bf',
player2: 'dad',
speed:
{
easy: 1,
normal: 1,
hard: 1
},
validScore: false,
voiceList: ["BF", "BF-pixel"],
extraNotes: []
};
}
public static function getDefaultNoteData():NoteData
{
return new NoteData();
}
/**
* Casts the an array to NOTE data (for LOADING shit from json usually)
*/
public static function castArrayToNoteData(noteStuff:Array<SwagSection>)
{
if (noteStuff == null) return;
for (sectionIndex => section in noteStuff)
{
if (section == null || section.sectionNotes == null) continue;
for (noteIndex => noteDataArray in section.sectionNotes)
{
var arrayDipshit:Array<Dynamic> = cast noteDataArray; // crackhead
if (arrayDipshit != null) // array isnt null, that means it loaded it as an array and needs to be manually parsed?
{
// at this point noteStuff[sectionIndex].sectionNotes[noteIndex] is an array because of the cast from the first line in this function
// so this line right here turns it back into the NoteData typedef type because of another bastard cast
noteStuff[sectionIndex].sectionNotes[noteIndex] = cast SongLoad.getDefaultNoteData(); // turn it from an array (because of the cast), back to noteData? yeah that works
noteStuff[sectionIndex].sectionNotes[noteIndex].strumTime = arrayDipshit[0];
noteStuff[sectionIndex].sectionNotes[noteIndex].noteData = arrayDipshit[1];
noteStuff[sectionIndex].sectionNotes[noteIndex].sustainLength = arrayDipshit[2];
if (arrayDipshit.length > 3)
{
noteStuff[sectionIndex].sectionNotes[noteIndex].noteKind = arrayDipshit[3];
}
}
else if (noteDataArray != null)
{
// array is NULL, so it checks if noteDataArray (doesnt exactly NEED to be an 'array' is also null or not.)
// At this point it should be an OBJECT that can be easily casted!!!
noteStuff[sectionIndex].sectionNotes[noteIndex] = cast noteDataArray;
}
else
throw "shit brokey"; // i actually dont know how throw works lol
}
}
}
/**
* Cast notedata to ARRAY (usually used for level SAVING)
*/
public static function castNoteDataToArray(noteStuff:Array<SwagSection>)
{
if (noteStuff == null) return;
for (sectionIndex => section in noteStuff)
{
for (noteIndex => noteTypeDefShit in section.sectionNotes)
{
var dipshitArray:Array<Dynamic> = [
noteTypeDefShit.strumTime,
noteTypeDefShit.noteData,
noteTypeDefShit.sustainLength,
noteTypeDefShit.noteKind
];
noteStuff[sectionIndex].sectionNotes[noteIndex] = cast dipshitArray;
}
}
}
public static function castNoteDataToNoteData(noteStuff:Array<SwagSection>)
{
if (noteStuff == null) return;
for (sectionIndex => section in noteStuff)
{
for (noteIndex => noteTypedefShit in section.sectionNotes)
{
trace(noteTypedefShit);
noteStuff[sectionIndex].sectionNotes[noteIndex] = noteTypedefShit;
}
}
}
public static function parseJSONshit(rawJson:String):SwagSong
{
var songParsed:Dynamic;
try
{
songParsed = Json.parse(rawJson);
}
catch (e)
{
FlxG.log.warn("Error parsing JSON: " + e.message);
trace("Error parsing JSON: " + e.message);
return null;
}
var swagShit:SwagSong = cast songParsed.song;
swagShit.difficulties = []; // reset it to default before load
swagShit.noteMap = new Map();
swagShit.speedMap = new Map();
for (diff in Reflect.fields(songParsed.song.notes))
{
swagShit.difficulties.push(diff);
swagShit.noteMap[diff] = cast Reflect.field(songParsed.song.notes, diff);
castArrayToNoteData(swagShit.noteMap[diff]);
// castNoteDataToNoteData(swagShit.noteMap[diff]);
/*
switch (diff)
{
case "easy":
castArrayToNoteData(swagShit.notes.hard);
case "normal":
castArrayToNoteData(swagShit.notes.normal);
case "hard":
castArrayToNoteData(swagShit.notes.hard);
}
*/
}
for (diff in swagShit.difficulties)
{
swagShit.speedMap[diff] = cast Reflect.field(songParsed.song.speed, diff);
}
// trace(swagShit.noteMap.toString());
// trace(swagShit.speedMap.toString());
// trace('that was just notemap string lol');
swagShit.validScore = true;
trace("SONG SHIT ABOUTTA WEEK AGOOO");
for (field in Reflect.fields(Json.parse(rawJson).song.speed))
{
// swagShit.speed[field] = Reflect.field(Json.parse(rawJson).song.speed, field);
// swagShit.notes[field] = Reflect.field(Json.parse(rawJson).song.notes, field);
// trace(swagShit.notes[field]);
}
// swagShit.notes = cast Json.parse(rawJson).song.notes[SongLoad.curDiff]; // by default uses
trace('THAT SHIT WAS JUST THE NORMAL NOTES!!!');
songData = swagShit;
// curNotes = songData.notes.get('normal');
return swagShit;
}
}

View file

@ -16,7 +16,6 @@ import funkin.play.song.SongData.SongDataParser;
import funkin.play.song.SongData.SongMetadata;
import funkin.shaderslmfao.TitleOutline;
import funkin.ui.AtlasText;
import funkin.util.Constants;
import openfl.Assets;
import openfl.display.Sprite;
import openfl.events.AsyncErrorEvent;
@ -46,6 +45,7 @@ class TitleState extends MusicBeatState
override public function create():Void
{
super.create();
swagShader = new ColorSwap();
curWacky = FlxG.random.getObject(getIntroTextShit());
@ -53,38 +53,6 @@ class TitleState extends MusicBeatState
// DEBUG BULLSHIT
super.create();
/*
#elseif web
if (!initialized)
{
video = new Video();
FlxG.stage.addChild(video);
var netConnection = new NetConnection();
netConnection.connect(null);
netStream = new NetStream(netConnection);
netStream.client = {onMetaData: client_onMetaData};
netStream.addEventListener(AsyncErrorEvent.ASYNC_ERROR, netStream_onAsyncError);
netConnection.addEventListener(NetStatusEvent.NET_STATUS, netConnection_onNetStatus);
// netStream.addEventListener(NetStatusEvent.NET_STATUS) // netStream.play(Paths.file('music/kickstarterTrailer.mp4'));
overlay = new Sprite();
overlay.graphics.beginFill(0, 0.5);
overlay.graphics.drawRect(0, 0, 1280, 720);
overlay.addEventListener(MouseEvent.MOUSE_DOWN, overlay_onMouseDown);
overlay.buttonMode = true;
// FlxG.stage.addChild(overlay);
}
*/
// netConnection.addEventListener(MouseEvent.MOUSE_DOWN, overlay_onMouseDown);
new FlxTimer().start(1, function(tmr:FlxTimer) {
startIntro();
@ -146,7 +114,6 @@ class TitleState extends MusicBeatState
logoBl = new FlxSprite(-150, -100);
logoBl.frames = Paths.getSparrowAtlas('logoBumpin');
logoBl.antialiasing = true;
logoBl.animation.addByPrefix('bump', 'logo bumpin', 24);
logoBl.animation.play('bump');
@ -158,7 +125,6 @@ class TitleState extends MusicBeatState
gfDance.frames = Paths.getSparrowAtlas('gfDanceTitle');
gfDance.animation.addByIndices('danceLeft', 'gfDance', [30, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "", 24, false);
gfDance.animation.addByIndices('danceRight', 'gfDance', [15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29], "", 24, false);
gfDance.antialiasing = true;
add(gfDance);
@ -177,7 +143,6 @@ class TitleState extends MusicBeatState
titleText.frames = Paths.getSparrowAtlas('titleEnter');
titleText.animation.addByPrefix('idle', "Press Enter to Begin", 24);
titleText.animation.addByPrefix('press', "ENTER PRESSED", 24);
titleText.antialiasing = true;
titleText.animation.play('idle');
titleText.updateHitbox();
// titleText.screenCenter(X);
@ -220,7 +185,6 @@ class TitleState extends MusicBeatState
ngSpr.updateHitbox();
ngSpr.screenCenter(X);
ngSpr.antialiasing = true;
FlxG.mouse.visible = false;

View file

@ -65,4 +65,11 @@ class VoicesGroup extends SoundGroup
opponentVoices.clear();
super.clear();
}
public override function destroy():Void
{
playerVoices.destroy();
opponentVoices.destroy();
super.destroy();
}
}

View file

@ -84,6 +84,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
}
catch (e:Dynamic)
{
// Print the error.
trace(' Failed to load entry data: ${entryId}');
trace(e);
continue;
@ -91,16 +92,29 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
}
}
/**
* Retrieve a list of all entry IDs in this registry.
* @return The list of entry IDs.
*/
public function listEntryIds():Array<String>
{
return entries.keys().array();
}
/**
* Count the number of entries in this registry.
* @return The number of entries.
*/
public function countEntries():Int
{
return entries.size();
}
/**
* Fetch an entry by its ID.
* @param id The ID of the entry to fetch.
* @return The entry, or `null` if it does not exist.
*/
public function fetchEntry(id:String):Null<T>
{
return entries.get(id);

View file

@ -1,14 +1,60 @@
package funkin.play;
package funkin.data.animation;
class AnimationDataUtil
{
public static function toNamed(data:UnnamedAnimationData, ?name:String = ""):AnimationData
{
return {
name: name,
prefix: data.prefix,
assetPath: data.assetPath,
offsets: data.offsets,
looped: data.looped,
flipX: data.flipX,
flipY: data.flipY,
frameRate: data.frameRate,
frameIndices: data.frameIndices
};
}
public static function toUnnamed(data:AnimationData):UnnamedAnimationData
{
return {
prefix: data.prefix,
assetPath: data.assetPath,
offsets: data.offsets,
looped: data.looped,
flipX: data.flipX,
flipY: data.flipY,
frameRate: data.frameRate,
frameIndices: data.frameIndices
};
}
}
/**
* A data structure representing an animation in a spritesheet.
* This is a generic data structure used by characters, stage props, and more!
* BE CAREFUL when changing it.
*/
typedef AnimationData =
{
> UnnamedAnimationData,
/**
* The name for the animation.
* This should match the animation name queried by the game;
* for example, characters need animations with names `idle`, `singDOWN`, `singUPmiss`, etc.
*/
var name:String;
}
/**
* A data structure representing an animation in a spritesheet.
* This animation doesn't specify a name, that's presumably specified by the parent data structure.
*/
typedef UnnamedAnimationData =
{
/**
* The prefix for the frames of the animation as defined by the XML file.
* This will may or may not differ from the `name` of the animation,

View file

@ -1,6 +1,6 @@
package funkin.data.level;
import funkin.play.AnimationData;
import funkin.data.animation.AnimationData;
/**
* A type definition for the data in a story mode level JSON file.

View file

@ -0,0 +1,171 @@
package funkin.data.notestyle;
import haxe.DynamicAccess;
import funkin.data.animation.AnimationData;
/**
* A type definition for the data in a note style JSON file.
* @see https://lib.haxe.org/p/json2object/
*/
typedef NoteStyleData =
{
/**
* The version number of the note style data schema.
* When making changes to the note style data format, this should be incremented,
* and a migration function should be added to NoteStyleDataParser to handle old versions.
*/
@:default(funkin.data.notestyle.NoteStyleRegistry.NOTE_STYLE_DATA_VERSION)
var version:String;
/**
* The readable title of the note style.
*/
var name:String;
/**
* The author of the note style.
*/
var author:String;
/**
* The note style to use as a fallback/parent.
* @default null
*/
@:optional
var fallback:Null<String>;
/**
* Data for each of the assets in the note style.
*/
var assets:NoteStyleAssetsData;
}
typedef NoteStyleAssetsData =
{
/**
* The sprites for the notes.
* @default The sprites from the fallback note style.
*/
@:optional
var note:NoteStyleAssetData<NoteStyleData_Note>;
/**
* The sprites for the hold notes.
* @default The sprites from the fallback note style.
*/
@:optional
var holdNote:NoteStyleAssetData<NoteStyleData_HoldNote>;
/**
* The sprites for the strumline.
* @default The sprites from the fallback note style.
*/
@:optional
var noteStrumline:NoteStyleAssetData<NoteStyleData_NoteStrumline>;
/**
* The sprites for the note splashes.
*/
@:optional
var noteSplash:NoteStyleAssetData<NoteStyleData_NoteSplash>;
/**
* The sprites for the hold note covers.
*/
@:optional
var holdNoteCover:NoteStyleAssetData<NoteStyleData_HoldNoteCover>;
}
/**
* Data shared by all note style assets.
*/
typedef NoteStyleAssetData<T> =
{
/**
* The image to use for the asset. May be a Sparrow sprite sheet.
*/
var assetPath:String;
/**
* The scale to render the prop at.
* @default 1.0
*/
@:default(1.0)
@:optional
var scale:Float;
/**
* Offset the sprite's position by this amount.
* @default [0, 0]
*/
@:default([0, 0])
@:optional
var offsets:Null<Array<Float>>;
/**
* If true, the prop is a pixel sprite, and will be rendered without anti-aliasing.
*/
@:default(false)
@:optional
var isPixel:Bool;
/**
* The structure of this data depends on the asset.
*/
var data:T;
}
typedef NoteStyleData_Note =
{
var left:UnnamedAnimationData;
var down:UnnamedAnimationData;
var up:UnnamedAnimationData;
var right:UnnamedAnimationData;
}
typedef NoteStyleData_HoldNote = {}
/**
* Data on animations for each direction of the strumline.
*/
typedef NoteStyleData_NoteStrumline =
{
var leftStatic:UnnamedAnimationData;
var leftPress:UnnamedAnimationData;
var leftConfirm:UnnamedAnimationData;
var leftConfirmHold:UnnamedAnimationData;
var downStatic:UnnamedAnimationData;
var downPress:UnnamedAnimationData;
var downConfirm:UnnamedAnimationData;
var downConfirmHold:UnnamedAnimationData;
var upStatic:UnnamedAnimationData;
var upPress:UnnamedAnimationData;
var upConfirm:UnnamedAnimationData;
var upConfirmHold:UnnamedAnimationData;
var rightStatic:UnnamedAnimationData;
var rightPress:UnnamedAnimationData;
var rightConfirm:UnnamedAnimationData;
var rightConfirmHold:UnnamedAnimationData;
}
typedef NoteStyleData_NoteSplash =
{
/**
* If false, note splashes are entirely hidden on this note style.
* @default Note splashes are enabled.
*/
@:optional
@:default(true)
var enabled:Bool;
};
typedef NoteStyleData_HoldNoteCover =
{
/**
* If false, hold note covers are entirely hidden on this note style.
* @default Hold note covers are enabled.
*/
@:optional
@:default(true)
var enabled:Bool;
};

View file

@ -0,0 +1,65 @@
package funkin.data.notestyle;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.notes.notestyle.ScriptedNoteStyle;
import funkin.data.notestyle.NoteStyleData;
class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
{
/**
* The current version string for the note style data format.
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateNoteStyleData()` function.
*/
public static final NOTE_STYLE_DATA_VERSION:String = "1.0.0";
public static final DEFAULT_NOTE_STYLE_ID:String = "funkin";
public static final instance:NoteStyleRegistry = new NoteStyleRegistry();
public function new()
{
super('NOTESTYLE', 'notestyles');
}
public function fetchDefault():NoteStyle
{
return fetchEntry(DEFAULT_NOTE_STYLE_ID);
}
/**
* Read, parse, and validate the JSON data and produce the corresponding data object.
*/
public function parseEntryData(id:String):Null<NoteStyleData>
{
if (id == null) id = DEFAULT_NOTE_STYLE_ID;
// JsonParser does not take type parameters,
// otherwise this function would be in BaseRegistry.
var parser = new json2object.JsonParser<NoteStyleData>();
var jsonStr:String = loadEntryFile(id);
parser.fromJson(jsonStr);
if (parser.errors.length > 0)
{
trace('Failed to parse entry data: ${id}');
for (error in parser.errors)
{
trace(error);
}
return null;
}
return parser.value;
}
function createScriptedEntry(clsName:String):NoteStyle
{
return ScriptedNoteStyle.init(clsName, "unknown");
}
function getScriptedClassNames():Array<String>
{
return ScriptedNoteStyle.listScriptClasses();
}
}

View file

@ -117,7 +117,6 @@ class ScoreNum extends FlxSprite
this.digit = initDigit;
animation.play(numToString[digit], true);
antialiasing = true;
setGraphicSize(Std.int(width * 0.4));
updateHitbox();

View file

@ -47,7 +47,6 @@ class SongMenuItem extends FlxSpriteGroup
favIcon.frames = Paths.getSparrowAtlas('freeplay/favHeart');
favIcon.animation.addByPrefix('fav', "favorite heart", 24, false);
favIcon.animation.play('fav');
favIcon.antialiasing = true;
favIcon.setGraphicSize(60, 60);
add(favIcon);

View file

@ -42,8 +42,6 @@ class FlxAtlasSprite extends FlxAnimate
throw 'FlxAtlasSprite not initialized properly. Are you sure the path (${path}) exists?';
}
this.antialiasing = true;
onAnimationFinish.add(cleanupAnimation);
// This defaults the sprite to play the first animation in the atlas,

View file

@ -1,233 +0,0 @@
package funkin.graphics.rendering;
import flixel.FlxSprite;
import flixel.graphics.FlxGraphic;
import flixel.graphics.tile.FlxDrawTrianglesItem;
import flixel.math.FlxMath;
/**
* This is based heavily on the `FlxStrip` class. It uses `drawTriangles()` to clip a sustain note
* trail at a certain time.
* The whole `FlxGraphic` is used as a texture map. See the `NOTE_hold_assets.fla` file for specifics
* on how it should be constructed.
*
* @author MtH
*/
class SustainTrail extends FlxSprite
{
/**
* Used to determine which note color/direction to draw for the sustain.
*/
public var noteData:Int = 0;
/**
* The zoom level to render the sustain at.
* Defaults to 1.0, increased to 6.0 for pixel notes.
*/
public var zoom(default, set):Float = 1;
/**
* The strumtime of the note, in milliseconds.
*/
public var strumTime:Float = 0; // millis
/**
* The sustain length of the note, in milliseconds.
*/
public var sustainLength(default, set):Float = 0; // millis
/**
* The scroll speed of the note, as a multiplier.
*/
public var scrollSpeed(default, set):Float = 1.0; // stand-in for PlayState scroll speed
/**
* Whether the note was missed.
*/
public var missed:Bool = false; // maybe BlendMode.MULTIPLY if missed somehow, drawTriangles does not support!
/**
* A `Vector` of floats where each pair of numbers is treated as a coordinate location (an x, y pair).
*/
var vertices:DrawData<Float> = new DrawData<Float>();
/**
* A `Vector` of integers or indexes, where every three indexes define a triangle.
*/
var indices:DrawData<Int> = new DrawData<Int>();
/**
* A `Vector` of normalized coordinates used to apply texture mapping.
*/
var uvtData:DrawData<Float> = new DrawData<Float>();
var processedGraphic:FlxGraphic;
/**
* What part of the trail's end actually represents the end of the note.
* This can be used to have a little bit sticking out.
*/
public var endOffset:Float = 0.5; // 0.73 is roughly the bottom of the sprite in the normal graphic!
/**
* At what point the bottom for the trail's end should be clipped off.
* Used in cases where there's an extra bit of the graphic on the bottom to avoid antialiasing issues with overflow.
*/
public var bottomClip:Float = 0.9;
/**
* Normally you would take strumTime:Float, noteData:Int, sustainLength:Float, parentNote:Note (?)
* @param NoteData
* @param SustainLength
* @param FileName
*/
public function new(NoteData:Int, SustainLength:Float, Path:String, ?Alpha:Float = 0.6, ?Pixel:Bool = false)
{
super(0, 0, Path);
// BASIC SETUP
this.sustainLength = SustainLength;
this.noteData = NoteData;
// CALCULATE SIZE
if (Pixel)
{
this.endOffset = bottomClip = 1;
this.antialiasing = false;
this.zoom = 6.0;
}
else
{
this.antialiasing = true;
this.zoom = 1.0;
}
// width = graphic.width / 8 * zoom; // amount of notes * 2
height = sustainHeight(sustainLength, scrollSpeed);
// instead of scrollSpeed, PlayState.SONG.speed
alpha = Alpha; // setting alpha calls updateColorTransform(), which initializes processedGraphic!
updateClipping();
indices = new DrawData<Int>(12, true, [0, 1, 2, 2, 3, 0, 4, 5, 6, 6, 7, 4]);
}
/**
* Calculates height of a sustain note for a given length (milliseconds) and scroll speed.
* @param susLength The length of the sustain note in milliseconds.
* @param scroll The current scroll speed.
*/
public static inline function sustainHeight(susLength:Float, scroll:Float)
{
return (susLength * 0.45 * scroll);
}
function set_zoom(z:Float)
{
this.zoom = z;
width = graphic.width / 8 * z;
updateClipping();
return this.zoom;
}
function set_sustainLength(s:Float)
{
height = sustainHeight(s, scrollSpeed);
return sustainLength = s;
}
function set_scrollSpeed(s:Float)
{
height = sustainHeight(sustainLength, s);
return scrollSpeed = s;
}
/**
* Sets up new vertex and UV data to clip the trail.
* If flipY is true, top and bottom bounds swap places.
* @param songTime The time to clip the note at, in milliseconds.
*/
public function updateClipping(songTime:Float = 0):Void
{
var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), scrollSpeed), 0, height);
if (clipHeight == 0)
{
visible = false;
return;
}
else
visible = true;
var bottomHeight:Float = graphic.height * zoom * endOffset;
var partHeight:Float = clipHeight - bottomHeight;
// == HOLD == //
// left bound
vertices[6] = vertices[0] = 0.0;
// top bound
vertices[3] = vertices[1] = flipY ? clipHeight : height - clipHeight;
// right bound
vertices[4] = vertices[2] = width;
// bottom bound (also top bound for hold ends)
if (partHeight > 0) vertices[7] = vertices[5] = flipY ? 0.0 + bottomHeight : vertices[1] + partHeight;
else
vertices[7] = vertices[5] = vertices[1];
// same shit with da bounds, just in relation to the texture
uvtData[6] = uvtData[0] = 1 / 4 * (noteData % 4);
// height overflows past image bounds so wraps around, looping the texture
// flipY bounds are not swapped for UV data, so the graphic is actually flipped
// top bound
uvtData[3] = uvtData[1] = (-partHeight) / graphic.height / zoom;
uvtData[4] = uvtData[2] = uvtData[0] + 1 / 8; // 1
// bottom bound
uvtData[7] = uvtData[5] = 0.0;
// == HOLD ENDS == //
// left bound
vertices[14] = vertices[8] = vertices[0];
// top bound
vertices[11] = vertices[9] = vertices[5];
// right bound
vertices[12] = vertices[10] = vertices[2];
// bottom bound, mind the bottomClip because it clips off bottom of graphic!!
vertices[15] = vertices[13] = flipY ? graphic.height * (-bottomClip + endOffset) : height + graphic.height * (bottomClip - endOffset);
uvtData[14] = uvtData[8] = uvtData[2];
if (partHeight > 0) uvtData[11] = uvtData[9] = 0.0;
else
uvtData[11] = uvtData[9] = (bottomHeight - clipHeight) / zoom / graphic.height;
uvtData[12] = uvtData[10] = uvtData[8] + 1 / 8;
// again, clips off bottom !!
uvtData[15] = uvtData[13] = bottomClip;
}
@:access(flixel.FlxCamera)
override public function draw():Void
{
if (alpha == 0 || graphic == null || vertices == null) return;
for (camera in cameras)
{
if (!camera.visible || !camera.exists || !isOnScreen(camera)) continue;
getScreenPosition(_point, camera).subtractPoint(offset);
camera.drawTriangles(processedGraphic, vertices, indices, uvtData, null, _point, blend, true, antialiasing);
}
}
override public function destroy():Void
{
vertices = null;
indices = null;
uvtData = null;
processedGraphic.destroy();
super.destroy();
}
override function updateColorTransform():Void
{
super.updateColorTransform();
if (processedGraphic != null) processedGraphic.destroy();
processedGraphic = FlxGraphic.fromGraphic(graphic, true);
processedGraphic.bitmap.colorTransform(processedGraphic.bitmap.rect, colorTransform);
}
}

View file

@ -2,6 +2,7 @@ package;
#if !macro
// Only import these when we aren't in a macro.
import funkin.util.Constants;
import funkin.Paths;
import flixel.FlxG; // This one in particular causes a compile error if you're using macros.
@ -9,6 +10,7 @@ import flixel.FlxG; // This one in particular causes a compile error if you're u
using Lambda;
using StringTools;
using funkin.util.tools.ArrayTools;
using funkin.util.tools.ArraySortTools;
using funkin.util.tools.IteratorTools;
using funkin.util.tools.MapTools;
using funkin.util.tools.StringTools;

View file

@ -0,0 +1,303 @@
package funkin.input;
import openfl.ui.Keyboard;
import funkin.play.notes.NoteDirection;
import flixel.input.keyboard.FlxKeyboard.FlxKeyInput;
import openfl.events.KeyboardEvent;
import flixel.FlxG;
import flixel.input.FlxInput.FlxInputState;
import flixel.input.FlxKeyManager;
import flixel.input.keyboard.FlxKey;
import flixel.input.keyboard.FlxKeyList;
import flixel.util.FlxSignal.FlxTypedSignal;
import haxe.Int64;
import lime.ui.KeyCode;
import lime.ui.KeyModifier;
/**
* A precise input manager that:
* - Records the exact timestamp of when a key was pressed or released
* - Only records key presses for keys bound to game inputs (up/down/left/right)
*/
class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
{
public static var instance(get, null):PreciseInputManager;
static function get_instance():PreciseInputManager
{
return instance ?? (instance = new PreciseInputManager());
}
static final MS_TO_US:Int64 = 1000;
static final US_TO_NS:Int64 = 1000;
static final MS_TO_NS:Int64 = MS_TO_US * US_TO_NS;
static final DIRECTIONS:Array<NoteDirection> = [NoteDirection.LEFT, NoteDirection.DOWN, NoteDirection.UP, NoteDirection.RIGHT];
public var onInputPressed:FlxTypedSignal<PreciseInputEvent->Void>;
public var onInputReleased:FlxTypedSignal<PreciseInputEvent->Void>;
/**
* The list of keys that are bound to game inputs (up/down/left/right).
*/
var _keyList:Array<FlxKey>;
/**
* The direction that a given key is bound to.
*/
var _keyListDir:Map<FlxKey, NoteDirection>;
/**
* The timestamp at which a given note direction was last pressed.
*/
var _dirPressTimestamps:Map<NoteDirection, Int64>;
/**
* The timestamp at which a given note direction was last released.
*/
var _dirReleaseTimestamps:Map<NoteDirection, Int64>;
public function new()
{
super(PreciseInputList.new);
_keyList = [];
_dirPressTimestamps = new Map<NoteDirection, Int64>();
_dirReleaseTimestamps = new Map<NoteDirection, Int64>();
_keyListDir = new Map<FlxKey, NoteDirection>();
FlxG.stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
FlxG.stage.removeEventListener(KeyboardEvent.KEY_UP, onKeyUp);
FlxG.stage.application.window.onKeyDownPrecise.add(handleKeyDown);
FlxG.stage.application.window.onKeyUpPrecise.add(handleKeyUp);
preventDefaultKeys = getPreventDefaultKeys();
onInputPressed = new FlxTypedSignal<PreciseInputEvent->Void>();
onInputReleased = new FlxTypedSignal<PreciseInputEvent->Void>();
}
public static function getKeysForDirection(controls:Controls, noteDirection:NoteDirection)
{
return switch (noteDirection)
{
case NoteDirection.LEFT: controls.getKeysForAction(NOTE_LEFT);
case NoteDirection.DOWN: controls.getKeysForAction(NOTE_DOWN);
case NoteDirection.UP: controls.getKeysForAction(NOTE_UP);
case NoteDirection.RIGHT: controls.getKeysForAction(NOTE_RIGHT);
};
}
/**
* Returns a precise timestamp, measured in nanoseconds.
* Timestamp is only useful for comparing against other timestamps.
*
* @return Int64
*/
@:access(lime._internal.backend.native.NativeCFFI)
public static function getCurrentTimestamp():Int64
{
#if html5
// NOTE: This timestamp isn't that precise on standard HTML5 builds.
// This is because of browser safeguards against timing attacks.
// See https://web.dev/coop-coep to enable headers which allow for more precise timestamps.
return js.Browser.window.performance.now() * MS_TO_NS;
#elseif cpp
// NOTE: If the game hard crashes on this line, rebuild Lime!
// `lime rebuild windows -clean`
return lime._internal.backend.native.NativeCFFI.lime_sdl_get_ticks() * MS_TO_NS;
#else
throw "Eric didn't implement precise timestamps on this platform!";
#end
}
static function getPreventDefaultKeys():Array<FlxKey>
{
return [];
}
/**
* Call this whenever the user's inputs change.
*/
public function initializeKeys(controls:Controls):Void
{
clearKeys();
for (noteDirection in DIRECTIONS)
{
var keys = getKeysForDirection(controls, noteDirection);
for (key in keys)
{
var input = new FlxKeyInput(key);
_keyList.push(key);
_keyListArray.push(input);
_keyListMap.set(key, input);
_keyListDir.set(key, noteDirection);
}
}
}
/**
* Get the time, in nanoseconds, since the given note direction was last pressed.
* @param noteDirection The note direction to check.
* @return An Int64 representing the time since the given note direction was last pressed.
*/
public function getTimeSincePressed(noteDirection:NoteDirection):Int64
{
return getCurrentTimestamp() - _dirPressTimestamps.get(noteDirection);
}
/**
* Get the time, in nanoseconds, since the given note direction was last released.
* @param noteDirection The note direction to check.
* @return An Int64 representing the time since the given note direction was last released.
*/
public function getTimeSinceReleased(noteDirection:NoteDirection):Int64
{
return getCurrentTimestamp() - _dirReleaseTimestamps.get(noteDirection);
}
// TODO: Why doesn't this work?
// @:allow(funkin.input.PreciseInputManager.PreciseInputList)
public function getInputByKey(key:FlxKey):FlxKeyInput
{
return _keyListMap.get(key);
}
public function getDirectionForKey(key:FlxKey):NoteDirection
{
return _keyListDir.get(key);
}
function handleKeyDown(keyCode:KeyCode, _:KeyModifier, timestamp:Int64):Void
{
var key:FlxKey = convertKeyCode(keyCode);
if (_keyList.indexOf(key) == -1) return;
// TODO: Remove this line with SDL3 when timestamps change meaning.
// This is because SDL3's timestamps are measured in nanoseconds, not milliseconds.
timestamp *= MS_TO_NS;
updateKeyStates(key, true);
if (getInputByKey(key) ?.justPressed ?? false)
{
onInputPressed.dispatch(
{
noteDirection: getDirectionForKey(key),
timestamp: timestamp
});
_dirPressTimestamps.set(getDirectionForKey(key), timestamp);
}
}
function handleKeyUp(keyCode:KeyCode, _:KeyModifier, timestamp:Int64):Void
{
var key:FlxKey = convertKeyCode(keyCode);
if (_keyList.indexOf(key) == -1) return;
// TODO: Remove this line with SDL3 when timestamps change meaning.
// This is because SDL3's timestamps are measured in nanoseconds, not milliseconds.
timestamp *= MS_TO_NS;
updateKeyStates(key, false);
if (getInputByKey(key) ?.justReleased ?? false)
{
onInputReleased.dispatch(
{
noteDirection: getDirectionForKey(key),
timestamp: timestamp
});
_dirReleaseTimestamps.set(getDirectionForKey(key), timestamp);
}
}
static function convertKeyCode(input:KeyCode):FlxKey
{
@:privateAccess
{
return Keyboard.__convertKeyCode(input);
}
}
function clearKeys():Void
{
_keyListArray = [];
_keyListMap.clear();
_keyListDir.clear();
}
}
class PreciseInputList extends FlxKeyList
{
var _preciseInputManager:PreciseInputManager;
public function new(state:FlxInputState, preciseInputManager:FlxKeyManager<Dynamic, Dynamic>)
{
super(state, preciseInputManager);
_preciseInputManager = cast preciseInputManager;
}
static function getKeysForDir(noteDir:NoteDirection):Array<FlxKey>
{
return PreciseInputManager.getKeysForDirection(PlayerSettings.player1.controls, noteDir);
}
function isKeyValid(key:FlxKey):Bool
{
@:privateAccess
{
return _preciseInputManager._keyListMap.exists(key);
}
}
public function checkFlxKey(key:FlxKey):Bool
{
if (isKeyValid(key)) return check(cast key);
return false;
}
public function checkDir(noteDir:NoteDirection):Bool
{
for (key in getKeysForDir(noteDir))
{
if (check(_preciseInputManager.getInputByKey(key) ?.ID)) return true;
}
return false;
}
public var NOTE_LEFT(get, never):Bool;
function get_NOTE_LEFT():Bool
return checkDir(NoteDirection.LEFT);
public var NOTE_DOWN(get, never):Bool;
function get_NOTE_DOWN():Bool
return checkDir(NoteDirection.DOWN);
public var NOTE_UP(get, never):Bool;
function get_NOTE_UP():Bool
return checkDir(NoteDirection.UP);
public var NOTE_RIGHT(get, never):Bool;
function get_NOTE_RIGHT():Bool
return checkDir(NoteDirection.RIGHT);
}
typedef PreciseInputEvent =
{
/**
* The direction of the input.
*/
noteDirection:NoteDirection,
/**
* The timestamp of the input. Measured in nanoseconds.
*/
timestamp:Int64,
};

View file

@ -10,6 +10,11 @@ import polymod.backends.PolymodAssets.PolymodAssetType;
import polymod.format.ParseRules.TextFileFormat;
import funkin.play.event.SongEventData.SongEventParser;
import funkin.util.FileUtil;
import funkin.data.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.cutscene.dialogue.ConversationDataParser;
import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
import funkin.play.cutscene.dialogue.SpeakerDataParser;
class PolymodHandler
{
@ -279,12 +284,14 @@ class PolymodHandler
// TODO: Reload event callbacks
funkin.data.level.LevelRegistry.instance.loadEntries();
// These MUST be imported at the top of the file and not referred to by fully qualified name,
// to ensure build macros work properly.
LevelRegistry.instance.loadEntries();
NoteStyleRegistry.instance.loadEntries();
SongEventParser.loadEventCache();
// TODO: Uncomment this once conversation data is implemented.
// ConversationDataParser.loadConversationCache();
// DialogueBoxDataParser.loadDialogueBoxCache();
// SpeakerDataParser.loadSpeakerCache();
ConversationDataParser.loadConversationCache();
DialogueBoxDataParser.loadDialogueBoxCache();
SpeakerDataParser.loadSpeakerCache();
SongDataParser.loadSongCache();
StageDataParser.loadStageCache();
CharacterDataParser.loadCharacterCache();

View file

@ -1,10 +1,12 @@
package funkin.modding.events;
import funkin.play.song.SongData.SongNoteData;
import flixel.FlxState;
import flixel.FlxSubState;
import funkin.noteStuff.NoteBasic.NoteDir;
import funkin.play.notes.NoteSprite;
import funkin.play.cutscene.dialogue.Conversation;
import funkin.play.Countdown.CountdownStep;
import funkin.play.notes.NoteDirection;
import openfl.events.EventType;
import openfl.events.KeyboardEvent;
@ -344,7 +346,7 @@ class NoteScriptEvent extends ScriptEvent
* The note associated with this event.
* You cannot replace it, but you can edit it.
*/
public var note(default, null):Note;
public var note(default, null):NoteSprite;
/**
* The combo count as it is with this event.
@ -357,7 +359,7 @@ class NoteScriptEvent extends ScriptEvent
*/
public var playSound(default, default):Bool;
public function new(type:ScriptEventType, note:Note, comboCount:Int = 0, cancelable:Bool = false):Void
public function new(type:ScriptEventType, note:NoteSprite, comboCount:Int = 0, cancelable:Bool = false):Void
{
super(type, cancelable);
this.note = note;
@ -379,7 +381,7 @@ class GhostMissNoteScriptEvent extends ScriptEvent
/**
* The direction that was mistakenly pressed.
*/
public var dir(default, null):NoteDir;
public var dir(default, null):NoteDirection;
/**
* Whether there was a note within judgement range when this ghost note was pressed.
@ -407,7 +409,7 @@ class GhostMissNoteScriptEvent extends ScriptEvent
*/
public var playAnim(default, default):Bool;
public function new(dir:NoteDir, hasPossibleNotes:Bool, healthChange:Float, scoreChange:Int):Void
public function new(dir:NoteDirection, hasPossibleNotes:Bool, healthChange:Float, scoreChange:Int):Void
{
super(ScriptEvent.NOTE_GHOST_MISS, true);
this.dir = dir;
@ -575,19 +577,19 @@ class SongLoadScriptEvent extends ScriptEvent
* The note associated with this event.
* You cannot replace it, but you can edit it.
*/
public var notes(default, set):Array<Note>;
public var notes(default, set):Array<SongNoteData>;
public var id(default, null):String;
public var difficulty(default, null):String;
function set_notes(notes:Array<Note>):Array<Note>
function set_notes(notes:Array<SongNoteData>):Array<SongNoteData>
{
this.notes = notes;
return this.notes;
}
public function new(id:String, difficulty:String, notes:Array<Note>):Void
public function new(id:String, difficulty:String, notes:Array<SongNoteData>):Void
{
super(ScriptEvent.SONG_LOADED, false);
this.id = id;

View file

@ -1,197 +0,0 @@
package funkin.noteStuff;
import flixel.FlxSprite;
import flixel.text.FlxText;
typedef RawNoteData =
{
var strumTime:Float;
var noteData:NoteType;
var sustainLength:Float;
var altNote:String;
var noteKind:NoteKind;
}
@:forward
abstract NoteData(RawNoteData)
{
public function new(strumTime = 0.0, noteData:NoteType = 0, sustainLength = 0.0, altNote = "", noteKind = NORMAL)
{
this =
{
strumTime: strumTime,
noteData: noteData,
sustainLength: sustainLength,
altNote: altNote,
noteKind: noteKind
}
}
public var note(get, never):NoteType;
inline function get_note()
return this.noteData.value;
public var int(get, never):Int;
inline function get_int()
return this.noteData.int;
public var dir(get, never):NoteDir;
inline function get_dir()
return this.noteData.value;
public var dirName(get, never):String;
inline function get_dirName()
return dir.name;
public var dirNameUpper(get, never):String;
inline function get_dirNameUpper()
return dir.nameUpper;
public var color(get, never):NoteColor;
inline function get_color()
return this.noteData.value;
public var colorName(get, never):String;
inline function get_colorName()
return color.name;
public var colorNameUpper(get, never):String;
inline function get_colorNameUpper()
return color.nameUpper;
public var highStakes(get, never):Bool;
inline function get_highStakes()
return this.noteData.highStakes;
public var lowStakes(get, never):Bool;
inline function get_lowStakes()
return this.noteData.lowStakes;
}
enum abstract NoteType(Int) from Int to Int
{
// public var raw(get, never):Int;
// inline function get_raw() return this;
public var int(get, never):Int;
inline function get_int()
return this < 0 ? -this : this % 4;
public var value(get, never):NoteType;
inline function get_value()
return int;
public var highStakes(get, never):Bool;
inline function get_highStakes()
return this > 3;
public var lowStakes(get, never):Bool;
inline function get_lowStakes()
return this < 0;
}
@:forward
enum abstract NoteDir(NoteType) from Int to Int from NoteType
{
var LEFT = 0;
var DOWN = 1;
var UP = 2;
var RIGHT = 3;
var value(get, never):NoteDir;
inline function get_value()
return this.value;
public var name(get, never):String;
function get_name()
{
return switch (value)
{
case LEFT: "left";
case DOWN: "down";
case UP: "up";
case RIGHT: "right";
}
}
public var nameUpper(get, never):String;
function get_nameUpper()
{
return switch (value)
{
case LEFT: "LEFT";
case DOWN: "DOWN";
case UP: "UP";
case RIGHT: "RIGHT";
}
}
}
@:forward
enum abstract NoteColor(NoteType) from Int to Int from NoteType
{
var PURPLE = 0;
var BLUE = 1;
var GREEN = 2;
var RED = 3;
var value(get, never):NoteColor;
inline function get_value()
return this.value;
public var name(get, never):String;
function get_name()
{
return switch (value)
{
case PURPLE: "purple";
case BLUE: "blue";
case GREEN: "green";
case RED: "red";
}
}
public var nameUpper(get, never):String;
function get_nameUpper()
{
return switch (value)
{
case PURPLE: "PURPLE";
case BLUE: "BLUE";
case GREEN: "GREEN";
case RED: "RED";
}
}
}
enum abstract NoteKind(String) from String to String
{
/**
* The default note type.
*/
var NORMAL = "normal";
// Testing shiz
var PYRO_LIGHT = "pyro_light";
var PYRO_KICK = "pyro_kick";
var PYRO_TOSS = "pyro_toss";
var PYRO_COCK = "pyro_cock"; // lol
var PYRO_SHOOT = "pyro_shoot";
}

View file

@ -1,12 +0,0 @@
package funkin.noteStuff;
import funkin.noteStuff.NoteBasic.NoteType;
import funkin.play.Strumline.StrumlineStyle;
class NoteEvent extends Note
{
public function new(strumTime:Float = 0, noteData:NoteType, ?prevNote:Note, ?sustainNote:Bool = false, ?style:StrumlineStyle = NORMAL)
{
super(strumTime, noteData, prevNote, sustainNote, style);
}
}

View file

@ -1,98 +0,0 @@
package funkin.noteStuff;
import haxe.Json;
import openfl.Assets;
/**
* Just various functions that IDK where to put em!!!
* Semi-temp for now? the note stuff is super clutter-y right now
* so I am putting this new stuff here right now XDD
*
* A lot of this stuff can probably be moved to where appropriate!
* i dont care about NoteUtil.hx at all!!!
*/
class NoteUtil
{
/**
* IDK THING FOR BOTH LOL! DIS SHIT HACK-Y
* @param jsonPath
* @return Map<Int, Array<SongEventInfo>>
*/
public static function loadSongEvents(jsonPath:String):Map<Int, Array<SongEventInfo>>
{
return parseSongEvents(loadSongEventFromJson(jsonPath));
}
public static function loadSongEventFromJson(jsonPath:String):Array<SongEvent>
{
var daEvents:Array<SongEvent>;
daEvents = cast Json.parse(Assets.getText(jsonPath)).events; // DUMB LIL DETAIL HERE: MAKE SURE THAT .events IS THERE??
trace('GET JSON SONG EVENTS:');
trace(daEvents);
return daEvents;
}
/**
* Parses song event json stuff into a neater lil map grouping?
* @param songEvents
*/
public static function parseSongEvents(songEvents:Array<SongEvent>):Map<Int, Array<SongEventInfo>>
{
var songData:Map<Int, Array<SongEventInfo>> = new Map();
for (songEvent in songEvents)
{
trace(songEvent);
if (songData[songEvent.t] == null) songData[songEvent.t] = [];
songData[songEvent.t].push({songEventType: songEvent.e, value: songEvent.v, activated: false});
}
trace("FINISH SONG EVENTS!");
trace(songData);
return songData;
}
public static function checkSongEvents(songData:Map<Int, Array<SongEventInfo>>, time:Float)
{
for (eventGrp in songData.keys())
{
if (time >= eventGrp)
{
for (events in songData[eventGrp])
{
if (!events.activated)
{
// TURN TO NICER SWITCH STATEMENT CHECKER OF EVENT TYPES!!
trace(events.value);
trace(eventGrp);
trace(Conductor.songPosition);
events.activated = true;
}
}
}
}
}
}
typedef SongEventInfo =
{
var songEventType:SongEventType;
var value:Dynamic;
var activated:Bool;
}
typedef SongEvent =
{
var t:Int;
var e:SongEventType;
var v:Dynamic;
}
enum abstract SongEventType(String)
{
var FocusCamera;
var PlayCharAnim;
var Trace;
}

View file

@ -1,6 +1,5 @@
package funkin.play;
import funkin.util.Constants;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.FlxSprite;

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,8 @@
package funkin.play;
import funkin.util.Constants;
/**
* Manages playback of multiple songs in a row.
*
*
* TODO: Add getters/setters for all these properties to validate them.
*/
class PlayStatePlaylist

View file

@ -114,7 +114,6 @@ class ResultState extends MusicBeatSubState
soundSystem.animation.play("idle");
soundSystem.visible = true;
});
soundSystem.antialiasing = true;
add(soundSystem);
difficulty = new FlxSprite(555);
@ -132,7 +131,6 @@ class ResultState extends MusicBeatSubState
}
difficulty.loadGraphic(Paths.image("resultScreen/" + diffSpr));
difficulty.antialiasing = true;
add(difficulty);
var fontLetters:String = "AaBbCcDdEeFfGgHhiIJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:1234567890";
@ -148,7 +146,6 @@ class ResultState extends MusicBeatSubState
songName.text += PlayState.instance.currentSong.songId;
}
songName.antialiasing = true;
songName.letterSpacing = -15;
songName.angle = -4.1;
add(songName);
@ -164,22 +161,18 @@ class ResultState extends MusicBeatSubState
var blackTopBar:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/topBarBlack"));
blackTopBar.y = -blackTopBar.height;
FlxTween.tween(blackTopBar, {y: 0}, 0.4, {ease: FlxEase.quartOut, startDelay: 0.5});
blackTopBar.antialiasing = true;
add(blackTopBar);
var resultsAnim:FlxSprite = new FlxSprite(-200, -10);
resultsAnim.frames = Paths.getSparrowAtlas("resultScreen/results");
resultsAnim.animation.addByPrefix("result", "results", 24, false);
resultsAnim.animation.play("result");
resultsAnim.antialiasing = true;
add(resultsAnim);
var ratingsPopin:FlxSprite = new FlxSprite(-150, 120);
ratingsPopin.frames = Paths.getSparrowAtlas("resultScreen/ratingsPopin");
ratingsPopin.animation.addByPrefix("idle", "Categories", 24, false);
// ratingsPopin.animation.play("idle");
ratingsPopin.visible = false;
ratingsPopin.antialiasing = true;
add(ratingsPopin);
var scorePopin:FlxSprite = new FlxSprite(-180, 520);

View file

@ -1,253 +0,0 @@
package funkin.play;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxPoint;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import funkin.noteStuff.NoteBasic.NoteColor;
import funkin.noteStuff.NoteBasic.NoteDir;
import funkin.noteStuff.NoteBasic.NoteType;
import funkin.ui.PreferencesMenu;
import funkin.util.Constants;
/**
* A group controlling the individual notes of the strumline for a given player.
*
* FUN FACT: Setting the X and Y of a FlxSpriteGroup will move all the sprites in the group.
*/
class Strumline extends FlxTypedSpriteGroup<StrumlineArrow>
{
/**
* The style of the strumline.
* Options are normal and pixel.
*/
var style:StrumlineStyle;
/**
* The player this strumline belongs to.
* 0 is Player 1, etc.
*/
var playerId:Int;
/**
* The number of notes in the strumline.
*/
var size:Int;
public function new(playerId:Int = 0, style:StrumlineStyle = NORMAL, size:Int = 4)
{
super(0);
this.playerId = playerId;
this.style = style;
this.size = size;
generateStrumline();
}
function generateStrumline():Void
{
for (index in 0...size)
{
createStrumlineArrow(index);
}
}
function createStrumlineArrow(index:Int):Void
{
var arrow:StrumlineArrow = new StrumlineArrow(index, style);
add(arrow);
}
/**
* Apply a small animation which moves the arrow down and fades it in.
* Only plays at the start of Free Play songs.
*
* Note that modifying the offset of the whole strumline won't have the
* @param arrow The arrow to animate.
* @param index The index of the arrow in the strumline.
*/
function fadeInArrow(arrow:FlxSprite):Void
{
arrow.y -= 10;
arrow.alpha = 0;
FlxTween.tween(arrow, {y: arrow.y + 10, alpha: 1}, 1, {ease: FlxEase.circOut, startDelay: 0.5 + (0.2 * arrow.ID)});
}
public function fadeInArrows():Void
{
for (arrow in this.members)
{
fadeInArrow(arrow);
}
}
function updatePositions()
{
for (arrow in members)
{
arrow.x = Note.swagWidth * arrow.ID;
arrow.x += offset.x;
arrow.y = 0;
arrow.y += offset.y;
}
}
/**
* Retrieves the arrow at the given position in the strumline.
* @param index The index to retrieve.
* @return The corresponding FlxSprite.
*/
public inline function getArrow(value:Int):StrumlineArrow
{
// members maintains the order that the arrows were added.
return this.members[value];
}
public inline function getArrowByNoteType(value:NoteType):StrumlineArrow
{
return getArrow(value.int);
}
public inline function getArrowByNoteDir(value:NoteDir):StrumlineArrow
{
return getArrow(value.int);
}
public inline function getArrowByNoteColor(value:funkin.noteStuff.NoteBasic.NoteColor):StrumlineArrow
{
return getArrow(value.int);
}
/**
* Get the default Y offset of the strumline.
* @return Int
*/
public static inline function getYPos():Int
{
return PreferencesMenu.getPref('downscroll') ? (FlxG.height - 150) : 50;
}
}
class StrumlineArrow extends FlxSprite
{
var style:StrumlineStyle;
public function new(id:Int, style:StrumlineStyle)
{
super(0, 0);
this.ID = id;
this.style = style;
// TODO: Unhardcode this. Maybe use a note style system>
switch (style)
{
case PIXEL:
buildPixelGraphic();
case NORMAL:
buildNormalGraphic();
}
this.updateHitbox();
scrollFactor.set(0, 0);
animation.play('static');
}
public function playAnimation(anim:String, force:Bool = false)
{
animation.play(anim, force);
centerOffsets();
centerOrigin();
}
/**
* Applies the default note style to an arrow.
*/
function buildNormalGraphic():Void
{
this.frames = Paths.getSparrowAtlas('NOTE_assets');
this.animation.addByPrefix('green', 'arrowUP');
this.animation.addByPrefix('blue', 'arrowDOWN');
this.animation.addByPrefix('purple', 'arrowLEFT');
this.animation.addByPrefix('red', 'arrowRIGHT');
this.setGraphicSize(Std.int(this.width * 0.7));
this.antialiasing = true;
this.x += Note.swagWidth * this.ID;
switch (Math.abs(this.ID))
{
case 0:
this.animation.addByPrefix('static', 'arrow static instance 1');
this.animation.addByPrefix('pressed', 'left press', 24, false);
this.animation.addByPrefix('confirm', 'left confirm', 24, false);
case 1:
this.animation.addByPrefix('static', 'arrow static instance 2');
this.animation.addByPrefix('pressed', 'down press', 24, false);
this.animation.addByPrefix('confirm', 'down confirm', 24, false);
case 2:
this.animation.addByPrefix('static', 'arrow static instance 4');
this.animation.addByPrefix('pressed', 'up press', 24, false);
this.animation.addByPrefix('confirm', 'up confirm', 24, false);
case 3:
this.animation.addByPrefix('static', 'arrow static instance 3');
this.animation.addByPrefix('pressed', 'right press', 24, false);
this.animation.addByPrefix('confirm', 'right confirm', 24, false);
}
}
/**
* Applies the pixel note style to an arrow.
*/
function buildPixelGraphic():Void
{
this.loadGraphic(Paths.image('weeb/pixelUI/arrows-pixels'), true, 17, 17);
this.animation.add('purplel', [4]);
this.animation.add('blue', [5]);
this.animation.add('green', [6]);
this.animation.add('red', [7]);
this.setGraphicSize(Std.int(this.width * Constants.PIXEL_ART_SCALE));
this.updateHitbox();
// Forcibly disable anti-aliasing on pixel graphics to stop blur.
this.antialiasing = false;
this.x += Note.swagWidth * this.ID;
// TODO: Seems weird that these are hardcoded like this... no XML?
switch (Math.abs(this.ID))
{
case 0:
this.animation.add('static', [0]);
this.animation.add('pressed', [4, 8], 12, false);
this.animation.add('confirm', [12, 16], 24, false);
case 1:
this.animation.add('static', [1]);
this.animation.add('pressed', [5, 9], 12, false);
this.animation.add('confirm', [13, 17], 24, false);
case 2:
this.animation.add('static', [2]);
this.animation.add('pressed', [6, 10], 12, false);
this.animation.add('confirm', [14, 18], 12, false);
case 3:
this.animation.add('static', [3]);
this.animation.add('pressed', [7, 11], 12, false);
this.animation.add('confirm', [15, 19], 24, false);
}
}
}
/**
* TODO: Unhardcode this and make it part of the note style system.
*/
enum StrumlineStyle
{
NORMAL;
PIXEL;
}

View file

@ -2,10 +2,10 @@ package funkin.play.character;
import flixel.math.FlxPoint;
import funkin.modding.events.ScriptEvent;
import funkin.noteStuff.NoteBasic.NoteDir;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.character.CharacterData.CharacterRenderType;
import funkin.play.stage.Bopper;
import funkin.play.notes.NoteDirection;
/**
* A Character is a stage prop which bops to the music as well as controlled by the strumlines.
@ -359,7 +359,7 @@ class BaseCharacter extends Bopper
}
// Handle character note hold time.
if (getCurrentAnimation().startsWith('sing'))
if (isSinging())
{
// TODO: Rework this code (and all character animations ugh)
// such that the hold time is handled by padding frames,
@ -405,6 +405,11 @@ class BaseCharacter extends Bopper
}
}
public function isSinging():Bool
{
return getCurrentAnimation().startsWith('sing');
}
override function dance(force:Bool = false):Void
{
// Prevent default dancing behavior.
@ -412,13 +417,13 @@ class BaseCharacter extends Bopper
if (!force)
{
if (getCurrentAnimation().startsWith('sing')) return;
if (isSinging()) return;
if (['hey', 'cheer'].contains(getCurrentAnimation()) && !isAnimationFinished()) return;
}
// Prevent dancing while another animation is playing.
if (!force && getCurrentAnimation().startsWith('sing')) return;
if (!force && isSinging()) return;
// Otherwise, fallback to the super dance() method, which handles playing the idle animation.
super.dance();
@ -488,16 +493,16 @@ class BaseCharacter extends Bopper
{
super.onNoteHit(event);
if (event.note.mustPress && characterType == BF)
if (event.note.noteData.getMustHitNote() && characterType == BF)
{
// If the note is from the same strumline, play the sing animation.
this.playSingAnimation(event.note.data.dir, false);
this.playSingAnimation(event.note.noteData.getDirection(), false);
holdTimer = 0;
}
else if (!event.note.mustPress && characterType == DAD)
else if (!event.note.noteData.getMustHitNote() && characterType == DAD)
{
// If the note is from the same strumline, play the sing animation.
this.playSingAnimation(event.note.data.dir, false);
this.playSingAnimation(event.note.noteData.getDirection(), false);
holdTimer = 0;
}
}
@ -510,17 +515,17 @@ class BaseCharacter extends Bopper
{
super.onNoteMiss(event);
if (event.note.mustPress && characterType == BF)
if (event.note.noteData.getMustHitNote() && characterType == BF)
{
// If the note is from the same strumline, play the sing animation.
this.playSingAnimation(event.note.data.dir, true);
this.playSingAnimation(event.note.noteData.getDirection(), true);
}
else if (!event.note.mustPress && characterType == DAD)
else if (!event.note.noteData.getMustHitNote() && characterType == DAD)
{
// If the note is from the same strumline, play the sing animation.
this.playSingAnimation(event.note.data.dir, true);
this.playSingAnimation(event.note.noteData.getDirection(), true);
}
else if (event.note.mustPress && characterType == GF)
else if (event.note.noteData.getMustHitNote() && characterType == GF)
{
var dropAnim = '';
@ -575,7 +580,7 @@ class BaseCharacter extends Bopper
* @param miss If true, play the miss animation instead of the sing animation.
* @param suffix A suffix to append to the animation name, like `alt`.
*/
public function playSingAnimation(dir:NoteDir, ?miss:Bool = false, ?suffix:String = ''):Void
public function playSingAnimation(dir:NoteDirection, ?miss:Bool = false, ?suffix:String = ''):Void
{
var anim:String = 'sing${dir.nameUpper}${miss ? 'miss' : ''}${suffix != '' ? '-${suffix}' : ''}';

View file

@ -1,5 +1,6 @@
package funkin.play.character;
import funkin.data.animation.AnimationData;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.character.ScriptedCharacter.ScriptedAnimateAtlasCharacter;

View file

@ -4,7 +4,6 @@ import flixel.FlxState;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.events.ScriptEvent;
import flixel.util.FlxColor;
import funkin.Paths;
/**
* A state with displays a conversation with no background.

View file

@ -1,5 +1,6 @@
package funkin.play.cutscene.dialogue;
import funkin.data.animation.AnimationData;
import funkin.util.SerializerUtil;
/**

View file

@ -21,7 +21,7 @@ class DialogueBoxDataParser
/**
* Parses and preloads the game's dialogueBox data and scripts when the game starts.
*
*
* If you want to force dialogue boxes to be reloaded, you can just call this function again.
*/
public static function loadDialogueBoxCache():Void
@ -123,7 +123,7 @@ class DialogueBoxDataParser
/**
* Load a dialogueBox's JSON file, parse its data, and return it.
*
*
* @param dialogueBoxId The dialogueBox to load.
* @return The dialogueBox data, or null if validation failed.
*/

View file

@ -1,4 +1,9 @@
package funkin.play.cutscene.dialogue;
/**
* A script that can be tied to a Speaker.
* Create a scripted class that extends Speaker to use this.
* This allows you to customize how a specific conversation speaker appears.
*/
@:hscriptClass
class ScriptedSpeaker extends Speaker implements polymod.hscript.HScriptedClass {}
class ScriptedSpeaker extends funkin.play.cutscene.dialogue.Speaker implements polymod.hscript.HScriptedClass {}

View file

@ -1,5 +1,7 @@
package funkin.play.cutscene.dialogue;
import funkin.data.animation.AnimationData;
/**
* Data about a conversation.
* Includes what speakers are in the conversation, and what phrases they say.

View file

@ -21,7 +21,7 @@ class SpeakerDataParser
/**
* Parses and preloads the game's speaker data and scripts when the game starts.
*
*
* If you want to force speakers to be reloaded, you can just call this function again.
*/
public static function loadSpeakerCache():Void
@ -123,7 +123,7 @@ class SpeakerDataParser
/**
* Load a speaker's JSON file, parse its data, and return it.
*
*
* @param speakerId The speaker to load.
* @return The speaker data, or null if validation failed.
*/

View file

@ -1,6 +1,5 @@
package funkin.play.event;
import funkin.util.Constants;
import flixel.tweens.FlxTween;
import flixel.FlxCamera;
import flixel.tweens.FlxEase;
@ -11,7 +10,7 @@ import funkin.play.event.SongEventData.SongEventFieldType;
/**
* This class represents a handler for configuring camera bop intensity and rate.
*
*
* Example: Bop the camera twice as hard, once per beat (rather than once every four beats).
* ```
* {
@ -22,7 +21,7 @@ import funkin.play.event.SongEventData.SongEventFieldType;
* }
* }
* ```
*
*
* Example: Reset the camera bop to default values.
* ```
* {

View file

@ -0,0 +1,81 @@
package funkin.play.notes;
import flixel.util.FlxColor;
/**
* The direction of a note.
* This has implicit casting set up, so you can use this as an integer.
*/
enum abstract NoteDirection(Int) from Int to Int
{
var LEFT = 0;
var DOWN = 1;
var UP = 2;
var RIGHT = 3;
public var name(get, never):String;
public var nameUpper(get, never):String;
public var color(get, never):FlxColor;
public var colorName(get, never):String;
@:from
public static function fromInt(value:Int):NoteDirection
{
return switch (value % 4)
{
case 0: LEFT;
case 1: DOWN;
case 2: UP;
case 3: RIGHT;
default: LEFT;
}
}
function get_name():String
{
return switch (abstract)
{
case LEFT:
'left';
case DOWN:
'down';
case UP:
'up';
case RIGHT:
'right';
default:
'unknown';
}
}
function get_nameUpper():String
{
return abstract.name.toUpperCase();
}
function get_color():FlxColor
{
return Constants.COLOR_NOTES[this];
}
function get_colorName():String
{
return switch (abstract)
{
case LEFT:
'purple';
case DOWN:
'blue';
case UP:
'green';
case RIGHT:
'red';
default:
'unknown';
}
}
public function toString():String
{
return abstract.name;
}
}

View file

@ -0,0 +1,141 @@
package funkin.play.notes;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import funkin.play.notes.NoteDirection;
import flixel.graphics.frames.FlxFramesCollection;
import funkin.util.assets.FlxAnimationUtil;
import flixel.FlxG;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.FlxSprite;
class NoteHoldCover extends FlxTypedSpriteGroup<FlxSprite>
{
static final FRAMERATE_DEFAULT:Int = 24;
static var glowFrames:FlxFramesCollection;
public var holdNote:SustainTrail;
var glow:FlxSprite;
var sparks:FlxSprite;
public function new()
{
super(0, 0);
setup();
}
public static function preloadFrames():Void
{
glowFrames = null;
for (direction in Strumline.DIRECTIONS)
{
var directionName = direction.colorName.toTitleCase();
var atlas:FlxFramesCollection = Paths.getSparrowAtlas('holdCover${directionName}');
atlas.parent.persist = true;
if (glowFrames != null)
{
glowFrames = FlxAnimationUtil.combineFramesCollections(glowFrames, atlas);
}
else
{
glowFrames = atlas;
}
}
}
/**
* Add ALL the animations to this sprite. We will recycle and reuse the FlxSprite multiple times.
*/
function setup():Void
{
glow = new FlxSprite();
add(glow);
if (glowFrames == null) preloadFrames();
glow.frames = glowFrames;
for (direction in Strumline.DIRECTIONS)
{
var directionName = direction.colorName.toTitleCase();
glow.animation.addByPrefix('holdCoverStart$directionName', 'holdCoverStart${directionName}0', FRAMERATE_DEFAULT, false, false, false);
glow.animation.addByPrefix('holdCover$directionName', 'holdCover${directionName}0', FRAMERATE_DEFAULT, true, false, false);
glow.animation.addByPrefix('holdCoverEnd$directionName', 'holdCoverEnd${directionName}0', FRAMERATE_DEFAULT, false, false, false);
}
glow.animation.finishCallback = this.onAnimationFinished;
if (glow.animation.getAnimationList().length < 3 * 4)
{
trace('WARNING: NoteHoldCover failed to initialize all animations.');
}
}
public override function update(elapsed):Void
{
super.update(elapsed);
if ((!holdNote.alive || holdNote.missedNote) && !glow.animation.curAnim.name.startsWith('holdCoverEnd'))
{
// If alive is false, the hold note was held to completion.
// If missedNote is true, the hold note was "dropped".
playEnd();
}
}
public function playStart():Void
{
var direction:NoteDirection = holdNote.noteDirection;
glow.animation.play('holdCoverStart${direction.colorName.toTitleCase()}');
}
public function playContinue():Void
{
var direction:NoteDirection = holdNote.noteDirection;
glow.animation.play('holdCover${direction.colorName.toTitleCase()}');
}
public function playEnd():Void
{
var direction:NoteDirection = holdNote.noteDirection;
glow.animation.play('holdCoverEnd${direction.colorName.toTitleCase()}');
}
public override function kill():Void
{
super.kill();
this.visible = false;
if (glow != null) glow.visible = false;
if (sparks != null) sparks.visible = false;
}
public override function revive():Void
{
super.revive();
this.visible = true;
this.alpha = 1.0;
if (glow != null) glow.visible = true;
if (sparks != null) sparks.visible = true;
}
public function onAnimationFinished(animationName:String):Void
{
if (animationName.startsWith('holdCoverStart'))
{
playContinue();
}
if (animationName.startsWith('holdCoverEnd'))
{
// *lightning* *zap* *crackle*
this.visible = false;
this.kill();
}
}
}

View file

@ -0,0 +1,89 @@
package funkin.play.notes;
import funkin.play.notes.NoteDirection;
import flixel.graphics.frames.FlxFramesCollection;
import flixel.FlxG;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.FlxSprite;
class NoteSplash extends FlxSprite
{
static final ALPHA:Float = 0.6;
static final FRAMERATE_DEFAULT:Int = 24;
static final FRAMERATE_VARIANCE:Int = 2;
static var frameCollection:FlxFramesCollection;
public static function preloadFrames():Void
{
frameCollection = Paths.getSparrowAtlas('noteSplashes');
}
public function new()
{
super(0, 0);
setup();
this.alpha = ALPHA;
this.animation.finishCallback = this.onAnimationFinished;
}
/**
* Add ALL the animations to this sprite. We will recycle and reuse the FlxSprite multiple times.
*/
function setup():Void
{
if (frameCollection == null) preloadFrames();
this.frames = frameCollection;
this.animation.addByPrefix('splash1Left', 'note impact 1 purple0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash1Down', 'note impact 1 blue0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash1Up', 'note impact 1 green0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash1Right', 'note impact 1 red0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash2Left', 'note impact 2 purple0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash2Down', 'note impact 2 blue0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash2Up', 'note impact 2 green0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash2Right', 'note impact 2 red0', FRAMERATE_DEFAULT, false, false, false);
if (this.animation.getAnimationList().length < 8)
{
trace('WARNING: NoteSplash failed to initialize all animations.');
}
}
public function playAnimation(name:String, force:Bool = false, reversed:Bool = false, startFrame:Int = 0):Void
{
this.animation.play(name, force, reversed, startFrame);
}
public function play(direction:NoteDirection, variant:Int = null):Void
{
if (variant == null) variant = FlxG.random.int(1, 2);
switch (direction)
{
case NoteDirection.LEFT:
this.playAnimation('splash${variant}Left');
case NoteDirection.DOWN:
this.playAnimation('splash${variant}Down');
case NoteDirection.UP:
this.playAnimation('splash${variant}Up');
case NoteDirection.RIGHT:
this.playAnimation('splash${variant}Right');
}
// Vary the speed of the animation a bit.
animation.curAnim.frameRate = FRAMERATE_DEFAULT + FlxG.random.int(-FRAMERATE_VARIANCE, FRAMERATE_VARIANCE);
// Center the animation on the note splash.
offset.set(width * 0.3, height * 0.3);
}
public function onAnimationFinished(animationName:String):Void
{
// *lightning* *zap* *crackle*
this.kill();
}
}

View file

@ -0,0 +1,153 @@
package funkin.play.notes;
import funkin.play.song.SongData.SongNoteData;
import funkin.play.notes.notestyle.NoteStyle;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.FlxSprite;
class NoteSprite extends FlxSprite
{
static final DIRECTION_COLORS:Array<String> = ['purple', 'blue', 'green', 'red'];
public var holdNoteSprite:SustainTrail;
/**
* The time at which the note should be hit, in milliseconds.
*/
public var strumTime(default, set):Float;
function set_strumTime(value:Float):Float
{
this.strumTime = value;
return this.strumTime;
}
/**
* The time at which the note should be hit, in steps.
*/
public var stepTime(get, never):Float;
function get_stepTime():Float
{
// TODO: Account for changes in BPM.
return this.strumTime / Conductor.stepLengthMs;
}
/**
* An extra attribute for the note.
* For example, whether the note is an "alt" note, or whether it has custom behavior on hit.
*/
public var kind(default, set):String;
function set_kind(value:String):String
{
this.kind = value;
return this.kind;
}
/**
* The data of the note (i.e. the direction.)
*/
public var direction(default, set):NoteDirection;
function set_direction(value:Int):Int
{
if (frames == null) return value;
animation.play(DIRECTION_COLORS[value] + 'Scroll');
this.direction = value;
return this.direction;
}
public var noteData:SongNoteData;
public var isHoldNote(get, never):Bool;
function get_isHoldNote():Bool
{
return noteData.length > 0;
}
/**
* Set this flag to true when hitting the note to avoid scoring it multiple times.
*/
public var hasBeenHit:Bool = false;
/**
* Register this note as hit only after any other notes
*/
public var lowPriority:Bool = false;
/**
* This is true if the note is later than 10 frames within the strumline,
* and thus can't be hit by the player.
* It will be destroyed after it moves offscreen.
* Managed by PlayState.
*/
public var hasMissed:Bool;
/**
* This is true if the note is earlier than 10 frames within the strumline.
* and thus can't be hit by the player.
* Managed by PlayState.
*/
public var tooEarly:Bool;
/**
* This is true if the note is within 10 frames of the strumline,
* and thus may be hit by the player.
* Managed by PlayState.
*/
public var mayHit:Bool;
/**
* This is true if the PlayState has performed the logic for missing this note.
* Subtracting score, subtracting health, etc.
*/
public var handledMiss:Bool;
public function new(noteStyle:NoteStyle, strumTime:Float = 0, direction:Int = 0)
{
super(0, -9999);
this.strumTime = strumTime;
this.direction = direction;
if (this.strumTime < 0) this.strumTime = 0;
setupNoteGraphic(noteStyle);
// Disables the update() function for performance.
this.active = false;
}
function setupNoteGraphic(noteStyle:NoteStyle):Void
{
noteStyle.buildNoteSprite(this);
setGraphicSize(Strumline.STRUMLINE_SIZE);
updateHitbox();
}
public override function revive():Void
{
super.revive();
this.active = false;
this.tooEarly = false;
this.hasBeenHit = false;
this.mayHit = false;
this.hasMissed = false;
}
public override function kill():Void
{
super.kill();
}
public override function destroy():Void
{
// This function should ONLY get called as you leave PlayState entirely.
// Otherwise, we want the game to keep reusing note sprites to save memory.
super.destroy();
}
}

View file

@ -0,0 +1,737 @@
package funkin.play.notes;
import flixel.FlxG;
import funkin.play.notes.notestyle.NoteStyle;
import flixel.group.FlxSpriteGroup;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxSort;
import funkin.play.notes.NoteHoldCover;
import funkin.play.notes.NoteSplash;
import funkin.play.notes.NoteSprite;
import funkin.play.notes.SustainTrail;
import funkin.play.song.SongData.SongNoteData;
import funkin.ui.PreferencesMenu;
import funkin.util.SortUtil;
/**
* A group of sprites which handles the receptor, the note splashes, and the notes (with sustains) for a given player.
*/
class Strumline extends FlxSpriteGroup
{
public static final DIRECTIONS:Array<NoteDirection> = [NoteDirection.LEFT, NoteDirection.DOWN, NoteDirection.UP, NoteDirection.RIGHT];
public static final STRUMLINE_SIZE:Int = 104;
public static final NOTE_SPACING:Int = STRUMLINE_SIZE + 8;
// Positional fixes for new strumline graphics.
static final INITIAL_OFFSET = -0.275 * STRUMLINE_SIZE;
static final NUDGE:Float = 2.0;
static final KEY_COUNT:Int = 4;
static final NOTE_SPLASH_CAP:Int = 6;
static var RENDER_DISTANCE_MS(get, null):Float;
static function get_RENDER_DISTANCE_MS():Float
{
return FlxG.height / 0.45;
}
public var isPlayer:Bool;
/**
* The notes currently being rendered on the strumline.
* This group iterates over this every frame to update note positions.
* The PlayState also iterates over this to calculate user inputs.
*/
public var notes:FlxTypedSpriteGroup<NoteSprite>;
public var holdNotes:FlxTypedSpriteGroup<SustainTrail>;
var strumlineNotes:FlxTypedSpriteGroup<StrumlineNote>;
var noteSplashes:FlxTypedSpriteGroup<NoteSplash>;
var noteHoldCovers:FlxTypedSpriteGroup<NoteHoldCover>;
final noteStyle:NoteStyle;
var noteData:Array<SongNoteData> = [];
var nextNoteIndex:Int = -1;
var heldKeys:Array<Bool> = [];
public function new(noteStyle:NoteStyle, isPlayer:Bool)
{
super();
this.isPlayer = isPlayer;
this.noteStyle = noteStyle;
this.strumlineNotes = new FlxTypedSpriteGroup<StrumlineNote>();
this.strumlineNotes.zIndex = 10;
this.add(this.strumlineNotes);
// Hold notes are added first so they render behind regular notes.
this.holdNotes = new FlxTypedSpriteGroup<SustainTrail>();
this.holdNotes.zIndex = 20;
this.add(this.holdNotes);
this.notes = new FlxTypedSpriteGroup<NoteSprite>();
this.notes.zIndex = 30;
this.add(this.notes);
this.noteHoldCovers = new FlxTypedSpriteGroup<NoteHoldCover>(0, 0, 4);
this.noteHoldCovers.zIndex = 40;
this.add(this.noteHoldCovers);
this.noteSplashes = new FlxTypedSpriteGroup<NoteSplash>(0, 0, NOTE_SPLASH_CAP);
this.noteSplashes.zIndex = 50;
this.add(this.noteSplashes);
this.refresh();
for (i in 0...KEY_COUNT)
{
var child:StrumlineNote = new StrumlineNote(noteStyle, isPlayer, DIRECTIONS[i]);
child.x = getXPos(DIRECTIONS[i]);
child.x += INITIAL_OFFSET;
child.y = 0;
noteStyle.applyStrumlineOffsets(child);
this.strumlineNotes.add(child);
}
for (i in 0...KEY_COUNT)
{
heldKeys.push(false);
}
// This MUST be true for children to update!
this.active = true;
}
public function refresh():Void
{
sort(SortUtil.byZIndex, FlxSort.ASCENDING);
}
override function get_width():Float
{
return KEY_COUNT * Strumline.NOTE_SPACING;
}
public override function update(elapsed:Float):Void
{
super.update(elapsed);
updateNotes();
}
var frameMax:Int;
var animFinishedEver:Bool;
/**
* Get a list of notes within + or - the given strumtime.
* @param strumTime The current time.
* @param hitWindow The hit window to check.
*/
public function getNotesInRange(strumTime:Float, hitWindow:Float):Array<NoteSprite>
{
var hitWindowStart:Float = strumTime - hitWindow;
var hitWindowEnd:Float = strumTime + hitWindow;
return notes.members.filter(function(note:NoteSprite) {
return note != null && note.alive && !note.hasBeenHit && note.strumTime >= hitWindowStart && note.strumTime <= hitWindowEnd;
});
}
public function getNotesMayHit():Array<NoteSprite>
{
return notes.members.filter(function(note:NoteSprite) {
return note != null && note.alive && !note.hasBeenHit && note.mayHit;
});
}
public function getHoldNotesHitOrMissed():Array<SustainTrail>
{
return holdNotes.members.filter(function(holdNote:SustainTrail) {
return holdNote != null && holdNote.alive && (holdNote.hitNote || holdNote.missedNote);
});
}
public function getHoldNotesInRange(strumTime:Float, hitWindow:Float):Array<SustainTrail>
{
var hitWindowStart:Float = strumTime - hitWindow;
var hitWindowEnd:Float = strumTime + hitWindow;
return holdNotes.members.filter(function(note:SustainTrail) {
return note != null
&& note.alive
&& note.strumTime >= hitWindowStart
&& (note.strumTime + note.fullSustainLength) <= hitWindowEnd;
});
}
public function getNoteSprite(noteData:SongNoteData):NoteSprite
{
if (noteData == null) return null;
for (note in notes.members)
{
if (note == null) continue;
if (note.alive) continue;
if (note.noteData == noteData) return note;
}
return null;
}
public function getHoldNoteSprite(noteData:SongNoteData):SustainTrail
{
if (noteData == null || ((noteData.length ?? 0.0) <= 0.0)) return null;
for (holdNote in holdNotes.members)
{
if (holdNote == null) continue;
if (holdNote.alive) continue;
if (holdNote.noteData == noteData) return holdNote;
}
return null;
}
/**
* For a note's strumTime, calculate its Y position relative to the strumline.
* NOTE: Assumes Conductor and PlayState are both initialized.
* @param strumTime
* @return Float
*/
static function calculateNoteYPos(strumTime:Float, ?vwoosh:Bool = true):Float
{
// Make the note move faster visually as it moves offscreen.
var vwoosh:Float = (strumTime < Conductor.songPosition) && vwoosh ? 2.0 : 1.0;
var scrollSpeed:Float = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0;
return Conductor.PIXELS_PER_MS * (Conductor.songPosition - strumTime) * scrollSpeed * vwoosh * (PreferencesMenu.getPref('downscroll') ? 1 : -1);
}
function updateNotes():Void
{
if (noteData.length == 0) return;
var renderWindowStart:Float = Conductor.songPosition + RENDER_DISTANCE_MS;
for (noteIndex in nextNoteIndex...noteData.length)
{
var note:Null<SongNoteData> = noteData[noteIndex];
if (note == null) continue;
if (note.time > renderWindowStart) break;
var noteSprite = buildNoteSprite(note);
if (note.length > 0)
{
noteSprite.holdNoteSprite = buildHoldNoteSprite(note);
}
nextNoteIndex++; // Increment the nextNoteIndex rather than splicing the array, because splicing is slow.
}
// Update rendering of notes.
for (note in notes.members)
{
if (note == null || !note.alive || note.hasBeenHit) continue;
var vwoosh:Bool = note.holdNoteSprite == null;
// Set the note's position.
note.y = this.y - INITIAL_OFFSET + calculateNoteYPos(note.strumTime, vwoosh);
// If the note is miss
var isOffscreen = PreferencesMenu.getPref('downscroll') ? note.y > FlxG.height : note.y < -note.height;
if (note.handledMiss && isOffscreen)
{
killNote(note);
}
}
// Update rendering of hold notes.
for (holdNote in holdNotes.members)
{
if (holdNote == null || !holdNote.alive) continue;
if (Conductor.songPosition > holdNote.strumTime && holdNote.hitNote && !holdNote.missedNote)
{
if (isPlayer && !isKeyHeld(holdNote.noteDirection))
{
// Stopped pressing the hold note.
playStatic(holdNote.noteDirection);
holdNote.missedNote = true;
holdNote.visible = true;
holdNote.alpha = 0.0;
}
}
var renderWindowEnd = holdNote.strumTime + holdNote.fullSustainLength + Conductor.HIT_WINDOW_MS + RENDER_DISTANCE_MS / 8;
if (holdNote.missedNote && Conductor.songPosition >= renderWindowEnd)
{
// Hold note is offscreen, kill it.
holdNote.visible = false;
holdNote.kill(); // Do not destroy! Recycling is faster.
// The cover will see this and clean itself up.
}
else if (holdNote.hitNote && holdNote.sustainLength <= 0)
{
// Hold note is completed, kill it.
if (isKeyHeld(holdNote.noteDirection))
{
playPress(holdNote.noteDirection);
}
else
{
playStatic(holdNote.noteDirection);
}
if (holdNote.cover != null)
{
holdNote.cover.playEnd();
}
holdNote.visible = false;
holdNote.kill();
}
else if (holdNote.missedNote && (holdNote.fullSustainLength > holdNote.sustainLength))
{
// Hold note was dropped before completing, keep it in its clipped state.
holdNote.visible = true;
var yOffset:Float = (holdNote.fullSustainLength - holdNote.sustainLength) * Conductor.PIXELS_PER_MS;
trace('yOffset: ' + yOffset);
trace('holdNote.fullSustainLength: ' + holdNote.fullSustainLength);
trace('holdNote.sustainLength: ' + holdNote.sustainLength);
var vwoosh:Bool = false;
if (PreferencesMenu.getPref('downscroll'))
{
holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
}
else
{
holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) + yOffset + STRUMLINE_SIZE / 2;
}
}
else if (Conductor.songPosition > holdNote.strumTime && holdNote.hitNote)
{
// Hold note is currently being hit, clip it off.
holdConfirm(holdNote.noteDirection);
holdNote.visible = true;
holdNote.sustainLength = (holdNote.strumTime + holdNote.fullSustainLength) - Conductor.songPosition;
if (holdNote.sustainLength <= 10)
{
holdNote.visible = false;
}
if (PreferencesMenu.getPref('downscroll'))
{
holdNote.y = this.y - holdNote.height + STRUMLINE_SIZE / 2;
}
else
{
holdNote.y = this.y - INITIAL_OFFSET + STRUMLINE_SIZE / 2;
}
}
else
{
// Hold note is new, render it normally.
holdNote.visible = true;
var vwoosh:Bool = false;
if (PreferencesMenu.getPref('downscroll'))
{
holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
}
else
{
holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) + STRUMLINE_SIZE / 2;
}
}
}
// Update rendering of pressed keys.
for (dir in DIRECTIONS)
{
if (isKeyHeld(dir) && getByDirection(dir).getCurrentAnimation() == "static")
{
playPress(dir);
}
}
}
public function onBeatHit():Void
{
if (notes.members.length > 1) notes.members.insertionSort(compareNoteSprites.bind(FlxSort.ASCENDING));
if (holdNotes.members.length > 1) holdNotes.members.insertionSort(compareHoldNoteSprites.bind(FlxSort.ASCENDING));
}
public function pressKey(dir:NoteDirection):Void
{
heldKeys[dir] = true;
}
public function releaseKey(dir:NoteDirection):Void
{
heldKeys[dir] = false;
}
public function isKeyHeld(dir:NoteDirection):Bool
{
return heldKeys[dir];
}
public function applyNoteData(data:Array<SongNoteData>):Void
{
this.notes.clear();
this.noteData = data.copy();
this.nextNoteIndex = 0;
// Sort the notes by strumtime.
this.noteData.insertionSort(compareNoteData.bind(FlxSort.ASCENDING));
}
public function hitNote(note:NoteSprite):Void
{
playConfirm(note.direction);
note.hasBeenHit = true;
killNote(note);
if (note.holdNoteSprite != null)
{
note.holdNoteSprite.hitNote = true;
note.holdNoteSprite.missedNote = false;
note.holdNoteSprite.alpha = 1.0;
}
}
public function killNote(note:NoteSprite):Void
{
note.visible = false;
notes.remove(note, false);
note.kill();
if (note.holdNoteSprite != null)
{
note.holdNoteSprite.missedNote = true;
note.holdNoteSprite.visible = false;
}
}
public function getByIndex(index:Int):StrumlineNote
{
return this.strumlineNotes.members[index];
}
public function getByDirection(direction:NoteDirection):StrumlineNote
{
return getByIndex(DIRECTIONS.indexOf(direction));
}
public function playStatic(direction:NoteDirection):Void
{
getByDirection(direction).playStatic();
}
public function playPress(direction:NoteDirection):Void
{
getByDirection(direction).playPress();
}
public function playConfirm(direction:NoteDirection):Void
{
getByDirection(direction).playConfirm();
}
public function holdConfirm(direction:NoteDirection):Void
{
getByDirection(direction).holdConfirm();
}
public function isConfirm(direction:NoteDirection):Bool
{
return getByDirection(direction).isConfirm();
}
public function playNoteSplash(direction:NoteDirection):Void
{
// TODO: Add a setting to disable note splashes.
// if (Settings.noSplash) return;
if (!noteStyle.isNoteSplashEnabled()) return;
var splash:NoteSplash = this.constructNoteSplash();
if (splash != null)
{
splash.play(direction);
splash.x = this.x;
splash.x += getXPos(direction);
splash.x += INITIAL_OFFSET;
splash.y = this.y;
splash.y -= INITIAL_OFFSET;
splash.y += 0;
}
}
public function playNoteHoldCover(holdNote:SustainTrail):Void
{
// TODO: Add a setting to disable note splashes.
// if (Settings.noSplash) return;
if (!noteStyle.isHoldNoteCoverEnabled()) return;
var cover:NoteHoldCover = this.constructNoteHoldCover();
if (cover != null)
{
cover.holdNote = holdNote;
holdNote.cover = cover;
cover.visible = true;
cover.playStart();
cover.x = this.x;
cover.x += getXPos(holdNote.noteDirection);
cover.x += STRUMLINE_SIZE / 2;
cover.x -= cover.width / 2;
cover.x += -12; // Manual tweaking because fuck.
cover.y = this.y;
cover.y += INITIAL_OFFSET;
cover.y += STRUMLINE_SIZE / 2;
cover.y += -96; // Manual tweaking because fuck.
}
}
public function buildNoteSprite(note:SongNoteData):NoteSprite
{
var noteSprite:NoteSprite = constructNoteSprite();
if (noteSprite != null)
{
noteSprite.strumTime = note.time;
noteSprite.direction = note.getDirection();
noteSprite.noteData = note;
noteSprite.x = this.x;
noteSprite.x += getXPos(DIRECTIONS[note.getDirection() % KEY_COUNT]);
noteSprite.x -= NUDGE;
// noteSprite.x += INITIAL_OFFSET;
noteSprite.y = -9999;
}
return noteSprite;
}
public function buildHoldNoteSprite(note:SongNoteData):SustainTrail
{
var holdNoteSprite:SustainTrail = constructHoldNoteSprite();
if (holdNoteSprite != null)
{
holdNoteSprite.noteData = note;
holdNoteSprite.strumTime = note.time;
holdNoteSprite.noteDirection = note.getDirection();
holdNoteSprite.fullSustainLength = note.length;
holdNoteSprite.sustainLength = note.length;
holdNoteSprite.missedNote = false;
holdNoteSprite.hitNote = false;
holdNoteSprite.visible = true;
holdNoteSprite.alpha = 1.0;
holdNoteSprite.x = this.x;
holdNoteSprite.x += getXPos(DIRECTIONS[note.getDirection() % KEY_COUNT]);
holdNoteSprite.x += STRUMLINE_SIZE / 2;
holdNoteSprite.x -= holdNoteSprite.width / 2;
holdNoteSprite.y = -9999;
}
return holdNoteSprite;
}
/**
* Custom recycling behavior.
*/
function constructNoteSplash():NoteSplash
{
var result:NoteSplash = null;
// If we haven't filled the pool yet...
if (noteSplashes.length < noteSplashes.maxSize)
{
// Create a new note splash.
result = new NoteSplash();
this.noteSplashes.add(result);
}
else
{
// Else, find a note splash which is inactive so we can revive it.
result = this.noteSplashes.getFirstAvailable();
if (result != null)
{
result.revive();
}
else
{
// The note splash pool is full and all note splashes are active,
// so we just pick one at random to destroy and restart.
result = FlxG.random.getObject(this.noteSplashes.members);
}
}
return result;
}
/**
* Custom recycling behavior.
*/
function constructNoteHoldCover():NoteHoldCover
{
var result:NoteHoldCover = null;
// If we haven't filled the pool yet...
if (noteHoldCovers.length < noteHoldCovers.maxSize)
{
// Create a new note hold cover.
result = new NoteHoldCover();
this.noteHoldCovers.add(result);
}
else
{
// Else, find a note splash which is inactive so we can revive it.
result = this.noteHoldCovers.getFirstAvailable();
if (result != null)
{
result.revive();
}
else
{
// The note hold cover pool is full and all note hold covers are active,
// so we just pick one at random to destroy and restart.
result = FlxG.random.getObject(this.noteHoldCovers.members);
}
}
return result;
}
/**
* Custom recycling behavior.
*/
function constructNoteSprite():NoteSprite
{
var result:NoteSprite = null;
// Else, find a note which is inactive so we can revive it.
result = this.notes.getFirstAvailable();
if (result != null)
{
// Revive and reuse the note.
result.revive();
}
else
{
// The note sprite pool is full and all note splashes are active.
// We have to create a new note.
result = new NoteSprite(noteStyle);
this.notes.add(result);
}
return result;
}
/**
* Custom recycling behavior.
*/
function constructHoldNoteSprite():SustainTrail
{
var result:SustainTrail = null;
// Else, find a note which is inactive so we can revive it.
result = this.holdNotes.getFirstAvailable();
if (result != null)
{
// Revive and reuse the note.
result.revive();
}
else
{
// The note sprite pool is full and all note splashes are active.
// We have to create a new note.
result = new SustainTrail(0, 100, noteStyle.getHoldNoteAssetPath(), noteStyle);
this.holdNotes.add(result);
}
return result;
}
function getXPos(direction:NoteDirection):Float
{
return switch (direction)
{
case NoteDirection.LEFT: 0;
case NoteDirection.DOWN: 0 + (1 * Strumline.NOTE_SPACING);
case NoteDirection.UP: 0 + (2 * Strumline.NOTE_SPACING);
case NoteDirection.RIGHT: 0 + (3 * Strumline.NOTE_SPACING);
default: 0;
}
}
/**
* Apply a small animation which moves the arrow down and fades it in.
* Only plays at the start of Free Play songs.
*
* Note that modifying the offset of the whole strumline won't have the
* @param arrow The arrow to animate.
* @param index The index of the arrow in the strumline.
*/
function fadeInArrow(index:Int, arrow:StrumlineNote):Void
{
arrow.y -= 10;
arrow.alpha = 0.0;
FlxTween.tween(arrow, {y: arrow.y + 10, alpha: 1}, 1, {ease: FlxEase.circOut, startDelay: 0.5 + (0.2 * index)});
}
public function fadeInArrows():Void
{
for (index => arrow in this.strumlineNotes.members.keyValueIterator())
{
fadeInArrow(index, arrow);
}
}
function compareNoteData(order:Int, a:SongNoteData, b:SongNoteData):Int
{
return FlxSort.byValues(order, a.time, b.time);
}
function compareNoteSprites(order:Int, a:NoteSprite, b:NoteSprite):Int
{
return FlxSort.byValues(order, a?.strumTime, b?.strumTime);
}
function compareHoldNoteSprites(order:Int, a:SustainTrail, b:SustainTrail):Int
{
return FlxSort.byValues(order, a?.strumTime, b?.strumTime);
}
}

View file

@ -0,0 +1,179 @@
package funkin.play.notes;
import funkin.play.notes.notestyle.NoteStyle;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.FlxSprite;
import funkin.play.notes.NoteSprite;
/**
* The actual receptor that you see on screen.
*/
class StrumlineNote extends FlxSprite
{
public var isPlayer(default, null):Bool;
public var direction(default, set):NoteDirection;
var confirmHoldTimer:Float = -1;
static final CONFIRM_HOLD_TIME:Float = 0.1;
function set_direction(value:NoteDirection):NoteDirection
{
this.direction = value;
return this.direction;
}
public function new(noteStyle:NoteStyle, isPlayer:Bool, direction:NoteDirection)
{
super(0, 0);
this.isPlayer = isPlayer;
this.direction = direction;
setup(noteStyle);
this.animation.callback = onAnimationFrame;
this.animation.finishCallback = onAnimationFinished;
// Must be true for animations to play.
this.active = true;
}
function onAnimationFrame(name:String, frameNumber:Int, frameIndex:Int):Void {}
function onAnimationFinished(name:String):Void
{
// Run a timer before we stop playing the confirm animation.
// On opponent, this prevent issues with hold notes.
// On player, this allows holding the confirm key to fall back to press.
if (name == 'confirm')
{
confirmHoldTimer = 0;
}
}
override function update(elapsed:Float)
{
super.update(elapsed);
centerOrigin();
if (confirmHoldTimer >= 0)
{
confirmHoldTimer += elapsed;
// Ensure the opponent stops holding the key after a certain amount of time.
if (confirmHoldTimer >= CONFIRM_HOLD_TIME)
{
confirmHoldTimer = -1;
playStatic();
}
}
}
function setup(noteStyle:NoteStyle):Void
{
noteStyle.applyStrumlineFrames(this);
noteStyle.applyStrumlineAnimations(this, this.direction);
this.setGraphicSize(Std.int(Strumline.STRUMLINE_SIZE * noteStyle.getStrumlineScale()));
this.updateHitbox();
noteStyle.applyStrumlineOffsets(this);
this.playStatic();
}
public function playAnimation(name:String = 'static', force:Bool = false, reversed:Bool = false, startFrame:Int = 0):Void
{
this.animation.play(name, force, reversed, startFrame);
centerOffsets();
centerOrigin();
}
public function playStatic():Void
{
this.active = false;
this.playAnimation('static', true);
}
public function playPress():Void
{
this.active = true;
this.playAnimation('press', true);
}
public function playConfirm():Void
{
this.active = true;
this.playAnimation('confirm', true);
}
public function isConfirm():Bool
{
return getCurrentAnimation().startsWith('confirm');
}
public function holdConfirm():Void
{
this.active = true;
if (getCurrentAnimation() == "confirm-hold")
{
return;
}
else if (getCurrentAnimation() == "confirm")
{
if (isAnimationFinished())
{
this.confirmHoldTimer = -1;
this.playAnimation('confirm-hold', false, false);
}
}
else
{
this.playAnimation('confirm', false, false);
}
}
/**
* Returns the name of the animation that is currently playing.
* If no animation is playing (usually this means the sprite is BROKEN!),
* returns an empty string to prevent NPEs.
*/
public function getCurrentAnimation():String
{
if (this.animation == null || this.animation.curAnim == null) return "";
return this.animation.curAnim.name;
}
public function isAnimationFinished():Bool
{
return this.animation.finished;
}
static final DEFAULT_OFFSET:Int = 13;
/**
* Adjusts the position of the sprite's graphic relative to the hitbox.
*/
function fixOffsets():Void
{
// Automatically center the bounding box within the graphic.
this.centerOffsets();
if (getCurrentAnimation() == "confirm")
{
// Move the graphic down and to the right to compensate for
// the "glow" effect on the strumline note.
this.offset.x -= DEFAULT_OFFSET;
this.offset.y -= DEFAULT_OFFSET;
}
else
{
this.centerOrigin();
}
}
}

View file

@ -0,0 +1,302 @@
package funkin.play.notes;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.notes.NoteDirection;
import funkin.play.song.SongData.SongNoteData;
import flixel.util.FlxDirectionFlags;
import flixel.FlxSprite;
import flixel.graphics.FlxGraphic;
import flixel.graphics.tile.FlxDrawTrianglesItem;
import flixel.math.FlxMath;
import funkin.ui.PreferencesMenu;
/**
* This is based heavily on the `FlxStrip` class. It uses `drawTriangles()` to clip a sustain note
* trail at a certain time.
* The whole `FlxGraphic` is used as a texture map. See the `NOTE_hold_assets.fla` file for specifics
* on how it should be constructed.
*
* @author MtH
*/
class SustainTrail extends FlxSprite
{
/**
* The triangles corresponding to the hold, followed by the endcap.
* `top left, top right, bottom left`
* `top left, bottom left, bottom right`
*/
static final TRIANGLE_VERTEX_INDICES:Array<Int> = [0, 1, 2, 1, 2, 3, 4, 5, 6, 5, 6, 7];
public var strumTime:Float = 0; // millis
public var noteDirection:NoteDirection = 0;
public var sustainLength(default, set):Float = 0; // millis
public var fullSustainLength:Float = 0;
public var noteData:SongNoteData;
public var cover:NoteHoldCover = null;
/**
* Set to `true` if the user hit the note and is currently holding the sustain.
* Should display associated effects.
*/
public var hitNote:Bool = false;
/**
* Set to `true` if the user missed the note or released the sustain.
* Should make the trail transparent.
*/
public var missedNote:Bool = false;
// maybe BlendMode.MULTIPLY if missed somehow, drawTriangles does not support!
/**
* A `Vector` of floats where each pair of numbers is treated as a coordinate location (an x, y pair).
*/
public var vertices:DrawData<Float> = new DrawData<Float>();
/**
* A `Vector` of integers or indexes, where every three indexes define a triangle.
*/
public var indices:DrawData<Int> = new DrawData<Int>();
/**
* A `Vector` of normalized coordinates used to apply texture mapping.
*/
public var uvtData:DrawData<Float> = new DrawData<Float>();
private var processedGraphic:FlxGraphic;
private var zoom:Float = 1;
/**
* What part of the trail's end actually represents the end of the note.
* This can be used to have a little bit sticking out.
*/
public var endOffset:Float = 0.5; // 0.73 is roughly the bottom of the sprite in the normal graphic!
/**
* At what point the bottom for the trail's end should be clipped off.
* Used in cases where there's an extra bit of the graphic on the bottom to avoid antialiasing issues with overflow.
*/
public var bottomClip:Float = 0.9;
public var isPixel:Bool;
/**
* Normally you would take strumTime:Float, noteData:Int, sustainLength:Float, parentNote:Note (?)
* @param NoteData
* @param SustainLength Length in milliseconds.
* @param fileName
*/
public function new(noteDirection:NoteDirection, sustainLength:Float, fileName:String, noteStyle:NoteStyle)
{
super(0, 0, fileName);
antialiasing = true;
this.isPixel = noteStyle.isHoldNotePixel();
if (isPixel)
{
endOffset = bottomClip = 1;
antialiasing = false;
}
zoom *= noteStyle.fetchHoldNoteScale();
// BASIC SETUP
this.sustainLength = sustainLength;
this.fullSustainLength = sustainLength;
this.noteDirection = noteDirection;
zoom *= 0.7;
// CALCULATE SIZE
width = graphic.width / 8 * zoom; // amount of notes * 2
height = sustainHeight(sustainLength, PlayState.instance.currentChart.scrollSpeed);
// instead of scrollSpeed, PlayState.SONG.speed
flipY = PreferencesMenu.getPref('downscroll');
// alpha = 0.6;
alpha = 1.0;
// calls updateColorTransform(), which initializes processedGraphic!
updateColorTransform();
updateClipping();
indices = new DrawData<Int>(12, true, TRIANGLE_VERTEX_INDICES);
}
/**
* Calculates height of a sustain note for a given length (milliseconds) and scroll speed.
* @param susLength The length of the sustain note in milliseconds.
* @param scroll The current scroll speed.
*/
public static inline function sustainHeight(susLength:Float, scroll:Float)
{
return (susLength * 0.45 * scroll);
}
function set_sustainLength(s:Float)
{
if (s < 0) s = 0;
height = sustainHeight(s, PlayState.instance.currentChart.scrollSpeed);
updateColorTransform();
updateClipping();
return sustainLength = s;
}
/**
* Sets up new vertex and UV data to clip the trail.
* If flipY is true, top and bottom bounds swap places.
* @param songTime The time to clip the note at, in milliseconds.
*/
public function updateClipping(songTime:Float = 0):Void
{
var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), PlayState.instance.currentChart.scrollSpeed), 0, height);
if (clipHeight == 0)
{
visible = false;
return;
}
else
visible = true;
var bottomHeight:Float = graphic.height * zoom * endOffset;
var partHeight:Float = clipHeight - bottomHeight;
// ===HOLD VERTICES==
// Top left
vertices[0 * 2] = 0.0; // Inline with left side
vertices[0 * 2 + 1] = flipY ? clipHeight : height - clipHeight;
// Top right
vertices[1 * 2] = width;
vertices[1 * 2 + 1] = vertices[0 * 2 + 1]; // Inline with top left vertex
// Bottom left
vertices[2 * 2] = 0.0; // Inline with left side
vertices[2 * 2 + 1] = if (partHeight > 0)
{
// flipY makes the sustain render upside down.
flipY ? 0.0 + bottomHeight : vertices[1] + partHeight;
}
else
{
vertices[0 * 2 + 1]; // Inline with top left vertex (no partHeight available)
}
// Bottom right
vertices[3 * 2] = width;
vertices[3 * 2 + 1] = vertices[2 * 2 + 1]; // Inline with bottom left vertex
// ===HOLD UVs===
// The UVs are a bit more complicated.
// UV coordinates are normalized, so they range from 0 to 1.
// We are expecting an image containing 8 horizontal segments, each representing a different colored hold note followed by its end cap.
uvtData[0 * 2] = 1 / 4 * (noteDirection % 4); // 0%/25%/50%/75% of the way through the image
uvtData[0 * 2 + 1] = (-partHeight) / graphic.height / zoom; // top bound
// Top left
// Top right
uvtData[1 * 2] = uvtData[0 * 2] + 1 / 8; // 12.5%/37.5%/62.5%/87.5% of the way through the image (1/8th past the top left)
uvtData[1 * 2 + 1] = uvtData[0 * 2 + 1]; // top bound
// Bottom left
uvtData[2 * 2] = uvtData[0 * 2]; // 0%/25%/50%/75% of the way through the image
uvtData[2 * 2 + 1] = 0.0; // bottom bound
// Bottom right
uvtData[3 * 2] = uvtData[1 * 2]; // 12.5%/37.5%/62.5%/87.5% of the way through the image (1/8th past the top left)
uvtData[3 * 2 + 1] = uvtData[2 * 2 + 1]; // bottom bound
// === END CAP VERTICES ===
// Top left
vertices[4 * 2] = vertices[2 * 2]; // Inline with bottom left vertex of hold
vertices[4 * 2 + 1] = vertices[2 * 2 + 1]; // Inline with bottom left vertex of hold
// Top right
vertices[5 * 2] = vertices[3 * 2]; // Inline with bottom right vertex of hold
vertices[5 * 2 + 1] = vertices[3 * 2 + 1]; // Inline with bottom right vertex of hold
// Bottom left
vertices[6 * 2] = vertices[2 * 2]; // Inline with left side
vertices[6 * 2 + 1] = flipY ? (graphic.height * (-bottomClip + endOffset) * zoom) : (height + graphic.height * (bottomClip - endOffset) * zoom);
// Bottom right
vertices[7 * 2] = vertices[3 * 2]; // Inline with right side
vertices[7 * 2 + 1] = vertices[6 * 2 + 1]; // Inline with bottom of end cap
// === END CAP UVs ===
// Top left
uvtData[4 * 2] = uvtData[2 * 2] + 1 / 8; // 12.5%/37.5%/62.5%/87.5% of the way through the image (1/8th past the top left of hold)
uvtData[4 * 2 + 1] = if (partHeight > 0)
{
0;
}
else
{
(bottomHeight - clipHeight) / zoom / graphic.height;
};
// Top right
uvtData[5 * 2] = uvtData[4 * 2] + 1 / 8; // 25%/50%/75%/100% of the way through the image (1/8th past the top left of cap)
uvtData[5 * 2 + 1] = uvtData[4 * 2 + 1]; // top bound
// Bottom left
uvtData[6 * 2] = uvtData[4 * 2]; // 12.5%/37.5%/62.5%/87.5% of the way through the image (1/8th past the top left of hold)
uvtData[6 * 2 + 1] = bottomClip; // bottom bound
// Bottom right
uvtData[7 * 2] = uvtData[5 * 2]; // 25%/50%/75%/100% of the way through the image (1/8th past the top left of cap)
uvtData[7 * 2 + 1] = uvtData[6 * 2 + 1]; // bottom bound
}
@:access(flixel.FlxCamera)
override public function draw():Void
{
if (alpha == 0 || graphic == null || vertices == null) return;
for (camera in cameras)
{
if (!camera.visible || !camera.exists) continue;
// if (!isOnScreen(camera)) continue; // TODO: Update this code to make it work properly.
getScreenPosition(_point, camera).subtractPoint(offset);
camera.drawTriangles(processedGraphic, vertices, indices, uvtData, null, _point, blend, true, antialiasing);
}
}
public override function kill():Void
{
super.kill();
strumTime = 0;
noteDirection = 0;
sustainLength = 0;
fullSustainLength = 0;
noteData = null;
hitNote = false;
missedNote = false;
}
override public function destroy():Void
{
vertices = null;
indices = null;
uvtData = null;
processedGraphic.destroy();
super.destroy();
}
override function updateColorTransform():Void
{
super.updateColorTransform();
if (processedGraphic != null) processedGraphic.destroy();
processedGraphic = FlxGraphic.fromGraphic(graphic, true);
processedGraphic.bitmap.colorTransform(processedGraphic.bitmap.rect, colorTransform);
}
}

View file

@ -0,0 +1,304 @@
package funkin.play.notes.notestyle;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.graphics.frames.FlxFramesCollection;
import funkin.data.animation.AnimationData;
import funkin.data.IRegistryEntry;
import funkin.data.notestyle.NoteStyleData;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.util.assets.FlxAnimationUtil;
using funkin.data.animation.AnimationData.AnimationDataUtil;
/**
* Holds the data for what assets to use for a note style,
* and provides convenience methods for building sprites based on them.
*/
class NoteStyle implements IRegistryEntry<NoteStyleData>
{
/**
* The ID of the note style.
*/
public final id:String;
/**
* Note style data as parsed from the JSON file.
*/
public final _data:NoteStyleData;
/**
* The note style to use if this one doesn't have a certain asset.
* This can be recursive, ehe.
*/
final fallback:Null<NoteStyle>;
/**
* @param id The ID of the JSON file to parse.
*/
public function new(id:String)
{
this.id = id;
_data = _fetchData(id);
if (_data == null)
{
throw 'Could not parse note style data for id: $id';
}
this.fallback = NoteStyleRegistry.instance.fetchEntry(getFallbackID());
}
/**
* Get the readable name of the note style.
* @return String
*/
public function getName():String
{
return _data.name;
}
/**
* Get the author of the note style.
* @return String
*/
public function getAuthor():String
{
return _data.author;
}
/**
* Get the note style ID of the parent note style.
* @return The string ID, or `null` if there is no parent.
*/
function getFallbackID():Null<String>
{
return _data.fallback;
}
public function buildNoteSprite(target:NoteSprite):Void
{
// Apply the note sprite frames.
var atlas:FlxAtlasFrames = buildNoteFrames(false);
if (atlas == null)
{
throw 'Could not load spritesheet for note style: $id';
}
target.frames = atlas;
target.scale.x = _data.assets.note.scale;
target.scale.y = _data.assets.note.scale;
target.antialiasing = !_data.assets.note.isPixel;
// Apply the animations.
buildNoteAnimations(target);
}
var noteFrames:FlxAtlasFrames = null;
function buildNoteFrames(force:Bool = false):FlxAtlasFrames
{
if (noteFrames != null && !force) return noteFrames;
noteFrames = Paths.getSparrowAtlas(getNoteAssetPath(), getNoteAssetLibrary());
noteFrames.parent.persist = true;
return noteFrames;
}
function getNoteAssetPath(?raw:Bool = false):String
{
if (raw)
{
var rawPath:Null<String> = _data?.assets?.note?.assetPath;
if (rawPath == null) return fallback.getNoteAssetPath(true);
return rawPath;
}
// library:path
var parts = getNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR);
if (parts.length == 1) return getNoteAssetPath(true);
return parts[1];
}
function getNoteAssetLibrary():Null<String>
{
// library:path
var parts = getNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR);
if (parts.length == 1) return null;
return parts[0];
}
function buildNoteAnimations(target:NoteSprite):Void
{
var leftData:AnimationData = fetchNoteAnimationData(LEFT);
target.animation.addByPrefix('purpleScroll', leftData.prefix);
var downData:AnimationData = fetchNoteAnimationData(DOWN);
target.animation.addByPrefix('blueScroll', downData.prefix);
var upData:AnimationData = fetchNoteAnimationData(UP);
target.animation.addByPrefix('greenScroll', upData.prefix);
var rightData:AnimationData = fetchNoteAnimationData(RIGHT);
target.animation.addByPrefix('redScroll', rightData.prefix);
}
function fetchNoteAnimationData(dir:NoteDirection):AnimationData
{
var result:Null<AnimationData> = switch (dir)
{
case LEFT: _data.assets.note.data.left.toNamed();
case DOWN: _data.assets.note.data.down.toNamed();
case UP: _data.assets.note.data.up.toNamed();
case RIGHT: _data.assets.note.data.right.toNamed();
};
return (result == null) ? fallback.fetchNoteAnimationData(dir) : result;
}
public function getHoldNoteAssetPath(?raw:Bool = false):String
{
if (raw)
{
var rawPath:Null<String> = _data?.assets?.holdNote?.assetPath;
return (rawPath == null) ? fallback.getHoldNoteAssetPath(true) : rawPath;
}
// library:path
var parts = getHoldNoteAssetPath(true).split(Constants.LIBRARY_SEPARATOR);
if (parts.length == 1) return Paths.image(parts[0]);
return Paths.image(parts[1], parts[0]);
}
public function isHoldNotePixel():Bool
{
var data = _data?.assets?.holdNote;
if (data == null) return fallback.isHoldNotePixel();
return data.isPixel;
}
public function fetchHoldNoteScale():Float
{
var data = _data?.assets?.holdNote;
if (data == null) return fallback.fetchHoldNoteScale();
return data.scale;
}
public function applyStrumlineFrames(target:StrumlineNote):Void
{
// TODO: Add support for multi-Sparrow.
// Will be less annoying after this is merged: https://github.com/HaxeFlixel/flixel/pull/2772
var atlas:FlxAtlasFrames = Paths.getSparrowAtlas(getStrumlineAssetPath(), getStrumlineAssetLibrary());
if (atlas == null)
{
throw 'Could not load spritesheet for note style: $id';
}
target.frames = atlas;
target.scale.x = _data.assets.noteStrumline.scale;
target.scale.y = _data.assets.noteStrumline.scale;
target.antialiasing = !_data.assets.noteStrumline.isPixel;
}
function getStrumlineAssetPath(?raw:Bool = false):String
{
if (raw)
{
var rawPath:Null<String> = _data?.assets?.noteStrumline?.assetPath;
if (rawPath == null) return fallback.getStrumlineAssetPath(true);
return rawPath;
}
// library:path
var parts = getStrumlineAssetPath(true).split(Constants.LIBRARY_SEPARATOR);
if (parts.length == 1) return getStrumlineAssetPath(true);
return parts[1];
}
function getStrumlineAssetLibrary():Null<String>
{
// library:path
var parts = getStrumlineAssetPath(true).split(Constants.LIBRARY_SEPARATOR);
if (parts.length == 1) return null;
return parts[0];
}
public function applyStrumlineAnimations(target:StrumlineNote, dir:NoteDirection):Void
{
FlxAnimationUtil.addAtlasAnimations(target, getStrumlineAnimationData(dir));
}
function getStrumlineAnimationData(dir:NoteDirection):Array<AnimationData>
{
var result:Array<AnimationData> = switch (dir)
{
case NoteDirection.LEFT: [
_data.assets.noteStrumline.data.leftStatic.toNamed('static'),
_data.assets.noteStrumline.data.leftPress.toNamed('press'),
_data.assets.noteStrumline.data.leftConfirm.toNamed('confirm'),
_data.assets.noteStrumline.data.leftConfirmHold.toNamed('confirm-hold'),
];
case NoteDirection.DOWN: [
_data.assets.noteStrumline.data.downStatic.toNamed('static'),
_data.assets.noteStrumline.data.downPress.toNamed('press'),
_data.assets.noteStrumline.data.downConfirm.toNamed('confirm'),
_data.assets.noteStrumline.data.downConfirmHold.toNamed('confirm-hold'),
];
case NoteDirection.UP: [
_data.assets.noteStrumline.data.upStatic.toNamed('static'),
_data.assets.noteStrumline.data.upPress.toNamed('press'),
_data.assets.noteStrumline.data.upConfirm.toNamed('confirm'),
_data.assets.noteStrumline.data.upConfirmHold.toNamed('confirm-hold'),
];
case NoteDirection.RIGHT: [
_data.assets.noteStrumline.data.rightStatic.toNamed('static'),
_data.assets.noteStrumline.data.rightPress.toNamed('press'),
_data.assets.noteStrumline.data.rightConfirm.toNamed('confirm'),
_data.assets.noteStrumline.data.rightConfirmHold.toNamed('confirm-hold'),
];
};
return result;
}
public function applyStrumlineOffsets(target:StrumlineNote)
{
target.x += _data.assets.noteStrumline.offsets[0];
target.y += _data.assets.noteStrumline.offsets[1];
}
public function getStrumlineScale():Float
{
return _data.assets.noteStrumline.scale;
}
public function isNoteSplashEnabled():Bool
{
var data = _data?.assets?.noteSplash?.data;
if (data == null) return fallback.isNoteSplashEnabled();
return data.enabled;
}
public function isHoldNoteCoverEnabled():Bool
{
var data = _data?.assets?.holdNoteCover?.data;
if (data == null) return fallback.isHoldNoteCoverEnabled();
return data.enabled;
}
public function destroy():Void {}
public function toString():String
{
return 'NoteStyle($id)';
}
public function _fetchData(id:String):Null<NoteStyleData>
{
return NoteStyleRegistry.instance.parseEntryData(id);
}
}

View file

@ -0,0 +1,9 @@
package funkin.play.notes.notestyle;
/**
* A script that can be tied to a NoteStyle.
* Create a scripted class that extends NoteStyle to use this.
* This allows you to customize how a specific note style appears.
*/
@:hscriptClass
class ScriptedNoteStyle extends funkin.play.notes.notestyle.NoteStyle implements polymod.hscript.HScriptedClass {}

View file

@ -283,8 +283,9 @@ class SongDifficulty
return timeChanges[0].bpm;
}
public function getPlayableChar(id:String):SongPlayableChar
public function getPlayableChar(id:String):Null<SongPlayableChar>
{
if (id == null || id == '') return null;
return chars.get(id);
}
@ -298,9 +299,17 @@ class SongDifficulty
return cast events;
}
public inline function cacheInst():Void
public inline function cacheInst(?currentPlayerId:String = null):Void
{
FlxG.sound.cache(Paths.inst(this.song.songId));
var currentPlayer:Null<SongPlayableChar> = getPlayableChar(currentPlayerId);
if (currentPlayer != null)
{
FlxG.sound.cache(Paths.inst(this.song.songId, currentPlayer.inst));
}
else
{
FlxG.sound.cache(Paths.inst(this.song.songId));
}
}
public inline function playInst(volume:Float = 1.0, looped:Bool = false):Void

View file

@ -459,6 +459,12 @@ abstract SongNoteData(RawSongNoteData)
return Math.floor(abstract.data / strumlineSize);
}
/**
* Returns true if the note is one that Boyfriend should try to hit (i.e. it's on his side).
* TODO: The name of this function is a little misleading; what about mines?
* @param strumlineSize Defaults to 4.
* @return True if it's Boyfriend's note.
*/
public inline function getMustHitNote(strumlineSize:Int = 4):Bool
{
return getStrumlineIndex(strumlineSize) == 0;
@ -488,6 +494,13 @@ abstract SongNoteData(RawSongNoteData)
return abstract.length = Conductor.getStepTimeInMs(value) - abstract.time;
}
public var isHoldNote(get, never):Bool;
public function get_isHoldNote():Bool
{
return this.l > 0;
}
public var kind(get, set):String;
function get_kind():String

View file

@ -5,7 +5,6 @@ import funkin.play.song.SongData.SongMetadata;
import funkin.play.song.SongData.SongPlayData;
import funkin.play.song.SongData.SongTimeChange;
import funkin.play.song.SongData.SongTimeFormat;
import funkin.util.Constants;
/**
* For SongMetadata and SongChartData objects,

View file

@ -1,5 +1,6 @@
package funkin.play.stage;
import funkin.data.animation.AnimationData;
import flixel.util.typeLimit.OneOfTwo;
import funkin.play.stage.ScriptedStage;
import funkin.play.stage.Stage;

View file

@ -171,7 +171,6 @@ class AtlasChar extends FlxSprite
super(x, y);
frames = atlas;
this.char = char;
antialiasing = true;
}
function set_char(value:String)
@ -179,8 +178,15 @@ class AtlasChar extends FlxSprite
if (this.char != value)
{
var prefix = getAnimPrefix(value);
animation.addByPrefix("anim", prefix, 24);
animation.play("anim");
animation.addByPrefix('anim', prefix, 24);
if (animation.exists('anim'))
{
animation.play('anim');
}
else
{
trace('Could not find animation for char "' + value + '"');
}
updateHitbox();
}

View file

@ -1,27 +1,29 @@
package funkin.ui;
import funkin.data.notestyle.NoteStyleRegistry;
import flixel.addons.effects.chainable.FlxEffectSprite;
import flixel.addons.effects.chainable.FlxOutlineEffect;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.util.FlxColor;
import funkin.ui.OptionsState.Page;
import funkin.play.notes.NoteSprite;
class ColorsMenu extends Page
{
var curSelected:Int = 0;
var grpNotes:FlxTypedGroup<Note>;
var grpNotes:FlxTypedGroup<NoteSprite>;
public function new()
{
super();
grpNotes = new FlxTypedGroup<Note>();
grpNotes = new FlxTypedGroup<NoteSprite>();
add(grpNotes);
for (i in 0...4)
{
var note:Note = new Note(0, i);
var note:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault(), 0, i);
note.x = (100 * i) + i;
note.screenCenter(Y);
@ -30,7 +32,6 @@ class ColorsMenu extends Page
add(_effectSpr);
_effectSpr.y = 0;
_effectSpr.x = i * 130;
_effectSpr.antialiasing = true;
_effectSpr.scale.x = _effectSpr.scale.y = 0.7;
// _effectSpr.setGraphicSize();
_effectSpr.height = note.height;
@ -52,14 +53,14 @@ class ColorsMenu extends Page
if (controls.UI_UP)
{
grpNotes.members[curSelected].colorSwap.update(elapsed * 0.3);
Note.arrowColors[curSelected] += elapsed * 0.3;
// grpNotes.members[curSelected].colorSwap.update(elapsed * 0.3);
// Note.arrowColors[curSelected] += elapsed * 0.3;
}
if (controls.UI_DOWN)
{
grpNotes.members[curSelected].colorSwap.update(-elapsed * 0.3);
Note.arrowColors[curSelected] += -elapsed * 0.3;
// grpNotes.members[curSelected].colorSwap.update(-elapsed * 0.3);
// Note.arrowColors[curSelected] += -elapsed * 0.3;
}
super.update(elapsed);

View file

@ -225,7 +225,6 @@ class MenuItem extends FlxSprite
{
super(x, y);
antialiasing = true;
setData(name, callback);
idle();
}

View file

@ -5,7 +5,6 @@ import flixel.FlxSubState;
import flixel.addons.transition.FlxTransitionableState;
import flixel.group.FlxGroup;
import flixel.util.FlxSignal;
import funkin.util.Constants;
import funkin.util.WindowUtil;
class OptionsState extends MusicBeatState

View file

@ -4,7 +4,6 @@ import flixel.FlxSprite;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.tweens.FlxTween;
import funkin.play.PlayState;
import funkin.util.Constants;
class PopUpStuff extends FlxTypedGroup<FlxSprite>
{
@ -45,6 +44,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
if (PlayState.instance.currentStageId.startsWith('school'))
{
rating.setGraphicSize(Std.int(rating.width * Constants.PIXEL_ART_SCALE * 0.7));
rating.antialiasing = false;
}
else
{
@ -95,6 +95,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
if (PlayState.instance.currentStageId.startsWith('school'))
{
comboSpr.setGraphicSize(Std.int(comboSpr.width * Constants.PIXEL_ART_SCALE * 0.7));
comboSpr.antialiasing = false;
}
else
{
@ -134,11 +135,12 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
if (PlayState.instance.currentStageId.startsWith('school'))
{
numScore.setGraphicSize(Std.int(numScore.width * Constants.PIXEL_ART_SCALE));
numScore.antialiasing = false;
}
else
{
numScore.antialiasing = true;
numScore.setGraphicSize(Std.int(numScore.width * 0.5));
numScore.antialiasing = true;
}
numScore.updateHitbox();

View file

@ -177,8 +177,6 @@ class CheckboxThingie extends FlxSprite
animation.addByPrefix('static', 'Check Box unselected', 24, false);
animation.addByPrefix('checked', 'Check Box selecting animation', 24, false);
antialiasing = true;
setGraphicSize(Std.int(width * 0.7));
updateHitbox();

View file

@ -44,7 +44,6 @@ class StickerSubState extends MusicBeatSubState
for (sticker in oldStickers)
{
grpStickers.add(sticker);
trace(sticker);
}
degenStickers();
@ -89,31 +88,6 @@ class StickerSubState extends MusicBeatSubState
for (stickerSets in stickerInfo.getPack("all"))
{
stickers.set(stickerSets, stickerInfo.getStickers(stickerSets));
trace(stickers);
// for (stickerShit in stickerInfo.getStickers(stickerSets))
// {
// // for loop jus to repeat it easy easy easy
// for (i in 0...FlxG.random.int(1, 5))
// {
// var sticky:StickerSprite = new StickerSprite(0, 0, stickerInfo.name, stickerShit);
// sticky.x -= sticky.width / 2;
// sticky.y -= sticky.height * 0.9;
// // random location by default
// sticky.x += FlxG.random.float(0, FlxG.width);
// sticky.y += FlxG.random.float(0, FlxG.height);
// sticky.visible = false;
// sticky.scrollFactor.set();
// sticky.angle = FlxG.random.int(-60, 70);
// // sticky.flipX = FlxG.random.bool();
// grpStickers.add(sticky);
// sticky.timing = FlxG.random.float(0, 0.8);
// }
// }
}
var xPos:Float = -100;
@ -185,6 +159,8 @@ class StickerSubState extends MusicBeatSubState
if (ind == grpStickers.members.length - 1) frameTimer = 2;
new FlxTimer().start((1 / 24) * frameTimer, _ -> {
if (sticker == null) return;
sticker.scale.x = sticker.scale.y = FlxG.random.float(0.97, 1.02);
if (ind == grpStickers.members.length - 1)
@ -266,7 +242,6 @@ class StickerSprite extends FlxSprite
super(x, y);
loadGraphic(Paths.image('transitionSwag/' + stickerSet + '/' + stickerName));
updateHitbox();
antialiasing = true;
scrollFactor.set();
}
}
@ -282,7 +257,6 @@ class StickerInfo
{
var path = Paths.file('images/transitionSwag/' + stickerSet + '/stickers.json');
var json = Json.parse(Assets.getText(path));
trace(json);
// doin this dipshit nonsense cuz i dunno how to deal with casting a json object with
// a dash in its name (sticker-packs)
@ -299,13 +273,8 @@ class StickerInfo
var stickerStuff = Reflect.field(stickerFunny, field);
stickerPacks.set(field, cast stickerStuff);
trace(field);
trace(Reflect.field(stickerFunny, field));
}
trace(stickerPacks);
// creates a similar for loop as before but for the stickers
stickers = new Map<String, Array<String>>();
@ -315,24 +284,7 @@ class StickerInfo
var stickerStuff = Reflect.field(stickerFunny, field);
stickers.set(field, cast stickerStuff);
trace(field);
trace(Reflect.field(stickerFunny, field));
}
trace(stickers);
// this.stickerPacks = cast jsonInfo.stickerPacks;
// this.stickers = cast jsonInfo.stickers;
// trace(stickerPacks);
// trace(stickers);
// for (packs in stickers)
// {
// // this.stickers.set(packs, Reflect.field(json, "sticker-packs"));
// trace(packs);
// }
}
public function getStickers(stickerName:String):Array<String>

View file

@ -80,7 +80,6 @@ class TallyNumber extends FlxSprite
animation.addByPrefix(Std.string(i), i + " small", 24, false);
animation.play(Std.string(digit));
antialiasing = true;
updateHitbox();
}
}

View file

@ -143,7 +143,6 @@ class DebugBoundingState extends FlxState
addInfo('Width', bf.width);
addInfo('Height', bf.height);
swagOutlines.antialiasing = true;
spriteSheetView.add(swagOutlines);
FlxG.stage.window.onDropFile.add(function(path:String) {

View file

@ -2,6 +2,7 @@ package funkin.ui.debug.charting;
import funkin.graphics.rendering.SustainTrail;
import funkin.util.SortUtil;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.ui.debug.charting.ChartEditorCommand;
import flixel.input.keyboard.FlxKey;
import funkin.input.TurboKeyHandler;
@ -24,6 +25,7 @@ import funkin.audio.VoicesGroup;
import funkin.input.Cursor;
import funkin.modding.events.ScriptEvent;
import funkin.play.HealthIcon;
import funkin.play.notes.NoteSprite;
import funkin.play.song.Song;
import funkin.play.song.SongData.SongChartData;
import funkin.play.song.SongData.SongDataParser;
@ -35,7 +37,6 @@ import funkin.ui.debug.charting.ChartEditorThemeHandler.ChartEditorTheme;
import funkin.ui.debug.charting.ChartEditorToolboxHandler.ChartEditorToolMode;
import funkin.ui.haxeui.components.CharacterPlayer;
import funkin.ui.haxeui.HaxeUIState;
import funkin.util.Constants;
import funkin.util.FileUtil;
import funkin.util.DateUtil;
import funkin.util.SerializerUtil;
@ -2850,11 +2851,9 @@ class ChartEditorState extends HaxeUIState
// Character preview.
// Why does NOTESCRIPTEVENT TAKE A SPRITE AAAAA
var tempNote:Note = new Note(noteData.time, noteData.data, null, false, NORMAL);
tempNote.mustPress = noteData.getMustHitNote();
tempNote.data.sustainLength = noteData.length;
tempNote.data.noteKind = noteData.kind;
// NoteScriptEvent takes a sprite, ehe. Need to rework that.
var tempNote:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault());
tempNote.noteData = noteData;
tempNote.scrollFactor.set(0, 0);
var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, tempNote, 1, true);
dispatchEvent(event);

View file

@ -17,7 +17,6 @@ import funkin.play.PlayStatePlaylist;
import funkin.play.song.Song;
import funkin.play.song.SongData.SongMetadata;
import funkin.play.song.SongData.SongDataParser;
import funkin.util.Constants;
class StoryMenuState extends MusicBeatState
{

View file

@ -88,9 +88,9 @@ class Constants
public static final COLOR_HEALTH_BAR_GREEN:FlxColor = 0xFF66FF33;
/**
* Default variation for charts.
* The base colors of the notes.
*/
public static final DEFAULT_VARIATION:String = 'default';
public static final COLOR_NOTES:Array<FlxColor> = [0xFFFF22AA, 0xFF00EEFF, 0xFF00CC00, 0xFFCC1111];
/**
* STAGE DEFAULTS
@ -118,10 +118,95 @@ class Constants
public static final DEFAULT_SONG:String = 'tutorial';
/**
* TIMING
* Default variation for charts.
*/
public static final DEFAULT_VARIATION:String = 'default';
/**
* HEALTH VALUES
*/
// ==============================
/**
* The player's maximum health.
* If the player is at this value, they can't gain any more health.
*/
public static final HEALTH_MAX:Float = 2.0;
/**
* The player's starting health.
*/
public static final HEALTH_STARTING = HEALTH_MAX / 2.0;
/**
* The player's minimum health.
* If the player is at or below this value, they lose.
*/
public static final HEALTH_MIN:Float = 0.0;
/**
* The amount of health the player gains when hitting a note with the KILLER rating.
*/
public static final HEALTH_KILLER_BONUS:Float = 2.0 / 100.0 * HEALTH_MAX; // +2.0%
/**
* The amount of health the player gains when hitting a note with the SICK rating.
*/
public static final HEALTH_SICK_BONUS:Float = 1.5 / 100.0 * HEALTH_MAX; // +1.0%
/**
* The amount of health the player gains when hitting a note with the GOOD rating.
*/
public static final HEALTH_GOOD_BONUS:Float = 0.75 / 100.0 * HEALTH_MAX; // +0.75%
/**
* The amount of health the player gains when hitting a note with the BAD rating.
*/
public static final HEALTH_BAD_BONUS:Float = 0.0 / 100.0 * HEALTH_MAX; // +0.0%
/**
* The amount of health the player gains when hitting a note with the SHIT rating.
* If negative, the player will actually lose health.
*/
public static final HEALTH_SHIT_BONUS:Float = -1.0 / 100.0 * HEALTH_MAX; // -1.0%
/**
* The amount of health the player gains, while holding a hold note, per second.
*/
public static final HEALTH_HOLD_BONUS_PER_SECOND:Float = 7.5 / 100.0 * HEALTH_MAX; // +7.5% / second
/**
* The amount of health the player loses upon missing a note.
*/
public static final HEALTH_MISS_PENALTY:Float = 4.0 / 100.0 * HEALTH_MAX; // 4.0%
/**
* The amount of health the player loses upon pressing a key when no note is there.
*/
public static final HEALTH_GHOST_MISS_PENALTY:Float = 2.0 / 100.0 * HEALTH_MAX; // 2.0%
/**
* The amount of health the player loses upon letting go of a hold note while it is still going.
*/
public static final HEALTH_HOLD_DROP_PENALTY:Float = 0.0; // 0.0%
/**
* The amount of health the player loses upon hitting a mine.
*/
public static final HEALTH_MINE_PENALTY:Float = 15.0 / 100.0 * HEALTH_MAX; // 15.0%
/**
* If true, the player will not receive the ghost miss penalty if there are no notes within the hit window.
* This is the thing people have been begging for forever lolol.
*/
public static final GHOST_TAPPING:Bool = false;
/**
* OTHER
*/
// ==============================
public static final LIBRARY_SEPARATOR:String = ':';
/**
* The number of seconds in a minute.
*/
@ -189,6 +274,9 @@ class Constants
*/
public static final COUNTDOWN_VOLUME:Float = 0.6;
public static final STRUMLINE_X_OFFSET:Float = 48;
public static final STRUMLINE_Y_OFFSET:Float = 24;
/**
* The default intensity for camera zooms.
*/

View file

@ -4,6 +4,7 @@ package funkin.util;
import flixel.FlxBasic;
import flixel.util.FlxSort;
#end
import funkin.play.notes.NoteSprite;
class SortUtil
{
@ -22,9 +23,9 @@ class SortUtil
*
* @param order Either `FlxSort.ASCENDING` or `FlxSort.DESCENDING`
*/
public static inline function byStrumtime(order:Int, a:Note, b:Note)
public static inline function byStrumtime(order:Int, a:NoteSprite, b:NoteSprite)
{
return FlxSort.byValues(order, a.data.strumTime, b.data.strumTime);
return FlxSort.byValues(order, a.noteData.time, b.noteData.time);
}
/**

View file

@ -11,17 +11,21 @@ import flixel.util.FlxSignal.FlxTypedSignal;
#end
class WindowUtil
{
/**
* Runs platform-specific code to open a URL in a web browser.
* @param targetUrl The URL to open.
*/
public static function openURL(targetUrl:String)
{
#if CAN_OPEN_LINKS
#if linux
// Sys.command('/usr/bin/xdg-open', [, "&"]);
Sys.command('/usr/bin/xdg-open', [targetUrl, "&"]);
#else
// This should work on Windows and HTML5.
FlxG.openURL(targetUrl);
#end
#else
trace('Cannot open');
throw 'Cannot open URLs on this platform.';
#end
}
@ -30,6 +34,10 @@ class WindowUtil
*/
public static final windowExit:FlxTypedSignal<Int->Void> = new FlxTypedSignal<Int->Void>();
/**
* Wires up FlxSignals that happen based on window activity.
* For example, we can run a callback when the window is closed.
*/
public static function initWindowEvents()
{
// onUpdate is called every frame just before rendering.
@ -51,4 +59,13 @@ class WindowUtil
// Do nothing.
#end
}
/**
* Sets the title of the application window.
* @param value The title to use.
*/
public static function setWindowTitle(value:String):Void
{
lime.app.Application.current.window.title = value;
}
}

View file

@ -2,12 +2,12 @@ package funkin.util.assets;
import flixel.FlxSprite;
import flixel.graphics.frames.FlxFramesCollection;
import funkin.play.AnimationData;
import funkin.data.animation.AnimationData;
class FlxAnimationUtil
{
/**
* Properly adds an animation to a sprite based on JSON data.
* Properly adds an animation to a sprite based on the provided animation data.
*/
public static function addAtlasAnimation(target:FlxSprite, anim:AnimationData)
{
@ -31,7 +31,7 @@ class FlxAnimationUtil
}
/**
* Properly adds multiple animations to a sprite based on JSON data.
* Properly adds multiple animations to a sprite based on the provided animation data.
*/
public static function addAtlasAnimations(target:FlxSprite, animations:Array<AnimationData>)
{

View file

@ -0,0 +1,154 @@
package funkin.util.tools;
/**
* Contains code for sorting arrays using various algorithms.
* @see https://algs4.cs.princeton.edu/20sorting/
*/
class ArraySortTools
{
/**
* Sorts the input array using the merge sort algorithm.
* Stable and guaranteed to run in linearithmic time `O(n log n)`,
* but less efficient in "best-case" situations.
*
* @param input The array to sort in-place.
* @param compare The comparison function to use.
*/
public static function mergeSort<T>(input:Array<T>, compare:CompareFunction<T>):Void
{
if (input == null || input.length <= 1) return;
if (compare == null) throw 'No comparison function provided.';
// Haxe implements merge sort by default.
haxe.ds.ArraySort.sort(input, compare);
}
/**
* Sorts the input array using the quick sort algorithm.
* More efficient on smaller arrays, but is inefficient `O(n^2)` in "worst-case" situations.
* Not stable; relative order of equal elements is not preserved.
*
* @see https://stackoverflow.com/questions/33884057/quick-sort-stackoverflow-error-for-large-arrays
* Fix for stack overflow issues.
* @param input The array to sort in-place.
* @param compare The comparison function to use.
*/
public static function quickSort<T>(input:Array<T>, compare:CompareFunction<T>):Void
{
if (input == null || input.length <= 1) return;
if (compare == null) throw 'No comparison function provided.';
quickSortInner(input, 0, input.length - 1, compare);
}
/**
* Internal recursive function for the quick sort algorithm.
* Written with ChatGPT!
*/
static function quickSortInner<T>(input:Array<T>, low:Int, high:Int, compare:CompareFunction<T>):Void
{
// When low == high, the array is empty or too small to sort.
// EDIT: Recurse on the smaller partition, and loop for the larger partition.
while (low < high)
{
// Designate the first element in the array as the pivot, then partition the array around it.
// Elements less than the pivot will be to the left, and elements greater than the pivot will be to the right.
// Return the index of the pivot.
var pivot:Int = quickSortPartition(input, low, high, compare);
if ((pivot) - low <= high - (pivot + 1))
{
quickSortInner(input, low, pivot, compare);
low = pivot + 1;
}
else
{
quickSortInner(input, pivot + 1, high, compare);
high = pivot;
}
}
}
/**
* Internal function for sorting a partition of an array in the quick sort algorithm.
* Written with ChatGPT!
*/
static function quickSortPartition<T>(input:Array<T>, low:Int, high:Int, compare:CompareFunction<T>):Int
{
// Designate the first element in the array as the pivot.
var pivot:T = input[low];
// Designate two pointers, used to divide the array into two partitions.
var i:Int = low - 1;
var j:Int = high + 1;
while (true)
{
// Move the left pointer to the right until it finds an element greater than the pivot.
do
{
i++;
}
while (compare(input[i], pivot) < 0);
// Move the right pointer to the left until it finds an element less than the pivot.
do
{
j--;
}
while (compare(input[j], pivot) > 0);
// If i and j have crossed, the array has been partitioned, and the pivot will be at the index j.
if (i >= j) return j;
// Else, swap the elements at i and j, and start over.
// This slowly moves the pivot towards the middle of the partition,
// while moving elements less than the pivot to the left and elements greater than the pivot to the right.
var temp:T = input[i];
input[i] = input[j];
input[j] = temp;
}
}
/**
* Sorts the input array using the insertion sort algorithm.
* Stable and is very fast on nearly-sorted arrays,
* but is inefficient `O(n^2)` in "worst-case" situations.
*
* @param input The array to sort in-place.
* @param compare The comparison function to use.
*/
public static function insertionSort<T>(input:Array<T>, compare:CompareFunction<T>):Void
{
if (input == null || input.length <= 1) return;
if (compare == null) throw 'No comparison function provided.';
// Iterate through the array, starting at the second element.
for (i in 1...input.length)
{
// Store the current element.
var current:T = input[i];
// Store the index of the previous element.
var j:Int = i - 1;
// While the previous element is greater than the current element,
// move the previous element to the right and move the index to the left.
while (j >= 0 && compare(input[j], current) > 0)
{
input[j + 1] = input[j];
j--;
}
// Insert the current element into the array.
input[j + 1] = current;
}
}
}
/**
* A comparison function.
* Returns a negative number if the first argument is less than the second,
* a positive number if the first argument is greater than the second,
* or zero if the two arguments are equal.
*/
typedef CompareFunction<T> = T->T->Int;

View file

@ -22,4 +22,19 @@ class ArrayTools
}
return result;
}
/**
* Return the first element of the array that satisfies the predicate, or null if none do.
* @param input The array to search
* @param predicate The predicate to call
* @return The result
*/
public static function find<T>(input:Array<T>, predicate:T->Bool):Null<T>
{
for (element in input)
{
if (predicate(element)) return element;
}
return null;
}
}