1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-07-05 10:16:39 +00:00

Merge branch 'rewrite/master' into bugfix/freeplay-fixins

This commit is contained in:
EliteMasterEric 2023-10-19 00:46:25 -04:00
commit 33644d0c7f
48 changed files with 1234 additions and 449 deletions

View file

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

View file

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

1
.gitmodules vendored
View file

@ -1,6 +1,7 @@
[submodule "assets"] [submodule "assets"]
path = assets path = assets
url = https://github.com/FunkinCrew/Funkin-history-rewrite-assets url = https://github.com/FunkinCrew/Funkin-history-rewrite-assets
branch = master
[submodule "art"] [submodule "art"]
path = art path = art
url = https://github.com/FunkinCrew/Funkin-history-rewrite-art url = https://github.com/FunkinCrew/Funkin-history-rewrite-art

6
.vscode/launch.json vendored
View file

@ -23,6 +23,12 @@
"name": "Haxe Eval", "name": "Haxe Eval",
"type": "haxe-eval", "type": "haxe-eval",
"request": "launch" "request": "launch"
},
{
// Attaches the debugger to an already running game
"name": "HXCPP - Attach",
"type": "hxcpp",
"request": "attach"
} }
] ]
} }

2
assets

@ -1 +1 @@
Subproject commit 8104d43e584a1f25e574438d7b21a7e671358969 Subproject commit ef79a6cf1ae3dcbd86a5b798f8117a6c692c0156

View file

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

View file

@ -85,6 +85,13 @@ class Main extends Sprite
initHaxeUI(); 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. // George recommends binding the save before FlxGame is created.
Save.load(); Save.load();
@ -93,15 +100,6 @@ class Main extends Sprite
#if hxcpp_debug_server #if hxcpp_debug_server
trace('hxcpp_debug_server is enabled! You can now connect to the game with a debugger.'); trace('hxcpp_debug_server is enabled! You can now connect to the game with a debugger.');
#end #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 function initHaxeUI():Void

View file

@ -1,5 +1,7 @@
package funkin; package funkin;
import flixel.input.gamepad.FlxGamepad;
import flixel.util.FlxDirectionFlags; import flixel.util.FlxDirectionFlags;
import flixel.FlxObject; import flixel.FlxObject;
import flixel.input.FlxInput; import flixel.input.FlxInput;
@ -832,6 +834,14 @@ class Controls extends FlxActionSet
fromSaveData(padData, Gamepad(id)); 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 inline function addGamepadLiteral(id:Int, ?buttonMap:Map<Control, Array<FlxGamepadInputID>>):Void
{ {
gamepadsAdded.push(id); gamepadsAdded.push(id);

View file

@ -1,5 +1,6 @@
package funkin; package funkin;
import funkin.play.song.Song;
import flash.text.TextField; import flash.text.TextField;
import flixel.addons.display.FlxGridOverlay; import flixel.addons.display.FlxGridOverlay;
import flixel.addons.transition.FlxTransitionableState; import flixel.addons.transition.FlxTransitionableState;
@ -48,10 +49,13 @@ import lime.utils.Assets;
class FreeplayState extends MusicBeatSubState class FreeplayState extends MusicBeatSubState
{ {
var songs:Array<FreeplaySongData> = []; var songs:Array<Null<FreeplaySongData>> = [];
var diffIdsCurrent:Array<String> = [];
var diffIdsTotal:Array<String> = [];
var curSelected:Int = 0; var curSelected:Int = 0;
var curDifficulty:Int = 1; var currentDifficulty:String = Constants.DEFAULT_DIFFICULTY;
var fp:FreeplayScore; var fp:FreeplayScore;
var txtCompletion:FlxText; var txtCompletion:FlxText;
@ -60,7 +64,7 @@ class FreeplayState extends MusicBeatSubState
var lerpScore:Float = 0; var lerpScore:Float = 0;
var intendedScore:Int = 0; var intendedScore:Int = 0;
var grpDifficulties:FlxSpriteGroup; var grpDifficulties:FlxTypedSpriteGroup<DifficultySprite>;
var coolColors:Array<Int> = [ var coolColors:Array<Int> = [
0xff9271fd, 0xff9271fd,
@ -85,6 +89,10 @@ class FreeplayState extends MusicBeatSubState
var stickerSubState:StickerSubState; var stickerSubState:StickerSubState;
//
static var rememberedDifficulty:Null<String> = "normal";
static var rememberedSongId:Null<String> = null;
public function new(?stickers:StickerSubState = null) public function new(?stickers:StickerSubState = null)
{ {
if (stickers != null) if (stickers != null)
@ -130,14 +138,23 @@ class FreeplayState extends MusicBeatSubState
songs.push(null); songs.push(null);
// programmatically adds the songs via LevelRegistry and SongRegistry // programmatically adds the songs via LevelRegistry and SongRegistry
for (coolWeek in LevelRegistry.instance.listBaseGameLevelIds()) for (levelId in LevelRegistry.instance.listBaseGameLevelIds())
{ {
for (songId in LevelRegistry.instance.parseEntryData(coolWeek).songs) for (songId in LevelRegistry.instance.parseEntryData(levelId).songs)
{ {
var metadata = SongRegistry.instance.parseEntryMetadata(songId); var song:Song = SongRegistry.instance.fetchEntry(songId);
var char = metadata.playData.characters.opponent; var songBaseDifficulty:SongDifficulty = song.getDifficulty(Constants.DEFAULT_DIFFICULTY);
var songName = metadata.songName;
addSong(songId, songName, coolWeek, char); var songName = songBaseDifficulty.songName;
var songOpponent = songBaseDifficulty.characters.opponent;
var songDifficulties = song.listDifficulties();
songs.push(new FreeplaySongData(songId, songName, levelId, songOpponent, songDifficulties));
for (difficulty in songDifficulties)
{
diffIdsTotal.pushUnique(difficulty);
}
} }
} }
@ -283,7 +300,7 @@ class FreeplayState extends MusicBeatSubState
grpCapsules = new FlxTypedGroup<SongMenuItem>(); grpCapsules = new FlxTypedGroup<SongMenuItem>();
add(grpCapsules); add(grpCapsules);
grpDifficulties = new FlxSpriteGroup(-300, 80); grpDifficulties = new FlxTypedSpriteGroup<DifficultySprite>(-300, 80);
add(grpDifficulties); add(grpDifficulties);
exitMovers.set([grpDifficulties], exitMovers.set([grpDifficulties],
@ -293,15 +310,22 @@ class FreeplayState extends MusicBeatSubState
wait: 0 wait: 0
}); });
grpDifficulties.add(new FlxSprite().loadGraphic(Paths.image('freeplay/freeplayEasy'))); for (diffId in diffIdsTotal)
grpDifficulties.add(new FlxSprite().loadGraphic(Paths.image('freeplay/freeplayNorm'))); {
grpDifficulties.add(new FlxSprite().loadGraphic(Paths.image('freeplay/freeplayHard'))); var diffSprite:DifficultySprite = new DifficultySprite(diffId);
diffSprite.difficultyId = diffId;
grpDifficulties.add(diffSprite);
}
grpDifficulties.group.forEach(function(spr) { grpDifficulties.group.forEach(function(spr) {
spr.visible = false; spr.visible = false;
}); });
grpDifficulties.group.members[curDifficulty].visible = true; for (diffSprite in grpDifficulties.group.members)
{
if (diffSprite == null) continue;
if (diffSprite.difficultyId == currentDifficulty) diffSprite.visible = true;
}
var albumArt:FlxAtlasSprite = new FlxAtlasSprite(640, 360, Paths.animateAtlas("freeplay/albumRoll")); var albumArt:FlxAtlasSprite = new FlxAtlasSprite(640, 360, Paths.animateAtlas("freeplay/albumRoll"));
albumArt.visible = false; albumArt.visible = false;
@ -574,15 +598,12 @@ class FreeplayState extends MusicBeatSubState
FlxG.console.registerFunction("changeSelection", changeSelection); FlxG.console.registerFunction("changeSelection", changeSelection);
rememberSelection();
changeSelection(); changeSelection();
changeDiff(); changeDiff();
} }
public function addSong(songId:String, songName:String, levelId:String, songCharacter:String)
{
songs.push(new FreeplaySongData(songId, songName, levelId, songCharacter));
}
var touchY:Float = 0; var touchY:Float = 0;
var touchX:Float = 0; var touchX:Float = 0;
var dxTouch:Float = 0; var dxTouch:Float = 0;
@ -861,28 +882,24 @@ class FreeplayState extends MusicBeatSubState
{ {
touchTimer = 0; touchTimer = 0;
curDifficulty += change; var currentDifficultyIndex = diffIdsCurrent.indexOf(currentDifficulty);
if (curDifficulty < 0) curDifficulty = 2; if (currentDifficultyIndex == -1) currentDifficultyIndex = diffIdsCurrent.indexOf(Constants.DEFAULT_DIFFICULTY);
if (curDifficulty > 2) curDifficulty = 0;
var targetDifficulty:String = switch (curDifficulty) currentDifficultyIndex += change;
{
case 0: if (currentDifficultyIndex < 0) currentDifficultyIndex = diffIdsCurrent.length - 1;
'easy'; if (currentDifficultyIndex >= diffIdsCurrent.length) currentDifficultyIndex = 0;
case 1:
'normal'; currentDifficulty = diffIdsCurrent[currentDifficultyIndex];
case 2:
'hard';
default: 'normal';
};
var daSong = songs[curSelected]; var daSong = songs[curSelected];
if (daSong != null) if (daSong != null)
{ {
var songScore:SaveScoreData = Save.get().getSongScore(songs[curSelected].songId, targetDifficulty); var songScore:SaveScoreData = Save.get().getSongScore(songs[curSelected].songId, currentDifficulty);
intendedScore = songScore?.score ?? 0; intendedScore = songScore?.score ?? 0;
intendedCompletion = songScore?.accuracy ?? 0.0; intendedCompletion = songScore?.accuracy ?? 0.0;
rememberedDifficulty = currentDifficulty;
} }
else else
{ {
@ -890,19 +907,31 @@ class FreeplayState extends MusicBeatSubState
intendedCompletion = 0.0; intendedCompletion = 0.0;
} }
grpDifficulties.group.forEach(function(spr) { grpDifficulties.group.forEach(function(diffSprite) {
spr.visible = false; diffSprite.visible = false;
}); });
var curShit:FlxSprite = grpDifficulties.group.members[curDifficulty]; for (diffSprite in grpDifficulties.group.members)
{
curShit.visible = true; if (diffSprite == null) continue;
curShit.offset.y += 5; if (diffSprite.difficultyId == currentDifficulty)
curShit.alpha = 0.5; {
new FlxTimer().start(1 / 24, function(swag) { if (change != 0)
curShit.alpha = 1; {
curShit.updateHitbox(); diffSprite.visible = true;
}); diffSprite.offset.y += 5;
diffSprite.alpha = 0.5;
new FlxTimer().start(1 / 24, function(swag) {
diffSprite.alpha = 1;
diffSprite.updateHitbox();
});
}
else
{
diffSprite.visible = true;
}
}
}
} }
// Clears the cache of songs, frees up memory, they' ll have to be loaded in later tho function clearDaCache(actualSongTho:String) // Clears the cache of songs, frees up memory, they' ll have to be loaded in later tho function clearDaCache(actualSongTho:String)
@ -950,35 +979,9 @@ class FreeplayState extends MusicBeatSubState
PlayStatePlaylist.isStoryMode = false; PlayStatePlaylist.isStoryMode = false;
if (cap.songData == null) var songId:String = cap.songTitle.toLowerCase();
{ var targetSong:Song = SongRegistry.instance.fetchEntry(songId);
trace('[WARN] Failure while trying to load song!'); var targetDifficulty:String = currentDifficulty;
busy = false;
return;
}
var targetSong:Null<Song> = SongRegistry.instance.fetchEntry(cap.songData.songId);
if (targetSong == null)
{
trace('[WARN] Could not retrieve song ${targetSong}.');
}
var targetDifficulty:String = switch (curDifficulty)
{
case 0:
'easy';
case 1:
'normal';
case 2:
'hard';
default: 'normal';
};
// TODO: Implement additional difficulties into the interface properly.
if (FlxG.keys.pressed.E)
{
targetDifficulty = 'erect';
}
// TODO: Implement Pico into the interface properly. // TODO: Implement Pico into the interface properly.
var targetCharacter:String = 'bf'; var targetCharacter:String = 'bf';
@ -1008,6 +1011,22 @@ class FreeplayState extends MusicBeatSubState
}); });
} }
function rememberSelection():Void
{
if (rememberedSongId != null)
{
curSelected = songs.findIndex(function(song) {
if (song == null) return false;
return song.songId == rememberedSongId;
});
}
if (rememberedDifficulty != null)
{
currentDifficulty = rememberedDifficulty;
}
}
function changeSelection(change:Int = 0) function changeSelection(change:Int = 0)
{ {
// NGio.logEvent('Fresh'); // NGio.logEvent('Fresh');
@ -1038,11 +1057,16 @@ class FreeplayState extends MusicBeatSubState
var songScore:SaveScoreData = Save.get().getSongScore(daSongCapsule.songData.songId, targetDifficulty); var songScore:SaveScoreData = Save.get().getSongScore(daSongCapsule.songData.songId, targetDifficulty);
intendedScore = songScore?.score ?? 0; intendedScore = songScore?.score ?? 0;
intendedCompletion = songScore?.accuracy ?? 0.0; intendedCompletion = songScore?.accuracy ?? 0.0;
diffIdsCurrent = daSong.songDifficulties;
rememberedSongId = daSong.songId;
changeDiff();
} }
else else
{ {
intendedScore = 0; intendedScore = 0;
intendedCompletion = 0.0; intendedCompletion = 0.0;
rememberedSongId = null;
rememberedDifficulty = null;
} }
for (index => capsule in grpCapsules.members) for (index => capsule in grpCapsules.members)
@ -1140,19 +1164,21 @@ enum abstract FilterType(String)
class FreeplaySongData class FreeplaySongData
{ {
public var isFav:Bool = false;
public var songId:String = ""; public var songId:String = "";
public var songName:String = ""; public var songName:String = "";
public var levelId:String = ""; public var levelId:String = "";
public var songCharacter:String = ""; public var songCharacter:String = "";
public var isFav:Bool = false; public var songDifficulties:Array<String> = [];
public function new(songId:String, songName:String, levelId:String, songCharacter:String, isFav:Bool = false) public function new(songId:String, songName:String, levelId:String, songCharacter:String, songDifficulties:Array<String>)
{ {
this.songId = songId; this.songId = songId;
this.songName = songName; this.songName = songName;
this.levelId = levelId; this.levelId = levelId;
this.songCharacter = songCharacter; this.songCharacter = songCharacter;
this.isFav = isFav; this.songDifficulties = songDifficulties;
} }
} }
@ -1163,3 +1189,17 @@ typedef MoveData =
var ?speed:Float; var ?speed:Float;
var ?wait:Float; var ?wait:Float;
} }
class DifficultySprite extends FlxSprite
{
public var difficultyId:String;
public function new(diffId:String)
{
super();
difficultyId = diffId;
loadGraphic(Paths.image('freeplay/freeplay' + diffId));
}
}

View file

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

View file

@ -13,23 +13,13 @@ import flixel.input.touch.FlxTouch;
import flixel.text.FlxText; import flixel.text.FlxText;
import flixel.tweens.FlxEase; import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween; import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxTimer; 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.AtlasMenuList;
import funkin.ui.MenuList.MenuItem;
import funkin.ui.MenuList; import funkin.ui.MenuList;
import funkin.ui.title.TitleState; import funkin.ui.title.TitleState;
import funkin.ui.story.StoryMenuState; import funkin.ui.story.StoryMenuState;
import funkin.ui.OptionsState;
import funkin.ui.PreferencesMenu;
import funkin.ui.Prompt; import funkin.ui.Prompt;
import funkin.util.WindowUtil; import funkin.util.WindowUtil;
import lime.app.Application;
import openfl.filters.ShaderFilter;
#if discord_rpc #if discord_rpc
import Discord.DiscordClient; import Discord.DiscordClient;
#end #end
@ -82,8 +72,10 @@ class MainMenuState extends MusicBeatState
magenta.y = bg.y; magenta.y = bg.y;
magenta.visible = false; magenta.visible = false;
magenta.color = 0xFFfd719b; 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>(); menuItems = new MenuTypedList<AtlasMenuItem>();
add(menuItems); add(menuItems);
@ -116,7 +108,7 @@ class MainMenuState extends MusicBeatState
#end #end
createMenuItem('options', 'mainmenu/options', function() { createMenuItem('options', 'mainmenu/options', function() {
startExitState(new OptionsState()); startExitState(new funkin.ui.OptionsState());
}); });
// Reset position of menu items. // Reset position of menu items.

View file

@ -16,17 +16,18 @@ class PauseSubState extends MusicBeatSubState
{ {
var grpMenuShit:FlxTypedGroup<Alphabet>; var grpMenuShit:FlxTypedGroup<Alphabet>;
var pauseOptionsBase:Array<String> = [ final pauseOptionsBase:Array<String> = [
'Resume', 'Resume',
'Restart Song', 'Restart Song',
'Change Difficulty', 'Change Difficulty',
'Toggle Practice Mode', 'Toggle Practice Mode',
'Exit to Menu' '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 menuItems:Array<String> = [];
var curSelected:Int = 0; var curSelected:Int = 0;
@ -48,6 +49,12 @@ class PauseSubState extends MusicBeatSubState
this.isChartingMode = isChartingMode; this.isChartingMode = isChartingMode;
menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase; 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') if (PlayStatePlaylist.campaignId == 'week6')
{ {
@ -201,18 +208,6 @@ class PauseSubState extends MusicBeatSubState
menuItems = pauseOptionsDifficulty; menuItems = pauseOptionsDifficulty;
regenMenu(); 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': case 'Toggle Practice Mode':
PlayState.instance.isPracticeMode = true; PlayState.instance.isPracticeMode = true;
practiceText.visible = PlayState.instance.isPracticeMode; practiceText.visible = PlayState.instance.isPracticeMode;
@ -245,8 +240,32 @@ class PauseSubState extends MusicBeatSubState
case 'Exit to Chart Editor': case 'Exit to Chart Editor':
this.close(); this.close();
if (FlxG.sound.music != null) FlxG.sound.music.stop(); if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position!
PlayState.instance.close(); // This only works because PlayState is a substate! 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.id = id;
this.controls = new Controls('player$id', None); this.controls = new Controls('player$id', None);
addKeyboard();
}
function addKeyboard():Void
{
var useDefault = true; var useDefault = true;
if (Save.get().hasControls(id, Keys)) if (Save.get().hasControls(id, Keys))
{ {
@ -96,7 +101,6 @@ class PlayerSettings
controls.setKeyboardScheme(Solo); controls.setKeyboardScheme(Solo);
} }
// Apply loaded settings.
PreciseInputManager.instance.initializeKeys(controls); PreciseInputManager.instance.initializeKeys(controls);
} }
@ -124,6 +128,7 @@ class PlayerSettings
trace("Loading gamepad control scheme"); trace("Loading gamepad control scheme");
controls.addDefaultGamepad(gamepad.id); 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.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxMath; import flixel.math.FlxMath;
import flixel.sound.FlxSound; import flixel.sound.FlxSound;
import funkin.ui.PreferencesMenu.CheckboxThingie;
using Lambda; using Lambda;

View file

@ -1,13 +1,15 @@
package funkin.data; package funkin.data;
import funkin.data.song.importer.FNFLegacyData.LegacyNote; import funkin.data.song.importer.FNFLegacyData.LegacyNote;
import hxjsonast.Json;
import hxjsonast.Tools;
import hxjsonast.Json.JObjectField;
import haxe.ds.Either;
import funkin.data.song.importer.FNFLegacyData.LegacyNoteSection;
import funkin.data.song.importer.FNFLegacyData.LegacyNoteData; import funkin.data.song.importer.FNFLegacyData.LegacyNoteData;
import funkin.data.song.importer.FNFLegacyData.LegacyNoteSection;
import funkin.data.song.importer.FNFLegacyData.LegacyScrollSpeeds; import funkin.data.song.importer.FNFLegacyData.LegacyScrollSpeeds;
import haxe.ds.Either;
import hxjsonast.Json;
import hxjsonast.Json.JObjectField;
import hxjsonast.Tools;
import thx.semver.Version;
import thx.semver.VersionRule;
/** /**
* `json2object` has an annotation `@:jcustomparse` which allows for mutation of parsed values. * `json2object` has an annotation `@:jcustomparse` which allows for mutation of parsed values.
@ -23,7 +25,8 @@ class DataParse
* `@:jcustomparse(funkin.data.DataParse.stringNotEmpty)` * `@:jcustomparse(funkin.data.DataParse.stringNotEmpty)`
* @param json Contains the `pos` and `value` of the property. * @param json Contains the `pos` and `value` of the property.
* @param name The name of the property. * @param name The name of the property.
* @throws If the property is not a string or is empty. * @throws Error If the property is not a string or is empty.
* @return The string value.
*/ */
public static function stringNotEmpty(json:Json, name:String):String public static function stringNotEmpty(json:Json, name:String):String
{ {
@ -37,6 +40,42 @@ class DataParse
} }
} }
/**
* `@:jcustomparse(funkin.data.DataParse.semverVersion)`
* @param json Contains the `pos` and `value` of the property.
* @param name The name of the property.
* @return The value of the property as a `thx.semver.Version`.
*/
public static function semverVersion(json:Json, name:String):Version
{
switch (json.value)
{
case JString(s):
if (s == "") throw 'Expected version property $name to be non-empty.';
return s;
default:
throw 'Expected version property $name to be a string, but it was ${json.value}.';
}
}
/**
* `@:jcustomparse(funkin.data.DataParse.semverVersionRule)`
* @param json Contains the `pos` and `value` of the property.
* @param name The name of the property.
* @return The value of the property as a `thx.semver.VersionRule`.
*/
public static function semverVersionRule(json:Json, name:String):VersionRule
{
switch (json.value)
{
case JString(s):
if (s == "") throw 'Expected version rule property $name to be non-empty.';
return s;
default:
throw 'Expected version rule property $name to be a string, but it was ${json.value}.';
}
}
/** /**
* Parser which outputs a Dynamic value, either a object or something else. * Parser which outputs a Dynamic value, either a object or something else.
* @param json * @param json

View file

@ -1,6 +1,8 @@
package funkin.data; package funkin.data;
import funkin.util.SerializerUtil; import funkin.util.SerializerUtil;
import thx.semver.Version;
import thx.semver.VersionRule;
/** /**
* `json2object` has an annotation `@:jcustomwrite` which allows for custom serialization of values to be written to JSON. * `json2object` has an annotation `@:jcustomwrite` which allows for custom serialization of values to be written to JSON.
@ -9,9 +11,30 @@ import funkin.util.SerializerUtil;
*/ */
class DataWrite class DataWrite
{ {
/**
* `@:jcustomwrite(funkin.data.DataWrite.dynamicValue)`
* @param value
* @return String
*/
public static function dynamicValue(value:Dynamic):String public static function dynamicValue(value:Dynamic):String
{ {
// Is this cheating? Yes. Do I care? No. // Is this cheating? Yes. Do I care? No.
return SerializerUtil.toJSON(value); return SerializerUtil.toJSON(value);
} }
/**
* `@:jcustomwrite(funkin.data.DataWrite.semverVersion)`
*/
public static function semverVersion(value:Version):String
{
return value.toString();
}
/**
* `@:jcustomwrite(funkin.data.DataWrite.semverVersionRule)`
*/
public static function semverVersionRule(value:VersionRule):String
{
return value.toString();
}
} }

View file

@ -12,6 +12,8 @@ class SongMetadata
* *
*/ */
// @:default(funkin.data.song.SongRegistry.SONG_METADATA_VERSION) // @:default(funkin.data.song.SongRegistry.SONG_METADATA_VERSION)
@:jcustomparse(funkin.data.DataParse.semverVersion)
@:jcustomwrite(funkin.data.DataWrite.semverVersion)
public var version:Version; public var version:Version;
@:default("Unknown") @:default("Unknown")
@ -203,6 +205,8 @@ class SongMusicData
* *
*/ */
// @:default(funkin.data.song.SongRegistry.SONG_METADATA_VERSION) // @:default(funkin.data.song.SongRegistry.SONG_METADATA_VERSION)
@:jcustomparse(funkin.data.DataParse.semverVersion)
@:jcustomwrite(funkin.data.DataWrite.semverVersion)
public var version:Version; public var version:Version;
@:default("Unknown") @:default("Unknown")
@ -367,6 +371,8 @@ class SongCharacterData
class SongChartData class SongChartData
{ {
@:default(funkin.data.song.SongRegistry.SONG_CHART_DATA_VERSION) @:default(funkin.data.song.SongRegistry.SONG_CHART_DATA_VERSION)
@:jcustomparse(funkin.data.DataParse.semverVersion)
@:jcustomwrite(funkin.data.DataWrite.semverVersion)
public var version:Version; public var version:Version;
public var scrollSpeed:Map<String, Float>; public var scrollSpeed:Map<String, Float>;

View file

@ -246,7 +246,8 @@ class SongDataUtils
typedef SongClipboardItems = typedef SongClipboardItems =
{ {
?valid:Bool, @:optional
notes:Array<SongNoteData>, var valid:Bool;
events:Array<SongEventData> var notes:Array<SongNoteData>;
var events:Array<SongEventData>;
} }

View file

@ -24,6 +24,8 @@ class SongMetadata_v2_0_0
// ========== // ==========
// UNMODIFIED VALUES // UNMODIFIED VALUES
// ========== // ==========
@:jcustomparse(funkin.data.DataParse.semverVersion)
@:jcustomwrite(funkin.data.DataWrite.semverVersion)
public var version:Version; public var version:Version;
@:default("Unknown") @:default("Unknown")

View file

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

View file

@ -1,18 +1,25 @@
package funkin.input; 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.FlxG;
import flixel.input.FlxInput;
import flixel.input.FlxInput.FlxInputState; import flixel.input.FlxInput.FlxInputState;
import flixel.input.FlxKeyManager; import flixel.input.FlxKeyManager;
import flixel.input.gamepad.FlxGamepad;
import flixel.input.gamepad.FlxGamepadInputID;
import flixel.input.keyboard.FlxKey; import flixel.input.keyboard.FlxKey;
import flixel.input.keyboard.FlxKeyboard.FlxKeyInput;
import flixel.input.keyboard.FlxKeyList; import flixel.input.keyboard.FlxKeyList;
import flixel.util.FlxSignal.FlxTypedSignal; import flixel.util.FlxSignal.FlxTypedSignal;
import funkin.play.notes.NoteDirection;
import funkin.util.FlxGamepadUtil;
import haxe.Int64; 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.KeyCode;
import lime.ui.KeyModifier; import lime.ui.KeyModifier;
import openfl.events.KeyboardEvent;
import openfl.ui.Keyboard;
/** /**
* A precise input manager that: * A precise input manager that:
@ -43,6 +50,20 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
*/ */
var _keyListDir:Map<FlxKey, NoteDirection>; 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. * 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 _dirReleaseTimestamps:Map<NoteDirection, Int64>;
var _deviceBinds:Map<FlxGamepad,
{
onButtonDown:LimeGamepadButton->Int64->Void,
onButtonUp:LimeGamepadButton->Int64->Void
}>;
public function new() public function new()
{ {
super(PreciseInputList.new); super(PreciseInputList.new);
_deviceBinds = [];
_keyList = []; _keyList = [];
_dirPressTimestamps = new Map<NoteDirection, Int64>(); // _keyListMap
_dirReleaseTimestamps = new Map<NoteDirection, Int64>(); // _keyListArray
_keyListDir = new Map<FlxKey, NoteDirection>(); _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_DOWN, onKeyDown);
FlxG.stage.removeEventListener(KeyboardEvent.KEY_UP, onKeyUp); FlxG.stage.removeEventListener(KeyboardEvent.KEY_UP, onKeyUp);
FlxG.stage.application.window.onKeyDownPrecise.add(handleKeyDown); 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. * 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. * Get the time, in nanoseconds, since the given note direction was last pressed.
* @param noteDirection The note direction to check. * @param noteDirection The note direction to check.
@ -165,11 +251,41 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
return _keyListMap.get(key); 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 public function getDirectionForKey(key:FlxKey):NoteDirection
{ {
return _keyListDir.get(key); 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 function handleKeyDown(keyCode:KeyCode, _:KeyModifier, timestamp:Int64):Void
{ {
var key:FlxKey = convertKeyCode(keyCode); var key:FlxKey = convertKeyCode(keyCode);
@ -198,7 +314,7 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
if (_keyList.indexOf(key) == -1) return; if (_keyList.indexOf(key) == -1) return;
// TODO: Remove this line with SDL3 when timestamps change meaning. // 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; timestamp *= Constants.NS_PER_MS;
updateKeyStates(key, false); 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 static function convertKeyCode(input:KeyCode):FlxKey
{ {
@:privateAccess @:privateAccess
@ -228,6 +392,31 @@ class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
_keyListMap.clear(); _keyListMap.clear();
_keyListDir.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 class PreciseInputList extends FlxKeyList

View file

@ -11,7 +11,6 @@ import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher; import funkin.modding.events.ScriptEventDispatcher;
import funkin.play.PlayState; import funkin.play.PlayState;
import funkin.play.character.BaseCharacter; import funkin.play.character.BaseCharacter;
import funkin.ui.PreferencesMenu;
/** /**
* A substate which renders over the PlayState when the player dies. * A substate which renders over the PlayState when the player dies.
@ -103,6 +102,9 @@ class GameOverSubState extends MusicBeatSubState
cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1); cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x; cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x;
cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y; cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y;
var offsets:Array<Float> = boyfriend.getDeathCameraOffsets();
cameraFollowPoint.x += offsets[0];
cameraFollowPoint.y += offsets[1];
add(cameraFollowPoint); add(cameraFollowPoint);
FlxG.camera.target = null; FlxG.camera.target = null;
@ -292,7 +294,7 @@ class GameOverSubState extends MusicBeatSubState
{ {
var randomCensor:Array<Int> = []; 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() { 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. // Once the quote ends, fade in the game over music.

View file

@ -24,13 +24,14 @@ import openfl.utils.Assets;
* - i.e. `PlayState.instance.iconP1.animation.addByPrefix("jumpscare", "jumpscare", 24, false);` * - i.e. `PlayState.instance.iconP1.animation.addByPrefix("jumpscare", "jumpscare", 24, false);`
* @author MasterEric * @author MasterEric
*/ */
@:nullSafety
class HealthIcon extends FlxSprite class HealthIcon extends FlxSprite
{ {
/** /**
* The character this icon is representing. * The character this icon is representing.
* Setting this variable will automatically update the graphic. * Setting this variable will automatically update the graphic.
*/ */
public var characterId(default, set):String; public var characterId(default, set):Null<String>;
/** /**
* Whether this health icon should automatically update its state based on the character's health. * Whether this health icon should automatically update its state based on the character's health.
@ -123,13 +124,12 @@ class HealthIcon extends FlxSprite
initTargetSize(); initTargetSize();
} }
function set_characterId(value:String):String function set_characterId(value:Null<String>):Null<String>
{ {
if (value == characterId) return value; if (value == characterId) return value;
characterId = value; characterId = value ?? Constants.DEFAULT_HEALTH_ICON;
loadCharacter(characterId); return characterId;
return value;
} }
function set_isPixel(value:Bool):Bool function set_isPixel(value:Bool):Bool
@ -137,8 +137,7 @@ class HealthIcon extends FlxSprite
if (value == isPixel) return value; if (value == isPixel) return value;
isPixel = value; isPixel = value;
loadCharacter(characterId); return isPixel;
return value;
} }
/** /**
@ -156,6 +155,38 @@ class HealthIcon extends FlxSprite
} }
} }
/**
* Use the provided CharacterHealthIconData to configure this health icon's appearance.
* @param data The data to use to configure this health icon.
*/
public function configure(data:Null<HealthIconData>):Void
{
if (data == null)
{
this.characterId = Constants.DEFAULT_HEALTH_ICON;
this.isPixel = false;
loadCharacter(characterId);
this.size.set(1.0, 1.0);
this.offset.x = 0.0;
this.offset.y = 0.0;
this.flipX = false;
}
else
{
this.characterId = data.id;
this.isPixel = data.isPixel ?? false;
loadCharacter(characterId);
this.size.set(data.scale ?? 1.0, data.scale ?? 1.0);
this.offset.x = (data.offsets != null) ? data.offsets[0] : 0.0;
this.offset.y = (data.offsets != null) ? data.offsets[1] : 0.0;
this.flipX = data.flipX ?? false; // Face the OTHER way by default, since that is more common.
}
}
/** /**
* Called by Flixel every frame. Includes logic to manage the currently playing animation. * Called by Flixel every frame. Includes logic to manage the currently playing animation.
*/ */
@ -341,12 +372,17 @@ class HealthIcon extends FlxSprite
this.animation.add(Losing, [1], 0, false, false); this.animation.add(Losing, [1], 0, false, false);
} }
function correctCharacterId(charId:String):String function correctCharacterId(charId:Null<String>):String
{ {
if (charId == null)
{
return Constants.DEFAULT_HEALTH_ICON;
}
if (!Assets.exists(Paths.image('icons/icon-$charId'))) if (!Assets.exists(Paths.image('icons/icon-$charId')))
{ {
FlxG.log.warn('No icon for character: $charId : using default placeholder face instead!'); FlxG.log.warn('No icon for character: $charId : using default placeholder face instead!');
return 'face'; return Constants.DEFAULT_HEALTH_ICON;
} }
return charId; return charId;
@ -357,10 +393,11 @@ class HealthIcon extends FlxSprite
return Assets.exists(Paths.file('images/icons/icon-$characterId.xml')); return Assets.exists(Paths.file('images/icons/icon-$characterId.xml'));
} }
function loadCharacter(charId:String):Void function loadCharacter(charId:Null<String>):Void
{ {
if (correctCharacterId(charId) != charId) if (charId == null || correctCharacterId(charId) != charId)
{ {
// This will recursively trigger loadCharacter to be called again.
characterId = correctCharacterId(charId); characterId = correctCharacterId(charId);
return; return;
} }

View file

@ -698,7 +698,15 @@ class PlayState extends MusicBeatSubState
FlxG.sound.music.pause(); FlxG.sound.music.pause();
FlxG.sound.music.time = (startTimestamp); FlxG.sound.music.time = (startTimestamp);
vocals = currentChart.buildVocals(); if (!overrideMusic)
{
vocals = currentChart.buildVocals();
if (vocals.members.length == 0)
{
trace('WARNING: No vocals found for this song.');
}
}
vocals.pause(); vocals.pause();
vocals.time = 0; vocals.time = 0;
@ -920,7 +928,6 @@ class PlayState extends MusicBeatSubState
} }
// Handle keybinds. // Handle keybinds.
// if (!isInCutscene && !disableKeys) keyShit(true);
processInputQueue(); processInputQueue();
if (!isInCutscene && !disableKeys) debugKeyShit(); if (!isInCutscene && !disableKeys) debugKeyShit();
if (isInCutscene && !disableKeys) handleCutsceneKeys(elapsed); if (isInCutscene && !disableKeys) handleCutsceneKeys(elapsed);
@ -1268,7 +1275,7 @@ class PlayState extends MusicBeatSubState
*/ */
function initHealthBar():Void 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 = new FlxSprite(0, healthBarYPos).loadGraphic(Paths.image('healthBar'));
healthBarBG.screenCenter(X); healthBarBG.screenCenter(X);
healthBarBG.scrollFactor.set(0, 0); healthBarBG.scrollFactor.set(0, 0);
@ -1477,13 +1484,13 @@ class PlayState extends MusicBeatSubState
// Position the player strumline on the right half of the screen // 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 / 2 + Constants.STRUMLINE_X_OFFSET; // Classic style
// playerStrumline.x = FlxG.width - playerStrumline.width - Constants.STRUMLINE_X_OFFSET; // Centered 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.zIndex = 200;
playerStrumline.cameras = [camHUD]; playerStrumline.cameras = [camHUD];
// Position the opponent strumline on the left half of the screen // Position the opponent strumline on the left half of the screen
opponentStrumline.x = Constants.STRUMLINE_X_OFFSET; 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.zIndex = 100;
opponentStrumline.cameras = [camHUD]; opponentStrumline.cameras = [camHUD];
@ -1642,7 +1649,7 @@ class PlayState extends MusicBeatSubState
*/ */
function onConversationComplete():Void function onConversationComplete():Void
{ {
isInCutscene = true; isInCutscene = false;
remove(currentConversation); remove(currentConversation);
currentConversation = null; currentConversation = null;
@ -2464,9 +2471,9 @@ class PlayState extends MusicBeatSubState
accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes, 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 #if newgrounds
NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}'); NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}');
#end #end
@ -2514,7 +2521,7 @@ class PlayState extends MusicBeatSubState
var nextPlayState:PlayState = new PlayState( var nextPlayState:PlayState = new PlayState(
{ {
targetSong: targetSong, targetSong: targetSong,
targetDifficulty: currentDifficulty, targetDifficulty: PlayStatePlaylist.campaignDifficulty,
targetCharacter: currentPlayerId, targetCharacter: currentPlayerId,
}); });
nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y); nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y);
@ -2530,7 +2537,7 @@ class PlayState extends MusicBeatSubState
var nextPlayState:PlayState = new PlayState( var nextPlayState:PlayState = new PlayState(
{ {
targetSong: targetSong, targetSong: targetSong,
targetDifficulty: currentDifficulty, targetDifficulty: PlayStatePlaylist.campaignDifficulty,
targetCharacter: currentPlayerId, targetCharacter: currentPlayerId,
}); });
nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y); nextPlayState.previousCameraFollowPoint = new FlxSprite(cameraFollowPoint.x, cameraFollowPoint.y);
@ -2656,7 +2663,12 @@ class PlayState extends MusicBeatSubState
persistentUpdate = false; persistentUpdate = false;
vocals.stop(); vocals.stop();
camHUD.alpha = 1; 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; res.camera = camHUD;
openSubState(res); openSubState(res);
} }

View file

@ -34,10 +34,7 @@ class PlayStatePlaylist
*/ */
public static var campaignId:String = 'unknown'; public static var campaignId:String = 'unknown';
/** public static var campaignDifficulty:String = Constants.DEFAULT_DIFFICULTY;
* The current difficulty selected for this level (as a named ID).
*/
public static var currentDifficulty(default, default):String = Constants.DEFAULT_DIFFICULTY;
/** /**
* Resets the playlist to its default state. * Resets the playlist to its default state.
@ -49,6 +46,6 @@ class PlayStatePlaylist
campaignScore = 0; campaignScore = 0;
campaignTitle = 'UNKNOWN'; campaignTitle = 'UNKNOWN';
campaignId = '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 class ResultState extends MusicBeatSubState
{ {
final params:ResultsStateParams;
var resultsVariation:ResultVariations; var resultsVariation:ResultVariations;
var songName:FlxBitmapText; var songName:FlxBitmapText;
var difficulty:FlxSprite; var difficulty:FlxSprite;
@ -29,13 +31,18 @@ class ResultState extends MusicBeatSubState
var maskShaderSongName = new LeftMaskShader(); var maskShaderSongName = new LeftMaskShader();
var maskShaderDifficulty = new LeftMaskShader(); var maskShaderDifficulty = new LeftMaskShader();
public function new(params:ResultsStateParams)
{
super();
this.params = params;
}
override function create():Void override function create():Void
{ {
if (Highscore.tallies.sick == Highscore.tallies.totalNotesHit if (params.tallies.sick == params.tallies.totalNotesHit
&& Highscore.tallies.maxCombo == Highscore.tallies.totalNotesHit) resultsVariation = PERFECT; && params.tallies.maxCombo == params.tallies.totalNotesHit) resultsVariation = PERFECT;
else if (Highscore.tallies.missed else if (params.tallies.missed + params.tallies.bad + params.tallies.shit >= params.tallies.totalNotes * 0.50)
+ Highscore.tallies.bad
+ Highscore.tallies.shit >= Highscore.tallies.totalNotes * 0.50)
resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending! resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending!
else else
resultsVariation = NORMAL; resultsVariation = NORMAL;
@ -135,17 +142,7 @@ class ResultState extends MusicBeatSubState
var fontLetters:String = "AaBbCcDdEeFfGgHhiIJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:1234567890"; var fontLetters:String = "AaBbCcDdEeFfGgHhiIJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:1234567890";
songName = new FlxBitmapText(FlxBitmapFont.fromMonospace(Paths.image("resultScreen/tardlingSpritesheet"), fontLetters, FlxPoint.get(49, 62))); songName = new FlxBitmapText(FlxBitmapFont.fromMonospace(Paths.image("resultScreen/tardlingSpritesheet"), fontLetters, FlxPoint.get(49, 62)));
songName.text = params.title;
// 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.letterSpacing = -15; songName.letterSpacing = -15;
songName.angle = -4.1; songName.angle = -4.1;
add(songName); add(songName);
@ -194,27 +191,27 @@ class ResultState extends MusicBeatSubState
var ratingGrp:FlxTypedGroup<TallyCounter> = new FlxTypedGroup<TallyCounter>(); var ratingGrp:FlxTypedGroup<TallyCounter> = new FlxTypedGroup<TallyCounter>();
add(ratingGrp); 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); 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); ratingGrp.add(maxCombo);
hStuf += 2; hStuf += 2;
var extraYOffset:Float = 5; 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); 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); 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); 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); 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); ratingGrp.add(tallyMissed);
for (ind => rating in ratingGrp.members) 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(); super.create();
} }
@ -351,7 +348,7 @@ class ResultState extends MusicBeatSubState
if (controls.PAUSE) if (controls.PAUSE)
{ {
if (PlayStatePlaylist.isStoryMode) if (params.storyMode)
{ {
FlxG.switchState(new StoryMenuState()); FlxG.switchState(new StoryMenuState());
} }
@ -372,3 +369,21 @@ enum abstract ResultVariations(String)
var NORMAL; var NORMAL;
var SHIT; 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

@ -188,6 +188,11 @@ class BaseCharacter extends Bopper
shouldBop = false; shouldBop = false;
} }
public function getDeathCameraOffsets():Array<Float>
{
return _data.death?.cameraOffsets ?? [0.0, 0.0];
}
/** /**
* Gets the value of flipX from the character data. * Gets the value of flipX from the character data.
* `!getFlipX()` is the direction Boyfriend should face. * `!getFlipX()` is the direction Boyfriend should face.
@ -312,12 +317,8 @@ class BaseCharacter extends Bopper
trace('[WARN] Player 1 health icon not found!'); trace('[WARN] Player 1 health icon not found!');
return; return;
} }
PlayState.instance.iconP1.isPixel = _data.healthIcon?.isPixel ?? false; PlayState.instance.iconP1.configure(_data.healthIcon);
PlayState.instance.iconP1.characterId = _data.healthIcon.id; PlayState.instance.iconP1.flipX = !PlayState.instance.iconP1.flipX; // BF is looking the other way.
PlayState.instance.iconP1.size.set(_data.healthIcon.scale, _data.healthIcon.scale);
PlayState.instance.iconP1.offset.x = _data.healthIcon.offsets[0];
PlayState.instance.iconP1.offset.y = _data.healthIcon.offsets[1];
PlayState.instance.iconP1.flipX = !_data.healthIcon.flipX;
} }
else else
{ {
@ -326,12 +327,7 @@ class BaseCharacter extends Bopper
trace('[WARN] Player 2 health icon not found!'); trace('[WARN] Player 2 health icon not found!');
return; return;
} }
PlayState.instance.iconP2.isPixel = _data.healthIcon?.isPixel ?? false; PlayState.instance.iconP2.configure(_data.healthIcon);
PlayState.instance.iconP2.characterId = _data.healthIcon.id;
PlayState.instance.iconP2.size.set(_data.healthIcon.scale, _data.healthIcon.scale);
PlayState.instance.iconP2.offset.x = _data.healthIcon.offsets[0];
PlayState.instance.iconP2.offset.y = _data.healthIcon.offsets[1];
PlayState.instance.iconP2.flipX = _data.healthIcon.flipX;
} }
} }
@ -580,8 +576,7 @@ class BaseCharacter extends Bopper
public override function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reversed:Bool = false):Void public override function playAnimation(name:String, restart:Bool = false, ignoreOther:Bool = false, reversed:Bool = false):Void
{ {
FlxG.watch.addQuick('playAnim(${characterName})', name); // FlxG.watch.addQuick('playAnim(${characterName})', name);
// trace('playAnim(${characterName}): ${name}');
super.playAnimation(name, restart, ignoreOther, reversed); super.playAnimation(name, restart, ignoreOther, reversed);
} }
} }

View file

@ -19,8 +19,10 @@ class CharacterDataParser
* The current version string for the stage data format. * The current version string for the stage data format.
* Handle breaking changes by incrementing this value * Handle breaking changes by incrementing this value
* and adding migration to the `migrateStageData()` function. * and adding migration to the `migrateStageData()` function.
*
* - Version 1.0.1 adds `death.cameraOffsets`
*/ */
public static final CHARACTER_DATA_VERSION:String = '1.0.0'; public static final CHARACTER_DATA_VERSION:String = '1.0.1';
/** /**
* The current version rule check for the stage data format. * The current version rule check for the stage data format.
@ -603,6 +605,8 @@ typedef CharacterData =
*/ */
var healthIcon:Null<HealthIconData>; var healthIcon:Null<HealthIconData>;
var death:Null<DeathData>;
/** /**
* The global offset to the character's position, in pixels. * The global offset to the character's position, in pixels.
* @default [0, 0] * @default [0, 0]
@ -695,3 +699,13 @@ typedef HealthIconData =
*/ */
var offsets:Null<Array<Float>>; var offsets:Null<Array<Float>>;
} }
typedef DeathData =
{
/**
* The amount to offset the camera by while focusing on this character as they die.
* Default value focuses on the character's graphic midpoint.
* @default [0, 0]
*/
var ?cameraOffsets:Array<Float>;
}

View file

@ -231,7 +231,7 @@ class Strumline extends FlxSpriteGroup
notesVwoosh.add(note); notesVwoosh.add(note);
var targetY:Float = FlxG.height + note.y; 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, FlxTween.tween(note, {y: targetY}, 0.5,
{ {
ease: FlxEase.expoIn, ease: FlxEase.expoIn,
@ -252,7 +252,7 @@ class Strumline extends FlxSpriteGroup
holdNotesVwoosh.add(holdNote); holdNotesVwoosh.add(holdNote);
var targetY:Float = FlxG.height + holdNote.y; 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, FlxTween.tween(holdNote, {y: targetY}, 0.5,
{ {
ease: FlxEase.expoIn, ease: FlxEase.expoIn,
@ -277,7 +277,7 @@ class Strumline extends FlxSpriteGroup
var vwoosh:Float = (strumTime < Conductor.songPosition) && vwoosh ? 2.0 : 1.0; var vwoosh:Float = (strumTime < Conductor.songPosition) && vwoosh ? 2.0 : 1.0;
var scrollSpeed:Float = PlayState.instance?.currentChart?.scrollSpeed ?? 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 function updateNotes():Void
@ -321,7 +321,7 @@ class Strumline extends FlxSpriteGroup
note.y = this.y - INITIAL_OFFSET + calculateNoteYPos(note.strumTime, vwoosh); note.y = this.y - INITIAL_OFFSET + calculateNoteYPos(note.strumTime, vwoosh);
// If the note is miss // 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) if (note.handledMiss && isOffscreen)
{ {
killNote(note); killNote(note);
@ -388,7 +388,7 @@ class Strumline extends FlxSpriteGroup
var vwoosh:Bool = false; var vwoosh:Bool = false;
if (PreferencesMenu.getPref('downscroll')) if (Preferences.downscroll)
{ {
holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2; holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
} }
@ -410,7 +410,7 @@ class Strumline extends FlxSpriteGroup
holdNote.visible = false; holdNote.visible = false;
} }
if (PreferencesMenu.getPref('downscroll')) if (Preferences.downscroll)
{ {
holdNote.y = this.y - holdNote.height + STRUMLINE_SIZE / 2; holdNote.y = this.y - holdNote.height + STRUMLINE_SIZE / 2;
} }
@ -425,7 +425,7 @@ class Strumline extends FlxSpriteGroup
holdNote.visible = true; holdNote.visible = true;
var vwoosh:Bool = false; var vwoosh:Bool = false;
if (PreferencesMenu.getPref('downscroll')) if (Preferences.downscroll)
{ {
holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2; 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()); height = sustainHeight(sustainLength, getScrollSpeed());
// instead of scrollSpeed, PlayState.SONG.speed // instead of scrollSpeed, PlayState.SONG.speed
flipY = PreferencesMenu.getPref('downscroll'); flipY = Preferences.downscroll;
// alpha = 0.6; // alpha = 0.6;
alpha = 1.0; alpha = 1.0;

View file

@ -1,5 +1,6 @@
package funkin.play.song; package funkin.play.song;
import funkin.util.SortUtil;
import flixel.sound.FlxSound; import flixel.sound.FlxSound;
import openfl.utils.Assets; import openfl.utils.Assets;
import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEvent;
@ -56,8 +57,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
*/ */
public var validScore:Bool = true; public var validScore:Bool = true;
var difficultyIds:Array<String>;
public var songName(get, never):String; public var songName(get, never):String;
function get_songName():String function get_songName():String
@ -85,7 +84,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
this.id = id; this.id = id;
variations = []; variations = [];
difficultyIds = [];
difficulties = new Map<String, SongDifficulty>(); difficulties = new Map<String, SongDifficulty>();
_data = _fetchData(id); _data = _fetchData(id);
@ -127,8 +125,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
for (vari in variations) for (vari in variations)
result.variations.push(vari); result.variations.push(vari);
result.difficultyIds.clear(); result.difficulties.clear();
result.populateDifficulties(); result.populateDifficulties();
for (variation => chartData in charts) for (variation => chartData in charts)
@ -157,13 +154,17 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
{ {
if (metadata == null || metadata.playData == null) continue; if (metadata == null || metadata.playData == null) continue;
// If there are no difficulties in the metadata, there's a problem.
if (metadata.playData.difficulties.length == 0)
{
throw 'Song $id has no difficulties listed in metadata!';
}
// There may be more difficulties in the chart file than in the metadata, // There may be more difficulties in the chart file than in the metadata,
// (i.e. non-playable charts like the one used for Pico on the speaker in Stress) // (i.e. non-playable charts like the one used for Pico on the speaker in Stress)
// but all the difficulties in the metadata must be in the chart file. // but all the difficulties in the metadata must be in the chart file.
for (diffId in metadata.playData.difficulties) for (diffId in metadata.playData.difficulties)
{ {
difficultyIds.push(diffId);
var difficulty:SongDifficulty = new SongDifficulty(this, diffId, metadata.variation); var difficulty:SongDifficulty = new SongDifficulty(this, diffId, metadata.variation);
variations.push(metadata.variation); variations.push(metadata.variation);
@ -237,19 +238,37 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
*/ */
public inline function getDifficulty(?diffId:String):Null<SongDifficulty> 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); 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;
});
diffFiltered.sort(SortUtil.defaultsThenAlphabetically.bind(Constants.DEFAULT_DIFFICULTY_LIST));
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. // Reasonable defaults.
naughtyness: true, naughtyness: true,
downscroll: false, downscroll: false,
flashingMenu: true, flashingLights: true,
zoomCamera: true, zoomCamera: true,
debugDisplay: false, debugDisplay: false,
pauseOnTabOut: true, autoPause: true,
controls: controls:
{ {
@ -88,7 +88,7 @@ abstract Save(RawSaveData)
{ {
// No mods enabled. // No mods enabled.
enabledMods: [], enabledMods: [],
modSettings: [], modOptions: [],
}, },
optionsChartEditor: 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. * The current session ID for the logged-in Newgrounds user, or null if the user is cringe.
*/ */
@ -407,6 +421,8 @@ typedef RawSaveData =
/** /**
* A semantic versioning string for the save data format. * A semantic versioning string for the save data format.
*/ */
@:jcustomparse(funkin.data.DataParse.semverVersion)
@:jcustomwrite(funkin.data.DataWrite.semverVersion)
var version:Version; var version:Version;
var api:SaveApiData; var api:SaveApiData;
@ -458,7 +474,7 @@ typedef SaveHighScoresData =
typedef SaveDataMods = typedef SaveDataMods =
{ {
var enabledMods:Array<String>; var enabledMods:Array<String>;
var modSettings:Map<String, Dynamic>; var modOptions:Map<String, Dynamic>;
} }
/** /**
@ -530,10 +546,10 @@ typedef SaveDataOptions =
var downscroll:Bool; 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` * @default `true`
*/ */
var flashingMenu:Bool; var flashingLights:Bool;
/** /**
* If disabled, the camera bump synchronized to the beat. * If disabled, the camera bump synchronized to the beat.
@ -551,7 +567,7 @@ typedef SaveDataOptions =
* If enabled, the game will automatically pause when tabbing out. * If enabled, the game will automatically pause when tabbing out.
* @default `true` * @default `true`
*/ */
var pauseOnTabOut:Bool; var autoPause:Bool;
var controls: var controls:
{ {

View file

@ -163,7 +163,17 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
function onSelect():Void 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; controlGrid.enabled = false;
canExit = false; canExit = false;
@ -204,6 +214,7 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
} }
var keyUsedToEnterPrompt:Null<Int> = null; var keyUsedToEnterPrompt:Null<Int> = null;
var buttonUsedToEnterPrompt:Null<Int> = null;
override function update(elapsed:Float):Void override function update(elapsed:Float):Void
{ {
@ -246,19 +257,49 @@ class ControlsMenu extends funkin.ui.OptionsState.Page
case Gamepad(id): case Gamepad(id):
{ {
var button = FlxG.gamepads.getByID(id).firstJustReleasedID(); var button = FlxG.gamepads.getByID(id).firstJustReleasedID();
if (button != NONE && button != keyUsedToEnterPrompt) if (button != NONE && button != buttonUsedToEnterPrompt)
{ {
if (button != BACK) onInputSelect(button); if (button != BACK) onInputSelect(button);
closePrompt(); 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(); switch (currentDevice)
if (keyJustReleased != NONE && keyJustReleased == keyUsedToEnterPrompt)
{ {
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.FlxCamera;
import flixel.FlxObject; import flixel.FlxObject;
import flixel.FlxSprite; import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import funkin.ui.AtlasText.AtlasFont; import funkin.ui.AtlasText.AtlasFont;
import funkin.ui.OptionsState.Page; import funkin.ui.OptionsState.Page;
import funkin.ui.TextMenuList.TextMenuItem; import funkin.ui.TextMenuList.TextMenuItem;
class PreferencesMenu extends Page class PreferencesMenu extends Page
{ {
public static var preferences:Map<String, Dynamic> = new Map();
var items:TextMenuList; var items:TextMenuList;
var preferenceItems:FlxTypedSpriteGroup<FlxSprite>;
var checkboxes:Array<CheckboxThingie> = [];
var menuCamera:FlxCamera; var menuCamera:FlxCamera;
var camFollow:FlxObject; var camFollow:FlxObject;
@ -27,13 +26,9 @@ class PreferencesMenu extends Page
camera = menuCamera; camera = menuCamera;
add(items = new TextMenuList()); add(items = new TextMenuList());
add(preferenceItems = new FlxTypedSpriteGroup<FlxSprite>());
createPrefItem('naughtyness', 'censor-naughty', true); createPrefItems();
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);
camFollow = new FlxObject(FlxG.width / 2, 0, 140, 70); camFollow = new FlxObject(FlxG.width / 2, 0, 140, 70);
if (items != null) camFollow.y = items.selectedItem.y; 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? function createPrefItemCheckbox(prefName:String, prefDesc:String, onChange:Bool->Void, defaultValue:Bool):Void
public static function setPref(pref:String, value:Dynamic):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() { items.createItem(120, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() {
preferenceCheck(prefString, prefValue); var value = !checkbox.currentValue;
onChange(value);
switch (Type.typeof(prefValue).getName()) checkbox.currentValue = value;
{
case 'TBool':
prefToggle(prefString);
default:
trace('swag');
}
}); });
switch (Type.typeof(prefValue).getName()) preferenceItems.add(checkbox);
{
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') {}
} }
override function update(elapsed:Float) override function update(elapsed:Float)
{ {
super.update(elapsed); 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) { items.forEach(function(daItem:TextMenuItem) {
if (items.selectedItem == daItem) daItem.x = 150; if (items.selectedItem == daItem) daItem.x = 150;
else else
daItem.x = 120; 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); super(x, y);
@ -180,7 +110,7 @@ class CheckboxThingie extends FlxSprite
setGraphicSize(Std.int(width * 0.7)); setGraphicSize(Std.int(width * 0.7));
updateHitbox(); updateHitbox();
this.daValue = daValue; this.currentValue = defaultValue;
} }
override function update(elapsed:Float) 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 else
{
animation.play('static'); animation.play('static');
}
return value; return currentValue = value;
} }
} }

View file

@ -122,7 +122,7 @@ class StickerSubState extends MusicBeatSubState
var daSound:String = FlxG.random.getObject(sounds); var daSound:String = FlxG.random.getObject(sounds);
FlxG.sound.play(Paths.sound(daSound)); FlxG.sound.play(Paths.sound(daSound));
if (ind == grpStickers.members.length - 1) if (grpStickers == null || ind == grpStickers.members.length - 1)
{ {
switchingState = false; switchingState = false;
close(); close();

View file

@ -10,7 +10,7 @@ class TextMenuList extends MenuTypedList<TextMenuItem>
super(navControls, wrapMode); 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); var item = new TextMenuItem(x, y, name, font, callback);
item.fireInstantly = fireInstantly; item.fireInstantly = fireInstantly;
@ -20,7 +20,7 @@ class TextMenuList extends MenuTypedList<TextMenuItem>
class TextMenuItem extends TextTypedMenuItem<AtlasText> 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); super(x, y, new AtlasText(0, 0, name, font), name, callback);
setEmptyBackground(); setEmptyBackground();
@ -29,7 +29,7 @@ class TextMenuItem extends TextTypedMenuItem<AtlasText>
class TextTypedMenuItem<T:AtlasText> extends MenuTypedItem<T> 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); super(x, y, label, name, callback);
} }

View file

@ -253,6 +253,10 @@ class DebugBoundingState extends FlxState
offsetView.add(animDropDownMenu); offsetView.add(animDropDownMenu);
var characters:Array<String> = CharacterDataParser.listCharacterIds(); var characters:Array<String> = CharacterDataParser.listCharacterIds();
characters = characters.filter(function(charId:String) {
var char = CharacterDataParser.fetchCharacterData(charId);
return char.renderType != AnimateAtlas;
});
characters.sort(SortUtil.alphabetically); characters.sort(SortUtil.alphabetically);
var charDropdown:DropDown = cast uiStuff.findComponent('characterDropdown'); var charDropdown:DropDown = cast uiStuff.findComponent('characterDropdown');

View file

@ -66,7 +66,7 @@ class AddNotesCommand implements ChartEditorCommand
state.currentEventSelection = []; state.currentEventSelection = [];
} }
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-08')); ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/noteLay'));
state.saveDataDirty = true; state.saveDataDirty = true;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
@ -80,7 +80,7 @@ class AddNotesCommand implements ChartEditorCommand
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
state.currentNoteSelection = []; state.currentNoteSelection = [];
state.currentEventSelection = []; state.currentEventSelection = [];
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-01')); ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/undo'));
state.saveDataDirty = true; state.saveDataDirty = true;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
@ -116,7 +116,8 @@ class RemoveNotesCommand implements ChartEditorCommand
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes); state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
state.currentNoteSelection = []; state.currentNoteSelection = [];
state.currentEventSelection = []; state.currentEventSelection = [];
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/noteErase'));
state.saveDataDirty = true; state.saveDataDirty = true;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
@ -133,7 +134,7 @@ class RemoveNotesCommand implements ChartEditorCommand
} }
state.currentNoteSelection = notes; state.currentNoteSelection = notes;
state.currentEventSelection = []; state.currentEventSelection = [];
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-08')); ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/undo'));
state.saveDataDirty = true; state.saveDataDirty = true;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
@ -254,7 +255,7 @@ class AddEventsCommand implements ChartEditorCommand
state.currentEventSelection = events; state.currentEventSelection = events;
} }
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-08')); ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/noteLay'));
state.saveDataDirty = true; state.saveDataDirty = true;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
@ -298,7 +299,8 @@ class RemoveEventsCommand implements ChartEditorCommand
{ {
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events); state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
state.currentEventSelection = []; state.currentEventSelection = [];
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/noteErase'));
state.saveDataDirty = true; state.saveDataDirty = true;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
@ -314,7 +316,7 @@ class RemoveEventsCommand implements ChartEditorCommand
state.currentSongChartEventData.push(event); state.currentSongChartEventData.push(event);
} }
state.currentEventSelection = events; state.currentEventSelection = events;
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-08')); ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/undo'));
state.saveDataDirty = true; state.saveDataDirty = true;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
@ -354,7 +356,7 @@ class RemoveItemsCommand implements ChartEditorCommand
state.currentNoteSelection = []; state.currentNoteSelection = [];
state.currentEventSelection = []; state.currentEventSelection = [];
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-01')); ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/noteErase'));
state.saveDataDirty = true; state.saveDataDirty = true;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
@ -378,7 +380,7 @@ class RemoveItemsCommand implements ChartEditorCommand
state.currentNoteSelection = notes; state.currentNoteSelection = notes;
state.currentEventSelection = events; state.currentEventSelection = events;
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-08')); ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/undo'));
state.saveDataDirty = true; state.saveDataDirty = true;
state.noteDisplayDirty = true; state.noteDisplayDirty = true;
@ -805,6 +807,8 @@ class PasteItemsCommand implements ChartEditorCommand
public function undo(state:ChartEditorState):Void public function undo(state:ChartEditorState):Void
{ {
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/undo'));
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes); state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes);
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, addedEvents); state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, addedEvents);
state.currentNoteSelection = []; state.currentNoteSelection = [];
@ -857,6 +861,8 @@ class ExtendNoteLengthCommand implements ChartEditorCommand
public function undo(state:ChartEditorState):Void public function undo(state:ChartEditorState):Void
{ {
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/undo'));
note.length = oldLength; note.length = oldLength;
state.saveDataDirty = true; state.saveDataDirty = true;

View file

@ -404,7 +404,6 @@ class ChartEditorDialogHandler
{ {
if (ChartEditorAudioHandler.loadInstFromBytes(state, selectedFile.bytes, instId)) if (ChartEditorAudioHandler.loadInstFromBytes(state, selectedFile.bytes, instId))
{ {
trace('Selected file: ' + selectedFile.fullPath);
#if !mac #if !mac
NotificationManager.instance.addNotification( NotificationManager.instance.addNotification(
{ {
@ -415,13 +414,12 @@ class ChartEditorDialogHandler
}); });
#end #end
state.switchToCurrentInstrumental();
dialog.hideDialog(DialogButton.APPLY); dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile); removeDropHandler(onDropFile);
} }
else else
{ {
trace('Failed to load instrumental (${selectedFile.fullPath})');
#if !mac #if !mac
NotificationManager.instance.addNotification( NotificationManager.instance.addNotification(
{ {
@ -452,6 +450,7 @@ class ChartEditorDialogHandler
}); });
#end #end
state.switchToCurrentInstrumental();
dialog.hideDialog(DialogButton.APPLY); dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile); removeDropHandler(onDropFile);
} }
@ -570,6 +569,12 @@ class ChartEditorDialogHandler
var newSongMetadata:SongMetadata = new SongMetadata('', '', 'default'); var newSongMetadata:SongMetadata = new SongMetadata('', '', 'default');
newSongMetadata.playData.difficulties = switch (targetVariation)
{
case 'erect': ['erect', 'nightmare'];
default: ['easy', 'normal', 'hard'];
};
var inputSongName:Null<TextField> = dialog.findComponent('inputSongName', TextField); var inputSongName:Null<TextField> = dialog.findComponent('inputSongName', TextField);
if (inputSongName == null) throw 'Could not locate inputSongName TextField in Song Metadata dialog'; if (inputSongName == null) throw 'Could not locate inputSongName TextField in Song Metadata dialog';
inputSongName.onChange = function(event:UIEvent) { inputSongName.onChange = function(event:UIEvent) {
@ -667,8 +672,6 @@ class ChartEditorDialogHandler
timeChanges[0].bpm = event.value; timeChanges[0].bpm = event.value;
} }
Conductor.forceBPM(event.value);
newSongMetadata.timeChanges = timeChanges; newSongMetadata.timeChanges = timeChanges;
}; };
@ -677,6 +680,8 @@ class ChartEditorDialogHandler
dialogContinue.onClick = (_event) -> { dialogContinue.onClick = (_event) -> {
state.songMetadata.set(targetVariation, newSongMetadata); state.songMetadata.set(targetVariation, newSongMetadata);
Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges);
dialog.hideDialog(DialogButton.APPLY); dialog.hideDialog(DialogButton.APPLY);
} }
@ -696,6 +701,8 @@ class ChartEditorDialogHandler
var charData:SongCharacterData = state.currentSongMetadata.playData.characters; var charData:SongCharacterData = state.currentSongMetadata.playData.characters;
var hasClearedVocals:Bool = false;
charIdsForVocals.push(charData.player); charIdsForVocals.push(charData.player);
charIdsForVocals.push(charData.opponent); charIdsForVocals.push(charData.opponent);
@ -715,6 +722,7 @@ class ChartEditorDialogHandler
if (dialogNoVocals == null) throw 'Could not locate dialogNoVocals button in Upload Vocals dialog'; if (dialogNoVocals == null) throw 'Could not locate dialogNoVocals button in Upload Vocals dialog';
dialogNoVocals.onClick = function(_event) { dialogNoVocals.onClick = function(_event) {
// Dismiss // Dismiss
ChartEditorAudioHandler.stopExistingVocals(state);
dialog.hideDialog(DialogButton.APPLY); dialog.hideDialog(DialogButton.APPLY);
}; };
@ -738,6 +746,12 @@ class ChartEditorDialogHandler
trace('Selected file: $pathStr'); trace('Selected file: $pathStr');
var path:Path = new Path(pathStr); var path:Path = new Path(pathStr);
if (!hasClearedVocals)
{
hasClearedVocals = true;
ChartEditorAudioHandler.stopExistingVocals(state);
}
if (ChartEditorAudioHandler.loadVocalsFromPath(state, path, charKey, instId)) if (ChartEditorAudioHandler.loadVocalsFromPath(state, path, charKey, instId))
{ {
// Tell the user the load was successful. // Tell the user the load was successful.
@ -788,6 +802,11 @@ class ChartEditorDialogHandler
if (selectedFile != null && selectedFile.bytes != null) if (selectedFile != null && selectedFile.bytes != null)
{ {
trace('Selected file: ' + selectedFile.name); trace('Selected file: ' + selectedFile.name);
if (!hasClearedVocals)
{
hasClearedVocals = true;
ChartEditorAudioHandler.stopExistingVocals(state);
}
if (ChartEditorAudioHandler.loadVocalsFromBytes(state, selectedFile.bytes, charKey, instId)) if (ChartEditorAudioHandler.loadVocalsFromBytes(state, selectedFile.bytes, charKey, instId))
{ {
// Tell the user the load was successful. // Tell the user the load was successful.

View file

@ -87,9 +87,8 @@ using Lambda;
* *
* @author MasterEric * @author MasterEric
*/ */
@:nullSafety
// Give other classes access to private instance fields // Give other classes access to private instance fields
// @:nullSafety(Loose) // Enable this while developing, then disable to keep unit tests functional!
@:allow(funkin.ui.debug.charting.ChartEditorCommand) @:allow(funkin.ui.debug.charting.ChartEditorCommand)
@:allow(funkin.ui.debug.charting.ChartEditorDropdowns) @:allow(funkin.ui.debug.charting.ChartEditorDropdowns)
@:allow(funkin.ui.debug.charting.ChartEditorDialogHandler) @:allow(funkin.ui.debug.charting.ChartEditorDialogHandler)
@ -555,6 +554,9 @@ class ChartEditorState extends HaxeUIState
notePreviewDirty = true; notePreviewDirty = true;
notePreviewViewportBoundsDirty = true; notePreviewViewportBoundsDirty = true;
// Make sure the difficulty we selected is in the list of difficulties.
currentSongMetadata.playData.difficulties.pushUnique(selectedDifficulty);
return selectedDifficulty; return selectedDifficulty;
} }
@ -965,13 +967,14 @@ class ChartEditorState extends HaxeUIState
function get_currentSongChartNoteData():Array<SongNoteData> function get_currentSongChartNoteData():Array<SongNoteData>
{ {
var result:Array<SongNoteData> = currentSongChartData.notes.get(selectedDifficulty); var result:Null<Array<SongNoteData>> = currentSongChartData.notes.get(selectedDifficulty);
if (result == null) if (result == null)
{ {
// Initialize to the default value if not set. // Initialize to the default value if not set.
result = []; result = [];
trace('Initializing blank note data for difficulty ' + selectedDifficulty); trace('Initializing blank note data for difficulty ' + selectedDifficulty);
currentSongChartData.notes.set(selectedDifficulty, result); currentSongChartData.notes.set(selectedDifficulty, result);
currentSongMetadata.playData.difficulties.pushUnique(selectedDifficulty);
return result; return result;
} }
return result; return result;
@ -980,6 +983,7 @@ class ChartEditorState extends HaxeUIState
function set_currentSongChartNoteData(value:Array<SongNoteData>):Array<SongNoteData> function set_currentSongChartNoteData(value:Array<SongNoteData>):Array<SongNoteData>
{ {
currentSongChartData.notes.set(selectedDifficulty, value); currentSongChartData.notes.set(selectedDifficulty, value);
currentSongMetadata.playData.difficulties.pushUnique(selectedDifficulty);
return value; return value;
} }
@ -1391,16 +1395,12 @@ class ChartEditorState extends HaxeUIState
healthIconDad = new HealthIcon(currentSongMetadata.playData.characters.opponent); healthIconDad = new HealthIcon(currentSongMetadata.playData.characters.opponent);
healthIconDad.autoUpdate = false; healthIconDad.autoUpdate = false;
healthIconDad.size.set(0.5, 0.5); healthIconDad.size.set(0.5, 0.5);
healthIconDad.x = gridTiledSprite.x - 15 - (HealthIcon.HEALTH_ICON_SIZE * 0.5);
healthIconDad.y = gridTiledSprite.y + 5;
add(healthIconDad); add(healthIconDad);
healthIconDad.zIndex = 30; healthIconDad.zIndex = 30;
healthIconBF = new HealthIcon(currentSongMetadata.playData.characters.player); healthIconBF = new HealthIcon(currentSongMetadata.playData.characters.player);
healthIconBF.autoUpdate = false; healthIconBF.autoUpdate = false;
healthIconBF.size.set(0.5, 0.5); healthIconBF.size.set(0.5, 0.5);
healthIconBF.x = gridTiledSprite.x + gridTiledSprite.width + 15;
healthIconBF.y = gridTiledSprite.y + 5;
healthIconBF.flipX = true; healthIconBF.flipX = true;
add(healthIconBF); add(healthIconBF);
healthIconBF.zIndex = 30; healthIconBF.zIndex = 30;
@ -1627,6 +1627,12 @@ class ChartEditorState extends HaxeUIState
addUIClickListener('playbarForward', _ -> playbarButtonPressed = 'playbarForward'); addUIClickListener('playbarForward', _ -> playbarButtonPressed = 'playbarForward');
addUIClickListener('playbarEnd', _ -> playbarButtonPressed = 'playbarEnd'); addUIClickListener('playbarEnd', _ -> playbarButtonPressed = 'playbarEnd');
// Cycle note snap quant.
addUIClickListener('playbarNoteSnap', function(_) {
noteSnapQuantIndex++;
if (noteSnapQuantIndex >= SNAP_QUANTS.length) noteSnapQuantIndex = 0;
});
// Add functionality to the menu items. // Add functionality to the menu items.
addUIClickListener('menubarItemNewChart', _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true)); addUIClickListener('menubarItemNewChart', _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true));
@ -1897,6 +1903,16 @@ class ChartEditorState extends HaxeUIState
handleViewKeybinds(); handleViewKeybinds();
handleTestKeybinds(); handleTestKeybinds();
handleHelpKeybinds(); handleHelpKeybinds();
#if debug
handleQuickWatch();
#end
}
function handleQuickWatch():Void
{
FlxG.watch.addQuick('scrollPosInPixels', scrollPositionInPixels);
FlxG.watch.addQuick('playheadPosInPixels', playheadPositionInPixels);
} }
/** /**
@ -2128,11 +2144,18 @@ class ChartEditorState extends HaxeUIState
} }
} }
var dragLengthCurrent:Float = 0;
var stretchySounds:Bool = false;
/** /**
* Handle display of the mouse cursor. * Handle display of the mouse cursor.
*/ */
function handleCursor():Void function handleCursor():Void
{ {
// Mouse sounds
if (FlxG.mouse.justPressed) FlxG.sound.play(Paths.sound("chartingSounds/ClickDown"));
if (FlxG.mouse.justReleased) FlxG.sound.play(Paths.sound("chartingSounds/ClickUp"));
// Note: If a menu is open in HaxeUI, don't handle cursor behavior. // Note: If a menu is open in HaxeUI, don't handle cursor behavior.
var shouldHandleCursor:Bool = !isCursorOverHaxeUI || (selectionBoxStartPos != null); var shouldHandleCursor:Bool = !isCursorOverHaxeUI || (selectionBoxStartPos != null);
var eventColumn:Int = (STRUMLINE_SIZE * 2 + 1) - 1; var eventColumn:Int = (STRUMLINE_SIZE * 2 + 1) - 1;
@ -2477,25 +2500,37 @@ class ChartEditorState extends HaxeUIState
var dragLengthMs:Float = dragLengthSteps * Conductor.stepLengthMs; var dragLengthMs:Float = dragLengthSteps * Conductor.stepLengthMs;
var dragLengthPixels:Float = dragLengthSteps * GRID_SIZE; var dragLengthPixels:Float = dragLengthSteps * GRID_SIZE;
if (dragLengthSteps > 0) if (gridGhostNote != null && gridGhostNote.noteData != null && gridGhostHoldNote != null)
{ {
gridGhostHoldNote.visible = true; if (dragLengthSteps > 0)
gridGhostHoldNote.noteData = gridGhostNote.noteData; {
gridGhostHoldNote.noteDirection = gridGhostNote.noteData.getDirection(); if (dragLengthCurrent != dragLengthSteps)
{
stretchySounds = !stretchySounds;
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/stretch' + (stretchySounds ? '1' : '2') + '_UI'));
gridGhostHoldNote.setHeightDirectly(dragLengthPixels); dragLengthCurrent = dragLengthSteps;
}
gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes); gridGhostHoldNote.visible = true;
} gridGhostHoldNote.noteData = gridGhostNote.noteData;
else gridGhostHoldNote.noteDirection = gridGhostNote.noteData.getDirection();
{
gridGhostHoldNote.visible = false; gridGhostHoldNote.setHeightDirectly(dragLengthPixels);
gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);
}
else
{
gridGhostHoldNote.visible = false;
}
} }
if (FlxG.mouse.justReleased) if (FlxG.mouse.justReleased)
{ {
if (dragLengthSteps > 0) if (dragLengthSteps > 0)
{ {
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/stretchSNAP_UI'));
// Apply the new length. // Apply the new length.
performCommand(new ExtendNoteLengthCommand(currentPlaceNoteData, dragLengthMs)); performCommand(new ExtendNoteLengthCommand(currentPlaceNoteData, dragLengthMs));
} }
@ -2644,7 +2679,7 @@ class ChartEditorState extends HaxeUIState
if (cursorColumn == eventColumn) if (cursorColumn == eventColumn)
{ {
if (gridGhostNote != null) gridGhostNote.visible = false; if (gridGhostNote != null) gridGhostNote.visible = false;
gridGhostHoldNote.visible = false; if (gridGhostHoldNote != null) gridGhostHoldNote.visible = false;
if (gridGhostEvent == null) throw "ERROR: Tried to handle cursor, but gridGhostEvent is null! Check ChartEditorState.buildGrid()"; if (gridGhostEvent == null) throw "ERROR: Tried to handle cursor, but gridGhostEvent is null! Check ChartEditorState.buildGrid()";
@ -2704,11 +2739,11 @@ class ChartEditorState extends HaxeUIState
} }
else else
{ {
if (FlxG.mouse.overlaps(notePreview)) if (notePreview != null && FlxG.mouse.overlaps(notePreview))
{ {
targetCursorMode = Pointer; targetCursorMode = Pointer;
} }
else if (FlxG.mouse.overlaps(gridPlayheadScrollArea)) else if (gridPlayheadScrollArea != null && FlxG.mouse.overlaps(gridPlayheadScrollArea))
{ {
targetCursorMode = Pointer; targetCursorMode = Pointer;
} }
@ -3020,18 +3055,35 @@ class ChartEditorState extends HaxeUIState
{ {
if (healthIconsDirty) if (healthIconsDirty)
{ {
if (healthIconBF != null) healthIconBF.characterId = currentSongMetadata.playData.characters.player; var charDataBF = CharacterDataParser.fetchCharacterData(currentSongMetadata.playData.characters.player);
if (healthIconDad != null) healthIconDad.characterId = currentSongMetadata.playData.characters.opponent; var charDataDad = CharacterDataParser.fetchCharacterData(currentSongMetadata.playData.characters.opponent);
if (healthIconBF != null)
{
healthIconBF.configure(charDataBF?.healthIcon);
healthIconBF.size *= 0.5; // Make the icon smaller in Chart Editor.
healthIconBF.flipX = !healthIconBF.flipX; // BF faces the other way.
}
if (healthIconDad != null)
{
healthIconDad.configure(charDataDad?.healthIcon);
healthIconDad.size *= 0.5; // Make the icon smaller in Chart Editor.
}
healthIconsDirty = false;
} }
// Right align the BF health icon. // Right align, and visibly center, the BF health icon.
if (healthIconBF != null) if (healthIconBF != null)
{ {
// Base X position to the right of the grid. // Base X position to the right of the grid.
var baseHealthIconXPos:Float = (gridTiledSprite == null) ? (0) : (gridTiledSprite.x + gridTiledSprite.width + 15); healthIconBF.x = (gridTiledSprite == null) ? (0) : (gridTiledSprite.x + gridTiledSprite.width + 45 - (healthIconBF.width / 2));
// Will be 0 when not bopping. When bopping, will increase to push the icon left. healthIconBF.y = (gridTiledSprite == null) ? (0) : (MENU_BAR_HEIGHT + GRID_TOP_PAD + 30 - (healthIconBF.height / 2));
var healthIconOffset:Float = healthIconBF.width - (HealthIcon.HEALTH_ICON_SIZE * 0.5); }
healthIconBF.x = baseHealthIconXPos - healthIconOffset;
// Visibly center the Dad health icon.
if (healthIconDad != null)
{
healthIconDad.x = (gridTiledSprite == null) ? (0) : (gridTiledSprite.x - 45 - (healthIconDad.width / 2));
healthIconDad.y = (gridTiledSprite == null) ? (0) : (MENU_BAR_HEIGHT + GRID_TOP_PAD + 30 - (healthIconDad.height / 2));
} }
} }
@ -3119,6 +3171,7 @@ class ChartEditorState extends HaxeUIState
function quitChartEditor():Void function quitChartEditor():Void
{ {
autoSave(); autoSave();
stopWelcomeMusic();
FlxG.switchState(new MainMenuState()); FlxG.switchState(new MainMenuState());
} }
@ -3342,6 +3395,7 @@ class ChartEditorState extends HaxeUIState
if (!isHaxeUIDialogOpen && !isCursorOverHaxeUI && FlxG.keys.justPressed.ENTER) if (!isHaxeUIDialogOpen && !isCursorOverHaxeUI && FlxG.keys.justPressed.ENTER)
{ {
var minimal = FlxG.keys.pressed.SHIFT; var minimal = FlxG.keys.pressed.SHIFT;
ChartEditorToolboxHandler.hideAllToolboxes(this);
testSongInPlayState(minimal); testSongInPlayState(minimal);
} }
} }
@ -3656,49 +3710,41 @@ class ChartEditorState extends HaxeUIState
var inputStage:Null<DropDown> = toolbox.findComponent('inputStage', DropDown); var inputStage:Null<DropDown> = toolbox.findComponent('inputStage', DropDown);
var stageId:String = currentSongMetadata.playData.stage; var stageId:String = currentSongMetadata.playData.stage;
var stageData:Null<StageData> = StageDataParser.parseStageData(stageId); var stageData:Null<StageData> = StageDataParser.parseStageData(stageId);
if (stageData != null) if (inputStage != null)
{ {
inputStage.value = {id: stageId, text: stageData.name}; inputStage.value = (stageData != null) ?
} {id: stageId, text: stageData.name} :
else {id: "mainStage", text: "Main Stage"};
{
inputStage.value = {id: "mainStage", text: "Main Stage"};
} }
var inputCharacterPlayer:Null<DropDown> = toolbox.findComponent('inputCharacterPlayer', DropDown); var inputCharacterPlayer:Null<DropDown> = toolbox.findComponent('inputCharacterPlayer', DropDown);
var charIdPlayer:String = currentSongMetadata.playData.characters.player; var charIdPlayer:String = currentSongMetadata.playData.characters.player;
var charDataPlayer:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charIdPlayer); var charDataPlayer:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charIdPlayer);
if (charDataPlayer != null) if (inputCharacterPlayer != null)
{ {
inputCharacterPlayer.value = {id: charIdPlayer, text: charDataPlayer.name}; inputCharacterPlayer.value = (charDataPlayer != null) ?
} {id: charIdPlayer, text: charDataPlayer.name} :
else {id: "bf", text: "Boyfriend"};
{
inputCharacterPlayer.value = {id: "bf", text: "Boyfriend"};
} }
var inputCharacterOpponent:Null<DropDown> = toolbox.findComponent('inputCharacterOpponent', DropDown); var inputCharacterOpponent:Null<DropDown> = toolbox.findComponent('inputCharacterOpponent', DropDown);
var charIdOpponent:String = currentSongMetadata.playData.characters.opponent; var charIdOpponent:String = currentSongMetadata.playData.characters.opponent;
var charDataOpponent:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charIdOpponent); var charDataOpponent:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charIdOpponent);
if (charDataOpponent != null) if (inputCharacterOpponent != null)
{ {
inputCharacterOpponent.value = {id: charIdOpponent, text: charDataOpponent.name}; inputCharacterOpponent.value = (charDataOpponent != null) ?
} {id: charIdOpponent, text: charDataOpponent.name} :
else {id: "dad", text: "Dad"};
{
inputCharacterOpponent.value = {id: "dad", text: "Dad"};
} }
var inputCharacterGirlfriend:Null<DropDown> = toolbox.findComponent('inputCharacterGirlfriend', DropDown); var inputCharacterGirlfriend:Null<DropDown> = toolbox.findComponent('inputCharacterGirlfriend', DropDown);
var charIdGirlfriend:String = currentSongMetadata.playData.characters.girlfriend; var charIdGirlfriend:String = currentSongMetadata.playData.characters.girlfriend;
var charDataGirlfriend:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charIdGirlfriend); var charDataGirlfriend:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charIdGirlfriend);
if (charDataGirlfriend != null) if (inputCharacterGirlfriend != null)
{ {
inputCharacterGirlfriend.value = {id: charIdGirlfriend, text: charDataGirlfriend.name}; inputCharacterGirlfriend.value = (charDataGirlfriend != null) ?
} {id: charIdGirlfriend, text: charDataGirlfriend.name} :
else {id: "none", text: "None"};
{
inputCharacterGirlfriend.value = {id: "none", text: "None"};
} }
} }
@ -3885,9 +3931,9 @@ class ChartEditorState extends HaxeUIState
switch (noteData.getStrumlineIndex()) switch (noteData.getStrumlineIndex())
{ {
case 0: // Player case 0: // Player
if (hitsoundsEnabledPlayer) ChartEditorAudioHandler.playSound(Paths.sound('ui/chart-editor/playerHitsound')); if (hitsoundsEnabledPlayer) ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/hitNotePlayer'));
case 1: // Opponent case 1: // Opponent
if (hitsoundsEnabledOpponent) ChartEditorAudioHandler.playSound(Paths.sound('ui/chart-editor/opponentHitsound')); if (hitsoundsEnabledOpponent) ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/hitNoteOpponent'));
} }
} }
} }
@ -4004,7 +4050,7 @@ class ChartEditorState extends HaxeUIState
this.scrollPositionInPixels = value; this.scrollPositionInPixels = value;
// Move the grid sprite to the correct position. // Move the grid sprite to the correct position.
if (gridTiledSprite != null) if (gridTiledSprite != null && gridPlayheadScrollArea != null)
{ {
if (isViewDownscroll) if (isViewDownscroll)
{ {
@ -4064,7 +4110,7 @@ class ChartEditorState extends HaxeUIState
} }
subStateClosed.add(fixCamera); subStateClosed.add(fixCamera);
subStateClosed.add(updateConductor); subStateClosed.add(resetConductorAfterTest);
FlxTransitionableState.skipNextTransIn = false; FlxTransitionableState.skipNextTransIn = false;
FlxTransitionableState.skipNextTransOut = false; FlxTransitionableState.skipNextTransOut = false;
@ -4097,10 +4143,9 @@ class ChartEditorState extends HaxeUIState
add(this.component); add(this.component);
} }
function updateConductor(_:FlxSubState = null):Void function resetConductorAfterTest(_:FlxSubState = null):Void
{ {
var targetPos = scrollPositionInMs; moveSongToScrollPosition();
Conductor.update(targetPos);
} }
public function postLoadInstrumental():Void public function postLoadInstrumental():Void
@ -4153,12 +4198,13 @@ class ChartEditorState extends HaxeUIState
*/ */
function moveSongToScrollPosition():Void function moveSongToScrollPosition():Void
{ {
// Update the songPosition in the Conductor.
var targetPos = scrollPositionInMs;
Conductor.update(targetPos);
// Update the songPosition in the audio tracks. // Update the songPosition in the audio tracks.
if (audioInstTrack != null) audioInstTrack.time = scrollPositionInMs + playheadPositionInMs; if (audioInstTrack != null)
{
audioInstTrack.time = scrollPositionInMs + playheadPositionInMs;
// Update the songPosition in the Conductor.
Conductor.update(audioInstTrack.time);
}
if (audioVocalTrackGroup != null) audioVocalTrackGroup.time = scrollPositionInMs + playheadPositionInMs; if (audioVocalTrackGroup != null) audioVocalTrackGroup.time = scrollPositionInMs + playheadPositionInMs;
// We need to update the note sprites because we changed the scroll position. // We need to update the note sprites because we changed the scroll position.
@ -4264,7 +4310,7 @@ class ChartEditorState extends HaxeUIState
function playMetronomeTick(high:Bool = false):Void function playMetronomeTick(high:Bool = false):Void
{ {
ChartEditorAudioHandler.playSound(Paths.sound('pianoStuff/piano-${high ? '001' : '008'}')); ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/metronome${high ? '1' : '2'}'));
} }
function isNoteSelected(note:Null<SongNoteData>):Bool function isNoteSelected(note:Null<SongNoteData>):Bool

View file

@ -72,6 +72,8 @@ class ChartEditorToolboxHandler
{ {
toolbox.showDialog(false); toolbox.showDialog(false);
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/openWindow'));
switch (id) switch (id)
{ {
case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT: case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT:
@ -109,6 +111,8 @@ class ChartEditorToolboxHandler
{ {
toolbox.hideDialog(DialogButton.CANCEL); toolbox.hideDialog(DialogButton.CANCEL);
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/exitWindow'));
switch (id) switch (id)
{ {
case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT: case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT:
@ -136,6 +140,18 @@ class ChartEditorToolboxHandler
} }
} }
public static function rememberOpenToolboxes(state:ChartEditorState):Void {}
public static function openRememberedToolboxes(state:ChartEditorState):Void {}
public static function hideAllToolboxes(state:ChartEditorState):Void
{
for (toolbox in state.activeToolboxes.values())
{
toolbox.hideDialog(DialogButton.CANCEL);
}
}
public static function minimizeToolbox(state:ChartEditorState, id:String):Void public static function minimizeToolbox(state:ChartEditorState, id:String):Void
{ {
var toolbox:Null<CollapsibleDialog> = state.activeToolboxes.get(id); var toolbox:Null<CollapsibleDialog> = state.activeToolboxes.get(id);
@ -634,9 +650,9 @@ class ChartEditorToolboxHandler
timeChanges[0].bpm = event.value; timeChanges[0].bpm = event.value;
} }
Conductor.forceBPM(event.value);
state.currentSongMetadata.timeChanges = timeChanges; state.currentSongMetadata.timeChanges = timeChanges;
Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges);
}; };
inputBPM.value = state.currentSongMetadata.timeChanges[0].bpm; inputBPM.value = state.currentSongMetadata.timeChanges[0].bpm;

View file

@ -0,0 +1,30 @@
package funkin.ui.haxeui.components;
import haxe.ui.components.Label;
import funkin.input.Cursor;
import haxe.ui.events.MouseEvent;
/**
* A HaxeUI label which:
* - Changes the current cursor when hovered over (assume an onClick handler will be added!).
*/
class FunkinClickLabel extends Label
{
public function new()
{
super();
this.onMouseOver = handleMouseOver;
this.onMouseOut = handleMouseOut;
}
private function handleMouseOver(event:MouseEvent)
{
Cursor.cursorMode = Pointer;
}
private function handleMouseOut(event:MouseEvent)
{
Cursor.cursorMode = Default;
}
}

View file

@ -1,5 +1,6 @@
package funkin.ui.story; package funkin.ui.story;
import funkin.util.SortUtil;
import flixel.FlxSprite; import flixel.FlxSprite;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import funkin.play.song.Song; import funkin.play.song.Song;
@ -155,6 +156,8 @@ class Level implements IRegistryEntry<LevelData>
} }
} }
difficulties.sort(SortUtil.defaultsThenAlphabetically.bind(Constants.DEFAULT_DIFFICULTY_LIST));
// Filter to only include difficulties that are present in all songs // Filter to only include difficulties that are present in all songs
for (songIndex in 1...songList.length) for (songIndex in 1...songList.length)
{ {

View file

@ -99,6 +99,9 @@ class StoryMenuState extends MusicBeatState
var stickerSubState:StickerSubState; var stickerSubState:StickerSubState;
static var rememberedLevelId:Null<String> = null;
static var rememberedDifficulty:Null<String> = "normal";
public function new(?stickers:StickerSubState = null) public function new(?stickers:StickerSubState = null)
{ {
super(); super();
@ -133,6 +136,8 @@ class StoryMenuState extends MusicBeatState
updateData(); updateData();
rememberSelection();
// Explicitly define the background color. // Explicitly define the background color.
this.bgColor = FlxColor.BLACK; this.bgColor = FlxColor.BLACK;
@ -185,6 +190,7 @@ class StoryMenuState extends MusicBeatState
leftDifficultyArrow.animation.play('idle'); leftDifficultyArrow.animation.play('idle');
add(leftDifficultyArrow); add(leftDifficultyArrow);
buildDifficultySprite(Constants.DEFAULT_DIFFICULTY);
buildDifficultySprite(); buildDifficultySprite();
rightDifficultyArrow = new FlxSprite(difficultySprite.x + difficultySprite.width + 10, leftDifficultyArrow.y); rightDifficultyArrow = new FlxSprite(difficultySprite.x + difficultySprite.width + 10, leftDifficultyArrow.y);
@ -207,6 +213,18 @@ class StoryMenuState extends MusicBeatState
#end #end
} }
function rememberSelection():Void
{
if (rememberedLevelId != null)
{
currentLevelId = rememberedLevelId;
}
if (rememberedDifficulty != null)
{
currentDifficultyId = rememberedDifficulty;
}
}
function playMenuMusic():Void function playMenuMusic():Void
{ {
if (FlxG.sound.music == null || !FlxG.sound.music.playing) if (FlxG.sound.music == null || !FlxG.sound.music.playing)
@ -228,34 +246,35 @@ class StoryMenuState extends MusicBeatState
isLevelUnlocked = currentLevel == null ? false : currentLevel.isUnlocked(); isLevelUnlocked = currentLevel == null ? false : currentLevel.isUnlocked();
} }
function buildDifficultySprite():Void function buildDifficultySprite(?diff:String):Void
{ {
if (diff == null) diff = currentDifficultyId;
remove(difficultySprite); remove(difficultySprite);
difficultySprite = difficultySprites.get(currentDifficultyId); difficultySprite = difficultySprites.get(diff);
if (difficultySprite == null) if (difficultySprite == null)
{ {
difficultySprite = new FlxSprite(leftDifficultyArrow.x + leftDifficultyArrow.width + 10, leftDifficultyArrow.y); difficultySprite = new FlxSprite(leftDifficultyArrow.x + leftDifficultyArrow.width + 10, leftDifficultyArrow.y);
if (Assets.exists(Paths.file('images/storymenu/difficulties/${currentDifficultyId}.xml'))) if (Assets.exists(Paths.file('images/storymenu/difficulties/${diff}.xml')))
{ {
difficultySprite.frames = Paths.getSparrowAtlas('storymenu/difficulties/${currentDifficultyId}'); difficultySprite.frames = Paths.getSparrowAtlas('storymenu/difficulties/${diff}');
difficultySprite.animation.addByPrefix('idle', 'idle0', 24, true); difficultySprite.animation.addByPrefix('idle', 'idle0', 24, true);
difficultySprite.animation.play('idle'); difficultySprite.animation.play('idle');
} }
else else
{ {
difficultySprite.loadGraphic(Paths.image('storymenu/difficulties/${currentDifficultyId}')); difficultySprite.loadGraphic(Paths.image('storymenu/difficulties/${diff}'));
} }
difficultySprites.set(currentDifficultyId, difficultySprite); difficultySprites.set(diff, difficultySprite);
difficultySprite.x += (difficultySprites.get('normal').width - difficultySprite.width) / 2; difficultySprite.x += (difficultySprites.get(Constants.DEFAULT_DIFFICULTY).width - difficultySprite.width) / 2;
} }
difficultySprite.alpha = 0; difficultySprite.alpha = 0;
difficultySprite.y = leftDifficultyArrow.y - 15; difficultySprite.y = leftDifficultyArrow.y - 15;
var targetY:Float = leftDifficultyArrow.y + 10; var targetY:Float = leftDifficultyArrow.y + 10;
targetY -= (difficultySprite.height - difficultySprites.get('normal').height) / 2; targetY -= (difficultySprite.height - difficultySprites.get(Constants.DEFAULT_DIFFICULTY).height) / 2;
FlxTween.tween(difficultySprite, {y: targetY, alpha: 1}, 0.07); FlxTween.tween(difficultySprite, {y: targetY, alpha: 1}, 0.07);
add(difficultySprite); add(difficultySprite);
@ -399,6 +418,7 @@ class StoryMenuState extends MusicBeatState
var previousLevelId:String = currentLevelId; var previousLevelId:String = currentLevelId;
currentLevelId = levelList[currentIndex]; currentLevelId = levelList[currentIndex];
rememberedLevelId = currentLevelId;
updateData(); updateData();
@ -442,6 +462,7 @@ class StoryMenuState extends MusicBeatState
var hasChanged:Bool = currentDifficultyId != difficultyList[currentIndex]; var hasChanged:Bool = currentDifficultyId != difficultyList[currentIndex];
currentDifficultyId = difficultyList[currentIndex]; currentDifficultyId = difficultyList[currentIndex];
rememberedDifficulty = currentDifficultyId;
if (difficultyList.length <= 1) if (difficultyList.length <= 1)
{ {
@ -523,6 +544,7 @@ class StoryMenuState extends MusicBeatState
PlayStatePlaylist.campaignId = currentLevel.id; PlayStatePlaylist.campaignId = currentLevel.id;
PlayStatePlaylist.campaignTitle = currentLevel.getTitle(); PlayStatePlaylist.campaignTitle = currentLevel.getTitle();
PlayStatePlaylist.campaignDifficulty = currentDifficultyId;
if (targetSong != null) if (targetSong != null)
{ {
@ -538,7 +560,7 @@ class StoryMenuState extends MusicBeatState
LoadingState.loadAndSwitchState(new PlayState( LoadingState.loadAndSwitchState(new PlayState(
{ {
targetSong: targetSong, targetSong: targetSong,
targetDifficulty: currentDifficultyId, targetDifficulty: PlayStatePlaylist.campaignDifficulty,
}), true); }), true);
}); });
} }

View file

@ -4,6 +4,9 @@ import flixel.util.FlxColor;
import lime.app.Application; import lime.app.Application;
import funkin.data.song.SongData.SongTimeFormat; import funkin.data.song.SongData.SongTimeFormat;
/**
* A store of unchanging, globally relevant values.
*/
class Constants class Constants
{ {
/** /**
@ -118,11 +121,21 @@ class Constants
*/ */
public static final DEFAULT_DIFFICULTY:String = 'normal'; public static final DEFAULT_DIFFICULTY:String = 'normal';
/**
* Default list of difficulties for charts.
*/
public static final DEFAULT_DIFFICULTY_LIST:Array<String> = ['easy', 'normal', 'hard'];
/** /**
* Default player character for charts. * Default player character for charts.
*/ */
public static final DEFAULT_CHARACTER:String = 'bf'; public static final DEFAULT_CHARACTER:String = 'bf';
/**
* Default player character for health icons.
*/
public static final DEFAULT_HEALTH_ICON:String = 'face';
/** /**
* Default stage for charts. * Default stage for charts.
*/ */

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

@ -23,6 +23,13 @@ class ArrayTools
return result; return result;
} }
public static function pushUnique<T>(array:Array<T>, element:T):Bool
{
if (array.contains(element)) return false;
array.push(element);
return true;
}
/** /**
* Return the first element of the array that satisfies the predicate, or null if none do. * Return the first element of the array that satisfies the predicate, or null if none do.
* @param input The array to search * @param input The array to search
@ -38,6 +45,34 @@ class ArrayTools
return null; return null;
} }
/**
* Return the index of the first element of the array that satisfies the predicate, or `-1` if none do.
* @param input The array to search
* @param predicate The predicate to call
* @return The index of the result
*/
public static function findIndex<T>(input:Array<T>, predicate:T->Bool):Int
{
for (index in 0...input.length)
{
if (predicate(input[index])) return index;
}
return -1;
}
/*
* Push an element to the array if it is not already present.
* @param input The array to push to
* @param element The element to push
* @return Whether the element was pushed
*/
public static function pushUnique<T>(input:Array<T>, element:T):Bool
{
if (input.contains(element)) return false;
input.push(element);
return true;
}
/** /**
* Remove all elements from the array, without creating a new array. * Remove all elements from the array, without creating a new array.
* @param array The array to clear. * @param array The array to clear.