1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-03-21 17:39:20 +00:00

Merge branch 'rewrite/master' into rewrite/feature/remember-difficulty

This commit is contained in:
Cameron Taylor 2023-10-18 23:59:21 -04:00
commit c4e2f61e2d
20 changed files with 329 additions and 109 deletions

1
.gitmodules vendored
View file

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

6
.vscode/launch.json vendored
View file

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

View file

@ -240,7 +240,7 @@ class PauseSubState extends MusicBeatSubState
case 'Exit to Chart Editor': case 'Exit to Chart Editor':
this.close(); this.close();
if (FlxG.sound.music != null) FlxG.sound.music.stop(); if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position!
PlayState.instance.close(); // This only works because PlayState is a substate! PlayState.instance.close(); // This only works because PlayState is a substate!
case 'BACK': case 'BACK':

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -698,7 +698,15 @@ class PlayState extends MusicBeatSubState
FlxG.sound.music.pause(); FlxG.sound.music.pause();
FlxG.sound.music.time = (startTimestamp); FlxG.sound.music.time = (startTimestamp);
vocals = currentChart.buildVocals(); if (!overrideMusic)
{
vocals = currentChart.buildVocals();
if (vocals.members.length == 0)
{
trace('WARNING: No vocals found for this song.');
}
}
vocals.pause(); vocals.pause();
vocals.time = 0; vocals.time = 0;

View file

@ -317,12 +317,8 @@ class BaseCharacter extends Bopper
trace('[WARN] Player 1 health icon not found!'); trace('[WARN] Player 1 health icon not found!');
return; return;
} }
PlayState.instance.iconP1.isPixel = _data.healthIcon?.isPixel ?? false; PlayState.instance.iconP1.configure(_data.healthIcon);
PlayState.instance.iconP1.characterId = _data.healthIcon.id; PlayState.instance.iconP1.flipX = !PlayState.instance.iconP1.flipX; // BF is looking the other way.
PlayState.instance.iconP1.size.set(_data.healthIcon.scale, _data.healthIcon.scale);
PlayState.instance.iconP1.offset.x = _data.healthIcon.offsets[0];
PlayState.instance.iconP1.offset.y = _data.healthIcon.offsets[1];
PlayState.instance.iconP1.flipX = !_data.healthIcon.flipX;
} }
else else
{ {
@ -331,12 +327,7 @@ class BaseCharacter extends Bopper
trace('[WARN] Player 2 health icon not found!'); trace('[WARN] Player 2 health icon not found!');
return; return;
} }
PlayState.instance.iconP2.isPixel = _data.healthIcon?.isPixel ?? false; PlayState.instance.iconP2.configure(_data.healthIcon);
PlayState.instance.iconP2.characterId = _data.healthIcon.id;
PlayState.instance.iconP2.size.set(_data.healthIcon.scale, _data.healthIcon.scale);
PlayState.instance.iconP2.offset.x = _data.healthIcon.offsets[0];
PlayState.instance.iconP2.offset.y = _data.healthIcon.offsets[1];
PlayState.instance.iconP2.flipX = _data.healthIcon.flipX;
} }
} }

View file

@ -154,6 +154,12 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
{ {
if (metadata == null || metadata.playData == null) continue; if (metadata == null || metadata.playData == null) continue;
// If there are no difficulties in the metadata, there's a problem.
if (metadata.playData.difficulties.length == 0)
{
throw 'Song $id has no difficulties listed in metadata!';
}
// There may be more difficulties in the chart file than in the metadata, // There may be more difficulties in the chart file than in the metadata,
// (i.e. non-playable charts like the one used for Pico on the speaker in Stress) // (i.e. non-playable charts like the one used for Pico on the speaker in Stress)
// but all the difficulties in the metadata must be in the chart file. // but all the difficulties in the metadata must be in the chart file.

View file

@ -421,6 +421,8 @@ typedef RawSaveData =
/** /**
* A semantic versioning string for the save data format. * A semantic versioning string for the save data format.
*/ */
@:jcustomparse(funkin.data.DataParse.semverVersion)
@:jcustomwrite(funkin.data.DataWrite.semverVersion)
var version:Version; var version:Version;
var api:SaveApiData; var api:SaveApiData;

View file

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

View file

@ -404,7 +404,6 @@ class ChartEditorDialogHandler
{ {
if (ChartEditorAudioHandler.loadInstFromBytes(state, selectedFile.bytes, instId)) if (ChartEditorAudioHandler.loadInstFromBytes(state, selectedFile.bytes, instId))
{ {
trace('Selected file: ' + selectedFile.fullPath);
#if !mac #if !mac
NotificationManager.instance.addNotification( NotificationManager.instance.addNotification(
{ {
@ -415,13 +414,12 @@ class ChartEditorDialogHandler
}); });
#end #end
state.switchToCurrentInstrumental();
dialog.hideDialog(DialogButton.APPLY); dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile); removeDropHandler(onDropFile);
} }
else else
{ {
trace('Failed to load instrumental (${selectedFile.fullPath})');
#if !mac #if !mac
NotificationManager.instance.addNotification( NotificationManager.instance.addNotification(
{ {
@ -452,6 +450,7 @@ class ChartEditorDialogHandler
}); });
#end #end
state.switchToCurrentInstrumental();
dialog.hideDialog(DialogButton.APPLY); dialog.hideDialog(DialogButton.APPLY);
removeDropHandler(onDropFile); removeDropHandler(onDropFile);
} }
@ -570,6 +569,12 @@ class ChartEditorDialogHandler
var newSongMetadata:SongMetadata = new SongMetadata('', '', 'default'); var newSongMetadata:SongMetadata = new SongMetadata('', '', 'default');
newSongMetadata.playData.difficulties = switch (targetVariation)
{
case 'erect': ['erect', 'nightmare'];
default: ['easy', 'normal', 'hard'];
};
var inputSongName:Null<TextField> = dialog.findComponent('inputSongName', TextField); var inputSongName:Null<TextField> = dialog.findComponent('inputSongName', TextField);
if (inputSongName == null) throw 'Could not locate inputSongName TextField in Song Metadata dialog'; if (inputSongName == null) throw 'Could not locate inputSongName TextField in Song Metadata dialog';
inputSongName.onChange = function(event:UIEvent) { inputSongName.onChange = function(event:UIEvent) {

View file

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

View file

@ -72,6 +72,8 @@ class ChartEditorToolboxHandler
{ {
toolbox.showDialog(false); toolbox.showDialog(false);
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/openWindow'));
switch (id) switch (id)
{ {
case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT: case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT:
@ -109,6 +111,8 @@ class ChartEditorToolboxHandler
{ {
toolbox.hideDialog(DialogButton.CANCEL); toolbox.hideDialog(DialogButton.CANCEL);
ChartEditorAudioHandler.playSound(Paths.sound('chartingSounds/exitWindow'));
switch (id) switch (id)
{ {
case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT: case ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT:

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

@ -131,6 +131,11 @@ class Constants
*/ */
public static final DEFAULT_CHARACTER:String = 'bf'; public static final DEFAULT_CHARACTER:String = 'bf';
/**
* Default player character for health icons.
*/
public static final DEFAULT_HEALTH_ICON:String = 'face';
/** /**
* Default stage for charts. * Default stage for charts.
*/ */

View file

@ -60,6 +60,19 @@ class ArrayTools
return -1; return -1;
} }
/*
* Push an element to the array if it is not already present.
* @param input The array to push to
* @param element The element to push
* @return Whether the element was pushed
*/
public static function pushUnique<T>(input:Array<T>, element:T):Bool
{
if (input.contains(element)) return false;
input.push(element);
return true;
}
/** /**
* Remove all elements from the array, without creating a new array. * Remove all elements from the array, without creating a new array.
* @param array The array to clear. * @param array The array to clear.