1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-03-20 17:09:21 +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"]
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"
}
]
}

View file

@ -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':

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

@ -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;

View file

@ -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);
}
}

View file

@ -154,6 +154,12 @@ 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.

View file

@ -421,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;

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) {

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));
@ -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<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"};
}
}
@ -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<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:

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';
/**
* Default player character for health icons.
*/
public static final DEFAULT_HEALTH_ICON:String = 'face';
/**
* Default stage for charts.
*/

View file

@ -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<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.