diff --git a/.gitmodules b/.gitmodules index e0839baaf..8968471e3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/.vscode/launch.json b/.vscode/launch.json index d16f1ca4f..74f72b826 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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" } ] } diff --git a/source/funkin/PauseSubState.hx b/source/funkin/PauseSubState.hx index a074410ea..2ce9abf65 100644 --- a/source/funkin/PauseSubState.hx +++ b/source/funkin/PauseSubState.hx @@ -240,7 +240,7 @@ 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': diff --git a/source/funkin/data/DataParse.hx b/source/funkin/data/DataParse.hx index 4a422b368..64a53d2a4 100644 --- a/source/funkin/data/DataParse.hx +++ b/source/funkin/data/DataParse.hx @@ -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 diff --git a/source/funkin/data/DataWrite.hx b/source/funkin/data/DataWrite.hx index 41993107f..2f3a7632f 100644 --- a/source/funkin/data/DataWrite.hx +++ b/source/funkin/data/DataWrite.hx @@ -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(); + } } diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 9340e46c9..88993e519 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -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; diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx index ee3dfe98c..3ff3943c6 100644 --- a/source/funkin/data/song/SongDataUtils.hx +++ b/source/funkin/data/song/SongDataUtils.hx @@ -246,7 +246,8 @@ class SongDataUtils typedef SongClipboardItems = { - ?valid:Bool, - notes:Array, - events:Array + @:optional + var valid:Bool; + var notes:Array; + var events:Array; } diff --git a/source/funkin/data/song/migrator/SongData_v2_0_0.hx b/source/funkin/data/song/migrator/SongData_v2_0_0.hx index 935e7349c..eeeed2f2b 100644 --- a/source/funkin/data/song/migrator/SongData_v2_0_0.hx +++ b/source/funkin/data/song/migrator/SongData_v2_0_0.hx @@ -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") diff --git a/source/funkin/play/HealthIcon.hx b/source/funkin/play/HealthIcon.hx index 7785fb4b1..958933df8 100644 --- a/source/funkin/play/HealthIcon.hx +++ b/source/funkin/play/HealthIcon.hx @@ -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; /** * 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):Null { 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):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 { + 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):Void { - if (correctCharacterId(charId) != charId) + if (charId == null || correctCharacterId(charId) != charId) { + // This will recursively trigger loadCharacter to be called again. characterId = correctCharacterId(charId); return; } diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx index 048b6ed6e..1d3480efe 100644 --- a/source/funkin/play/PlayState.hx +++ b/source/funkin/play/PlayState.hx @@ -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; diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx index 5346ced61..588b5663d 100644 --- a/source/funkin/play/character/BaseCharacter.hx +++ b/source/funkin/play/character/BaseCharacter.hx @@ -317,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 { @@ -331,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); } } diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx index f996d75ef..60b8b9864 100644 --- a/source/funkin/play/song/Song.hx +++ b/source/funkin/play/song/Song.hx @@ -154,6 +154,12 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry = dialog.findComponent('inputSongName', TextField); if (inputSongName == null) throw 'Could not locate inputSongName TextField in Song Metadata dialog'; inputSongName.onChange = function(event:UIEvent) { diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index c28c1c8c7..05173726f 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -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 { - var result:Array = currentSongChartData.notes.get(selectedDifficulty); + var result:Null> = 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):Array { 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)); @@ -2138,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; @@ -2487,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)); } @@ -2654,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()"; @@ -2714,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; } @@ -3030,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)); } } @@ -3668,49 +3710,41 @@ class ChartEditorState extends HaxeUIState var inputStage:Null = toolbox.findComponent('inputStage', DropDown); var stageId:String = currentSongMetadata.playData.stage; var stageData:Null = 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 = toolbox.findComponent('inputCharacterPlayer', DropDown); var charIdPlayer:String = currentSongMetadata.playData.characters.player; var charDataPlayer:Null = 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 = toolbox.findComponent('inputCharacterOpponent', DropDown); var charIdOpponent:String = currentSongMetadata.playData.characters.opponent; var charDataOpponent:Null = 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 = toolbox.findComponent('inputCharacterGirlfriend', DropDown); var charIdGirlfriend:String = currentSongMetadata.playData.characters.girlfriend; var charDataGirlfriend:Null = 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"}; } } @@ -3897,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')); } } } @@ -4016,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) { @@ -4076,7 +4110,7 @@ class ChartEditorState extends HaxeUIState } subStateClosed.add(fixCamera); - subStateClosed.add(updateConductor); + subStateClosed.add(resetConductorAfterTest); FlxTransitionableState.skipNextTransIn = false; FlxTransitionableState.skipNextTransOut = false; @@ -4109,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 @@ -4166,12 +4199,14 @@ class ChartEditorState extends HaxeUIState function moveSongToScrollPosition():Void { // 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; - // Update the songPosition in the Conductor. - Conductor.update(audioInstTrack.time); - // We need to update the note sprites because we changed the scroll position. noteDisplayDirty = true; } @@ -4275,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):Bool diff --git a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx index d92e43cf2..7cee1edde 100644 --- a/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx +++ b/source/funkin/ui/debug/charting/ChartEditorToolboxHandler.hx @@ -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: diff --git a/source/funkin/ui/haxeui/components/FunkinClickLabel.hx b/source/funkin/ui/haxeui/components/FunkinClickLabel.hx new file mode 100644 index 000000000..77c9dbc0f --- /dev/null +++ b/source/funkin/ui/haxeui/components/FunkinClickLabel.hx @@ -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; + } +} diff --git a/source/funkin/util/Constants.hx b/source/funkin/util/Constants.hx index b27a7d2f5..edd95f946 100644 --- a/source/funkin/util/Constants.hx +++ b/source/funkin/util/Constants.hx @@ -131,6 +131,11 @@ class Constants */ 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. */ diff --git a/source/funkin/util/tools/ArrayTools.hx b/source/funkin/util/tools/ArrayTools.hx index ad24a23a7..077b1b8f8 100644 --- a/source/funkin/util/tools/ArrayTools.hx +++ b/source/funkin/util/tools/ArrayTools.hx @@ -60,6 +60,19 @@ class ArrayTools 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(input:Array, 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.