1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-07-04 17:56:24 +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:
path: .haxelib
key: ${{ runner.os }}-hmm-${{ hashFiles('**/hmm.json') }}
restore-keys: |
${{ runner.os }}-hmm-
- if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }}
name: hmm install
run: |

View file

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

1
.gitmodules vendored
View file

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

6
.vscode/launch.json vendored
View file

@ -23,6 +23,12 @@
"name": "Haxe Eval",
"type": "haxe-eval",
"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",
"type": "git",
"dir": null,
"ref": "f195121ebec688b417e38ab115185c8d93c349d3",
"ref": "737b86f121cdc90358d59e2e527934f267c94a2c",
"url": "https://github.com/EliteMasterEric/lime"
},
{
@ -139,7 +139,7 @@
"name": "openfl",
"type": "git",
"dir": null,
"ref": "de9395d2f367a80f93f082e1b639b9cde2258bf1",
"ref": "f229d76361c7e31025a048fe7909847f75bb5d5e",
"url": "https://github.com/EliteMasterEric/openfl"
},
{

View file

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

View file

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

View file

@ -1,5 +1,6 @@
package funkin;
import funkin.play.song.Song;
import flash.text.TextField;
import flixel.addons.display.FlxGridOverlay;
import flixel.addons.transition.FlxTransitionableState;
@ -48,10 +49,13 @@ import lime.utils.Assets;
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 curDifficulty:Int = 1;
var currentDifficulty:String = Constants.DEFAULT_DIFFICULTY;
var fp:FreeplayScore;
var txtCompletion:FlxText;
@ -60,7 +64,7 @@ class FreeplayState extends MusicBeatSubState
var lerpScore:Float = 0;
var intendedScore:Int = 0;
var grpDifficulties:FlxSpriteGroup;
var grpDifficulties:FlxTypedSpriteGroup<DifficultySprite>;
var coolColors:Array<Int> = [
0xff9271fd,
@ -85,6 +89,10 @@ class FreeplayState extends MusicBeatSubState
var stickerSubState:StickerSubState;
//
static var rememberedDifficulty:Null<String> = "normal";
static var rememberedSongId:Null<String> = null;
public function new(?stickers:StickerSubState = null)
{
if (stickers != null)
@ -130,14 +138,23 @@ class FreeplayState extends MusicBeatSubState
songs.push(null);
// 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 char = metadata.playData.characters.opponent;
var songName = metadata.songName;
addSong(songId, songName, coolWeek, char);
var song:Song = SongRegistry.instance.fetchEntry(songId);
var songBaseDifficulty:SongDifficulty = song.getDifficulty(Constants.DEFAULT_DIFFICULTY);
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>();
add(grpCapsules);
grpDifficulties = new FlxSpriteGroup(-300, 80);
grpDifficulties = new FlxTypedSpriteGroup<DifficultySprite>(-300, 80);
add(grpDifficulties);
exitMovers.set([grpDifficulties],
@ -293,15 +310,22 @@ class FreeplayState extends MusicBeatSubState
wait: 0
});
grpDifficulties.add(new FlxSprite().loadGraphic(Paths.image('freeplay/freeplayEasy')));
grpDifficulties.add(new FlxSprite().loadGraphic(Paths.image('freeplay/freeplayNorm')));
grpDifficulties.add(new FlxSprite().loadGraphic(Paths.image('freeplay/freeplayHard')));
for (diffId in diffIdsTotal)
{
var diffSprite:DifficultySprite = new DifficultySprite(diffId);
diffSprite.difficultyId = diffId;
grpDifficulties.add(diffSprite);
}
grpDifficulties.group.forEach(function(spr) {
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"));
albumArt.visible = false;
@ -574,15 +598,12 @@ class FreeplayState extends MusicBeatSubState
FlxG.console.registerFunction("changeSelection", changeSelection);
rememberSelection();
changeSelection();
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 touchX:Float = 0;
var dxTouch:Float = 0;
@ -861,28 +882,24 @@ class FreeplayState extends MusicBeatSubState
{
touchTimer = 0;
curDifficulty += change;
var currentDifficultyIndex = diffIdsCurrent.indexOf(currentDifficulty);
if (curDifficulty < 0) curDifficulty = 2;
if (curDifficulty > 2) curDifficulty = 0;
if (currentDifficultyIndex == -1) currentDifficultyIndex = diffIdsCurrent.indexOf(Constants.DEFAULT_DIFFICULTY);
var targetDifficulty:String = switch (curDifficulty)
{
case 0:
'easy';
case 1:
'normal';
case 2:
'hard';
default: 'normal';
};
currentDifficultyIndex += change;
if (currentDifficultyIndex < 0) currentDifficultyIndex = diffIdsCurrent.length - 1;
if (currentDifficultyIndex >= diffIdsCurrent.length) currentDifficultyIndex = 0;
currentDifficulty = diffIdsCurrent[currentDifficultyIndex];
var daSong = songs[curSelected];
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;
intendedCompletion = songScore?.accuracy ?? 0.0;
rememberedDifficulty = currentDifficulty;
}
else
{
@ -890,19 +907,31 @@ class FreeplayState extends MusicBeatSubState
intendedCompletion = 0.0;
}
grpDifficulties.group.forEach(function(spr) {
spr.visible = false;
grpDifficulties.group.forEach(function(diffSprite) {
diffSprite.visible = false;
});
var curShit:FlxSprite = grpDifficulties.group.members[curDifficulty];
curShit.visible = true;
curShit.offset.y += 5;
curShit.alpha = 0.5;
new FlxTimer().start(1 / 24, function(swag) {
curShit.alpha = 1;
curShit.updateHitbox();
});
for (diffSprite in grpDifficulties.group.members)
{
if (diffSprite == null) continue;
if (diffSprite.difficultyId == currentDifficulty)
{
if (change != 0)
{
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)
@ -950,35 +979,9 @@ class FreeplayState extends MusicBeatSubState
PlayStatePlaylist.isStoryMode = false;
if (cap.songData == null)
{
trace('[WARN] Failure while trying to load song!');
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';
}
var songId:String = cap.songTitle.toLowerCase();
var targetSong:Song = SongRegistry.instance.fetchEntry(songId);
var targetDifficulty:String = currentDifficulty;
// TODO: Implement Pico into the interface properly.
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)
{
// NGio.logEvent('Fresh');
@ -1038,11 +1057,16 @@ class FreeplayState extends MusicBeatSubState
var songScore:SaveScoreData = Save.get().getSongScore(daSongCapsule.songData.songId, targetDifficulty);
intendedScore = songScore?.score ?? 0;
intendedCompletion = songScore?.accuracy ?? 0.0;
diffIdsCurrent = daSong.songDifficulties;
rememberedSongId = daSong.songId;
changeDiff();
}
else
{
intendedScore = 0;
intendedCompletion = 0.0;
rememberedSongId = null;
rememberedDifficulty = null;
}
for (index => capsule in grpCapsules.members)
@ -1140,19 +1164,21 @@ enum abstract FilterType(String)
class FreeplaySongData
{
public var isFav:Bool = false;
public var songId:String = "";
public var songName:String = "";
public var levelId: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.songName = songName;
this.levelId = levelId;
this.songCharacter = songCharacter;
this.isFav = isFav;
this.songDifficulties = songDifficulties;
}
}
@ -1163,3 +1189,17 @@ typedef MoveData =
var ?speed: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
// Load player options from save data.
PreferencesMenu.initPrefs();
Preferences.init();
// Load controls from save data.
PlayerSettings.init();

View file

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

View file

@ -16,17 +16,18 @@ class PauseSubState extends MusicBeatSubState
{
var grpMenuShit:FlxTypedGroup<Alphabet>;
var pauseOptionsBase:Array<String> = [
final pauseOptionsBase:Array<String> = [
'Resume',
'Restart Song',
'Change Difficulty',
'Toggle Practice Mode',
'Exit to Menu'
];
final pauseOptionsCharting:Array<String> = ['Resume', 'Restart Song', 'Exit to Chart Editor'];
var pauseOptionsDifficulty:Array<String> = ['EASY', 'NORMAL', 'HARD', 'ERECT', 'BACK'];
final pauseOptionsDifficultyBase:Array<String> = ['BACK'];
var pauseOptionsCharting:Array<String> = ['Resume', 'Restart Song', 'Exit to Chart Editor'];
var pauseOptionsDifficulty:Array<String> = []; // AUTO-POPULATED
var menuItems:Array<String> = [];
var curSelected:Int = 0;
@ -48,6 +49,12 @@ class PauseSubState extends MusicBeatSubState
this.isChartingMode = isChartingMode;
menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase;
var difficultiesInVariation = PlayState.instance.currentSong.listDifficulties(PlayState.instance.currentChart.variation);
trace('DIFFICULTIES: ${difficultiesInVariation}');
pauseOptionsDifficulty = difficultiesInVariation.map(function(item:String):String {
return item.toUpperCase();
}).concat(pauseOptionsDifficultyBase);
if (PlayStatePlaylist.campaignId == 'week6')
{
@ -201,18 +208,6 @@ class PauseSubState extends MusicBeatSubState
menuItems = pauseOptionsDifficulty;
regenMenu();
case 'EASY' | 'NORMAL' | 'HARD' | 'ERECT':
PlayState.instance.currentSong = SongRegistry.instance.fetchEntry(PlayState.instance.currentSong.id.toLowerCase());
PlayState.instance.currentDifficulty = daSelected.toLowerCase();
PlayState.instance.needsReset = true;
close();
case 'BACK':
menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase;
regenMenu();
case 'Toggle Practice Mode':
PlayState.instance.isPracticeMode = true;
practiceText.visible = PlayState.instance.isPracticeMode;
@ -245,8 +240,32 @@ class PauseSubState extends MusicBeatSubState
case 'Exit to Chart Editor':
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!
case 'BACK':
menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase;
regenMenu();
default:
if (pauseOptionsDifficulty.contains(daSelected))
{
PlayState.instance.currentSong = SongRegistry.instance.fetchEntry(PlayState.instance.currentSong.id.toLowerCase());
// Reset campaign score when changing difficulty
// So if you switch difficulty on the last song of a week you get a really low overall score.
PlayStatePlaylist.campaignScore = 0;
PlayStatePlaylist.campaignDifficulty = daSelected.toLowerCase();
PlayState.instance.currentDifficulty = PlayStatePlaylist.campaignDifficulty;
PlayState.instance.needsReset = true;
close();
}
else
{
trace('[WARN] Unhandled pause menu option: ${daSelected}');
}
}
}

View file

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

View file

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

View file

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

View file

@ -1,13 +1,15 @@
package funkin.data;
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.LegacyNoteSection;
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.
@ -23,7 +25,8 @@ class DataParse
* `@:jcustomparse(funkin.data.DataParse.stringNotEmpty)`
* @param json Contains the `pos` and `value` 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
{
@ -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.
* @param json

View file

@ -1,6 +1,8 @@
package funkin.data;
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.
@ -9,9 +11,30 @@ import funkin.util.SerializerUtil;
*/
class DataWrite
{
/**
* `@:jcustomwrite(funkin.data.DataWrite.dynamicValue)`
* @param value
* @return String
*/
public static function dynamicValue(value:Dynamic):String
{
// Is this cheating? Yes. Do I care? No.
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)
@:jcustomparse(funkin.data.DataParse.semverVersion)
@:jcustomwrite(funkin.data.DataWrite.semverVersion)
public var version:Version;
@:default("Unknown")
@ -203,6 +205,8 @@ class SongMusicData
*
*/
// @:default(funkin.data.song.SongRegistry.SONG_METADATA_VERSION)
@:jcustomparse(funkin.data.DataParse.semverVersion)
@:jcustomwrite(funkin.data.DataWrite.semverVersion)
public var version:Version;
@:default("Unknown")
@ -367,6 +371,8 @@ class SongCharacterData
class SongChartData
{
@: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 scrollSpeed:Map<String, Float>;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,13 +24,14 @@ import openfl.utils.Assets;
* - i.e. `PlayState.instance.iconP1.animation.addByPrefix("jumpscare", "jumpscare", 24, false);`
* @author MasterEric
*/
@:nullSafety
class HealthIcon extends FlxSprite
{
/**
* The character this icon is representing.
* 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.
@ -123,13 +124,12 @@ class HealthIcon extends FlxSprite
initTargetSize();
}
function set_characterId(value:String):String
function set_characterId(value:Null<String>):Null<String>
{
if (value == characterId) return value;
characterId = value;
loadCharacter(characterId);
return value;
characterId = value ?? Constants.DEFAULT_HEALTH_ICON;
return characterId;
}
function set_isPixel(value:Bool):Bool
@ -137,8 +137,7 @@ class HealthIcon extends FlxSprite
if (value == isPixel) return value;
isPixel = value;
loadCharacter(characterId);
return value;
return isPixel;
}
/**
@ -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.
*/
@ -341,12 +372,17 @@ class HealthIcon extends FlxSprite
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')))
{
FlxG.log.warn('No icon for character: $charId : using default placeholder face instead!');
return 'face';
return Constants.DEFAULT_HEALTH_ICON;
}
return charId;
@ -357,10 +393,11 @@ class HealthIcon extends FlxSprite
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);
return;
}

View file

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

View file

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

View file

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

View file

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

View file

@ -19,8 +19,10 @@ class CharacterDataParser
* The current version string for the stage data format.
* Handle breaking changes by incrementing this value
* 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.
@ -603,6 +605,8 @@ typedef CharacterData =
*/
var healthIcon:Null<HealthIconData>;
var death:Null<DeathData>;
/**
* The global offset to the character's position, in pixels.
* @default [0, 0]
@ -695,3 +699,13 @@ typedef HealthIconData =
*/
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);
var targetY:Float = FlxG.height + note.y;
if (PreferencesMenu.getPref('downscroll')) targetY = 0 - note.height;
if (Preferences.downscroll) targetY = 0 - note.height;
FlxTween.tween(note, {y: targetY}, 0.5,
{
ease: FlxEase.expoIn,
@ -252,7 +252,7 @@ class Strumline extends FlxSpriteGroup
holdNotesVwoosh.add(holdNote);
var targetY:Float = FlxG.height + holdNote.y;
if (PreferencesMenu.getPref('downscroll')) targetY = 0 - holdNote.height;
if (Preferences.downscroll) targetY = 0 - holdNote.height;
FlxTween.tween(holdNote, {y: targetY}, 0.5,
{
ease: FlxEase.expoIn,
@ -277,7 +277,7 @@ class Strumline extends FlxSpriteGroup
var vwoosh:Float = (strumTime < Conductor.songPosition) && vwoosh ? 2.0 : 1.0;
var scrollSpeed:Float = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0;
return Constants.PIXELS_PER_MS * (Conductor.songPosition - strumTime) * scrollSpeed * vwoosh * (PreferencesMenu.getPref('downscroll') ? 1 : -1);
return Constants.PIXELS_PER_MS * (Conductor.songPosition - strumTime) * scrollSpeed * vwoosh * (Preferences.downscroll ? 1 : -1);
}
function updateNotes():Void
@ -321,7 +321,7 @@ class Strumline extends FlxSpriteGroup
note.y = this.y - INITIAL_OFFSET + calculateNoteYPos(note.strumTime, vwoosh);
// If the note is miss
var isOffscreen = PreferencesMenu.getPref('downscroll') ? note.y > FlxG.height : note.y < -note.height;
var isOffscreen = Preferences.downscroll ? note.y > FlxG.height : note.y < -note.height;
if (note.handledMiss && isOffscreen)
{
killNote(note);
@ -388,7 +388,7 @@ class Strumline extends FlxSpriteGroup
var vwoosh:Bool = false;
if (PreferencesMenu.getPref('downscroll'))
if (Preferences.downscroll)
{
holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
}
@ -410,7 +410,7 @@ class Strumline extends FlxSpriteGroup
holdNote.visible = false;
}
if (PreferencesMenu.getPref('downscroll'))
if (Preferences.downscroll)
{
holdNote.y = this.y - holdNote.height + STRUMLINE_SIZE / 2;
}
@ -425,7 +425,7 @@ class Strumline extends FlxSpriteGroup
holdNote.visible = true;
var vwoosh:Bool = false;
if (PreferencesMenu.getPref('downscroll'))
if (Preferences.downscroll)
{
holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
}

View file

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

View file

@ -1,5 +1,6 @@
package funkin.play.song;
import funkin.util.SortUtil;
import flixel.sound.FlxSound;
import openfl.utils.Assets;
import funkin.modding.events.ScriptEvent;
@ -56,8 +57,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
*/
public var validScore:Bool = true;
var difficultyIds:Array<String>;
public var songName(get, never):String;
function get_songName():String
@ -85,7 +84,6 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
this.id = id;
variations = [];
difficultyIds = [];
difficulties = new Map<String, SongDifficulty>();
_data = _fetchData(id);
@ -127,8 +125,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
for (vari in variations)
result.variations.push(vari);
result.difficultyIds.clear();
result.difficulties.clear();
result.populateDifficulties();
for (variation => chartData in charts)
@ -157,13 +154,17 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
{
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,
// (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.
for (diffId in metadata.playData.difficulties)
{
difficultyIds.push(diffId);
var difficulty:SongDifficulty = new SongDifficulty(this, diffId, 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>
{
if (diffId == null) diffId = difficulties.keys().array()[0];
if (diffId == null) diffId = listDifficulties()[0];
return difficulties.get(diffId);
}
public function listDifficulties():Array<String>
/**
* List all the difficulties in this song.
* @param variationId Optionally filter by variation.
* @return The list of difficulties.
*/
public function listDifficulties(?variationId:String):Array<String>
{
return difficultyIds;
if (variationId == '') variationId = null;
var diffFiltered:Array<String> = difficulties.keys().array().filter(function(diffId:String):Bool {
if (variationId == null) return true;
var difficulty:Null<SongDifficulty> = difficulties.get(diffId);
if (difficulty == null) return false;
return difficulty.variation == variationId;
});
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.
naughtyness: true,
downscroll: false,
flashingMenu: true,
flashingLights: true,
zoomCamera: true,
debugDisplay: false,
pauseOnTabOut: true,
autoPause: true,
controls:
{
@ -88,7 +88,7 @@ abstract Save(RawSaveData)
{
// No mods enabled.
enabledMods: [],
modSettings: [],
modOptions: [],
},
optionsChartEditor:
@ -98,6 +98,20 @@ abstract Save(RawSaveData)
};
}
public var options(get, never):SaveDataOptions;
function get_options():SaveDataOptions
{
return this.options;
}
public var modOptions(get, never):Map<String, Dynamic>;
function get_modOptions():Map<String, Dynamic>
{
return this.mods.modOptions;
}
/**
* The current session ID for the logged-in Newgrounds user, or null if the user is cringe.
*/
@ -407,6 +421,8 @@ typedef RawSaveData =
/**
* A semantic versioning string for the save data format.
*/
@:jcustomparse(funkin.data.DataParse.semverVersion)
@:jcustomwrite(funkin.data.DataWrite.semverVersion)
var version:Version;
var api:SaveApiData;
@ -458,7 +474,7 @@ typedef SaveHighScoresData =
typedef SaveDataMods =
{
var enabledMods:Array<String>;
var modSettings:Map<String, Dynamic>;
var modOptions:Map<String, Dynamic>;
}
/**
@ -530,10 +546,10 @@ typedef SaveDataOptions =
var downscroll:Bool;
/**
* If disabled, the main menu won't flash when entering a submenu.
* If disabled, flashing lights in the main menu and other areas will be less intense.
* @default `true`
*/
var flashingMenu:Bool;
var flashingLights:Bool;
/**
* If disabled, the camera bump synchronized to the beat.
@ -551,7 +567,7 @@ typedef SaveDataOptions =
* If enabled, the game will automatically pause when tabbing out.
* @default `true`
*/
var pauseOnTabOut:Bool;
var autoPause:Bool;
var controls:
{

View file

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

View file

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

View file

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

View file

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

View file

@ -253,6 +253,10 @@ class DebugBoundingState extends FlxState
offsetView.add(animDropDownMenu);
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);
var charDropdown:DropDown = cast uiStuff.findComponent('characterDropdown');

View file

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

View file

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

View file

@ -87,9 +87,8 @@ using Lambda;
*
* @author MasterEric
*/
@:nullSafety
// 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.ChartEditorDropdowns)
@:allow(funkin.ui.debug.charting.ChartEditorDialogHandler)
@ -555,6 +554,9 @@ class ChartEditorState extends HaxeUIState
notePreviewDirty = true;
notePreviewViewportBoundsDirty = true;
// Make sure the difficulty we selected is in the list of difficulties.
currentSongMetadata.playData.difficulties.pushUnique(selectedDifficulty);
return selectedDifficulty;
}
@ -965,13 +967,14 @@ class ChartEditorState extends HaxeUIState
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)
{
// Initialize to the default value if not set.
result = [];
trace('Initializing blank note data for difficulty ' + selectedDifficulty);
currentSongChartData.notes.set(selectedDifficulty, result);
currentSongMetadata.playData.difficulties.pushUnique(selectedDifficulty);
return result;
}
return result;
@ -980,6 +983,7 @@ class ChartEditorState extends HaxeUIState
function set_currentSongChartNoteData(value:Array<SongNoteData>):Array<SongNoteData>
{
currentSongChartData.notes.set(selectedDifficulty, value);
currentSongMetadata.playData.difficulties.pushUnique(selectedDifficulty);
return value;
}
@ -1391,16 +1395,12 @@ class ChartEditorState extends HaxeUIState
healthIconDad = new HealthIcon(currentSongMetadata.playData.characters.opponent);
healthIconDad.autoUpdate = false;
healthIconDad.size.set(0.5, 0.5);
healthIconDad.x = gridTiledSprite.x - 15 - (HealthIcon.HEALTH_ICON_SIZE * 0.5);
healthIconDad.y = gridTiledSprite.y + 5;
add(healthIconDad);
healthIconDad.zIndex = 30;
healthIconBF = new HealthIcon(currentSongMetadata.playData.characters.player);
healthIconBF.autoUpdate = false;
healthIconBF.size.set(0.5, 0.5);
healthIconBF.x = gridTiledSprite.x + gridTiledSprite.width + 15;
healthIconBF.y = gridTiledSprite.y + 5;
healthIconBF.flipX = true;
add(healthIconBF);
healthIconBF.zIndex = 30;
@ -1627,6 +1627,12 @@ class ChartEditorState extends HaxeUIState
addUIClickListener('playbarForward', _ -> playbarButtonPressed = 'playbarForward');
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.
addUIClickListener('menubarItemNewChart', _ -> ChartEditorDialogHandler.openWelcomeDialog(this, true));
@ -1897,6 +1903,16 @@ class ChartEditorState extends HaxeUIState
handleViewKeybinds();
handleTestKeybinds();
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.
*/
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.
var shouldHandleCursor:Bool = !isCursorOverHaxeUI || (selectionBoxStartPos != null);
var eventColumn:Int = (STRUMLINE_SIZE * 2 + 1) - 1;
@ -2477,25 +2500,37 @@ class ChartEditorState extends HaxeUIState
var dragLengthMs:Float = dragLengthSteps * Conductor.stepLengthMs;
var dragLengthPixels:Float = dragLengthSteps * GRID_SIZE;
if (dragLengthSteps > 0)
if (gridGhostNote != null && gridGhostNote.noteData != null && gridGhostHoldNote != null)
{
gridGhostHoldNote.visible = true;
gridGhostHoldNote.noteData = gridGhostNote.noteData;
gridGhostHoldNote.noteDirection = gridGhostNote.noteData.getDirection();
if (dragLengthSteps > 0)
{
if (dragLengthCurrent != dragLengthSteps)
{
stretchySounds = !stretchySounds;
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/stretch' + (stretchySounds ? '1' : '2') + '_UI'));
gridGhostHoldNote.setHeightDirectly(dragLengthPixels);
dragLengthCurrent = dragLengthSteps;
}
gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);
}
else
{
gridGhostHoldNote.visible = false;
gridGhostHoldNote.visible = true;
gridGhostHoldNote.noteData = gridGhostNote.noteData;
gridGhostHoldNote.noteDirection = gridGhostNote.noteData.getDirection();
gridGhostHoldNote.setHeightDirectly(dragLengthPixels);
gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);
}
else
{
gridGhostHoldNote.visible = false;
}
}
if (FlxG.mouse.justReleased)
{
if (dragLengthSteps > 0)
{
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/stretchSNAP_UI'));
// Apply the new length.
performCommand(new ExtendNoteLengthCommand(currentPlaceNoteData, dragLengthMs));
}
@ -2644,7 +2679,7 @@ class ChartEditorState extends HaxeUIState
if (cursorColumn == eventColumn)
{
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()";
@ -2704,11 +2739,11 @@ class ChartEditorState extends HaxeUIState
}
else
{
if (FlxG.mouse.overlaps(notePreview))
if (notePreview != null && FlxG.mouse.overlaps(notePreview))
{
targetCursorMode = Pointer;
}
else if (FlxG.mouse.overlaps(gridPlayheadScrollArea))
else if (gridPlayheadScrollArea != null && FlxG.mouse.overlaps(gridPlayheadScrollArea))
{
targetCursorMode = Pointer;
}
@ -3020,18 +3055,35 @@ class ChartEditorState extends HaxeUIState
{
if (healthIconsDirty)
{
if (healthIconBF != null) healthIconBF.characterId = currentSongMetadata.playData.characters.player;
if (healthIconDad != null) healthIconDad.characterId = currentSongMetadata.playData.characters.opponent;
var charDataBF = CharacterDataParser.fetchCharacterData(currentSongMetadata.playData.characters.player);
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)
{
// Base X position to the right of the grid.
var baseHealthIconXPos:Float = (gridTiledSprite == null) ? (0) : (gridTiledSprite.x + gridTiledSprite.width + 15);
// Will be 0 when not bopping. When bopping, will increase to push the icon left.
var healthIconOffset:Float = healthIconBF.width - (HealthIcon.HEALTH_ICON_SIZE * 0.5);
healthIconBF.x = baseHealthIconXPos - healthIconOffset;
healthIconBF.x = (gridTiledSprite == null) ? (0) : (gridTiledSprite.x + gridTiledSprite.width + 45 - (healthIconBF.width / 2));
healthIconBF.y = (gridTiledSprite == null) ? (0) : (MENU_BAR_HEIGHT + GRID_TOP_PAD + 30 - (healthIconBF.height / 2));
}
// 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
{
autoSave();
stopWelcomeMusic();
FlxG.switchState(new MainMenuState());
}
@ -3342,6 +3395,7 @@ class ChartEditorState extends HaxeUIState
if (!isHaxeUIDialogOpen && !isCursorOverHaxeUI && FlxG.keys.justPressed.ENTER)
{
var minimal = FlxG.keys.pressed.SHIFT;
ChartEditorToolboxHandler.hideAllToolboxes(this);
testSongInPlayState(minimal);
}
}
@ -3656,49 +3710,41 @@ class ChartEditorState extends HaxeUIState
var inputStage:Null<DropDown> = toolbox.findComponent('inputStage', DropDown);
var stageId:String = currentSongMetadata.playData.stage;
var stageData:Null<StageData> = StageDataParser.parseStageData(stageId);
if (stageData != null)
if (inputStage != null)
{
inputStage.value = {id: stageId, text: stageData.name};
}
else
{
inputStage.value = {id: "mainStage", text: "Main Stage"};
inputStage.value = (stageData != null) ?
{id: stageId, text: stageData.name} :
{id: "mainStage", text: "Main Stage"};
}
var inputCharacterPlayer:Null<DropDown> = toolbox.findComponent('inputCharacterPlayer', DropDown);
var charIdPlayer:String = currentSongMetadata.playData.characters.player;
var charDataPlayer:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charIdPlayer);
if (charDataPlayer != null)
if (inputCharacterPlayer != null)
{
inputCharacterPlayer.value = {id: charIdPlayer, text: charDataPlayer.name};
}
else
{
inputCharacterPlayer.value = {id: "bf", text: "Boyfriend"};
inputCharacterPlayer.value = (charDataPlayer != null) ?
{id: charIdPlayer, text: charDataPlayer.name} :
{id: "bf", text: "Boyfriend"};
}
var inputCharacterOpponent:Null<DropDown> = toolbox.findComponent('inputCharacterOpponent', DropDown);
var charIdOpponent:String = currentSongMetadata.playData.characters.opponent;
var charDataOpponent:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charIdOpponent);
if (charDataOpponent != null)
if (inputCharacterOpponent != null)
{
inputCharacterOpponent.value = {id: charIdOpponent, text: charDataOpponent.name};
}
else
{
inputCharacterOpponent.value = {id: "dad", text: "Dad"};
inputCharacterOpponent.value = (charDataOpponent != null) ?
{id: charIdOpponent, text: charDataOpponent.name} :
{id: "dad", text: "Dad"};
}
var inputCharacterGirlfriend:Null<DropDown> = toolbox.findComponent('inputCharacterGirlfriend', DropDown);
var charIdGirlfriend:String = currentSongMetadata.playData.characters.girlfriend;
var charDataGirlfriend:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charIdGirlfriend);
if (charDataGirlfriend != null)
if (inputCharacterGirlfriend != null)
{
inputCharacterGirlfriend.value = {id: charIdGirlfriend, text: charDataGirlfriend.name};
}
else
{
inputCharacterGirlfriend.value = {id: "none", text: "None"};
inputCharacterGirlfriend.value = (charDataGirlfriend != null) ?
{id: charIdGirlfriend, text: charDataGirlfriend.name} :
{id: "none", text: "None"};
}
}
@ -3885,9 +3931,9 @@ class ChartEditorState extends HaxeUIState
switch (noteData.getStrumlineIndex())
{
case 0: // Player
if (hitsoundsEnabledPlayer) ChartEditorAudioHandler.playSound(Paths.sound('ui/chart-editor/playerHitsound'));
if (hitsoundsEnabledPlayer) ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/hitNotePlayer'));
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;
// Move the grid sprite to the correct position.
if (gridTiledSprite != null)
if (gridTiledSprite != null && gridPlayheadScrollArea != null)
{
if (isViewDownscroll)
{
@ -4064,7 +4110,7 @@ class ChartEditorState extends HaxeUIState
}
subStateClosed.add(fixCamera);
subStateClosed.add(updateConductor);
subStateClosed.add(resetConductorAfterTest);
FlxTransitionableState.skipNextTransIn = false;
FlxTransitionableState.skipNextTransOut = false;
@ -4097,10 +4143,9 @@ class ChartEditorState extends HaxeUIState
add(this.component);
}
function updateConductor(_:FlxSubState = null):Void
function resetConductorAfterTest(_:FlxSubState = null):Void
{
var targetPos = scrollPositionInMs;
Conductor.update(targetPos);
moveSongToScrollPosition();
}
public function postLoadInstrumental():Void
@ -4153,12 +4198,13 @@ class ChartEditorState extends HaxeUIState
*/
function moveSongToScrollPosition():Void
{
// Update the songPosition in the Conductor.
var targetPos = scrollPositionInMs;
Conductor.update(targetPos);
// 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;
// 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
{
ChartEditorAudioHandler.playSound(Paths.sound('pianoStuff/piano-${high ? '001' : '008'}'));
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/metronome${high ? '1' : '2'}'));
}
function isNoteSelected(note:Null<SongNoteData>):Bool

View file

@ -72,6 +72,8 @@ class ChartEditorToolboxHandler
{
toolbox.showDialog(false);
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/openWindow'));
switch (id)
{
case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT:
@ -109,6 +111,8 @@ class ChartEditorToolboxHandler
{
toolbox.hideDialog(DialogButton.CANCEL);
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/exitWindow'));
switch (id)
{
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
{
var toolbox:Null<CollapsibleDialog> = state.activeToolboxes.get(id);
@ -634,9 +650,9 @@ class ChartEditorToolboxHandler
timeChanges[0].bpm = event.value;
}
Conductor.forceBPM(event.value);
state.currentSongMetadata.timeChanges = timeChanges;
Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges);
};
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;
import funkin.util.SortUtil;
import flixel.FlxSprite;
import flixel.util.FlxColor;
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
for (songIndex in 1...songList.length)
{

View file

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

View file

@ -4,6 +4,9 @@ import flixel.util.FlxColor;
import lime.app.Application;
import funkin.data.song.SongData.SongTimeFormat;
/**
* A store of unchanging, globally relevant values.
*/
class Constants
{
/**
@ -118,11 +121,21 @@ class Constants
*/
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.
*/
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.
*/

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;
}
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.
* @param input The array to search
@ -38,6 +45,34 @@ class ArrayTools
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.
* @param array The array to clear.