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:
commit
aaad06c97d
.github/workflows
.vscode
hmm.jsonsource
Main.hx
funkin
Alphabet.hxComboMilestone.hxConductor.hxControls.hxDialogueBox.hxFreeplayState.hxHighscore.hxInitState.hxLatencyState.hxLoadingState.hxMainMenuState.hxMusicBeatState.hxNote.hxNoteSplash.hxPlayerSettings.hxSection.hxSongLoad.hxTitleState.hx
audio
data
freeplayStuff
graphics
import.hxinput
modding
noteStuff
play
Countdown.hxPlayState.hxPlayStatePlaylist.hxResultState.hxStrumline.hx
character
cutscene/dialogue
ConversationDebugState.hxDialogueBoxData.hxDialogueBoxDataParser.hxScriptedSpeaker.hxSpeakerData.hxSpeakerDataParser.hx
event
notes
NoteDirection.hxNoteHoldCover.hxNoteSplash.hxNoteSprite.hxStrumline.hxStrumlineNote.hxSustainTrail.hx
notestyle
song
stage
ui
AtlasText.hxColorsMenu.hxMenuList.hxOptionsState.hxPopUpStuff.hxPreferencesMenu.hxStickerSubState.hxTallyCounter.hx
animDebugShit
debug/charting
story
util
25
.github/workflows/build-shit.yml
vendored
25
.github/workflows/build-shit.yml
vendored
|
@ -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
|
||||
|
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -116,5 +116,6 @@
|
|||
"target": "html5",
|
||||
"args": ["-debug", "-watch"]
|
||||
}
|
||||
]
|
||||
],
|
||||
"cmake.configureOnOpen": false
|
||||
}
|
||||
|
|
12
hmm.json
12
hmm.json
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package funkin;
|
||||
|
||||
import funkin.util.Constants;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.addons.text.FlxTypeText;
|
||||
import flixel.group.FlxSpriteGroup;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -65,4 +65,11 @@ class VoicesGroup extends SoundGroup
|
|||
opponentVoices.clear();
|
||||
super.clear();
|
||||
}
|
||||
|
||||
public override function destroy():Void
|
||||
{
|
||||
playerVoices.destroy();
|
||||
opponentVoices.destroy();
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
|
@ -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.
|
||||
|
|
171
source/funkin/data/notestyle/NoteStyleData.hx
Normal file
171
source/funkin/data/notestyle/NoteStyleData.hx
Normal 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;
|
||||
};
|
65
source/funkin/data/notestyle/NoteStyleRegistry.hx
Normal file
65
source/funkin/data/notestyle/NoteStyleRegistry.hx
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
303
source/funkin/input/PreciseInputManager.hx
Normal file
303
source/funkin/input/PreciseInputManager.hx
Normal 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,
|
||||
};
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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}' : ''}';
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
import funkin.data.animation.AnimationData;
|
||||
import funkin.util.SerializerUtil;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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.
|
||||
* ```
|
||||
* {
|
||||
|
|
81
source/funkin/play/notes/NoteDirection.hx
Normal file
81
source/funkin/play/notes/NoteDirection.hx
Normal 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;
|
||||
}
|
||||
}
|
141
source/funkin/play/notes/NoteHoldCover.hx
Normal file
141
source/funkin/play/notes/NoteHoldCover.hx
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
89
source/funkin/play/notes/NoteSplash.hx
Normal file
89
source/funkin/play/notes/NoteSplash.hx
Normal 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();
|
||||
}
|
||||
}
|
153
source/funkin/play/notes/NoteSprite.hx
Normal file
153
source/funkin/play/notes/NoteSprite.hx
Normal 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();
|
||||
}
|
||||
}
|
737
source/funkin/play/notes/Strumline.hx
Normal file
737
source/funkin/play/notes/Strumline.hx
Normal 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);
|
||||
}
|
||||
}
|
179
source/funkin/play/notes/StrumlineNote.hx
Normal file
179
source/funkin/play/notes/StrumlineNote.hx
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
302
source/funkin/play/notes/SustainTrail.hx
Normal file
302
source/funkin/play/notes/SustainTrail.hx
Normal 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);
|
||||
}
|
||||
}
|
304
source/funkin/play/notes/notestyle/NoteStyle.hx
Normal file
304
source/funkin/play/notes/notestyle/NoteStyle.hx
Normal 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);
|
||||
}
|
||||
}
|
9
source/funkin/play/notes/notestyle/ScriptedNoteStyle.hx
Normal file
9
source/funkin/play/notes/notestyle/ScriptedNoteStyle.hx
Normal 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 {}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -225,7 +225,6 @@ class MenuItem extends FlxSprite
|
|||
{
|
||||
super(x, y);
|
||||
|
||||
antialiasing = true;
|
||||
setData(name, callback);
|
||||
idle();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>)
|
||||
{
|
||||
|
|
154
source/funkin/util/tools/ArraySortTools.hx
Normal file
154
source/funkin/util/tools/ArraySortTools.hx
Normal 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;
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue