Chart Editor Character Previews

Co-Authored-By: ComedyLost <106041578+comedylost@users.noreply.github.com>
This commit is contained in:
PurSnake
2025-12-21 03:25:52 -05:00
committed by Hundrec
co-authored by ComedyLost
parent 592bf1a922
commit 9f59231d58
6 changed files with 178 additions and 76 deletions
+1 -1
Submodule assets updated: 7abfacf757...097c3691e8
@@ -3382,8 +3382,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
menubarItemToggleToolboxEventData.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT, event.value);
menubarItemToggleToolboxFreeplay.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_FREEPLAY_LAYOUT, event.value);
menubarItemToggleToolboxPlaytestProperties.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT, event.value);
menubarItemToggleToolboxPlayerPreview.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT, event.value);
menubarItemToggleToolboxOpponentPreview.onChange = event -> this.setToolboxState(CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT, event.value);
menubarItemToggleToolboxPlayerPreview.onChange = event -> {
this.setToolboxState(CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT, event.value);
playerPreviewDirty = event.value;
}
menubarItemToggleToolboxOpponentPreview.onChange = event -> {
this.setToolboxState(CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT, event.value);
opponentPreviewDirty = event.value;
}
// TODO: Pass specific HaxeUI components to add context menus to them.
// registerContextMenu(null, Paths.ui('chart-editor/context/test'));
@@ -3708,7 +3714,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var oldStepTime:Float = Conductor.instance.currentStepTime;
var oldSongPosition:Float = Conductor.instance.songPosition + Conductor.instance.instrumentalOffset;
updateSongTime();
handleHitsounds(oldSongPosition, Conductor.instance.songPosition + Conductor.instance.instrumentalOffset);
handleMusicPositionUpdate(oldSongPosition, Conductor.instance.songPosition + Conductor.instance.instrumentalOffset);
// Resync vocals.
if (Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100)
{
@@ -3726,7 +3732,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Else, move the entire view.
var oldSongPosition:Float = Conductor.instance.songPosition + Conductor.instance.instrumentalOffset;
updateSongTime();
handleHitsounds(oldSongPosition, Conductor.instance.songPosition + Conductor.instance.instrumentalOffset);
handleMusicPositionUpdate(oldSongPosition, Conductor.instance.songPosition + Conductor.instance.instrumentalOffset);
// Resync vocals.
if (Math.abs(audioInstTrack.time - audioVocalTrackGroup.time) > 100)
{
@@ -3943,7 +3949,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// If a new event is needed, call buildEventSprite.
var eventSprite:ChartEditorEventSprite = renderedEvents.recycle(() -> new ChartEditorEventSprite(this), false, true);
eventSprite.parentState = this;
trace('Creating new Event... (${renderedEvents.members.length})');
// trace('Creating new Event... (${renderedEvents.members.length})');
if (eventData?.value != null && (eventData?.value?.ease != null && eventData?.value?.easeDir == null))
{
@@ -5430,8 +5436,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function handleToolboxes():Void
{
handleDifficultyToolbox();
// handlePlayerPreviewToolbox();
// handleOpponentPreviewToolbox();
handlePlayerPreviewToolbox();
handleOpponentPreviewToolbox();
}
function handleDifficultyToolbox():Void
@@ -5454,15 +5460,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function handlePlayerPreviewToolbox():Void
{
// Manage the Select Difficulty tree view.
var charPreviewToolbox:Null<CollapsibleDialog> = this.getToolbox_OLD(CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);
var charPreviewToolbox:Null<CollapsibleDialog> = this.getToolboxUnCast(CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);
if (charPreviewToolbox == null) return;
// TODO: Re-enable the player preview once we figure out the performance issues.
var charPlayer:Null<CharacterPlayer> = null; // charPreviewToolbox.findComponent('charPlayer');
var charPlayer:Null<CharacterPlayer> = charPreviewToolbox.findComponent('charPlayer');
if (charPlayer == null) return;
currentPlayerCharacterPlayer = charPlayer;
if (playerPreviewDirty)
{
playerPreviewDirty = false;
@@ -5480,28 +5482,28 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
charPlayer.targetScale = 0.5;
charPreviewToolbox.title = 'Player Preview - ${charPlayer.charName}';
}
if (charPreviewToolbox != null && !charPreviewToolbox.minimized)
{
charPreviewToolbox.width = charPlayer.width + 32;
charPreviewToolbox.height = charPlayer.height + 64;
charPreviewToolbox.invalidateComponentLayout();
}
}
if (charPreviewToolbox != null && !charPreviewToolbox.minimized)
{
charPreviewToolbox.width = charPlayer.width + 32;
charPreviewToolbox.height = charPlayer.height + 64;
}
currentPlayerCharacterPlayer = charPlayer;
}
function handleOpponentPreviewToolbox():Void
{
// Manage the Select Difficulty tree view.
var charPreviewToolbox:Null<CollapsibleDialog> = this.getToolbox_OLD(CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT);
var charPreviewToolbox:Null<CollapsibleDialog> = this.getToolboxUnCast(CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT);
if (charPreviewToolbox == null) return;
// TODO: Re-enable the player preview once we figure out the performance issues.
var charPlayer:Null<CharacterPlayer> = null; // charPreviewToolbox.findComponent('charPlayer');
var charPlayer:Null<CharacterPlayer> = charPreviewToolbox.findComponent('charOpponent');
if (charPlayer == null) return;
currentOpponentCharacterPlayer = charPlayer;
if (opponentPreviewDirty)
{
opponentPreviewDirty = false;
@@ -5519,14 +5521,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
charPlayer.targetScale = 0.5;
charPreviewToolbox.title = 'Opponent Preview - ${charPlayer.charName}';
}
if (charPreviewToolbox != null && !charPreviewToolbox.minimized)
{
charPreviewToolbox.width = charPlayer.width + 32;
charPreviewToolbox.height = charPlayer.height + 64;
charPreviewToolbox.invalidateComponentLayout();
}
}
if (charPreviewToolbox != null && !charPreviewToolbox.minimized)
{
charPreviewToolbox.width = charPlayer.width + 32;
charPreviewToolbox.height = charPlayer.height + 64;
}
currentOpponentCharacterPlayer = charPlayer;
}
function handleSelectionButtons():Void
@@ -5783,32 +5787,40 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
/**
* Handle aligning the health icons next to the grid.
*/
var _charIconData = null;
@:access(funkin.play.character.BaseCharacter)
function handleHealthIcons():Void
{
if (healthIconsDirty)
{
var charDataBF = CharacterDataParser.fetchCharacterData(currentSongMetadata.playData.characters.player);
var charDataDad = CharacterDataParser.fetchCharacterData(currentSongMetadata.playData.characters.opponent);
_charIconData = currentPlayerCharacterPlayer?.character?._data ?? CharacterDataParser.fetchCharacterData(currentSongMetadata.playData.characters.player);
if (healthIconBF != null)
{
healthIconBF.configure(charDataBF?.healthIcon);
healthIconBF.configure(_charIconData?.healthIcon);
healthIconBF.size *= 0.5; // Make the icon smaller in Chart Editor.
healthIconBF.flipX = !healthIconBF.flipX; // BF faces the other way.
}
if (buttonSelectPlayer != null)
{
buttonSelectPlayer.text = charDataBF?.name ?? 'Player';
buttonSelectPlayer.text = _charIconData?.name ?? 'Player';
}
_charIconData = currentOpponentCharacterPlayer?.character?._data ?? CharacterDataParser.fetchCharacterData(currentSongMetadata.playData.characters.opponent);
if (healthIconDad != null)
{
healthIconDad.configure(charDataDad?.healthIcon);
healthIconDad.configure(_charIconData?.healthIcon);
healthIconDad.size *= 0.5; // Make the icon smaller in Chart Editor.
}
if (buttonSelectOpponent != null)
{
buttonSelectOpponent.text = charDataDad?.name ?? 'Opponent';
buttonSelectOpponent.text = _charIconData?.name ?? 'Opponent';
}
healthIconsDirty = false;
_charIconData = null;
}
// Right align, and visibly center, the BF health icon.
@@ -6897,9 +6909,20 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
/**
* Handle the playback of hitsounds.
*/
function handleHitsounds(oldSongPosition:Float, newSongPosition:Float):Void
var _scriptNoteObj:NoteSprite = null;
var _noteScriptEvent:NoteScriptEvent = null;
var _currentEvents = null;
var _allowedEvents = null;
var _eventTarget:Null<CharacterPlayer> = null;
public static var _allowedEventsNames:Array<String> = ['PlayAnimation'];
function handleMusicPositionUpdate(oldSongPosition:Float, newSongPosition:Float):Void
{
if (!hitsoundsEnabled) return;
_currentEvents = SongDataUtils.getEventsInTimeRange(currentSongChartEventData, oldSongPosition, newSongPosition);
_allowedEvents = SongDataUtils.getEventsWithKind(_currentEvents, _allowedEventsNames);
// Assume notes are sorted by time.
for (noteData in currentSongChartNoteData)
@@ -6909,25 +6932,34 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (noteData.time < oldSongPosition) // Note is in the past.
continue;
if (noteData.time > newSongPosition) // Note is in the future.
return; // Assume all notes are also in the future.
if (noteData.time > newSongPosition) break;
// Note was just hit.
/**
* We hit a note.
* We're gonna create scripted event and dispatch it al over ChartEditor.
*/
_scriptNoteObj = new NoteSprite(NoteStyleRegistry.instance.fetchDefault());
_scriptNoteObj.noteData = noteData;
_scriptNoteObj.kill();
_scriptNoteObj.direction = _scriptNoteObj.noteData?.getDirection() ?? 0;
_scriptNoteObj.scrollFactor.set();
// Character preview.
// NoteScriptEvent takes a sprite, ehe. Need to rework that.
var tempNote:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault());
tempNote.noteData = noteData;
tempNote.scrollFactor.set(0, 0);
var event:NoteScriptEvent = new HitNoteScriptEvent(tempNote, 0.0, 0, 'perfect', false, 0);
dispatchEvent(event);
_noteScriptEvent = new HitNoteScriptEvent(_scriptNoteObj, 0.0, 0, (noteData.getStrumlineIndex() == 0 ? 'perfect' : 'sick'), false, 0);
dispatchEvent(_noteScriptEvent);
// Calling event.cancelEvent() skips all the other logic! Neat!
if (event.eventCanceled) continue;
if (_noteScriptEvent.eventCanceled)
{
_scriptNoteObj.destroy();
_scriptNoteObj = null;
_noteScriptEvent = null;
continue;
}
// Hitsounds.
switch (noteData.getStrumlineIndex())
if (hitsoundsEnabled) switch (noteData.getStrumlineIndex())
{
case 0: // Player
if (hitsoundVolumePlayer > 0) this.playSound(Paths.sound('chartingSounds/hitNotePlayer'), hitsoundVolumePlayer);
@@ -6935,6 +6967,31 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
if (hitsoundVolumeOpponent > 0) this.playSound(Paths.sound('chartingSounds/hitNoteOpponent'), hitsoundVolumeOpponent);
}
}
// Clearing memory before next event call.
_scriptNoteObj?.destroy();
_scriptNoteObj = null;
_noteScriptEvent = null;
for (data in _allowedEvents)
{
switch (data.eventKind)
{
case "PlayAnimation":
switch (data.getString('target').toLowerCase().trim())
{
case 'boyfriend' | 'bf' | 'player':
_eventTarget = currentPlayerCharacterPlayer;
case 'dad' | 'opponent' | 'enemy':
_eventTarget = currentOpponentCharacterPlayer;
default:
}
if (_eventTarget != null) _eventTarget.playAnimManually(data.getString('anim') ?? 'idle', data.getBool('force') ?? false);
}
}
_currentEvents = null;
_allowedEvents.resize(0);
_eventTarget = null;
}
function stopAudioPlayback(themeMusic:Bool = true):Void
@@ -7005,6 +7062,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Many things get reset when song length changes.
healthIconsDirty = true;
playerPreviewDirty = true;
opponentPreviewDirty = true;
}
public function loadSubtitles():Void
@@ -116,14 +116,19 @@ class ChartEditorCharacterIconSelectorMenu extends ChartEditorBaseMenu
charButton.onClick = _ -> {
switch (charType)
{
case BF: chartEditorState.currentSongMetadata.playData.characters.player = charId;
case BF:
chartEditorState.currentSongMetadata.playData.characters.player = charId;
chartEditorState.playerPreviewDirty = true;
case GF: chartEditorState.currentSongMetadata.playData.characters.girlfriend = charId;
case DAD: chartEditorState.currentSongMetadata.playData.characters.opponent = charId;
case DAD:
chartEditorState.currentSongMetadata.playData.characters.opponent = charId;
chartEditorState.opponentPreviewDirty = true;
default: throw 'Invalid charType: ' + charType;
};
defaultText = (charId != "") ? '${charData.name} [${charId}]' : 'None';
chartEditorState.healthIconsDirty = true;
chartEditorState.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
};
@@ -201,14 +201,14 @@ class ChartEditorToolboxHandler
* @param id The asset ID of the toolbox layout.
* @return The toolbox.
*/
public static function getToolbox_OLD(state:ChartEditorState, id:String):Null<CollapsibleDialog>
public static function getToolboxUnCast(state:ChartEditorState, id:String):Null<CollapsibleDialog>
{
var toolbox:Null<CollapsibleDialog> = state.activeToolboxes.get(id);
// Initialize the toolbox without showing it.
if (toolbox == null) toolbox = initToolbox(state, id);
if (toolbox == null) throw 'ChartEditorToolboxHandler.getToolbox_OLD() - Could not retrieve or build toolbox: $id';
if (toolbox == null) throw 'ChartEditorToolboxHandler.getToolboxUnCast() - Could not retrieve or build toolbox: $id';
return toolbox;
}
@@ -364,8 +364,8 @@ class ChartEditorToolboxHandler
if (toolbox == null) return null;
// Starting position.
toolbox.x = 200;
toolbox.y = 350;
toolbox.x = 700;
toolbox.y = 150;
toolbox.onDialogClosed = function(event:DialogEvent) {
state.menubarItemToggleToolboxPlayerPreview.selected = false;
@@ -394,14 +394,14 @@ class ChartEditorToolboxHandler
// Starting position.
toolbox.x = 200;
toolbox.y = 350;
toolbox.y = 150;
toolbox.onDialogClosed = (event:DialogEvent) -> {
state.menubarItemToggleToolboxOpponentPreview.selected = false;
}
var charPlayer:Null<CharacterPlayer> = toolbox.findComponent('charPlayer');
if (charPlayer == null) throw 'ChartEditorToolboxHandler.buildToolboxOpponentPreviewLayout() - Could not find charPlayer component.';
var charPlayer:Null<CharacterPlayer> = toolbox.findComponent('charOpponent');
if (charPlayer == null) throw 'ChartEditorToolboxHandler.buildToolboxOpponentPreviewLayout() - Could not find charOpponent component.';
// TODO: We need to implement character swapping in ChartEditorState.
charPlayer.loadCharacter('dad');
charPlayer.characterType = CharacterType.DAD;
@@ -151,6 +151,9 @@ class ChartEditorDifficultyToolbox extends ChartEditorBaseToolbox
{
chartEditorState.currentSongMetadata = songMetadata;
chartEditorState.healthIconsDirty = true;
chartEditorState.playerPreviewDirty = true;
chartEditorState.opponentPreviewDirty = true;
chartEditorState.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
chartEditorState.success('Replaced Metadata', 'Replaced metadata with file (${fileReference.name})');
}
@@ -31,7 +31,7 @@ typedef AnimationInfo =
@:composite(Layout)
class CharacterPlayer extends Box
{
var character:Null<BaseCharacter>;
public var character:Null<BaseCharacter>;
public function new(defaultToBf:Bool = true)
{
@@ -74,6 +74,7 @@ class CharacterPlayer extends Box
* Loads a character by ID.
* @param id The ID of the character to load.
*/
@:access(funkin.play.character.BaseCharacter)
public function loadCharacter(id:String):Void
{
if (id == null) return;
@@ -101,12 +102,14 @@ class CharacterPlayer extends Box
if (flip) character.flipX = !character.flipX;
if (targetScale != 1.0) character.setScale(targetScale);
character.animation.onFrameChange.add(function(name:String = '', frameNumber:Int = -1, frameIndex:Int = -1) {
dispatch(new AnimationEvent(AnimationEvent.FRAME));
});
character.animation.onFinish.add(function(name:String = '') {
dispatch(new AnimationEvent(AnimationEvent.END));
});
if (character._data.isPixel)
{
character.scale.x *= Constants.PIXEL_ART_SCALE;
character.scale.y *= Constants.PIXEL_ART_SCALE;
}
character.animation.onFrameChange.add(onFrame);
character.animation.onFinish.add(onFinish);
add(character);
invalidateComponentLayout();
@@ -170,17 +173,25 @@ class CharacterPlayer extends Box
dispatch(new AnimationEvent(AnimationEvent.END));
}
public function playAnimManually(name:String, restart:Bool = false, ignoreOther:Bool = false, reversed:Bool = false):Void
{
if (character != null) character.playAnimation(name, restart, ignoreOther, reversed);
}
public static final animateCharactersYOffset:Float = 100.0;
override function repositionChildren():Void
{
super.repositionChildren();
character.x = this.cachedScreenX;
character.y = this.cachedScreenY;
// Apply animation offsets, so the character is positioned correctly based on the animation.
@:privateAccess var animOffsets:Array<Float> = character.animOffsets;
character.x -= animOffsets[0] * targetScale * (flip ? -1 : 1);
character.y -= animOffsets[1] * targetScale;
if (character.isAnimate)
{
final charSceenBounds = character.getScreenBounds();
character.x += charSceenBounds.width / 2;
character.y += charSceenBounds.height / 2 + animateCharactersYOffset;
}
}
/**
@@ -225,7 +236,14 @@ class CharacterPlayer extends Box
*/
public function onNoteHit(event:HitNoteScriptEvent):Void
{
if (character != null) character.onNoteHit(event);
if (character != null)
{
character.onNoteHit(event);
if ((event.note.noteData.getMustHitNote() && characterType == BF)
|| (!event.note.noteData.getMustHitNote() && characterType == DAD)) character.holdTimer = -event.note.noteData?.length / 1000;
// At least i tried yaknow?
}
}
/**
@@ -260,6 +278,7 @@ class CharacterPlayer extends Box
}
@:access(funkin.ui.haxeui.components.CharacterPlayer)
@:access(funkin.play.character.BaseCharacter)
private class Layout extends DefaultLayout
{
public override function resizeChildren():Void
@@ -273,8 +292,10 @@ private class Layout extends DefaultLayout
return super.resizeChildren();
}
character.cornerPosition.set(0, 0);
// character.cornerPosition.set(0, 0);
// character.setGraphicSize(Std.int(innerWidth), Std.int(innerHeight));
// if (character._data.isPixel) character.setGraphicSize(Std.int(innerWidth * Constants.PIXEL_ART_SCALE), Std.int(innerHeight * Constants.PIXEL_ART_SCALE));
}
public override function calcAutoSize(exclusions:Array<Component> = null):Size
@@ -286,8 +307,22 @@ private class Layout extends DefaultLayout
return super.calcAutoSize(exclusions);
}
var size:Size = new Size();
size.width = character.width + paddingLeft + paddingRight;
size.height = character.height + paddingTop + paddingBottom;
// size.width = (character.width * (character._data.isPixel ? Constants.PIXEL_ART_SCALE : 1)) + paddingLeft + paddingRight;
// size.height = (character.height * (character._data.isPixel ? Constants.PIXEL_ART_SCALE : 1)) + paddingTop + paddingBottom;
final charSceenBounds = character.getScreenBounds();
if (character.isAnimate)
{
size.width = charSceenBounds.width;
size.height = charSceenBounds.height;
}
else
{
size.width = charSceenBounds.width + paddingLeft + paddingRight;
size.height = charSceenBounds.height + paddingTop + paddingBottom;
}
return size;
}
}