1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-03-23 02:19:46 +00:00

Merge remote-tracking branch 'origin/rewrite/master' into rewrite/feature/remember-difficulty

This commit is contained in:
EliteMasterEric 2023-10-17 17:32:14 -04:00
commit 4a6904d52c
41 changed files with 1524 additions and 538 deletions

View file

@ -23,8 +23,6 @@ runs:
with:
path: .haxelib
key: ${{ runner.os }}-hmm-${{ hashFiles('**/hmm.json') }}
restore-keys: |
${{ runner.os }}-hmm-
- if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }}
name: hmm install
run: |

View file

@ -53,9 +53,8 @@ jobs:
token: ${{ secrets.GH_RO_PAT }}
- uses: ./.github/actions/setup-haxeshit
- name: Make HXCPP cache dir
shell: bash
run: |
mkdir -p ${{ runner.temp }}\\hxcpp_cache
mkdir -p ${{ runner.temp }}\hxcpp_cache
- name: Restore build cache
id: cache-build-win
uses: actions/cache@v3
@ -63,10 +62,8 @@ jobs:
path: |
.haxelib
export
${{ runner.temp }}\\hxcpp_cache
key: ${{ runner.os }}-build-win-${{ github.ref_name }}
restore-keys: |
${{ runner.os }}-build-win-
${{ runner.temp }}\hxcpp_cache
key: ${{ runner.os }}-build-win-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
- name: Build game
run: |
haxelib run lime build windows -release -DNO_REDIRECT_ASSETS_FOLDER

View file

@ -156,7 +156,6 @@
<haxedef name="HXCPP_CHECK_POINTER" />
<haxedef name="HXCPP_STACK_LINE" />
<haxedef name="HXCPP_STACK_TRACE" />
<haxedef name="openfl-enable-handle-error" />
<!-- This macro allows addition of new functionality to existing Flixel. -->
<haxeflag name="--macro" value="addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxBasic())', 'flixel.FlxBasic')" />
<!--Place custom nodes like icons here (higher priority to override the HaxeFlixel icon)-->
@ -196,6 +195,22 @@
<haxedef name="REDIRECT_ASSETS_FOLDER" />
</section>
<section>
<!--
This flag enables the popup/crashlog error handler.
However, it also messes with breakpoints on some platforms.
-->
<haxedef name="openfl-enable-handle-error" />
</section>
<section>
<!-- TODO: Add a flag to Github Actions to turn this on or something. -->
<!-- Forces the version string to include the Git hash even on release builds (which are used for performance reasons). -->
<haxedef name="FORCE_DEBUG_VERSION" />
</section>
<!-- Run a script before and after building. -->
<postbuild haxe="source/Prebuild.hx"/> -->
<postbuild haxe="source/Postbuild.hx"/> -->

2
assets

@ -1 +1 @@
Subproject commit 486ea1cdc37a1f1907ba9231b0a1946ff4051f27
Subproject commit 15a238b4c59914849df282f9ce2eec5b80030207

View file

@ -104,7 +104,7 @@
"name": "lime",
"type": "git",
"dir": null,
"ref": "f195121ebec688b417e38ab115185c8d93c349d3",
"ref": "737b86f121cdc90358d59e2e527934f267c94a2c",
"url": "https://github.com/EliteMasterEric/lime"
},
{
@ -139,7 +139,7 @@
"name": "openfl",
"type": "git",
"dir": null,
"ref": "de9395d2f367a80f93f082e1b639b9cde2258bf1",
"ref": "f229d76361c7e31025a048fe7909847f75bb5d5e",
"url": "https://github.com/EliteMasterEric/openfl"
},
{

View file

@ -85,6 +85,13 @@ class Main extends Sprite
initHaxeUI();
fpsCounter = new FPS(10, 3, 0xFFFFFF);
// addChild(fpsCounter); // Handled by Preferences.init
#if !html5
memoryCounter = new MemoryCounter(10, 13, 0xFFFFFF);
// addChild(memoryCounter);
#end
// George recommends binding the save before FlxGame is created.
Save.load();
@ -93,15 +100,6 @@ class Main extends Sprite
#if hxcpp_debug_server
trace('hxcpp_debug_server is enabled! You can now connect to the game with a debugger.');
#end
#if debug
fpsCounter = new FPS(10, 3, 0xFFFFFF);
addChild(fpsCounter);
#if !html5
memoryCounter = new MemoryCounter(10, 13, 0xFFFFFF);
addChild(memoryCounter);
#end
#end
}
function initHaxeUI():Void

View file

@ -1,5 +1,7 @@
package funkin;
import flixel.input.gamepad.FlxGamepad;
import flixel.util.FlxDirectionFlags;
import flixel.FlxObject;
import flixel.input.FlxInput;
@ -832,6 +834,14 @@ class Controls extends FlxActionSet
fromSaveData(padData, Gamepad(id));
}
public function getGamepadIds():Array<Int> {
return gamepadsAdded;
}
public function getGamepads():Array<FlxGamepad> {
return [for (id in gamepadsAdded) FlxG.gamepads.getByID(id)];
}
inline function addGamepadLiteral(id:Int, ?buttonMap:Map<Control, Array<FlxGamepadInputID>>):Void
{
gamepadsAdded.push(id);

View file

@ -557,7 +557,7 @@ class FreeplayState extends MusicBeatSubState
var randomCapsule:SongMenuItem = grpCapsules.recycle(SongMenuItem);
randomCapsule.init(FlxG.width, 0, "Random");
randomCapsule.onConfirm = function() {
trace("RANDOM SELECTED");
capsuleOnConfirmRandom(randomCapsule);
};
randomCapsule.y = randomCapsule.intendedY(0) + 10;
randomCapsule.targetPos.x = randomCapsule.x;
@ -616,6 +616,8 @@ class FreeplayState extends MusicBeatSubState
var spamTimer:Float = 0;
var spamming:Bool = false;
var busy:Bool = false; // Set to true once the user has pressed enter to select a song.
override function update(elapsed:Float)
{
super.update(elapsed);
@ -667,6 +669,13 @@ class FreeplayState extends MusicBeatSubState
txtCompletion.text = Math.floor(lerpCompletion * 100) + "%";
handleInputs(elapsed);
}
function handleInputs(elapsed:Float):Void
{
if (busy) return;
var upP = controls.UI_UP_P;
var downP = controls.UI_DOWN_P;
var accepted = controls.ACCEPT;
@ -928,7 +937,7 @@ class FreeplayState extends MusicBeatSubState
{
for (song in songs)
{
if (song == null) return;
if (song == null) continue;
if (song.songName != actualSongTho)
{
trace('trying to remove: ' + song.songName);
@ -937,8 +946,17 @@ class FreeplayState extends MusicBeatSubState
}
}
function capsuleOnConfirmRandom(cap:SongMenuItem):Void
{
trace("RANDOM SELECTED");
busy = true;
}
function capsuleOnConfirmDefault(cap:SongMenuItem):Void
{
busy = true;
PlayStatePlaylist.isStoryMode = false;
var songId:String = cap.songTitle.toLowerCase();
@ -963,6 +981,7 @@ class FreeplayState extends MusicBeatSubState
targetSong.cacheCharts(true);
new FlxTimer().start(1, function(tmr:FlxTimer) {
Paths.setCurrentLevel(songs[curSelected].levelId);
LoadingState.loadAndSwitchState(new PlayState(
{
targetSong: targetSong,

View file

@ -48,7 +48,7 @@ class InitState extends FlxState
// loadSaveData(); // Moved to Main.hx
// Load player options from save data.
PreferencesMenu.initPrefs();
Preferences.init();
// Load controls from save data.
PlayerSettings.init();

View file

@ -13,23 +13,13 @@ import flixel.input.touch.FlxTouch;
import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.NGio;
import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
import funkin.modding.module.ModuleHandler;
import funkin.shaderslmfao.ScreenWipeShader;
import funkin.ui.AtlasMenuList;
import funkin.ui.MenuList.MenuItem;
import funkin.ui.MenuList;
import funkin.ui.title.TitleState;
import funkin.ui.story.StoryMenuState;
import funkin.ui.OptionsState;
import funkin.ui.PreferencesMenu;
import funkin.ui.Prompt;
import funkin.util.WindowUtil;
import lime.app.Application;
import openfl.filters.ShaderFilter;
#if discord_rpc
import Discord.DiscordClient;
#end
@ -82,8 +72,10 @@ class MainMenuState extends MusicBeatState
magenta.y = bg.y;
magenta.visible = false;
magenta.color = 0xFFfd719b;
if (PreferencesMenu.preferences.get('flashing-menu')) add(magenta);
// magenta.scrollFactor.set();
// TODO: Why doesn't this line compile I'm going fucking feral
if (Preferences.flashingLights) add(magenta);
menuItems = new MenuTypedList<AtlasMenuItem>();
add(menuItems);
@ -116,7 +108,7 @@ class MainMenuState extends MusicBeatState
#end
createMenuItem('options', 'mainmenu/options', function() {
startExitState(new OptionsState());
startExitState(new funkin.ui.OptionsState());
});
// Reset position of menu items.

View file

@ -16,17 +16,18 @@ class PauseSubState extends MusicBeatSubState
{
var grpMenuShit:FlxTypedGroup<Alphabet>;
var pauseOptionsBase:Array<String> = [
final pauseOptionsBase:Array<String> = [
'Resume',
'Restart Song',
'Change Difficulty',
'Toggle Practice Mode',
'Exit to Menu'
];
final pauseOptionsCharting:Array<String> = ['Resume', 'Restart Song', 'Exit to Chart Editor'];
var pauseOptionsDifficulty:Array<String> = ['EASY', 'NORMAL', 'HARD', 'ERECT', 'BACK'];
final pauseOptionsDifficultyBase:Array<String> = ['BACK'];
var pauseOptionsCharting:Array<String> = ['Resume', 'Restart Song', 'Exit to Chart Editor'];
var pauseOptionsDifficulty:Array<String> = []; // AUTO-POPULATED
var menuItems:Array<String> = [];
var curSelected:Int = 0;
@ -48,6 +49,12 @@ class PauseSubState extends MusicBeatSubState
this.isChartingMode = isChartingMode;
menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase;
var difficultiesInVariation = PlayState.instance.currentSong.listDifficulties(PlayState.instance.currentChart.variation);
trace('DIFFICULTIES: ${difficultiesInVariation}');
pauseOptionsDifficulty = difficultiesInVariation.map(function(item:String):String {
return item.toUpperCase();
}).concat(pauseOptionsDifficultyBase);
if (PlayStatePlaylist.campaignId == 'week6')
{
@ -150,6 +157,11 @@ class PauseSubState extends MusicBeatSubState
super.update(elapsed);
handleInputs();
}
function handleInputs():Void
{
var upP = controls.UI_UP_P;
var downP = controls.UI_DOWN_P;
var accepted = controls.ACCEPT;
@ -196,18 +208,6 @@ class PauseSubState extends MusicBeatSubState
menuItems = pauseOptionsDifficulty;
regenMenu();
case 'EASY' | 'NORMAL' | 'HARD' | 'ERECT':
PlayState.instance.currentSong = SongRegistry.instance.fetchEntry(PlayState.instance.currentSong.id.toLowerCase());
PlayState.instance.currentDifficulty = daSelected.toLowerCase();
PlayState.instance.needsReset = true;
close();
case 'BACK':
menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase;
regenMenu();
case 'Toggle Practice Mode':
PlayState.instance.isPracticeMode = true;
practiceText.visible = PlayState.instance.isPracticeMode;
@ -229,14 +229,43 @@ class PauseSubState extends MusicBeatSubState
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
if (PlayStatePlaylist.isStoryMode) openSubState(new funkin.ui.StickerSubState(null, STORY));
if (PlayStatePlaylist.isStoryMode)
{
openSubState(new funkin.ui.StickerSubState(null, STORY));
}
else
{
openSubState(new funkin.ui.StickerSubState(null, FREEPLAY));
}
case 'Exit to Chart Editor':
this.close();
if (FlxG.sound.music != null) FlxG.sound.music.stop();
PlayState.instance.close(); // This only works because PlayState is a substate!
case 'BACK':
menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase;
regenMenu();
default:
if (pauseOptionsDifficulty.contains(daSelected))
{
PlayState.instance.currentSong = SongRegistry.instance.fetchEntry(PlayState.instance.currentSong.id.toLowerCase());
// Reset campaign score when changing difficulty
// So if you switch difficulty on the last song of a week you get a really low overall score.
PlayStatePlaylist.campaignScore = 0;
PlayStatePlaylist.campaignDifficulty = daSelected.toLowerCase();
PlayState.instance.currentDifficulty = PlayStatePlaylist.campaignDifficulty;
PlayState.instance.needsReset = true;
close();
}
else
{
trace('[WARN] Unhandled pause menu option: ${daSelected}');
}
}
}

View file

@ -77,6 +77,11 @@ class PlayerSettings
this.id = id;
this.controls = new Controls('player$id', None);
addKeyboard();
}
function addKeyboard():Void
{
var useDefault = true;
if (Save.get().hasControls(id, Keys))
{
@ -96,7 +101,6 @@ class PlayerSettings
controls.setKeyboardScheme(Solo);
}
// Apply loaded settings.
PreciseInputManager.instance.initializeKeys(controls);
}
@ -124,6 +128,7 @@ class PlayerSettings
trace("Loading gamepad control scheme");
controls.addDefaultGamepad(gamepad.id);
}
PreciseInputManager.instance.initializeButtons(controls, gamepad);
}
/**

View file

@ -0,0 +1,138 @@
package funkin;
import funkin.save.Save;
/**
* A store of user-configurable, globally relevant values.
*/
class Preferences
{
/**
* Whether some particularly fowl language is displayed.
* @default `true`
*/
public static var naughtyness(get, set):Bool;
static function get_naughtyness():Bool
{
return Save.get().options.naughtyness;
}
static function set_naughtyness(value:Bool):Bool
{
return Save.get().options.naughtyness = value;
}
/**
* If enabled, the strumline is at the bottom of the screen rather than the top.
* @default `false`
*/
public static var downscroll(get, set):Bool;
static function get_downscroll():Bool
{
return Save.get().options.downscroll;
}
static function set_downscroll(value:Bool):Bool
{
return Save.get().options.downscroll = value;
}
/**
* If disabled, flashing lights in the main menu and other areas will be less intense.
* @default `true`
*/
public static var flashingLights(get, set):Bool;
static function get_flashingLights():Bool
{
return Save.get().options.flashingLights;
}
static function set_flashingLights(value:Bool):Bool
{
return Save.get().options.flashingLights = value;
}
/**
* If disabled, the camera bump synchronized to the beat.
* @default `false`
*/
public static var zoomCamera(get, set):Bool;
static function get_zoomCamera():Bool
{
return Save.get().options.zoomCamera;
}
static function set_zoomCamera(value:Bool):Bool
{
return Save.get().options.zoomCamera = value;
}
/**
* If enabled, an FPS and memory counter will be displayed even if this is not a debug build.
* @default `false`
*/
public static var debugDisplay(get, set):Bool;
static function get_debugDisplay():Bool
{
return Save.get().options.debugDisplay;
}
static function set_debugDisplay(value:Bool):Bool
{
if (value != Save.get().options.debugDisplay)
{
toggleDebugDisplay(value);
}
return Save.get().options.debugDisplay = value;
}
/**
* If enabled, the game will automatically pause when tabbing out.
* @default `true`
*/
public static var autoPause(get, set):Bool;
static function get_autoPause():Bool
{
return Save.get().options.autoPause;
}
static function set_autoPause(value:Bool):Bool
{
if (value != Save.get().options.autoPause) FlxG.autoPause = value;
return Save.get().options.autoPause = value;
}
public static function init():Void
{
FlxG.autoPause = Preferences.autoPause;
toggleDebugDisplay(Preferences.debugDisplay);
}
static function toggleDebugDisplay(show:Bool):Void
{
if (show)
{
// Enable the debug display.
FlxG.stage.addChild(Main.fpsCounter);
#if !html5
FlxG.stage.addChild(Main.memoryCounter);
#end
}
else
{
// Disable the debug display.
FlxG.stage.removeChild(Main.fpsCounter);
#if !html5
FlxG.stage.removeChild(Main.memoryCounter);
#end
}
}
}

View file

@ -7,7 +7,6 @@ import flixel.graphics.frames.FlxAtlasFrames;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxMath;
import flixel.sound.FlxSound;
import funkin.ui.PreferencesMenu.CheckboxThingie;
using Lambda;

View file

@ -4,6 +4,7 @@ import flixel.util.typeLimit.OneOfTwo;
import funkin.data.song.SongRegistry;
import thx.semver.Version;
@:nullSafety
class SongMetadata
{
/**
@ -42,7 +43,7 @@ class SongMetadata
public var timeChanges:Array<SongTimeChange>;
/**
* Defaults to `default` or `''`. Populated later.
* Defaults to `Constants.DEFAULT_VARIATION`. Populated later.
*/
@:jignored
public var variation:String;
@ -228,10 +229,10 @@ class SongMusicData
public var timeChanges:Array<SongTimeChange>;
/**
* Defaults to `default` or `''`. Populated later.
* Defaults to `Constants.DEFAULT_VARIATION`. Populated later.
*/
@:jignored
public var variation:String = Constants.DEFAULT_VARIATION;
public var variation:String;
public function new(songName:String, artist:String, variation:String = 'default')
{
@ -375,6 +376,9 @@ class SongChartData
@:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY)
public var generatedBy:String;
/**
* Defaults to `Constants.DEFAULT_VARIATION`. Populated later.
*/
@:jignored
public var variation:String;

View file

@ -21,11 +21,21 @@ class SongDataUtils
* @param notes The notes to modify.
* @param offset The time difference to apply in milliseconds.
*/
public static function offsetSongNoteData(notes:Array<SongNoteData>, offset:Int):Array<SongNoteData>
public static function offsetSongNoteData(notes:Array<SongNoteData>, offset:Float):Array<SongNoteData>
{
return notes.map(function(note:SongNoteData):SongNoteData {
return new SongNoteData(note.time + offset, note.data, note.length, note.kind);
});
var offsetNote = function(note:SongNoteData):SongNoteData {
var time:Float = note.time + offset;
var data:Int = note.data;
var length:Float = note.length;
var kind:String = note.kind;
return new SongNoteData(time, data, length, kind);
};
trace(notes);
trace(notes[0]);
var result = [for (i in 0...notes.length) offsetNote(notes[i])];
trace(result);
return result;
}
/**
@ -36,7 +46,7 @@ class SongDataUtils
* @param events The events to modify.
* @param offset The time difference to apply in milliseconds.
*/
public static function offsetSongEventData(events:Array<SongEventData>, offset:Int):Array<SongEventData>
public static function offsetSongEventData(events:Array<SongEventData>, offset:Float):Array<SongEventData>
{
return events.map(function(event:SongEventData):SongEventData {
return new SongEventData(event.time + offset, event.event, event.value);
@ -152,7 +162,8 @@ class SongDataUtils
*/
public static function writeItemsToClipboard(data:SongClipboardItems):Void
{
var dataString = SerializerUtil.toJSON(data);
var writer = new json2object.JsonWriter<SongClipboardItems>();
var dataString:String = writer.write(data, ' ');
ClipboardUtil.setClipboard(dataString);
@ -170,19 +181,24 @@ class SongDataUtils
trace('Read ${notesString.length} characters from clipboard.');
var data:SongClipboardItems = notesString.parseJSON();
if (data == null)
var parser = new json2object.JsonParser<SongClipboardItems>();
parser.fromJson(notesString, 'clipboard');
if (parser.errors.length > 0)
{
trace('Failed to parse notes from clipboard.');
trace('[SongDataUtils] Error parsing note JSON data from clipboard.');
for (error in parser.errors)
DataError.printError(error);
return {
valid: false,
notes: [],
events: []
};
}
else
{
var data:SongClipboardItems = parser.value;
trace('Parsed ' + data.notes.length + ' notes and ' + data.events.length + ' from clipboard.');
data.valid = true;
return data;
}
}
@ -230,6 +246,7 @@ class SongDataUtils
typedef SongClipboardItems =
{
?valid:Bool,
notes:Array<SongNoteData>,
events:Array<SongEventData>
}

View file

@ -156,7 +156,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return cleanMetadata(parser.value, variation);
}
public function parseEntryMetadataWithMigration(id:String, ?variation:String, version:thx.semver.Version):Null<SongMetadata>
public function parseEntryMetadataWithMigration(id:String, variation:String, version:thx.semver.Version):Null<SongMetadata>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
@ -192,7 +192,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
}
}
function parseEntryMetadata_v2_0_0(id:String, variation:String = ""):Null<SongMetadata>
function parseEntryMetadata_v2_0_0(id:String, ?variation:String):Null<SongMetadata>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;

View file

@ -4,6 +4,7 @@ package;
// Only import these when we aren't in a macro.
import funkin.util.Constants;
import funkin.Paths;
import funkin.Preferences;
import flixel.FlxG; // This one in particular causes a compile error if you're using macros.
// These are great.

View file

@ -1,18 +1,25 @@
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;
import flixel.input.FlxInput.FlxInputState;
import flixel.input.FlxKeyManager;
import flixel.input.gamepad.FlxGamepad;
import flixel.input.gamepad.FlxGamepadInputID;
import flixel.input.keyboard.FlxKey;
import flixel.input.keyboard.FlxKeyboard.FlxKeyInput;
import flixel.input.keyboard.FlxKeyList;
import flixel.util.FlxSignal.FlxTypedSignal;
import funkin.play.notes.NoteDirection;
import funkin.util.FlxGamepadUtil;
import haxe.Int64;
import lime.ui.Gamepad as LimeGamepad;
import lime.ui.GamepadAxis as LimeGamepadAxis;
import lime.ui.GamepadButton as LimeGamepadButton;
import lime.ui.KeyCode;
import lime.ui.KeyModifier;
import openfl.events.KeyboardEvent;
import openfl.ui.Keyboard;
/**
* A precise input manager that:
@ -43,6 +50,20 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
*/
var _keyListDir:Map<FlxKey, NoteDirection>;
/**
* A FlxGamepadID->Array<FlxGamepadInputID>, with FlxGamepadInputID being the counterpart to FlxKey.
*/
var _buttonList:Map<Int, Array<FlxGamepadInputID>>;
var _buttonListArray:Array<FlxInput<FlxGamepadInputID>>;
var _buttonListMap:Map<Int, Map<FlxGamepadInputID, FlxInput<FlxGamepadInputID>>>;
/**
* A FlxGamepadID->FlxGamepadInputID->NoteDirection, with FlxGamepadInputID being the counterpart to FlxKey.
*/
var _buttonListDir:Map<Int, Map<FlxGamepadInputID, NoteDirection>>;
/**
* The timestamp at which a given note direction was last pressed.
*/
@ -53,15 +74,32 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
*/
var _dirReleaseTimestamps:Map<NoteDirection, Int64>;
var _deviceBinds:Map<FlxGamepad,
{
onButtonDown:LimeGamepadButton->Int64->Void,
onButtonUp:LimeGamepadButton->Int64->Void
}>;
public function new()
{
super(PreciseInputList.new);
_deviceBinds = [];
_keyList = [];
_dirPressTimestamps = new Map<NoteDirection, Int64>();
_dirReleaseTimestamps = new Map<NoteDirection, Int64>();
// _keyListMap
// _keyListArray
_keyListDir = new Map<FlxKey, NoteDirection>();
_buttonList = [];
_buttonListMap = [];
_buttonListArray = [];
_buttonListDir = new Map<Int, Map<FlxGamepadInputID, NoteDirection>>();
_dirPressTimestamps = new Map<NoteDirection, Int64>();
_dirReleaseTimestamps = new Map<NoteDirection, Int64>();
// Keyboard
FlxG.stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
FlxG.stage.removeEventListener(KeyboardEvent.KEY_UP, onKeyUp);
FlxG.stage.application.window.onKeyDownPrecise.add(handleKeyDown);
@ -84,6 +122,17 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
};
}
public static function getButtonsForDirection(controls:Controls, noteDirection:NoteDirection)
{
return switch (noteDirection)
{
case NoteDirection.LEFT: controls.getButtonsForAction(NOTE_LEFT);
case NoteDirection.DOWN: controls.getButtonsForAction(NOTE_DOWN);
case NoteDirection.UP: controls.getButtonsForAction(NOTE_UP);
case NoteDirection.RIGHT: controls.getButtonsForAction(NOTE_RIGHT);
};
}
/**
* Convert from int to Int64.
*/
@ -138,6 +187,43 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
}
}
public function initializeButtons(controls:Controls, gamepad:FlxGamepad):Void
{
clearButtons();
var limeGamepad = FlxGamepadUtil.getLimeGamepad(gamepad);
var callbacks =
{
onButtonDown: handleButtonDown.bind(gamepad),
onButtonUp: handleButtonUp.bind(gamepad)
};
limeGamepad.onButtonDownPrecise.add(callbacks.onButtonDown);
limeGamepad.onButtonUpPrecise.add(callbacks.onButtonUp);
for (noteDirection in DIRECTIONS)
{
var buttons = getButtonsForDirection(controls, noteDirection);
for (button in buttons)
{
var input = new FlxInput<FlxGamepadInputID>(button);
var buttonListEntry = _buttonList.get(gamepad.id);
if (buttonListEntry == null) _buttonList.set(gamepad.id, buttonListEntry = []);
buttonListEntry.push(button);
_buttonListArray.push(input);
var buttonListMapEntry = _buttonListMap.get(gamepad.id);
if (buttonListMapEntry == null) _buttonListMap.set(gamepad.id, buttonListMapEntry = new Map<FlxGamepadInputID, FlxInput<FlxGamepadInputID>>());
buttonListMapEntry.set(button, input);
var buttonListDirEntry = _buttonListDir.get(gamepad.id);
if (buttonListDirEntry == null) _buttonListDir.set(gamepad.id, buttonListDirEntry = new Map<FlxGamepadInputID, NoteDirection>());
buttonListDirEntry.set(button, noteDirection);
}
}
}
/**
* Get the time, in nanoseconds, since the given note direction was last pressed.
* @param noteDirection The note direction to check.
@ -165,11 +251,41 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
return _keyListMap.get(key);
}
public function getInputByButton(gamepad:FlxGamepad, button:FlxGamepadInputID):FlxInput<FlxGamepadInputID>
{
return _buttonListMap.get(gamepad.id).get(button);
}
public function getDirectionForKey(key:FlxKey):NoteDirection
{
return _keyListDir.get(key);
}
public function getDirectionForButton(gamepad:FlxGamepad, button:FlxGamepadInputID):NoteDirection
{
return _buttonListDir.get(gamepad.id).get(button);
}
function getButton(gamepad:FlxGamepad, button:FlxGamepadInputID):FlxInput<FlxGamepadInputID>
{
return _buttonListMap.get(gamepad.id).get(button);
}
function updateButtonStates(gamepad:FlxGamepad, button:FlxGamepadInputID, down:Bool):Void
{
var input = getButton(gamepad, button);
if (input == null) return;
if (down)
{
input.press();
}
else
{
input.release();
}
}
function handleKeyDown(keyCode:KeyCode, _:KeyModifier, timestamp:Int64):Void
{
var key:FlxKey = convertKeyCode(keyCode);
@ -198,7 +314,7 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
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.
// This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
timestamp *= Constants.NS_PER_MS;
updateKeyStates(key, false);
@ -214,6 +330,54 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
}
}
function handleButtonDown(gamepad:FlxGamepad, button:LimeGamepadButton, timestamp:Int64):Void
{
var buttonId:FlxGamepadInputID = FlxGamepadUtil.getInputID(gamepad, button);
var buttonListEntry = _buttonList.get(gamepad.id);
if (buttonListEntry == null || buttonListEntry.indexOf(buttonId) == -1) return;
// TODO: Remove this line with SDL3 when timestamps change meaning.
// This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
timestamp *= Constants.NS_PER_MS;
updateButtonStates(gamepad, buttonId, true);
if (getInputByButton(gamepad, buttonId)?.justPressed ?? false)
{
onInputPressed.dispatch(
{
noteDirection: getDirectionForButton(gamepad, buttonId),
timestamp: timestamp
});
_dirPressTimestamps.set(getDirectionForButton(gamepad, buttonId), timestamp);
}
}
function handleButtonUp(gamepad:FlxGamepad, button:LimeGamepadButton, timestamp:Int64):Void
{
var buttonId:FlxGamepadInputID = FlxGamepadUtil.getInputID(gamepad, button);
var buttonListEntry = _buttonList.get(gamepad.id);
if (buttonListEntry == null || buttonListEntry.indexOf(buttonId) == -1) return;
// TODO: Remove this line with SDL3 when timestamps change meaning.
// This is because SDL3's timestamps ar e measured in nanoseconds, not milliseconds.
timestamp *= Constants.NS_PER_MS;
updateButtonStates(gamepad, buttonId, false);
if (getInputByButton(gamepad, buttonId)?.justReleased ?? false)
{
onInputReleased.dispatch(
{
noteDirection: getDirectionForButton(gamepad, buttonId),
timestamp: timestamp
});
_dirReleaseTimestamps.set(getDirectionForButton(gamepad, buttonId), timestamp);
}
}
static function convertKeyCode(input:KeyCode):FlxKey
{
@:privateAccess
@ -228,6 +392,31 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
_keyListMap.clear();
_keyListDir.clear();
}
function clearButtons():Void
{
_buttonListArray = [];
_buttonListDir.clear();
for (gamepad in _deviceBinds.keys())
{
var callbacks = _deviceBinds.get(gamepad);
var limeGamepad = FlxGamepadUtil.getLimeGamepad(gamepad);
limeGamepad.onButtonDownPrecise.remove(callbacks.onButtonDown);
limeGamepad.onButtonUpPrecise.remove(callbacks.onButtonUp);
}
_deviceBinds.clear();
}
public override function destroy():Void
{
// Keyboard
FlxG.stage.application.window.onKeyDownPrecise.remove(handleKeyDown);
FlxG.stage.application.window.onKeyUpPrecise.remove(handleKeyUp);
clearKeys();
clearButtons();
}
}
class PreciseInputList extends FlxKeyList

View file

@ -11,7 +11,6 @@ import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.PlayState;
import funkin.play.character.BaseCharacter;
import funkin.ui.PreferencesMenu;
/**
* A substate which renders over the PlayState when the player dies.
@ -292,7 +291,7 @@ class GameOverSubState extends MusicBeatSubState
{
var randomCensor:Array<Int> = [];
if (PreferencesMenu.getPref('censor-naughty')) randomCensor = [1, 3, 8, 13, 17, 21];
if (!Preferences.naughtyness) randomCensor = [1, 3, 8, 13, 17, 21];
FlxG.sound.play(Paths.sound('jeffGameover/jeffGameover-' + FlxG.random.int(1, 25, randomCensor)), 1, false, null, true, function() {
// Once the quote ends, fade in the game over music.

View file

@ -920,7 +920,6 @@ class PlayState extends MusicBeatSubState
}
// Handle keybinds.
// if (!isInCutscene && !disableKeys) keyShit(true);
processInputQueue();
if (!isInCutscene && !disableKeys) debugKeyShit();
if (isInCutscene && !disableKeys) handleCutsceneKeys(elapsed);
@ -1268,7 +1267,7 @@ class PlayState extends MusicBeatSubState
*/
function initHealthBar():Void
{
var healthBarYPos:Float = PreferencesMenu.getPref('downscroll') ? FlxG.height * 0.1 : FlxG.height * 0.9;
var healthBarYPos:Float = Preferences.downscroll ? FlxG.height * 0.1 : FlxG.height * 0.9;
healthBarBG = new FlxSprite(0, healthBarYPos).loadGraphic(Paths.image('healthBar'));
healthBarBG.screenCenter(X);
healthBarBG.scrollFactor.set(0, 0);
@ -1477,13 +1476,13 @@ class PlayState extends MusicBeatSubState
// Position the player strumline on the right half of the screen
playerStrumline.x = FlxG.width / 2 + Constants.STRUMLINE_X_OFFSET; // Classic style
// playerStrumline.x = FlxG.width - playerStrumline.width - Constants.STRUMLINE_X_OFFSET; // Centered style
playerStrumline.y = PreferencesMenu.getPref('downscroll') ? FlxG.height - playerStrumline.height - Constants.STRUMLINE_Y_OFFSET : Constants.STRUMLINE_Y_OFFSET;
playerStrumline.y = Preferences.downscroll ? FlxG.height - playerStrumline.height - Constants.STRUMLINE_Y_OFFSET : Constants.STRUMLINE_Y_OFFSET;
playerStrumline.zIndex = 200;
playerStrumline.cameras = [camHUD];
// Position the opponent strumline on the left half of the screen
opponentStrumline.x = Constants.STRUMLINE_X_OFFSET;
opponentStrumline.y = PreferencesMenu.getPref('downscroll') ? FlxG.height - opponentStrumline.height - Constants.STRUMLINE_Y_OFFSET : Constants.STRUMLINE_Y_OFFSET;
opponentStrumline.y = Preferences.downscroll ? FlxG.height - opponentStrumline.height - Constants.STRUMLINE_Y_OFFSET : Constants.STRUMLINE_Y_OFFSET;
opponentStrumline.zIndex = 100;
opponentStrumline.cameras = [camHUD];
@ -2464,9 +2463,9 @@ class PlayState extends MusicBeatSubState
accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
};
if (Save.get().isLevelHighScore(PlayStatePlaylist.campaignId, currentDifficulty, data))
if (Save.get().isLevelHighScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data))
{
Save.get().setLevelScore(PlayStatePlaylist.campaignId, currentDifficulty, data);
Save.get().setLevelScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data);
#if newgrounds
NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}');
#end
@ -2514,7 +2513,7 @@ class PlayState extends MusicBeatSubState
var nextPlayState:PlayState = new PlayState(
{
targetSong: targetSong,
targetDifficulty: currentDifficulty,
targetDifficulty: PlayStatePlaylist.campaignDifficulty,
targetCharacter: currentPlayerId,
});
nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y);
@ -2530,7 +2529,7 @@ class PlayState extends MusicBeatSubState
var nextPlayState:PlayState = new PlayState(
{
targetSong: targetSong,
targetDifficulty: currentDifficulty,
targetDifficulty: PlayStatePlaylist.campaignDifficulty,
targetCharacter: currentPlayerId,
});
nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y);
@ -2656,7 +2655,12 @@ class PlayState extends MusicBeatSubState
persistentUpdate = false;
vocals.stop();
camHUD.alpha = 1;
var res:ResultState = new ResultState();
var res:ResultState = new ResultState(
{
storyMode: PlayStatePlaylist.isStoryMode,
title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
tallies: Highscore.tallies,
});
res.camera = camHUD;
openSubState(res);
}

View file

@ -34,10 +34,7 @@ class PlayStatePlaylist
*/
public static var campaignId:String = 'unknown';
/**
* The current difficulty selected for this level (as a named ID).
*/
public static var currentDifficulty(default, default):String = Constants.DEFAULT_DIFFICULTY;
public static var campaignDifficulty:String = Constants.DEFAULT_DIFFICULTY;
/**
* Resets the playlist to its default state.
@ -49,6 +46,6 @@ class PlayStatePlaylist
campaignScore = 0;
campaignTitle = 'UNKNOWN';
campaignId = 'unknown';
currentDifficulty = Constants.DEFAULT_DIFFICULTY;
campaignDifficulty = Constants.DEFAULT_DIFFICULTY;
}
}

View file

@ -22,6 +22,8 @@ import flxanimate.FlxAnimate.Settings;
class ResultState extends MusicBeatSubState
{
final params:ResultsStateParams;
var resultsVariation:ResultVariations;
var songName:FlxBitmapText;
var difficulty:FlxSprite;
@ -29,13 +31,18 @@ class ResultState extends MusicBeatSubState
var maskShaderSongName = new LeftMaskShader();
var maskShaderDifficulty = new LeftMaskShader();
public function new(params:ResultsStateParams)
{
super();
this.params = params;
}
override function create():Void
{
if (Highscore.tallies.sick == Highscore.tallies.totalNotesHit
&& Highscore.tallies.maxCombo == Highscore.tallies.totalNotesHit) resultsVariation = PERFECT;
else if (Highscore.tallies.missed
+ Highscore.tallies.bad
+ Highscore.tallies.shit >= Highscore.tallies.totalNotes * 0.50)
if (params.tallies.sick == params.tallies.totalNotesHit
&& params.tallies.maxCombo == params.tallies.totalNotesHit) resultsVariation = PERFECT;
else if (params.tallies.missed + params.tallies.bad + params.tallies.shit >= params.tallies.totalNotes * 0.50)
resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending!
else
resultsVariation = NORMAL;
@ -135,17 +142,7 @@ class ResultState extends MusicBeatSubState
var fontLetters:String = "AaBbCcDdEeFfGgHhiIJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:1234567890";
songName = new FlxBitmapText(FlxBitmapFont.fromMonospace(Paths.image("resultScreen/tardlingSpritesheet"), fontLetters, FlxPoint.get(49, 62)));
// stole this from PauseSubState, I think eric wrote it!!
if (PlayState.instance.currentChart != null)
{
songName.text += '${PlayState.instance.currentChart.songName}:${PlayState.instance.currentChart.songArtist}';
}
else
{
songName.text += PlayState.instance.currentSong.id;
}
songName.text = params.title;
songName.letterSpacing = -15;
songName.angle = -4.1;
add(songName);
@ -194,27 +191,27 @@ class ResultState extends MusicBeatSubState
var ratingGrp:FlxTypedGroup<TallyCounter> = new FlxTypedGroup<TallyCounter>();
add(ratingGrp);
var totalHit:TallyCounter = new TallyCounter(375, hStuf * 3, Highscore.tallies.totalNotesHit);
var totalHit:TallyCounter = new TallyCounter(375, hStuf * 3, params.tallies.totalNotesHit);
ratingGrp.add(totalHit);
var maxCombo:TallyCounter = new TallyCounter(375, hStuf * 4, Highscore.tallies.maxCombo);
var maxCombo:TallyCounter = new TallyCounter(375, hStuf * 4, params.tallies.maxCombo);
ratingGrp.add(maxCombo);
hStuf += 2;
var extraYOffset:Float = 5;
var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, Highscore.tallies.sick, 0xFF89E59E);
var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, params.tallies.sick, 0xFF89E59E);
ratingGrp.add(tallySick);
var tallyGood:TallyCounter = new TallyCounter(210, (hStuf * 6) + extraYOffset, Highscore.tallies.good, 0xFF89C9E5);
var tallyGood:TallyCounter = new TallyCounter(210, (hStuf * 6) + extraYOffset, params.tallies.good, 0xFF89C9E5);
ratingGrp.add(tallyGood);
var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, Highscore.tallies.bad, 0xffE6CF8A);
var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, params.tallies.bad, 0xffE6CF8A);
ratingGrp.add(tallyBad);
var tallyShit:TallyCounter = new TallyCounter(220, (hStuf * 8) + extraYOffset, Highscore.tallies.shit, 0xFFE68C8A);
var tallyShit:TallyCounter = new TallyCounter(220, (hStuf * 8) + extraYOffset, params.tallies.shit, 0xFFE68C8A);
ratingGrp.add(tallyShit);
var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, Highscore.tallies.missed, 0xFFC68AE6);
var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, params.tallies.missed, 0xFFC68AE6);
ratingGrp.add(tallyMissed);
for (ind => rating in ratingGrp.members)
@ -275,7 +272,7 @@ class ResultState extends MusicBeatSubState
}
});
if (Highscore.tallies.isNewHighscore) trace("ITS A NEW HIGHSCORE!!!");
if (params.tallies.isNewHighscore) trace("ITS A NEW HIGHSCORE!!!");
super.create();
}
@ -351,7 +348,7 @@ class ResultState extends MusicBeatSubState
if (controls.PAUSE)
{
if (PlayStatePlaylist.isStoryMode)
if (params.storyMode)
{
FlxG.switchState(new StoryMenuState());
}
@ -372,3 +369,21 @@ enum abstract ResultVariations(String)
var NORMAL;
var SHIT;
}
typedef ResultsStateParams =
{
/**
* True if results are for a level, false if results are for a single song.
*/
var storyMode:Bool;
/**
* Either "Song Name by Artist Name" or "Week Name"
*/
var title:String;
/**
* The score, accuracy, and judgements.
*/
var tallies:Highscore.Tallies;
};

View file

@ -231,7 +231,7 @@ class Strumline extends FlxSpriteGroup
notesVwoosh.add(note);
var targetY:Float = FlxG.height + note.y;
if (PreferencesMenu.getPref('downscroll')) targetY = 0 - note.height;
if (Preferences.downscroll) targetY = 0 - note.height;
FlxTween.tween(note, {y: targetY}, 0.5,
{
ease: FlxEase.expoIn,
@ -252,7 +252,7 @@ class Strumline extends FlxSpriteGroup
holdNotesVwoosh.add(holdNote);
var targetY:Float = FlxG.height + holdNote.y;
if (PreferencesMenu.getPref('downscroll')) targetY = 0 - holdNote.height;
if (Preferences.downscroll) targetY = 0 - holdNote.height;
FlxTween.tween(holdNote, {y: targetY}, 0.5,
{
ease: FlxEase.expoIn,
@ -277,7 +277,7 @@ class Strumline extends FlxSpriteGroup
var vwoosh:Float = (strumTime < Conductor.songPosition) && vwoosh ? 2.0 : 1.0;
var scrollSpeed:Float = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0;
return Constants.PIXELS_PER_MS * (Conductor.songPosition - strumTime) * scrollSpeed * vwoosh * (PreferencesMenu.getPref('downscroll') ? 1 : -1);
return Constants.PIXELS_PER_MS * (Conductor.songPosition - strumTime) * scrollSpeed * vwoosh * (Preferences.downscroll ? 1 : -1);
}
function updateNotes():Void
@ -321,7 +321,7 @@ class Strumline extends FlxSpriteGroup
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;
var isOffscreen = Preferences.downscroll ? note.y > FlxG.height : note.y < -note.height;
if (note.handledMiss && isOffscreen)
{
killNote(note);
@ -388,7 +388,7 @@ class Strumline extends FlxSpriteGroup
var vwoosh:Bool = false;
if (PreferencesMenu.getPref('downscroll'))
if (Preferences.downscroll)
{
holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
}
@ -410,7 +410,7 @@ class Strumline extends FlxSpriteGroup
holdNote.visible = false;
}
if (PreferencesMenu.getPref('downscroll'))
if (Preferences.downscroll)
{
holdNote.y = this.y - holdNote.height + STRUMLINE_SIZE / 2;
}
@ -425,7 +425,7 @@ class Strumline extends FlxSpriteGroup
holdNote.visible = true;
var vwoosh:Bool = false;
if (PreferencesMenu.getPref('downscroll'))
if (Preferences.downscroll)
{
holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
}

View file

@ -114,7 +114,7 @@ class SustainTrail extends FlxSprite
height = sustainHeight(sustainLength, getScrollSpeed());
// instead of scrollSpeed, PlayState.SONG.speed
flipY = PreferencesMenu.getPref('downscroll');
flipY = Preferences.downscroll;
// alpha = 0.6;
alpha = 1.0;

View file

@ -56,8 +56,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
*/
public var validScore:Bool = true;
var difficultyIds:Array<String>;
public var songName(get, never):String;
function get_songName():String
@ -85,7 +83,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
this.id = id;
variations = [];
difficultyIds = [];
difficulties = new Map<String, SongDifficulty>();
_data = _fetchData(id);
@ -127,8 +124,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
for (vari in variations)
result.variations.push(vari);
result.difficultyIds.clear();
result.difficulties.clear();
result.populateDifficulties();
for (variation => chartData in charts)
@ -162,8 +158,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
// but all the difficulties in the metadata must be in the chart file.
for (diffId in metadata.playData.difficulties)
{
difficultyIds.pushUnique(diffId);
var difficulty:SongDifficulty = new SongDifficulty(this, diffId, metadata.variation);
variations.push(metadata.variation);
@ -237,19 +231,62 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
*/
public inline function getDifficulty(?diffId:String):Null<SongDifficulty>
{
if (diffId == null) diffId = difficulties.keys().array()[0];
if (diffId == null) diffId = listDifficulties()[0];
return difficulties.get(diffId);
}
public function listDifficulties():Array<String>
/**
* List all the difficulties in this song.
* @param variationId Optionally filter by variation.
* @return The list of difficulties.
*/
public function listDifficulties(?variationId:String):Array<String>
{
return difficultyIds;
if (variationId == '') variationId = null;
var diffFiltered:Array<String> = difficulties.keys().array().filter(function(diffId:String):Bool {
if (variationId == null) return true;
var difficulty:Null<SongDifficulty> = difficulties.get(diffId);
if (difficulty == null) return false;
return difficulty.variation == variationId;
});
// sort the difficulties, since they may be out of order in the chart JSON
// maybe be careful of lowercase/uppercase?
// also used in Level.listDifficulties()!!
var diffMap:Map<String, Int> = new Map<String, Int>();
for (difficulty in diffFiltered)
{
var num:Int = 0;
switch (difficulty)
{
case 'easy':
num = 0;
case 'normal':
num = 1;
case 'hard':
num = 2;
case 'erect':
num = 3;
case 'nightmare':
num = 4;
}
diffMap.set(difficulty, num);
}
diffFiltered.sort(function(a:String, b:String) {
return (diffMap.get(a) ?? 0) - (diffMap.get(b) ?? 0);
});
return diffFiltered;
}
public function hasDifficulty(diffId:String):Bool
public function hasDifficulty(diffId:String, ?variationId:String):Bool
{
return difficulties.exists(diffId);
if (variationId == '') variationId = null;
var difficulty:Null<SongDifficulty> = difficulties.get(diffId);
return variationId == null ? (difficulty != null) : (difficulty != null && difficulty.variation == variationId);
}
/**

View file

@ -63,10 +63,10 @@ abstract Save(RawSaveData)
// Reasonable defaults.
naughtyness: true,
downscroll: false,
flashingMenu: true,
flashingLights: true,
zoomCamera: true,
debugDisplay: false,
pauseOnTabOut: true,
autoPause: true,
controls:
{
@ -88,7 +88,7 @@ abstract Save(RawSaveData)
{
// No mods enabled.
enabledMods: [],
modSettings: [],
modOptions: [],
},
optionsChartEditor:
@ -98,6 +98,20 @@ abstract Save(RawSaveData)
};
}
public var options(get, never):SaveDataOptions;
function get_options():SaveDataOptions
{
return this.options;
}
public var modOptions(get, never):Map<String, Dynamic>;
function get_modOptions():Map<String, Dynamic>
{
return this.mods.modOptions;
}
/**
* The current session ID for the logged-in Newgrounds user, or null if the user is cringe.
*/
@ -458,7 +472,7 @@ typedef SaveHighScoresData =
typedef SaveDataMods =
{
var enabledMods:Array<String>;
var modSettings:Map<String, Dynamic>;
var modOptions:Map<String, Dynamic>;
}
/**
@ -530,10 +544,10 @@ typedef SaveDataOptions =
var downscroll:Bool;
/**
* If disabled, the main menu won't flash when entering a submenu.
* If disabled, flashing lights in the main menu and other areas will be less intense.
* @default `true`
*/
var flashingMenu:Bool;
var flashingLights:Bool;
/**
* If disabled, the camera bump synchronized to the beat.
@ -551,7 +565,7 @@ typedef SaveDataOptions =
* If enabled, the game will automatically pause when tabbing out.
* @default `true`
*/
var pauseOnTabOut:Bool;
var autoPause:Bool;
var controls:
{

View file

@ -163,7 +163,17 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
function onSelect():Void
{
keyUsedToEnterPrompt = FlxG.keys.firstJustPressed();
switch (currentDevice)
{
case Keys:
{
keyUsedToEnterPrompt = FlxG.keys.firstJustPressed();
}
case Gamepad(id):
{
buttonUsedToEnterPrompt = FlxG.gamepads.getByID(id).firstJustPressedID();
}
}
controlGrid.enabled = false;
canExit = false;
@ -204,6 +214,7 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
}
var keyUsedToEnterPrompt:Null<Int> = null;
var buttonUsedToEnterPrompt:Null<Int> = null;
override function update(elapsed:Float):Void
{
@ -246,19 +257,49 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
case Gamepad(id):
{
var button = FlxG.gamepads.getByID(id).firstJustReleasedID();
if (button != NONE && button != keyUsedToEnterPrompt)
if (button != NONE && button != buttonUsedToEnterPrompt)
{
if (button != BACK) onInputSelect(button);
closePrompt();
}
var key = FlxG.keys.firstJustReleased();
if (key != NONE && key != keyUsedToEnterPrompt)
{
if (key == ESCAPE)
{
closePrompt();
}
else if (key == BACKSPACE)
{
onInputSelect(NONE);
closePrompt();
}
}
}
}
}
var keyJustReleased:Int = FlxG.keys.firstJustReleased();
if (keyJustReleased != NONE && keyJustReleased == keyUsedToEnterPrompt)
switch (currentDevice)
{
keyUsedToEnterPrompt = null;
case Keys:
{
var keyJustReleased:Int = FlxG.keys.firstJustReleased();
if (keyJustReleased != NONE && keyJustReleased == keyUsedToEnterPrompt)
{
keyUsedToEnterPrompt = null;
}
buttonUsedToEnterPrompt = null;
}
case Gamepad(id):
{
var buttonJustReleased:Int = FlxG.gamepads.getByID(id).firstJustReleasedID();
if (buttonJustReleased != NONE && buttonJustReleased == buttonUsedToEnterPrompt)
{
buttonUsedToEnterPrompt = null;
}
keyUsedToEnterPrompt = null;
}
}
}

View file

@ -3,17 +3,16 @@ package funkin.ui;
import flixel.FlxCamera;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import funkin.ui.AtlasText.AtlasFont;
import funkin.ui.OptionsState.Page;
import funkin.ui.TextMenuList.TextMenuItem;
class PreferencesMenu extends Page
{
public static var preferences:Map<String, Dynamic> = new Map();
var items:TextMenuList;
var preferenceItems:FlxTypedSpriteGroup<FlxSprite>;
var checkboxes:Array<CheckboxThingie> = [];
var menuCamera:FlxCamera;
var camFollow:FlxObject;
@ -27,13 +26,9 @@ class PreferencesMenu extends Page
camera = menuCamera;
add(items = new TextMenuList());
add(preferenceItems = new FlxTypedSpriteGroup<FlxSprite>());
createPrefItem('naughtyness', 'censor-naughty', true);
createPrefItem('downscroll', 'downscroll', false);
createPrefItem('flashing menu', 'flashing-menu', true);
createPrefItem('Camera Zooming on Beat', 'camera-zoom', true);
createPrefItem('FPS Counter', 'fps-counter', true);
createPrefItem('Auto Pause', 'auto-pause', false);
createPrefItems();
camFollow = new FlxObject(FlxG.width / 2, 0, 140, 70);
if (items != null) camFollow.y = items.selectedItem.y;
@ -48,128 +43,63 @@ class PreferencesMenu extends Page
});
}
public static function getPref(pref:String):Dynamic
/**
* Create the menu items for each of the preferences.
*/
function createPrefItems():Void
{
return preferences.get(pref);
createPrefItemCheckbox('Naughtyness', 'Toggle displaying raunchy content', function(value:Bool):Void {
Preferences.naughtyness = value;
}, Preferences.naughtyness);
createPrefItemCheckbox('Downscroll', 'Enable to make notes move downwards', function(value:Bool):Void {
Preferences.downscroll = value;
}, Preferences.downscroll);
createPrefItemCheckbox('Flashing Lights', 'Disable to dampen flashing effects', function(value:Bool):Void {
Preferences.flashingLights = value;
}, Preferences.flashingLights);
createPrefItemCheckbox('Camera Zooming on Beat', 'Disable to stop the camera bouncing to the song', function(value:Bool):Void {
Preferences.zoomCamera = value;
}, Preferences.zoomCamera);
createPrefItemCheckbox('Debug Display', 'Enable to show FPS and other debug stats', function(value:Bool):Void {
Preferences.debugDisplay = value;
}, Preferences.debugDisplay);
createPrefItemCheckbox('Auto Pause', 'Automatically pause the game when it loses focus', function(value:Bool):Void {
Preferences.autoPause = value;
}, Preferences.autoPause);
}
// easy shorthand?
public static function setPref(pref:String, value:Dynamic):Void
function createPrefItemCheckbox(prefName:String, prefDesc:String, onChange:Bool->Void, defaultValue:Bool):Void
{
preferences.set(pref, value);
}
var checkbox:CheckboxPreferenceItem = new CheckboxPreferenceItem(0, 120 * (items.length - 1 + 1), defaultValue);
public static function initPrefs():Void
{
preferenceCheck('censor-naughty', true);
preferenceCheck('downscroll', false);
preferenceCheck('flashing-menu', true);
preferenceCheck('camera-zoom', true);
preferenceCheck('fps-counter', true);
preferenceCheck('auto-pause', false);
preferenceCheck('master-volume', 1);
#if muted
setPref('master-volume', 0);
FlxG.sound.muted = true;
#end
if (!getPref('fps-counter')) FlxG.stage.removeChild(Main.fpsCounter);
FlxG.autoPause = getPref('auto-pause');
}
function createPrefItem(prefName:String, prefString:String, prefValue:Dynamic):Void
{
items.createItem(120, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() {
preferenceCheck(prefString, prefValue);
switch (Type.typeof(prefValue).getName())
{
case 'TBool':
prefToggle(prefString);
default:
trace('swag');
}
var value = !checkbox.currentValue;
onChange(value);
checkbox.currentValue = value;
});
switch (Type.typeof(prefValue).getName())
{
case 'TBool':
createCheckbox(prefString);
default:
trace('swag');
}
trace(Type.typeof(prefValue).getName());
}
function createCheckbox(prefString:String)
{
var checkbox:CheckboxThingie = new CheckboxThingie(0, 120 * (items.length - 1), preferences.get(prefString));
checkboxes.push(checkbox);
add(checkbox);
}
/**
* Assumes that the preference has already been checked/set?
*/
function prefToggle(prefName:String)
{
var daSwap:Bool = preferences.get(prefName);
daSwap = !daSwap;
preferences.set(prefName, daSwap);
checkboxes[items.selectedIndex].daValue = daSwap;
trace('toggled? ' + preferences.get(prefName));
switch (prefName)
{
case 'fps-counter':
if (getPref('fps-counter')) FlxG.stage.addChild(Main.fpsCounter);
else
FlxG.stage.removeChild(Main.fpsCounter);
case 'auto-pause':
FlxG.autoPause = getPref('auto-pause');
}
if (prefName == 'fps-counter') {}
preferenceItems.add(checkbox);
}
override function update(elapsed:Float)
{
super.update(elapsed);
// menuCamera.followLerp = CoolUtil.camLerpShit(0.05);
// Indent the selected item.
// TODO: Only do this on menu change?
items.forEach(function(daItem:TextMenuItem) {
if (items.selectedItem == daItem) daItem.x = 150;
else
daItem.x = 120;
});
}
static function preferenceCheck(prefString:String, defaultValue:Dynamic):Void
{
if (preferences.get(prefString) == null)
{
// Set the value to default.
preferences.set(prefString, defaultValue);
trace('Set preference to default: ${prefString} = ${defaultValue}');
}
else
{
trace('Found preference: ${prefString} = ${preferences.get(prefString)}');
}
}
}
class CheckboxThingie extends FlxSprite
class CheckboxPreferenceItem extends FlxSprite
{
public var daValue(default, set):Bool;
public var currentValue(default, set):Bool;
public function new(x:Float, y:Float, daValue:Bool = false)
public function new(x:Float, y:Float, defaultValue:Bool = false)
{
super(x, y);
@ -180,7 +110,7 @@ class CheckboxThingie extends FlxSprite
setGraphicSize(Std.int(width * 0.7));
updateHitbox();
this.daValue = daValue;
this.currentValue = defaultValue;
}
override function update(elapsed:Float)
@ -196,12 +126,17 @@ class CheckboxThingie extends FlxSprite
}
}
function set_daValue(value:Bool):Bool
function set_currentValue(value:Bool):Bool
{
if (value) animation.play('checked', true);
if (value)
{
animation.play('checked', true);
}
else
{
animation.play('static');
}
return value;
return currentValue = value;
}
}

View file

@ -17,6 +17,9 @@ import openfl.geom.Matrix;
import openfl.display.Sprite;
import openfl.display.Bitmap;
using Lambda;
using StringTools;
class StickerSubState extends MusicBeatSubState
{
public var grpStickers:FlxTypedGroup<StickerSprite>;
@ -26,10 +29,60 @@ class StickerSubState extends MusicBeatSubState
var nextState:NEXTSTATE = FREEPLAY;
// what "folders" to potentially load from (as of writing only "keys" exist)
var soundSelections:Array<String> = [];
// what "folder" was randomly selected
var soundSelection:String = "";
var sounds:Array<String> = [];
public function new(?oldStickers:Array<StickerSprite>, ?nextState:NEXTSTATE = FREEPLAY):Void
{
super();
// todo still
// make sure that ONLY plays mp3/ogg files
// if there's no mp3/ogg file, then it regenerates/reloads the random folder
var assetsInList = openfl.utils.Assets.list();
var soundFilterFunc = function(a:String) {
return a.startsWith('assets/shared/sounds/stickersounds/');
};
soundSelections = assetsInList.filter(soundFilterFunc);
soundSelections = soundSelections.map(function(a:String) {
return a.replace('assets/shared/sounds/stickersounds/', '').split('/')[0];
});
// cracked cleanup... yuchh...
for (i in soundSelections)
{
while (soundSelections.contains(i))
{
soundSelections.remove(i);
}
soundSelections.push(i);
}
trace(soundSelections);
soundSelection = FlxG.random.getObject(soundSelections);
var filterFunc = function(a:String) {
return a.startsWith('assets/shared/sounds/stickersounds/' + soundSelection + '/');
};
var assetsInList3 = openfl.utils.Assets.list();
sounds = assetsInList3.filter(filterFunc);
for (i in 0...sounds.length)
{
sounds[i] = sounds[i].replace('assets/shared/sounds/', '');
sounds[i] = sounds[i].substring(0, sounds[i].lastIndexOf('.'));
}
trace(sounds);
// trace(assetsInList);
this.nextState = nextState;
grpStickers = new FlxTypedGroup<StickerSprite>();
@ -66,8 +119,10 @@ class StickerSubState extends MusicBeatSubState
{
new FlxTimer().start(sticker.timing, _ -> {
sticker.visible = false;
var daSound:String = FlxG.random.getObject(sounds);
FlxG.sound.play(Paths.sound(daSound));
if (ind == grpStickers.members.length - 1)
if (grpStickers == null || ind == grpStickers.members.length - 1)
{
switchingState = false;
close();
@ -151,7 +206,11 @@ class StickerSubState extends MusicBeatSubState
sticker.timing = FlxMath.remapToRange(ind, 0, grpStickers.members.length, 0, 0.9);
new FlxTimer().start(sticker.timing, _ -> {
if (grpStickers == null) return;
sticker.visible = true;
var daSound:String = FlxG.random.getObject(sounds);
FlxG.sound.play(Paths.sound(daSound));
var frameTimer:Int = FlxG.random.int(0, 2);
@ -212,10 +271,10 @@ class StickerSubState extends MusicBeatSubState
{
super.update(elapsed);
if (FlxG.keys.justPressed.ANY)
{
regenStickers();
}
// if (FlxG.keys.justPressed.ANY)
// {
// regenStickers();
// }
}
var switchingState:Bool = false;

View file

@ -10,7 +10,7 @@ class TextMenuList extends MenuTypedList<TextMenuItem>
super(navControls, wrapMode);
}
public function createItem(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, callback, fireInstantly = false)
public function createItem(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, ?callback:Void->Void, fireInstantly = false)
{
var item = new TextMenuItem(x, y, name, font, callback);
item.fireInstantly = fireInstantly;
@ -20,7 +20,7 @@ class TextMenuList extends MenuTypedList<TextMenuItem>
class TextMenuItem extends TextTypedMenuItem<AtlasText>
{
public function new(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, callback)
public function new(x = 0.0, y = 0.0, name:String, font:AtlasFont = BOLD, ?callback:Void->Void)
{
super(x, y, new AtlasText(0, 0, name, font), name, callback);
setEmptyBackground();
@ -29,7 +29,7 @@ class TextMenuItem extends TextTypedMenuItem<AtlasText>
class TextTypedMenuItem<T:AtlasText> extends MenuTypedItem<T>
{
public function new(x = 0.0, y = 0.0, label:T, name:String, callback)
public function new(x = 0.0, y = 0.0, label:T, name:String, ?callback:Void->Void)
{
super(x, y, label, name, callback);
}

View file

@ -1,11 +1,14 @@
package funkin.ui.debug.charting;
import openfl.utils.Assets;
import flixel.system.FlxAssets.FlxSoundAsset;
import flixel.system.FlxSound;
import funkin.play.character.BaseCharacter.CharacterType;
import flixel.system.FlxSound;
import funkin.audio.VoicesGroup;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.util.FileUtil;
import haxe.io.Bytes;
import haxe.io.Path;
import openfl.utils.Assets;
/**
* Functions for loading audio for the chart editor.
@ -17,16 +20,18 @@ import haxe.io.Path;
class ChartEditorAudioHandler
{
/**
* Loads a vocal track from an absolute file path.
* Loads and stores byte data for a vocal track from an absolute file path
*
* @param path The absolute path to the audio file.
* @param charKey The character to load the vocal track for.
* @param charId The character this vocal track will be for.
* @param instId The instrumental this vocal track will be for.
* @return Success or failure.
*/
static function loadVocalsFromPath(state:ChartEditorState, path:Path, charKey:String = 'default'):Bool
static function loadVocalsFromPath(state:ChartEditorState, path:Path, charId:String, instId:String = ''):Bool
{
#if sys
var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString());
return loadVocalsFromBytes(state, fileBytes, charKey);
var fileBytes:Bytes = sys.io.File.getBytes(path.toString());
return loadVocalsFromBytes(state, fileBytes, charId, instId);
#else
trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
return false;
@ -34,137 +39,235 @@ class ChartEditorAudioHandler
}
/**
* Load a vocal track for a given song and character and add it to the voices group.
* Loads and stores byte data for a vocal track from an asset
*
* @param path ID of the asset.
* @param charKey Character to load the vocal track for.
* @param path The path to the asset. Use `Paths` to build this.
* @param charId The character this vocal track will be for.
* @param instId The instrumental this vocal track will be for.
* @return Success or failure.
*/
static function loadVocalsFromAsset(state:ChartEditorState, path:String, charType:CharacterType = OTHER):Bool
static function loadVocalsFromAsset(state:ChartEditorState, path:String, charId:String, instId:String = ''):Bool
{
var vocalTrack:FlxSound = FlxG.sound.load(path, 1.0, false);
var trackData:Null<Bytes> = Assets.getBytes(path);
if (trackData != null)
{
return loadVocalsFromBytes(state, trackData, charId, instId);
}
return false;
}
/**
* Loads and stores byte data for a vocal track
*
* @param bytes The audio byte data.
* @param charId The character this vocal track will be for.
* @param instId The instrumental this vocal track will be for.
*/
static function loadVocalsFromBytes(state:ChartEditorState, bytes:Bytes, charId:String, instId:String = ''):Bool
{
var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}';
state.audioVocalTrackData.set(trackId, bytes);
return true;
}
/**
* Loads and stores byte data for an instrumental track from an absolute file path
*
* @param path The absolute path to the audio file.
* @param instId The instrumental this vocal track will be for.
* @return Success or failure.
*/
static function loadInstFromPath(state:ChartEditorState, path:Path, instId:String = ''):Bool
{
#if sys
var fileBytes:Bytes = sys.io.File.getBytes(path.toString());
return loadInstFromBytes(state, fileBytes, instId);
#else
trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
return false;
#end
}
/**
* Loads and stores byte data for an instrumental track from an asset
*
* @param path The path to the asset. Use `Paths` to build this.
* @param instId The instrumental this vocal track will be for.
* @return Success or failure.
*/
static function loadInstFromAsset(state:ChartEditorState, path:String, instId:String = ''):Bool
{
var trackData:Null<Bytes> = Assets.getBytes(path);
if (trackData != null)
{
return loadInstFromBytes(state, trackData, instId);
}
return false;
}
/**
* Loads and stores byte data for a vocal track
*
* @param bytes The audio byte data.
* @param charId The character this vocal track will be for.
* @param instId The instrumental this vocal track will be for.
*/
static function loadInstFromBytes(state:ChartEditorState, bytes:Bytes, instId:String = ''):Bool
{
if (instId == '') instId = 'default';
state.audioInstTrackData.set(instId, bytes);
return true;
}
public static function switchToInstrumental(state:ChartEditorState, instId:String = '', playerId:String, opponentId:String):Bool
{
var result:Bool = playInstrumental(state, instId);
if (!result) return false;
stopExistingVocals(state);
result = playVocals(state, BF, playerId, instId);
if (!result) return false;
result = playVocals(state, DAD, opponentId, instId);
if (!result) return false;
return true;
}
/**
* Tell the Chart Editor to select a specific instrumental track, that is already loaded.
*/
static function playInstrumental(state:ChartEditorState, instId:String = ''):Bool
{
if (instId == '') instId = 'default';
var instTrackData:Null<Bytes> = state.audioInstTrackData.get(instId);
var instTrack:Null<FlxSound> = buildFlxSoundFromBytes(instTrackData);
if (instTrack == null) return false;
stopExistingInstrumental(state);
state.audioInstTrack = instTrack;
state.postLoadInstrumental();
return true;
}
static function stopExistingInstrumental(state:ChartEditorState):Void
{
if (state.audioInstTrack != null)
{
state.audioInstTrack.stop();
state.audioInstTrack.destroy();
state.audioInstTrack = null;
}
}
/**
* Tell the Chart Editor to select a specific vocal track, that is already loaded.
*/
static function playVocals(state:ChartEditorState, charType:CharacterType, charId:String, instId:String = ''):Bool
{
var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}';
var vocalTrackData:Null<Bytes> = state.audioVocalTrackData.get(trackId);
var vocalTrack:Null<FlxSound> = buildFlxSoundFromBytes(vocalTrackData);
if (state.audioVocalTrackGroup == null) state.audioVocalTrackGroup = new VoicesGroup();
if (vocalTrack != null)
{
switch (charType)
{
case CharacterType.BF:
if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.addPlayerVoice(vocalTrack);
state.audioVocalTrackData.set(state.currentSongCharacterPlayer, Assets.getBytes(path));
case CharacterType.DAD:
if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
state.audioVocalTrackData.set(state.currentSongCharacterOpponent, Assets.getBytes(path));
case BF:
state.audioVocalTrackGroup.addPlayerVoice(vocalTrack);
return true;
case DAD:
state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
return true;
case OTHER:
state.audioVocalTrackGroup.add(vocalTrack);
return true;
default:
if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.add(vocalTrack);
state.audioVocalTrackData.set('default', Assets.getBytes(path));
// Do nothing.
}
return true;
}
return false;
}
/**
* Loads a vocal track from audio byte data.
*/
static function loadVocalsFromBytes(state:ChartEditorState, bytes:haxe.io.Bytes, charKey:String = ''):Bool
static function stopExistingVocals(state:ChartEditorState):Void
{
var openflSound:openfl.media.Sound = new openfl.media.Sound();
openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length);
var vocalTrack:FlxSound = FlxG.sound.load(openflSound, 1.0, false);
if (state.audioVocalTrackGroup != null) state.audioVocalTrackGroup.add(vocalTrack);
state.audioVocalTrackData.set(charKey, bytes);
return true;
}
/**
* Loads an instrumental from an absolute file path, replacing the current instrumental.
*
* @param path The absolute path to the audio file.
*
* @return Success or failure.
*/
static function loadInstrumentalFromPath(state:ChartEditorState, path:Path):Bool
{
#if sys
// Validate file extension.
if (path.ext != null && !ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext))
if (state.audioVocalTrackGroup != null)
{
return false;
state.audioVocalTrackGroup.clear();
}
var fileBytes:haxe.io.Bytes = sys.io.File.getBytes(path.toString());
return loadInstrumentalFromBytes(state, fileBytes, '${path.file}.${path.ext}');
#else
trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
return false;
#end
}
/**
* Loads an instrumental from audio byte data, replacing the current instrumental.
* @param bytes The audio byte data.
* @param fileName The name of the file, if available. Used for notifications.
* @return Success or failure.
*/
static function loadInstrumentalFromBytes(state:ChartEditorState, bytes:haxe.io.Bytes, fileName:String = null):Bool
{
if (bytes == null)
{
return false;
}
var openflSound:openfl.media.Sound = new openfl.media.Sound();
openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(bytes), bytes.length);
state.audioInstTrack = FlxG.sound.load(openflSound, 1.0, false);
state.audioInstTrack.autoDestroy = false;
state.audioInstTrack.pause();
state.audioInstTrackData = bytes;
state.postLoadInstrumental();
return true;
}
/**
* Loads an instrumental from an OpenFL asset, replacing the current instrumental.
* @param path The path to the asset. Use `Paths` to build this.
* @return Success or failure.
*/
static function loadInstrumentalFromAsset(state:ChartEditorState, path:String):Bool
{
var instTrack:FlxSound = FlxG.sound.load(path, 1.0, false);
if (instTrack != null)
{
state.audioInstTrack = instTrack;
state.audioInstTrackData = Assets.getBytes(path);
state.postLoadInstrumental();
return true;
}
return false;
}
/**
* Play a sound effect.
* Automatically cleans up after itself and recycles previous FlxSound instances if available, for performance.
* @param path The path to the sound effect. Use `Paths` to build this.
*/
public static function playSound(path:String):Void
{
var snd:FlxSound = FlxG.sound.list.recycle(FlxSound) ?? new FlxSound();
var asset:Null<FlxSoundAsset> = FlxG.sound.cache(path);
if (asset == null)
{
trace('WARN: Failed to play sound $path, asset not found.');
return;
}
snd.loadEmbedded(asset);
snd.autoDestroy = true;
FlxG.sound.list.add(snd);
snd.play();
}
/**
* Convert byte data into a playable sound.
*
* @param input The byte data.
* @return The playable sound, or `null` if loading failed.
*/
public static function buildFlxSoundFromBytes(input:Null<Bytes>):Null<FlxSound>
{
if (input == null) return null;
var openflSound:openfl.media.Sound = new openfl.media.Sound();
openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(input), input.length);
var output:FlxSound = FlxG.sound.load(openflSound, 1.0, false);
return output;
}
static function makeZIPEntriesFromInstrumentals(state:ChartEditorState):Array<haxe.zip.Entry>
{
var zipEntries = [];
for (key in state.audioInstTrackData.keys())
{
if (key == 'default')
{
var data:Null<Bytes> = state.audioInstTrackData.get('default');
if (data == null) continue;
zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', data));
}
else
{
var data:Null<Bytes> = state.audioInstTrackData.get(key);
if (data == null) continue;
zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst-${key}.ogg', data));
}
}
return zipEntries;
}
static function makeZIPEntriesFromVocals(state:ChartEditorState):Array<haxe.zip.Entry>
{
var zipEntries = [];
for (key in state.audioVocalTrackData.keys())
{
var data:Null<Bytes> = state.audioVocalTrackData.get(key);
if (data == null) continue;
zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-${key}.ogg', data));
}
return zipEntries;
}
}

View file

@ -1,5 +1,7 @@
package funkin.ui.debug.charting;
import haxe.ui.notifications.NotificationType;
import haxe.ui.notifications.NotificationManager;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongDataUtils;
@ -760,6 +762,22 @@ class PasteItemsCommand implements ChartEditorCommand
{
var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard();
if (currentClipboard.valid != true)
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failed to Paste',
body: 'Could not parse clipboard contents.',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
#end
return;
}
trace(currentClipboard.notes);
addedNotes = SongDataUtils.offsetSongNoteData(currentClipboard.notes, Std.int(targetTimestamp));
addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp));
@ -773,6 +791,16 @@ class PasteItemsCommand implements ChartEditorCommand
state.notePreviewDirty = true;
state.sortChartData();
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Paste Successful',
body: 'Successfully pasted clipboard contents.',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
#end
}
public function undo(state:ChartEditorState):Void

View file

@ -83,7 +83,7 @@ class ChartEditorDialogHandler
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable);
if (dialog == null) throw 'Could not locate Welcome dialog';
// Add handlers to the "Create From Song" section.
// Create New Song "Easy/Normal/Hard"
var linkCreateBasic:Null<Link> = dialog.findComponent('splashCreateFromSongBasic', Link);
if (linkCreateBasic == null) throw 'Could not locate splashCreateFromSongBasic link in Welcome dialog';
linkCreateBasic.onClick = function(_event) {
@ -94,7 +94,20 @@ class ChartEditorDialogHandler
//
// Create Song Wizard
//
openCreateSongWizard(state, false);
openCreateSongWizardBasic(state, false);
}
// Create New Song "Erect/Nightmare"
var linkCreateErect:Null<Link> = dialog.findComponent('splashCreateFromSongErect', Link);
if (linkCreateErect == null) throw 'Could not locate splashCreateFromSongErect link in Welcome dialog';
linkCreateErect.onClick = function(_event) {
// Hide the welcome dialog
dialog.hideDialog(DialogButton.CANCEL);
//
// Create Song Wizard
//
openCreateSongWizardErect(state, false);
}
var linkImportChartLegacy:Null<Link> = dialog.findComponent('splashImportChartLegacy', Link);
@ -237,34 +250,112 @@ class ChartEditorDialogHandler
};
}
public static function openCreateSongWizard(state:ChartEditorState, closable:Bool):Void
public static function openCreateSongWizardBasic(state:ChartEditorState, closable:Bool):Void
{
// Step 1. Upload Instrumental
var uploadInstDialog:Dialog = openUploadInstDialog(state, closable);
uploadInstDialog.onDialogClosed = function(_event) {
// Step 1. Song Metadata
var songMetadataDialog:Dialog = openSongMetadataDialog(state);
songMetadataDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
// Step 2. Song Metadata
var songMetadataDialog:Dialog = openSongMetadataDialog(state);
songMetadataDialog.onDialogClosed = function(_event) {
// Step 2. Upload Instrumental
var uploadInstDialog:Dialog = openUploadInstDialog(state, closable);
uploadInstDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
// Step 3. Upload Vocals
// NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
openUploadVocalsDialog(state, false); // var uploadVocalsDialog:Dialog
var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog
uploadVocalsDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
state.switchToCurrentInstrumental();
state.postLoadInstrumental();
}
}
else
{
// User cancelled the wizard! Back to the welcome dialog.
// User cancelled the wizard at Step 2! Back to the welcome dialog.
openWelcomeDialog(state);
}
};
}
else
{
// User cancelled the wizard! Back to the welcome dialog.
// User cancelled the wizard at Step 1! Back to the welcome dialog.
openWelcomeDialog(state);
}
};
}
public static function openCreateSongWizardErect(state:ChartEditorState, closable:Bool):Void
{
// Step 1. Song Metadata
var songMetadataDialog:Dialog = openSongMetadataDialog(state);
songMetadataDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
// Step 2. Upload Instrumental
var uploadInstDialog:Dialog = openUploadInstDialog(state, closable);
uploadInstDialog.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
// Step 3. Upload Vocals
// NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
var uploadVocalsDialog:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog
uploadVocalsDialog.onDialogClosed = function(_event) {
state.switchToCurrentInstrumental();
// Step 4. Song Metadata (Erect)
var songMetadataDialogErect:Dialog = openSongMetadataDialog(state, 'erect');
songMetadataDialogErect.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
// Switch to the Erect variation so uploading the instrumental applies properly.
state.selectedVariation = 'erect';
// Step 5. Upload Instrumental (Erect)
var uploadInstDialogErect:Dialog = openUploadInstDialog(state, closable);
uploadInstDialogErect.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
if (_event.button == DialogButton.APPLY)
{
// Step 6. Upload Vocals (Erect)
// NOTE: Uploading vocals is optional, so we don't need to check if the user cancelled the wizard.
var uploadVocalsDialogErect:Dialog = openUploadVocalsDialog(state, closable); // var uploadVocalsDialog:Dialog
uploadVocalsDialogErect.onDialogClosed = function(_event) {
state.isHaxeUIDialogOpen = false;
state.switchToCurrentInstrumental();
state.postLoadInstrumental();
}
}
else
{
// User cancelled the wizard at Step 5! Back to the welcome dialog.
openWelcomeDialog(state);
}
};
}
else
{
// User cancelled the wizard at Step 4! Back to the welcome dialog.
openWelcomeDialog(state);
}
}
}
}
else
{
// User cancelled the wizard at Step 2! Back to the welcome dialog.
openWelcomeDialog(state);
}
};
}
else
{
// User cancelled the wizard at Step 1! Back to the welcome dialog.
openWelcomeDialog(state);
}
};
@ -302,6 +393,8 @@ class ChartEditorDialogHandler
Cursor.cursorMode = Default;
}
var instId:String = state.currentInstrumentalId;
var onDropFile:String->Void;
instrumentalBox.onClick = function(_event) {
@ -309,14 +402,14 @@ class ChartEditorDialogHandler
{label: 'Audio File (.ogg)', extension: 'ogg'}], function(selectedFile:SelectedFileInfo) {
if (selectedFile != null && selectedFile.bytes != null)
{
if (ChartEditorAudioHandler.loadInstrumentalFromBytes(state, selectedFile.bytes))
if (ChartEditorAudioHandler.loadInstFromBytes(state, selectedFile.bytes, instId))
{
trace('Selected file: ' + selectedFile.fullPath);
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded instrumental track (${selectedFile.name})',
body: 'Loaded instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
@ -333,7 +426,7 @@ class ChartEditorDialogHandler
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load instrumental track (${selectedFile.name})',
body: 'Failed to load instrumental track (${selectedFile.name}) for variation (${state.selectedVariation})',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
@ -346,14 +439,14 @@ class ChartEditorDialogHandler
onDropFile = function(pathStr:String) {
var path:Path = new Path(pathStr);
trace('Dropped file (${path})');
if (ChartEditorAudioHandler.loadInstrumentalFromPath(state, path))
if (ChartEditorAudioHandler.loadInstFromPath(state, path, instId))
{
// Tell the user the load was successful.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded instrumental track (${path.file}.${path.ext})',
body: 'Loaded instrumental track (${path.file}.${path.ext}) for variation (${state.selectedVariation})',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
@ -370,7 +463,7 @@ class ChartEditorDialogHandler
}
else
{
'Failed to load instrumental track (${path.file}.${path.ext})';
'Failed to load instrumental track (${path.file}.${path.ext}) for variation (${state.selectedVariation})';
}
// Tell the user the load was successful.
@ -457,11 +550,18 @@ class ChartEditorDialogHandler
* @return The dialog to open.
*/
@:haxe.warning("-WVarInit")
public static function openSongMetadataDialog(state:ChartEditorState):Dialog
public static function openSongMetadataDialog(state:ChartEditorState, ?targetVariation:String):Dialog
{
if (targetVariation == null) targetVariation = Constants.DEFAULT_VARIATION;
var dialog:Null<Dialog> = openDialog(state, CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT, true, false);
if (dialog == null) throw 'Could not locate Song Metadata dialog';
if (targetVariation != Constants.DEFAULT_VARIATION)
{
dialog.title = 'New Chart - Provide Song Metadata (${targetVariation.toTitleCase()})';
}
var buttonCancel:Null<Button> = dialog.findComponent('dialogCancel', Button);
if (buttonCancel == null) throw 'Could not locate dialogCancel button in Song Metadata dialog';
buttonCancel.onClick = function(_event) {
@ -574,7 +674,11 @@ class ChartEditorDialogHandler
var dialogContinue:Null<Button> = dialog.findComponent('dialogContinue', Button);
if (dialogContinue == null) throw 'Could not locate dialogContinue button in Song Metadata dialog';
dialogContinue.onClick = (_event) -> dialog.hideDialog(DialogButton.APPLY);
dialogContinue.onClick = (_event) -> {
state.songMetadata.set(targetVariation, newSongMetadata);
dialog.hideDialog(DialogButton.APPLY);
}
return dialog;
}
@ -587,6 +691,7 @@ class ChartEditorDialogHandler
*/
public static function openUploadVocalsDialog(state:ChartEditorState, closable:Bool = true):Dialog
{
var instId:String = state.currentInstrumentalId;
var charIdsForVocals:Array<String> = [];
var charData:SongCharacterData = state.currentSongMetadata.playData.characters;
@ -633,14 +738,14 @@ class ChartEditorDialogHandler
trace('Selected file: $pathStr');
var path:Path = new Path(pathStr);
if (ChartEditorAudioHandler.loadVocalsFromPath(state, path, charKey))
if (ChartEditorAudioHandler.loadVocalsFromPath(state, path, charKey, instId))
{
// Tell the user the load was successful.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded vocal track for $charName (${path.file}.${path.ext})',
body: 'Loaded vocals for $charName (${path.file}.${path.ext}), variation ${state.selectedVariation}',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
@ -656,21 +761,14 @@ class ChartEditorDialogHandler
}
else
{
var message:String = if (!ChartEditorState.SUPPORTED_MUSIC_FORMATS.contains(path.ext ?? ''))
{
'File format (${path.ext}) not supported for vocal track (${path.file}.${path.ext})';
}
else
{
'Failed to load vocal track (${path.file}.${path.ext})';
}
trace('Failed to load vocal track (${path.file}.${path.ext})');
// Vocals failed to load.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: message,
body: 'Failed to load vocal track (${path.file}.${path.ext}) for variation (${state.selectedVariation})',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
@ -690,14 +788,46 @@ class ChartEditorDialogHandler
if (selectedFile != null && selectedFile.bytes != null)
{
trace('Selected file: ' + selectedFile.name);
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Vocals for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
#else
vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${selectedFile.name}';
#end
ChartEditorAudioHandler.loadVocalsFromBytes(state, selectedFile.bytes, charKey);
dialogNoVocals.hidden = true;
removeDropHandler(onDropFile);
if (ChartEditorAudioHandler.loadVocalsFromBytes(state, selectedFile.bytes, charKey, instId))
{
// Tell the user the load was successful.
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded vocals for $charName (${selectedFile.name}), variation ${state.selectedVariation}',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
#end
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Vocals for $charName (drag and drop, or click to browse)\nSelected file: ${selectedFile.name}';
#else
vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${selectedFile.name}';
#end
dialogNoVocals.hidden = true;
}
else
{
trace('Failed to load vocal track (${selectedFile.fullPath})');
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failure',
body: 'Failed to load vocal track (${selectedFile.name}) for variation (${state.selectedVariation})',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
#end
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Drag and drop vocals for $charName here, or click to browse.';
#else
vocalsEntryLabel.text = 'Click to browse for vocals for $charName.';
#end
}
}
});
}

View file

@ -36,7 +36,7 @@ class ChartEditorImportExportHandler
for (metadata in rawSongMetadata)
{
if (metadata == null) continue;
var variation = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation;
var variation = (metadata.variation == null || metadata.variation == '') ? Constants.DEFAULT_VARIATION : metadata.variation;
// Clone to prevent modifying the original.
var metadataClone:SongMetadata = metadata.clone(variation);
@ -52,23 +52,44 @@ class ChartEditorImportExportHandler
state.clearVocals();
ChartEditorAudioHandler.loadInstrumentalFromAsset(state, Paths.inst(songId));
var diff:Null<SongDifficulty> = song.getDifficulty(state.selectedDifficulty);
var voiceList:Array<String> = diff != null ? diff.buildVoiceList() : [];
if (voiceList.length == 2)
var variations:Array<String> = state.availableVariations;
for (variation in variations)
{
ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[0], BF);
ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[1], DAD);
}
else
{
for (voicePath in voiceList)
if (variation == Constants.DEFAULT_VARIATION)
{
ChartEditorAudioHandler.loadVocalsFromAsset(state, voicePath);
ChartEditorAudioHandler.loadInstFromAsset(state, Paths.inst(songId));
}
else
{
ChartEditorAudioHandler.loadInstFromAsset(state, Paths.inst(songId, '-$variation'), variation);
}
}
for (difficultyId in song.listDifficulties())
{
var diff:Null<SongDifficulty> = song.getDifficulty(difficultyId);
if (diff == null) continue;
var instId:String = diff.variation == Constants.DEFAULT_VARIATION ? '' : diff.variation;
var voiceList:Array<String> = diff.buildVoiceList(); // SongDifficulty accounts for variation already.
if (voiceList.length == 2)
{
ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[0], diff.characters.player, instId);
ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[1], diff.characters.opponent, instId);
}
else if (voiceList.length == 1)
{
ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[0], diff.characters.player, instId);
}
else
{
trace('[WARN] Strange quantity of voice paths for difficulty ${difficultyId}: ${voiceList.length}');
}
}
state.switchToCurrentInstrumental();
state.refreshMetadataToolbox();
#if !mac
@ -116,10 +137,10 @@ class ChartEditorImportExportHandler
/**
* @param force Whether to force the export without prompting the user for a file location.
* @param tmp If true, save to the temporary directory instead of the local `backup` directory.
*/
public static function exportAllSongData(state:ChartEditorState, force:Bool = false, tmp:Bool = false):Void
public static function exportAllSongData(state:ChartEditorState, force:Bool = false):Void
{
var tmp = false;
var zipEntries:Array<haxe.zip.Entry> = [];
for (variation in state.availableVariations)
@ -133,9 +154,9 @@ class ChartEditorImportExportHandler
if (variationId == '')
{
var variationMetadata:Null<SongMetadata> = state.songMetadata.get(variation);
if (variationMetadata != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata.json', SerializerUtil.toJSON(variationMetadata)));
if (variationMetadata != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata.json', variationMetadata.serialize()));
var variationChart:Null<SongChartData> = state.songChartData.get(variation);
if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart.json', SerializerUtil.toJSON(variationChart)));
if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart.json', variationChart.serialize()));
}
else
{
@ -148,13 +169,8 @@ class ChartEditorImportExportHandler
}
}
if (state.audioInstTrackData != null) zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', state.audioInstTrackData));
for (charId in state.audioVocalTrackData.keys())
{
var entryData = state.audioVocalTrackData.get(charId);
if (entryData == null) continue;
zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-$charId.ogg', entryData));
}
if (state.audioInstTrackData != null) zipEntries.concat(ChartEditorAudioHandler.makeZIPEntriesFromInstrumentals(state));
if (state.audioVocalTrackData != null) zipEntries.concat(ChartEditorAudioHandler.makeZIPEntriesFromVocals(state));
trace('Exporting ${zipEntries.length} files to ZIP...');

View file

@ -461,6 +461,8 @@ class ChartEditorState extends HaxeUIState
notePreviewDirty = true;
notePreviewViewportBoundsDirty = true;
this.scrollPositionInPixels = this.scrollPositionInPixels;
// Characters have probably changed too.
healthIconsDirty = true;
return isViewDownscroll;
}
@ -519,8 +521,14 @@ class ChartEditorState extends HaxeUIState
*/
var selectedVariation(default, set):String = Constants.DEFAULT_VARIATION;
/**
* Setter called when we are switching variations.
* We will likely need to switch instrumentals as well.
*/
function set_selectedVariation(value:String):String
{
// Don't update if we're already on the variation.
if (selectedVariation == value) return selectedVariation;
selectedVariation = value;
// Make sure view is updated when the variation changes.
@ -528,6 +536,8 @@ class ChartEditorState extends HaxeUIState
notePreviewDirty = true;
notePreviewViewportBoundsDirty = true;
switchToCurrentInstrumental();
return selectedVariation;
}
@ -548,6 +558,23 @@ class ChartEditorState extends HaxeUIState
return selectedDifficulty;
}
/**
* The instrumental ID which is currently selected.
*/
var currentInstrumentalId(get, set):String;
function get_currentInstrumentalId():String
{
var instId:Null<String> = currentSongMetadata.playData.characters.instrumental;
if (instId == null || instId == '') instId = (selectedVariation == Constants.DEFAULT_VARIATION) ? '' : selectedVariation;
return instId;
}
function set_currentInstrumentalId(value:String):String
{
return currentSongMetadata.playData.characters.instrumental = value;
}
/**
* The character ID for the character which is currently selected.
*/
@ -592,6 +619,11 @@ class ChartEditorState extends HaxeUIState
*/
var noteDisplayDirty:Bool = true;
/**
* Whether the selected charactesr have been modified and the health icons need to be updated.
*/
var healthIconsDirty:Bool = true;
/**
* Whether the note preview graphic needs to be FULLY rebuilt.
*/
@ -695,6 +727,16 @@ class ChartEditorState extends HaxeUIState
*/
var downKeyHandler:TurboKeyHandler = TurboKeyHandler.build(FlxKey.DOWN);
/**
* Variable used to track how long the user has been holding the W keybind.
*/
var wKeyHandler:TurboKeyHandler = TurboKeyHandler.build(FlxKey.W);
/**
* Variable used to track how long the user has been holding the S keybind.
*/
var sKeyHandler:TurboKeyHandler = TurboKeyHandler.build(FlxKey.S);
/**
* Variable used to track how long the user has been holding the page-up keybind.
*/
@ -763,28 +805,29 @@ class ChartEditorState extends HaxeUIState
/**
* The audio track for the instrumental.
* Replaced when switching instrumentals.
* `null` until an instrumental track is loaded.
*/
var audioInstTrack:Null<FlxSound> = null;
/**
* The raw byte data for the instrumental audio track.
* The raw byte data for the instrumental audio tracks.
* Key is the instrumental name.
* `null` until an instrumental track is loaded.
*/
var audioInstTrackData:Null<Bytes> = null;
var audioInstTrackData:Map<String, Bytes> = [];
/**
* The audio track for the vocals.
* `null` until vocal track(s) are loaded.
* When switching characters, the elements of the VoicesGroup will be swapped to match the new character.
*/
var audioVocalTrackGroup:Null<VoicesGroup> = null;
/**
* A map of the audio tracks for each character's vocals.
* - Keys are the character IDs.
* - Values are the FlxSound objects to play that character's vocals.
*
* When switching characters, the elements of the VoicesGroup will be swapped to match the new character.
* - Keys are `characterId-variation` (with `characterId` being the default variation).
* - Values are the byte data for the audio track.
*/
var audioVocalTrackData:Map<String, Bytes> = [];
@ -1035,30 +1078,6 @@ class ChartEditorState extends HaxeUIState
return currentSongMetadata.artist = value;
}
var currentSongCharacterPlayer(get, set):String;
function get_currentSongCharacterPlayer():String
{
return currentSongMetadata.playData.characters.player;
}
function set_currentSongCharacterPlayer(value:String):String
{
return currentSongMetadata.playData.characters.player = value;
}
var currentSongCharacterOpponent(get, set):String;
function get_currentSongCharacterOpponent():String
{
return currentSongMetadata.playData.characters.opponent;
}
function set_currentSongCharacterOpponent(value:String):String
{
return currentSongMetadata.playData.characters.opponent = value;
}
/**
* SIGNALS
*/
@ -1369,7 +1388,7 @@ class ChartEditorState extends HaxeUIState
gridPlayhead.add(playheadBlock);
// Character icons.
healthIconDad = new HealthIcon(currentSongCharacterOpponent);
healthIconDad = new HealthIcon(currentSongMetadata.playData.characters.opponent);
healthIconDad.autoUpdate = false;
healthIconDad.size.set(0.5, 0.5);
healthIconDad.x = gridTiledSprite.x - 15 - (HealthIcon.HEALTH_ICON_SIZE * 0.5);
@ -1377,7 +1396,7 @@ class ChartEditorState extends HaxeUIState
add(healthIconDad);
healthIconDad.zIndex = 30;
healthIconBF = new HealthIcon(currentSongCharacterPlayer);
healthIconBF = new HealthIcon(currentSongMetadata.playData.characters.player);
healthIconBF.autoUpdate = false;
healthIconBF.size.set(0.5, 0.5);
healthIconBF.x = gridTiledSprite.x + gridTiledSprite.width + 15;
@ -1474,6 +1493,12 @@ class ChartEditorState extends HaxeUIState
return bounds;
}
public function switchToCurrentInstrumental():Void
{
ChartEditorAudioHandler.switchToInstrumental(this, currentInstrumentalId, currentSongMetadata.playData.characters.player,
currentSongMetadata.playData.characters.opponent);
}
function setNotePreviewViewportBounds(bounds:FlxRect = null):Void
{
if (notePreviewViewport == null)
@ -1522,11 +1547,11 @@ class ChartEditorState extends HaxeUIState
renderedEvents.setPosition(gridTiledSprite.x, gridTiledSprite.y);
add(renderedEvents);
renderedNotes.zIndex = 25;
renderedEvents.zIndex = 25;
renderedSelectionSquares.setPosition(gridTiledSprite.x, gridTiledSprite.y);
add(renderedSelectionSquares);
renderedNotes.zIndex = 26;
renderedSelectionSquares.zIndex = 26;
}
function buildAdditionalUI():Void
@ -1609,6 +1634,7 @@ class ChartEditorState extends HaxeUIState
addUIClickListener('menubarItemSaveChartAs', _ -> ChartEditorImportExportHandler.exportAllSongData(this));
addUIClickListener('menubarItemLoadInst', _ -> ChartEditorDialogHandler.openUploadInstDialog(this, true));
addUIClickListener('menubarItemImportChart', _ -> ChartEditorDialogHandler.openImportChartDialog(this, 'legacy', true));
addUIClickListener('menubarItemExit', _ -> quitChartEditor());
addUIClickListener('menubarItemUndo', _ -> undoLastCommand());
@ -1636,7 +1662,18 @@ class ChartEditorState extends HaxeUIState
addUIClickListener('menubarItemCut', _ -> performCommand(new CutItemsCommand(currentNoteSelection, currentEventSelection)));
addUIClickListener('menubarItemPaste', _ -> performCommand(new PasteItemsCommand(scrollPositionInMs + playheadPositionInMs)));
addUIClickListener('menubarItemPaste', _ -> {
var targetMs:Float = scrollPositionInMs + playheadPositionInMs;
var targetStep:Float = Conductor.getTimeInSteps(targetMs);
var targetSnappedStep:Float = Math.floor(targetStep / noteSnapRatio) * noteSnapRatio;
var targetSnappedMs:Float = Conductor.getStepTimeInMs(targetSnappedStep);
performCommand(new PasteItemsCommand(targetSnappedMs));
});
addUIClickListener('menubarItemPasteUnsnapped', _ -> {
var targetMs:Float = scrollPositionInMs + playheadPositionInMs;
performCommand(new PasteItemsCommand(targetMs));
});
addUIClickListener('menubarItemDelete', function(_) {
if (currentNoteSelection.length > 0 && currentEventSelection.length > 0)
@ -1663,19 +1700,24 @@ class ChartEditorState extends HaxeUIState
addUIClickListener('menubarItemSelectNone', _ -> performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection)));
// TODO: Implement these.
// addUIClickListener('menubarItemSelectRegion', _ -> doSomething());
// addUIClickListener('menubarItemSelectBeforeCursor', _ -> doSomething());
// addUIClickListener('menubarItemSelectAfterCursor', _ -> doSomething());
addUIClickListener('menubarItemPlaytestFull', _ -> testSongInPlayState(false));
addUIClickListener('menubarItemPlaytestMinimal', _ -> testSongInPlayState(true));
addUIChangeListener('menubarItemInputStyleGroup', function(event:UIEvent) {
trace('Change input style: ${event.target}');
addUIClickListener('menuBarItemNoteSnapDecrease', _ -> noteSnapQuantIndex--);
addUIClickListener('menuBarItemNoteSnapIncrease', _ -> noteSnapQuantIndex++);
addUIChangeListener('menuBarItemInputStyleNone', function(event:UIEvent) {
currentLiveInputStyle = None;
});
addUIChangeListener('menuBarItemInputStyleNumberKeys', function(event:UIEvent) {
currentLiveInputStyle = NumberKeys;
});
addUIChangeListener('menuBarItemInputStyleWASD', function(event:UIEvent) {
currentLiveInputStyle = WASD;
});
addUIClickListener('menubarItemAbout', _ -> ChartEditorDialogHandler.openAboutDialog(this));
addUIClickListener('menubarItemWelcomeDialog', _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true));
addUIClickListener('menubarItemUserGuide', _ -> ChartEditorDialogHandler.openUserGuideDialog(this));
@ -1698,6 +1740,11 @@ class ChartEditorState extends HaxeUIState
});
setUICheckboxSelected('menuBarItemThemeDark', currentTheme == ChartEditorTheme.Dark);
addUIClickListener('menubarItemPlayPause', _ -> toggleAudioPlayback());
addUIClickListener('menubarItemLoadInstrumental', _ -> ChartEditorDialogHandler.openUploadInstDialog(this, true));
addUIClickListener('menubarItemLoadVocals', _ -> ChartEditorDialogHandler.openUploadVocalsDialog(this, true));
addUIChangeListener('menubarItemMetronomeEnabled', event -> isMetronomeEnabled = event.value);
setUICheckboxSelected('menubarItemMetronomeEnabled', isMetronomeEnabled);
@ -1711,7 +1758,7 @@ class ChartEditorState extends HaxeUIState
if (instVolumeLabel != null)
{
addUIChangeListener('menubarItemVolumeInstrumental', function(event:UIEvent) {
var volume:Float = event?.value ?? 0 / 100.0;
var volume:Float = (event?.value ?? 0) / 100.0;
if (audioInstTrack != null) audioInstTrack.volume = volume;
instVolumeLabel.text = 'Instrumental - ${Std.int(event.value)}%';
});
@ -1721,7 +1768,7 @@ class ChartEditorState extends HaxeUIState
if (vocalsVolumeLabel != null)
{
addUIChangeListener('menubarItemVolumeVocals', function(event:UIEvent) {
var volume:Float = event?.value ?? 0 / 100.0;
var volume:Float = (event?.value ?? 0) / 100.0;
if (audioVocalTrackGroup != null) audioVocalTrackGroup.volume = volume;
vocalsVolumeLabel.text = 'Vocals - ${Std.int(event.value)}%';
});
@ -1769,6 +1816,8 @@ class ChartEditorState extends HaxeUIState
add(redoKeyHandler);
add(upKeyHandler);
add(downKeyHandler);
add(wKeyHandler);
add(sKeyHandler);
add(pageUpKeyHandler);
add(pageDownKeyHandler);
}
@ -1795,7 +1844,7 @@ class ChartEditorState extends HaxeUIState
// Auto-save to local storage.
#else
// Auto-save to temp file.
ChartEditorImportExportHandler.exportAllSongData(this, true, true);
ChartEditorImportExportHandler.exportAllSongData(this, true);
#end
}
@ -1817,6 +1866,13 @@ class ChartEditorState extends HaxeUIState
public override function update(elapsed:Float):Void
{
// Override F4 behavior to include the autosave.
if (FlxG.keys.justPressed.F4)
{
quitChartEditor();
return;
}
// dispatchEvent gets called here.
super.update(elapsed);
@ -1896,20 +1952,33 @@ class ChartEditorState extends HaxeUIState
// Mouse Wheel = Scroll
if (FlxG.mouse.wheel != 0 && !FlxG.keys.pressed.CONTROL)
{
scrollAmount = -10 * FlxG.mouse.wheel;
scrollAmount = -50 * FlxG.mouse.wheel;
shouldPause = true;
}
// Up Arrow = Scroll Up
if (upKeyHandler.activated && currentLiveInputStyle != LiveInputStyle.WASD)
if (upKeyHandler.activated && currentLiveInputStyle == None)
{
scrollAmount = -GRID_SIZE * 0.25 * 5.0;
scrollAmount = -GRID_SIZE * 0.25 * 25.0;
shouldPause = true;
}
// Down Arrow = Scroll Down
if (downKeyHandler.activated && currentLiveInputStyle != LiveInputStyle.WASD)
if (downKeyHandler.activated && currentLiveInputStyle == None)
{
scrollAmount = GRID_SIZE * 0.25 * 5.0;
scrollAmount = GRID_SIZE * 0.25 * 25.0;
shouldPause = true;
}
// W = Scroll Up (doesn't work with Ctrl+Scroll)
if (wKeyHandler.activated && currentLiveInputStyle == None && !FlxG.keys.pressed.CONTROL)
{
scrollAmount = -GRID_SIZE * 0.25 * 25.0;
shouldPause = true;
}
// S = Scroll Down (doesn't work with Ctrl+Scroll)
if (sKeyHandler.activated && currentLiveInputStyle == None && !FlxG.keys.pressed.CONTROL)
{
scrollAmount = GRID_SIZE * 0.25 * 25.0;
shouldPause = true;
}
@ -1974,7 +2043,7 @@ class ChartEditorState extends HaxeUIState
// SHIFT + Scroll = Scroll Fast
if (FlxG.keys.pressed.SHIFT)
{
scrollAmount *= 5;
scrollAmount *= 2;
}
// CONTROL + Scroll = Scroll Precise
if (FlxG.keys.pressed.CONTROL)
@ -2045,14 +2114,17 @@ class ChartEditorState extends HaxeUIState
function handleSnap():Void
{
if (FlxG.keys.justPressed.LEFT && !FlxG.keys.pressed.CONTROL)
if (currentLiveInputStyle == None)
{
noteSnapQuantIndex--;
}
if (FlxG.keys.justPressed.LEFT && !FlxG.keys.pressed.CONTROL)
{
noteSnapQuantIndex--;
}
if (FlxG.keys.justPressed.RIGHT && !FlxG.keys.pressed.CONTROL)
{
noteSnapQuantIndex++;
if (FlxG.keys.justPressed.RIGHT && !FlxG.keys.pressed.CONTROL)
{
noteSnapQuantIndex++;
}
}
}
@ -2274,7 +2346,6 @@ class ChartEditorState extends HaxeUIState
// Scroll up.
var diff:Float = MENU_BAR_HEIGHT - FlxG.mouse.screenY;
scrollPositionInPixels -= diff * 0.5; // Too fast!
trace('Scroll up: ' + diff);
moveSongToScrollPosition();
}
else if (FlxG.mouse.screenY > (playbarHeadLayout?.y ?? 0.0))
@ -2282,7 +2353,6 @@ class ChartEditorState extends HaxeUIState
// Scroll down.
var diff:Float = FlxG.mouse.screenY - (playbarHeadLayout?.y ?? 0.0);
scrollPositionInPixels += diff * 0.5; // Too fast!
trace('Scroll down: ' + diff);
moveSongToScrollPosition();
}
@ -2907,8 +2977,8 @@ class ChartEditorState extends HaxeUIState
// Set the position and size (because we might be recycling one with bad values).
selectionSquare.x = noteSprite.x;
selectionSquare.y = noteSprite.y;
selectionSquare.width = noteSprite.width;
selectionSquare.height = noteSprite.height;
selectionSquare.width = GRID_SIZE;
selectionSquare.height = GRID_SIZE;
}
}
@ -2939,6 +3009,8 @@ class ChartEditorState extends HaxeUIState
FlxG.watch.addQuick("tapNotesRendered", renderedNotes.members.length);
FlxG.watch.addQuick("holdNotesRendered", renderedHoldNotes.members.length);
FlxG.watch.addQuick("eventsRendered", renderedEvents.members.length);
FlxG.watch.addQuick("notesSelected", currentNoteSelection.length);
FlxG.watch.addQuick("eventsSelected", currentEventSelection.length);
}
/**
@ -2946,6 +3018,12 @@ class ChartEditorState extends HaxeUIState
*/
function handleHealthIcons():Void
{
if (healthIconsDirty)
{
if (healthIconBF != null) healthIconBF.characterId = currentSongMetadata.playData.characters.player;
if (healthIconDad != null) healthIconDad.characterId = currentSongMetadata.playData.characters.opponent;
}
// Right align the BF health icon.
if (healthIconBF != null)
{
@ -2962,6 +3040,8 @@ class ChartEditorState extends HaxeUIState
if (selectionSquareBitmap == null)
throw "ERROR: Tried to build selection square, but selectionSquareBitmap is null! Check ChartEditorThemeHandler.updateSelectionSquare()";
FlxG.bitmapLog.add(selectionSquareBitmap, "selectionSquareBitmap");
return new FlxSprite().loadGraphic(selectionSquareBitmap);
}
@ -3032,10 +3112,16 @@ class ChartEditorState extends HaxeUIState
// CTRL + Q = Quit to Menu
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.Q)
{
FlxG.switchState(new MainMenuState());
quitChartEditor();
}
}
function quitChartEditor():Void
{
autoSave();
FlxG.switchState(new MainMenuState());
}
/**
* Handle keybinds for edit menu items.
*/
@ -3075,8 +3161,20 @@ class ChartEditorState extends HaxeUIState
// CTRL + V = Paste
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.V)
{
// Paste notes from clipboard, at the playhead.
performCommand(new PasteItemsCommand(scrollPositionInMs + playheadPositionInMs));
// CTRL + SHIFT + V = Paste Unsnapped.
var targetMs:Float = if (FlxG.keys.pressed.SHIFT)
{
scrollPositionInMs + playheadPositionInMs;
}
else
{
var targetMs:Float = scrollPositionInMs + playheadPositionInMs;
var targetStep:Float = Conductor.getTimeInSteps(targetMs);
var targetSnappedStep:Float = Math.floor(targetStep / noteSnapRatio) * noteSnapRatio;
var targetSnappedMs:Float = Conductor.getStepTimeInMs(targetSnappedStep);
targetSnappedMs;
}
performCommand(new PasteItemsCommand(targetMs));
}
// DELETE = Delete
@ -3124,13 +3222,17 @@ class ChartEditorState extends HaxeUIState
*/
function handleViewKeybinds():Void
{
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.LEFT)
if (currentLiveInputStyle == None)
{
incrementDifficulty(-1);
}
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.RIGHT)
{
incrementDifficulty(1);
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.LEFT)
{
incrementDifficulty(-1);
}
if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.RIGHT)
{
incrementDifficulty(1);
}
// Would bind Ctrl+A and Ctrl+D here, but they are already bound to Select All and Select None.
}
}
@ -3237,7 +3339,7 @@ class ChartEditorState extends HaxeUIState
*/
function handleTestKeybinds():Void
{
if (!isHaxeUIDialogOpen && FlxG.keys.justPressed.ENTER)
if (!isHaxeUIDialogOpen && !isCursorOverHaxeUI && FlxG.keys.justPressed.ENTER)
{
var minimal = FlxG.keys.pressed.SHIFT;
testSongInPlayState(minimal);
@ -3363,11 +3465,11 @@ class ChartEditorState extends HaxeUIState
{
playerPreviewDirty = false;
if (currentSongCharacterPlayer != charPlayer.charId)
if (currentSongMetadata.playData.characters.player != charPlayer.charId)
{
if (healthIconBF != null) healthIconBF.characterId = currentSongCharacterPlayer;
if (healthIconBF != null) healthIconBF.characterId = currentSongMetadata.playData.characters.player;
charPlayer.loadCharacter(currentSongCharacterPlayer);
charPlayer.loadCharacter(currentSongMetadata.playData.characters.player);
charPlayer.characterType = CharacterType.BF;
charPlayer.flip = true;
charPlayer.targetScale = 0.5;
@ -3399,11 +3501,11 @@ class ChartEditorState extends HaxeUIState
{
opponentPreviewDirty = false;
if (currentSongCharacterOpponent != charPlayer.charId)
if (currentSongMetadata.playData.characters.opponent != charPlayer.charId)
{
if (healthIconDad != null) healthIconDad.characterId = currentSongCharacterOpponent;
if (healthIconDad != null) healthIconDad.characterId = currentSongMetadata.playData.characters.opponent;
charPlayer.loadCharacter(currentSongCharacterOpponent);
charPlayer.loadCharacter(currentSongMetadata.playData.characters.opponent);
charPlayer.characterType = CharacterType.DAD;
charPlayer.flip = false;
charPlayer.targetScale = 0.5;
@ -3783,9 +3885,9 @@ class ChartEditorState extends HaxeUIState
switch (noteData.getStrumlineIndex())
{
case 0: // Player
if (hitsoundsEnabledPlayer) ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-09'));
if (hitsoundsEnabledPlayer) ChartEditorAudioHandler.playSound(Paths.sound('ui/chart-editor/playerHitsound'));
case 1: // Opponent
if (hitsoundsEnabledOpponent) ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-010'));
if (hitsoundsEnabledOpponent) ChartEditorAudioHandler.playSound(Paths.sound('ui/chart-editor/opponentHitsound'));
}
}
}
@ -3829,25 +3931,26 @@ class ChartEditorState extends HaxeUIState
switch (currentLiveInputStyle)
{
case LiveInputStyle.WASD:
if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(0);
if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(1);
if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(2);
if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(3);
if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4);
if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5);
if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6);
if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(7);
if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(4);
if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(5);
if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(6);
if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(7);
if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(0);
if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(1);
if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(2);
if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(3);
case LiveInputStyle.NumberKeys:
if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(0);
if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(1);
if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(2);
if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(3);
// Flipped because Dad is on the left but represents data 0-3.
if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(4);
if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(5);
if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(6);
if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(7);
if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(4);
if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(5);
if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(6);
if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(7);
if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(0);
if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(1);
if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(2);
if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(3);
case LiveInputStyle.None:
// Do nothing.
}
@ -3856,12 +3959,24 @@ class ChartEditorState extends HaxeUIState
function placeNoteAtPlayhead(column:Int):Void
{
var playheadPos:Float = scrollPositionInPixels + playheadPositionInPixels;
var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / (16 / noteSnapQuant);
var playheadPosFractionalStep:Float = playheadPos / GRID_SIZE / noteSnapRatio;
var playheadPosStep:Int = Std.int(Math.floor(playheadPosFractionalStep));
var playheadPosMs:Float = playheadPosStep * Conductor.stepLengthMs * (16 / noteSnapQuant);
var playheadPosSnappedMs:Float = playheadPosStep * Conductor.stepLengthMs * noteSnapRatio;
var newNoteData:SongNoteData = new SongNoteData(playheadPosMs, column, 0, selectedNoteKind);
performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
// Look for notes within 1 step of the playhead.
var notesAtPos:Array<SongNoteData> = SongDataUtils.getNotesInTimeRange(currentSongChartNoteData, playheadPosSnappedMs,
playheadPosSnappedMs + Conductor.stepLengthMs * noteSnapRatio);
notesAtPos = SongDataUtils.getNotesWithData(notesAtPos, [column]);
if (notesAtPos.length == 0)
{
var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, selectedNoteKind);
performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
}
else
{
trace('Already a note there.');
}
}
function set_scrollPositionInPixels(value:Float):Float
@ -3920,6 +4035,8 @@ class ChartEditorState extends HaxeUIState
*/
public function testSongInPlayState(minimal:Bool = false):Void
{
autoSave();
var startTimestamp:Float = 0;
if (playtestStartTime) startTimestamp = scrollPositionInMs + playheadPositionInMs;
@ -4007,12 +4124,18 @@ class ChartEditorState extends HaxeUIState
buildSpectrogram(audioInstTrack);
}
else
{
trace('[WARN] Instrumental track was null!');
}
// Pretty much everything is going to need to be reset.
scrollPositionInPixels = 0;
playheadPositionInPixels = 0;
notePreviewDirty = true;
notePreviewViewportBoundsDirty = true;
noteDisplayDirty = true;
healthIconsDirty = true;
moveSongToScrollPosition();
}

View file

@ -155,6 +155,32 @@ class Level implements IRegistryEntry<LevelData>
}
}
// sort the difficulties, since they may be out of order in the chart JSON
// also copy/pasted to Song.listDifficulties()!
var diffMap:Map<String, Int> = new Map<String, Int>();
for (difficulty in difficulties)
{
var num:Int = 0;
switch (difficulty)
{
case 'easy':
num = 0;
case 'normal':
num = 1;
case 'hard':
num = 2;
case 'erect':
num = 3;
case 'nightmare':
num = 4;
}
diffMap.set(difficulty, num);
}
difficulties.sort(function(a:String, b:String) {
return diffMap.get(a) - diffMap.get(b);
});
// Filter to only include difficulties that are present in all songs
for (songIndex in 1...songList.length)
{

View file

@ -544,6 +544,7 @@ class StoryMenuState extends MusicBeatState
PlayStatePlaylist.campaignId = currentLevel.id;
PlayStatePlaylist.campaignTitle = currentLevel.getTitle();
PlayStatePlaylist.campaignDifficulty = currentDifficultyId;
if (targetSong != null)
{
@ -559,7 +560,7 @@ class StoryMenuState extends MusicBeatState
LoadingState.loadAndSwitchState(new PlayState(
{
targetSong: targetSong,
targetDifficulty: currentDifficultyId,
targetDifficulty: PlayStatePlaylist.campaignDifficulty,
}), true);
});
}

View file

@ -4,6 +4,9 @@ import flixel.util.FlxColor;
import lime.app.Application;
import funkin.data.song.SongData.SongTimeFormat;
/**
* A store of unchanging, globally relevant values.
*/
class Constants
{
/**
@ -39,7 +42,7 @@ class Constants
*/
public static final VERSION_SUFFIX:String = ' PROTOTYPE';
#if debug
#if (debug || FORCE_DEBUG_VERSION)
static function get_VERSION():String
{
return 'v${Application.current.meta.get('version')} (${GIT_BRANCH} : ${GIT_HASH})' + VERSION_SUFFIX;
@ -71,7 +74,7 @@ class Constants
*/
// ==============================
#if debug
#if (debug || FORCE_DEBUG_VERSION)
/**
* The current Git branch.
*/

View file

@ -0,0 +1,44 @@
package funkin.util;
import flixel.input.gamepad.FlxGamepad;
import flixel.input.gamepad.FlxGamepadInputID;
import lime.ui.Gamepad as LimeGamepad;
import lime.ui.GamepadAxis as LimeGamepadAxis;
import lime.ui.GamepadButton as LimeGamepadButton;
class FlxGamepadUtil
{
public static function getInputID(gamepad:FlxGamepad, button:LimeGamepadButton):FlxGamepadInputID
{
#if FLX_GAMEINPUT_API
// FLX_GAMEINPUT_API internally assigns 6 axes to IDs 0-5, which LimeGamepadButton doesn't account for, so we need to offset the ID by 6.
final OFFSET:Int = 6;
#else
final OFFSET:Int = 0;
#end
var result:FlxGamepadInputID = gamepad.mapping.getID(button + OFFSET);
if (result == NONE) return NONE;
return result;
}
public static function getLimeGamepad(input:FlxGamepad):Null<LimeGamepad>
{
#if FLX_GAMEINPUT_API @:privateAccess
return input._device.getLimeGamepad();
#else
return null;
#end
}
@:privateAccess
public static function getFlxGamepadByLimeGamepad(gamepad:LimeGamepad):FlxGamepad
{
// Why is this so elaborate?
@:privateAccess
var gameInputDevice:openfl.ui.GameInputDevice = openfl.ui.GameInput.__getDevice(gamepad);
@:privateAccess
var gamepadIndex:Int = FlxG.gamepads.findGamepadIndex(gameInputDevice);
return FlxG.gamepads.getByID(gamepadIndex);
}
}

View file

@ -1,6 +1,6 @@
package funkin.util.macro;
#if debug
#if (debug || FORCE_DEBUG_VERSION)
class GitCommit
{
/**