1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2024-12-26 23:16:46 +00:00

Merge pull request #120 from FunkinCrew/feature/chart-editor-bpm

Chart Editor: BPM Changes
This commit is contained in:
Cameron Taylor 2023-08-01 15:55:32 -04:00 committed by GitHub
commit 13c5297ccc
22 changed files with 768 additions and 382 deletions

View file

@ -42,7 +42,7 @@
"name": "haxeui-core",
"type": "git",
"dir": null,
"ref": "1e1151094f2ca0987025f1a9d33401e44dce3f28",
"ref": "3590c94858fc6dbcf9b4d522cd644ad571269677",
"url": "https://github.com/haxeui/haxeui-core/"
},
{
@ -116,4 +116,4 @@
"version": "0.11.0"
}
]
}
}

View file

@ -98,5 +98,6 @@ class Main extends Sprite
// - It scans the class path and registers any HaxeUI components.
Toolkit.init();
Toolkit.theme = 'dark'; // don't be cringe
Toolkit.autoScale = false;
}
}

View file

@ -75,17 +75,18 @@ class Conductor
}
/**
* Duration of a beat in milliseconds. Calculated based on bpm.
* Duration of a beat (quarter note) in milliseconds. Calculated based on bpm.
*/
public static var beatLengthMs(get, null):Float;
static function get_beatLengthMs():Float
{
// Tied directly to BPM.
return ((Constants.SECS_PER_MIN / bpm) * Constants.MS_PER_SEC);
}
/**
* Duration of a step (quarter) in milliseconds. Calculated based on bpm.
* Duration of a step (sixtennth note) in milliseconds. Calculated based on bpm.
*/
public static var stepLengthMs(get, null):Float;
@ -272,7 +273,8 @@ class Conductor
{
var prevTimeChange:SongTimeChange = timeChanges[timeChanges.length - 1];
currentTimeChange.beatTime = prevTimeChange.beatTime
+ ((currentTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC);
+ ((currentTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC)
+ 0.01;
}
}
}
@ -315,12 +317,86 @@ class Conductor
}
}
resultStep += Math.floor((ms - lastTimeChange.timeStamp) / stepLengthMs);
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
var resultFractionalStep:Float = (ms - lastTimeChange.timeStamp) / lastStepLengthMs;
resultStep += resultFractionalStep; // Math.floor();
return resultStep;
}
}
/**
* Given a time in steps and fractional steps, return a time in milliseconds.
*/
public static function getStepTimeInMs(stepTime:Float):Float
{
if (timeChanges.length == 0)
{
// Assume a constant BPM equal to the forced value.
return stepTime * stepLengthMs;
}
else
{
var resultMs:Float = 0;
var lastTimeChange:SongTimeChange = timeChanges[0];
for (timeChange in timeChanges)
{
if (stepTime >= timeChange.beatTime * 4)
{
lastTimeChange = timeChange;
resultMs = lastTimeChange.timeStamp;
}
else
{
// This time change is after the requested time.
break;
}
}
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
resultMs += (stepTime - lastTimeChange.beatTime * 4) * lastStepLengthMs;
return resultMs;
}
}
/**
* Given a time in beats and fractional beats, return a time in milliseconds.
*/
public static function getBeatTimeInMs(beatTime:Float):Float
{
if (timeChanges.length == 0)
{
// Assume a constant BPM equal to the forced value.
return beatTime * stepLengthMs * Constants.STEPS_PER_BEAT;
}
else
{
var resultMs:Float = 0;
var lastTimeChange:SongTimeChange = timeChanges[0];
for (timeChange in timeChanges)
{
if (beatTime >= timeChange.beatTime)
{
lastTimeChange = timeChange;
resultMs = lastTimeChange.timeStamp;
}
else
{
// This time change is after the requested time.
break;
}
}
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
resultMs += (beatTime - lastTimeChange.beatTime) * lastStepLengthMs * Constants.STEPS_PER_BEAT;
return resultMs;
}
}
public static function reset():Void
{
beatHit.removeAll();

View file

@ -127,7 +127,7 @@ class FreeplayState extends MusicBeatSubState
if (FlxG.sound.music != null)
{
if (!FlxG.sound.music.playing) FlxG.sound.playMusic(Paths.music('freakyMenu'));
if (!FlxG.sound.music.playing) FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
}
// if (StoryMenuState.weekUnlocked[2] || isDebug)

View file

@ -261,7 +261,7 @@ class InitState extends FlxTransitionableState
*/
function startGameNormally():Void
{
FlxG.sound.cache(Paths.music('freakyMenu'));
FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));
FlxG.switchState(new TitleState());
}

View file

@ -56,7 +56,7 @@ class MainMenuState extends MusicBeatState
if (!FlxG.sound.music.playing)
{
FlxG.sound.playMusic(Paths.music('freakyMenu'));
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
}
persistentUpdate = persistentDraw = true;

View file

@ -49,7 +49,7 @@ class TitleState extends MusicBeatState
swagShader = new ColorSwap();
curWacky = FlxG.random.getObject(getIntroTextShit());
FlxG.sound.cache(Paths.music('freakyMenu'));
FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));
// DEBUG BULLSHIT

View file

@ -1,5 +1,6 @@
package funkin.modding.module;
import funkin.util.SortUtil;
import funkin.modding.events.ScriptEvent.UpdateScriptEvent;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
@ -76,8 +77,7 @@ class ModuleHandler
}
else
{
// Sort alphabetically. Yes that's how this works.
return a > b ? 1 : -1;
return SortUtil.alphabetically(a, b);
}
});
}

View file

@ -2374,7 +2374,7 @@ class PlayState extends MusicBeatState
if (targetSongId == null)
{
FlxG.sound.playMusic(Paths.music('freakyMenu'));
FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
transIn = FlxTransitionableState.defaultTransIn;
transOut = FlxTransitionableState.defaultTransOut;

View file

@ -592,7 +592,7 @@ class BaseCharacter extends Bopper
public override function playAnimation(name:String, restart:Bool = false, ?ignoreOther:Bool = false, ?reversed:Bool = false):Void
{
FlxG.watch.addQuick('playAnim(${characterName})', name);
trace('playAnim(${characterName}): ${name}');
// trace('playAnim(${characterName}): ${name}');
super.playAnimation(name, restart, ignoreOther, reversed);
}
}

View file

@ -37,6 +37,15 @@ class SongEvent
return null;
}
/**
* Retrieves the asset path to the icon this event type should use in the chart editor.
* To customize this, override getIconPath().
*/
public function getIconPath():String
{
return 'ui/chart-editor/events/default';
}
/**
* Retrieves the human readable title of this song event type.
* Used for the chart editor.

View file

@ -770,7 +770,7 @@ class Strumline extends FlxSpriteGroup
{
// The note sprite pool is full and all note splashes are active.
// We have to create a new note.
result = new SustainTrail(0, 100, noteStyle.getHoldNoteAssetPath(), noteStyle);
result = new SustainTrail(0, 100, noteStyle);
this.holdNotes.add(result);
}

View file

@ -88,9 +88,9 @@ class SustainTrail extends FlxSprite
* @param SustainLength Length in milliseconds.
* @param fileName
*/
public function new(noteDirection:NoteDirection, sustainLength:Float, fileName:String, noteStyle:NoteStyle)
public function new(noteDirection:NoteDirection, sustainLength:Float, noteStyle:NoteStyle)
{
super(0, 0, fileName);
super(0, 0, noteStyle.getHoldNoteAssetPath());
antialiasing = true;
@ -111,7 +111,7 @@ class SustainTrail extends FlxSprite
// CALCULATE SIZE
width = graphic.width / 8 * zoom; // amount of notes * 2
height = sustainHeight(sustainLength, PlayState.instance.currentChart.scrollSpeed);
height = sustainHeight(sustainLength, getScrollSpeed());
// instead of scrollSpeed, PlayState.SONG.speed
flipY = PreferencesMenu.getPref('downscroll');
@ -123,6 +123,13 @@ class SustainTrail extends FlxSprite
updateClipping();
indices = new DrawData<Int>(12, true, TRIANGLE_VERTEX_INDICES);
this.active = true; // This NEEDS to be true for the note to be drawn!
}
function getScrollSpeed():Float
{
return PlayState?.instance?.currentChart?.scrollSpeed ?? 1.0;
}
/**
@ -139,7 +146,7 @@ class SustainTrail extends FlxSprite
{
if (s < 0) s = 0;
height = sustainHeight(s, PlayState.instance.currentChart.scrollSpeed);
height = sustainHeight(s, getScrollSpeed());
updateColorTransform();
updateClipping();
return sustainLength = s;
@ -152,7 +159,7 @@ class SustainTrail extends FlxSprite
*/
public function updateClipping(songTime:Float = 0):Void
{
var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), PlayState.instance.currentChart.scrollSpeed), 0, height);
var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), getScrollSpeed()), 0, height);
if (clipHeight == 0)
{
visible = false;

View file

@ -393,6 +393,9 @@ abstract SongNoteData(RawSongNoteData)
};
}
/**
* The timestamp of the note, in milliseconds.
*/
public var time(get, set):Float;
function get_time():Float
@ -405,11 +408,14 @@ abstract SongNoteData(RawSongNoteData)
return this.t = value;
}
/**
* The timestamp of the note, in steps.
*/
public var stepTime(get, never):Float;
function get_stepTime():Float
{
return Conductor.getTimeInSteps(this.t);
return Conductor.getTimeInSteps(abstract.time);
}
/**
@ -435,12 +441,12 @@ abstract SongNoteData(RawSongNoteData)
*/
public inline function getDirection(strumlineSize:Int = 4):Int
{
return this.d % strumlineSize;
return abstract.data % strumlineSize;
}
public function getDirectionName(strumlineSize:Int = 4):String
{
switch (this.d % strumlineSize)
switch (abstract.data % strumlineSize)
{
case 0:
return 'Left';
@ -463,7 +469,7 @@ abstract SongNoteData(RawSongNoteData)
*/
public inline function getStrumlineIndex(strumlineSize:Int = 4):Int
{
return Math.floor(this.d / strumlineSize);
return Math.floor(abstract.data / strumlineSize);
}
/**
@ -477,6 +483,10 @@ abstract SongNoteData(RawSongNoteData)
return getStrumlineIndex(strumlineSize) == 0;
}
/**
* If this is a hold note, this is the length of the hold note in milliseconds.
* @default 0 (not a hold note)
*/
public var length(get, set):Float;
function get_length():Float
@ -489,6 +499,22 @@ abstract SongNoteData(RawSongNoteData)
return this.l = value;
}
/**
* If this is a hold note, this is the length of the hold note in steps.
* @default 0 (not a hold note)
*/
public var stepLength(get, set):Float;
function get_stepLength():Float
{
return Conductor.getTimeInSteps(abstract.time + abstract.length) - abstract.stepTime;
}
function set_stepLength(value:Float):Float
{
return abstract.length = Conductor.getStepTimeInMs(value) - abstract.time;
}
public var isHoldNote(get, never):Bool;
public function get_isHoldNote():Bool
@ -514,21 +540,37 @@ abstract SongNoteData(RawSongNoteData)
@:op(A == B)
public function op_equals(other:SongNoteData):Bool
{
if (this.k == '') if (other.kind != '' && other.kind != 'normal') return false;
if (abstract.kind == '')
{
if (other.kind != '' && other.kind != 'normal') return false;
}
else
{
if (other.kind == '' || other.kind != abstract.kind) return false;
}
return this.t == other.time && this.d == other.data && this.l == other.length;
return abstract.time == other.time && abstract.data == other.data && abstract.length == other.length;
}
@:op(A != B)
public function op_notEquals(other:SongNoteData):Bool
{
return this.t != other.time || this.d != other.data || this.l != other.length || this.k != other.kind;
if (abstract.kind == '')
{
if (other.kind != '' && other.kind != 'normal') return true;
}
else
{
if (other.kind == '' || other.kind != abstract.kind) return true;
}
return abstract.time != other.time || abstract.data != other.data || abstract.length != other.length;
}
@:op(A > B)
public function op_greaterThan(other:SongNoteData):Bool
{
return this.t > other.time;
return abstract.time > other.time;
}
@:op(A < B)
@ -607,7 +649,7 @@ abstract SongEventData(RawSongEventData)
function get_stepTime():Float
{
return Conductor.getTimeInSteps(this.t);
return Conductor.getTimeInSteps(abstract.time);
}
public var event(get, set):String;

View file

@ -6,6 +6,7 @@ import funkin.util.SerializerUtil;
import funkin.play.song.SongData.SongChartData;
import funkin.play.song.SongData.SongMetadata;
import flixel.util.FlxTimer;
import funkin.util.SortUtil;
import funkin.input.Cursor;
import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData.CharacterDataParser;
@ -106,8 +107,7 @@ class ChartEditorDialogHandler
var splashTemplateContainer:VBox = dialog.findComponent('splashTemplateContainer', VBox);
var songList:Array<String> = SongDataParser.listSongIds();
// Sort alphabetically
songList.sort((a, b) -> a > b ? 1 : -1);
songList.sort(SortUtil.alphabetically);
for (targetSongId in songList)
{

View file

@ -1,5 +1,7 @@
package funkin.ui.debug.charting;
import funkin.play.event.SongEventData.SongEventParser;
import flixel.graphics.frames.FlxAtlasFrames;
import openfl.display.BitmapData;
import openfl.utils.Assets;
import flixel.FlxObject;
@ -16,6 +18,8 @@ import funkin.play.song.SongData.SongEventData;
*/
class ChartEditorEventSprite extends FlxSprite
{
public static final DEFAULT_EVENT = 'Default';
public var parentState:ChartEditorState;
/**
@ -35,17 +39,75 @@ class ChartEditorEventSprite extends FlxSprite
this.parentState = parent;
buildGraphic();
this.frames = buildFrames();
buildAnimations();
refresh();
}
function buildGraphic():Void
/**
* Build a set of animations to allow displaying different types of chart events.
* @param force `true` to force rebuilding the frames.
*/
static function buildFrames(?force:Bool = false):FlxFramesCollection
{
if (eventSpriteBasic == null)
static var eventFrames:FlxFramesCollection = null;
if (eventFrames != null && !force) return eventFrames;
eventFrames = new FlxAtlasFrames(null);
// Push the default event as a frame.
var defaultFrames:FlxAtlasFrames = Paths.getSparrowAtlas('ui/chart-editor/events/$DEFAULT_EVENT');
defaultFrames.parent.persist = true;
for (frame in defaultFrames.frames)
{
eventSpriteBasic = Assets.getBitmapData(Paths.image('ui/chart-editor/event'));
eventFrames.pushFrame(frame);
}
loadGraphic(eventSpriteBasic);
// Push all the other events as frames.
for (eventName in SongEventParser.listEventIds())
{
var exists:Bool = Assets.exists(Paths.image('ui/chart-editor/events/$eventName'));
if (!exists) continue; // No graphic for this event.
var frames:FlxAtlasFrames = Paths.getSparrowAtlas('ui/chart-editor/events/$eventName');
if (frames == null) continue; // Could not load graphic for this event.
frames.parent.persist = true;
for (frame in frames.frames)
{
eventFrames.pushFrame(frame);
}
}
return eventFrames;
}
function buildAnimations():Void
{
var eventNames:Array<String> = [DEFAULT_EVENT].concat(SongEventParser.listEventIds());
for (eventName in eventNames)
{
this.animation.addByPrefix(eventName, '${eventName}0', 24, false);
}
}
public function correctAnimationName(name:String):String
{
if (this.animation.exists(name)) return name;
trace('Warning: Invalid animation name "' + name + '" for song event. Using "${DEFAULT_EVENT}"');
return DEFAULT_EVENT;
}
public function playAnimation(name:String):Void
{
var correctedName = correctAnimationName(name);
this.animation.play(correctedName);
refresh();
}
function refresh():Void
{
setGraphicSize(ChartEditorState.GRID_SIZE);
this.updateHitbox();
}
@ -56,13 +118,13 @@ class ChartEditorEventSprite extends FlxSprite
if (this.eventData == null)
{
// Disown parent.
// Disown parent. MAKE SURE TO REVIVE BEFORE REUSING
this.kill();
return this.eventData;
}
this.visible = true;
playAnimation(this.eventData.event);
// Update the position to match the note data.
updateEventPosition();
@ -82,19 +144,34 @@ class ChartEditorEventSprite extends FlxSprite
}
/**
* Return whether this note (or its parent) is currently visible.
* Return whether this event is currently visible.
*/
public function isEventVisible(viewAreaBottom:Float, viewAreaTop:Float):Bool
{
var outsideViewArea = (this.y + this.height < viewAreaTop || this.y > viewAreaBottom);
// True if the note is above the view area.
var aboveViewArea = (this.y + this.height < viewAreaTop);
if (!outsideViewArea)
{
return true;
}
// True if the note is below the view area.
var belowViewArea = (this.y > viewAreaBottom);
// TODO: Check if this note's parent or child is visible.
return !aboveViewArea && !belowViewArea;
}
return false;
/**
* Return whether an event, if placed in the scene, would be visible.
*/
public static function wouldEventBeVisible(viewAreaBottom:Float, viewAreaTop:Float, eventData:SongEventData, ?origin:FlxObject):Bool
{
var noteHeight:Float = ChartEditorState.GRID_SIZE;
var notePosY:Float = eventData.stepTime * ChartEditorState.GRID_SIZE;
if (origin != null) notePosY += origin.y;
// True if the note is above the view area.
var aboveViewArea = (notePosY + noteHeight < viewAreaTop);
// True if the note is below the view area.
var belowViewArea = (notePosY > viewAreaBottom);
return !aboveViewArea && !belowViewArea;
}
}

View file

@ -0,0 +1,157 @@
package funkin.ui.debug.charting;
import funkin.play.notes.Strumline;
import funkin.data.notestyle.NoteStyleRegistry;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.graphics.frames.FlxFramesCollection;
import flixel.graphics.frames.FlxTileFrames;
import flixel.math.FlxPoint;
import funkin.play.notes.SustainTrail;
import funkin.play.song.SongData.SongNoteData;
/**
* A hold note sprite that can be used to display a note in a chart.
* Designed to be used and reused efficiently. Has no gameplay functionality.
*/
class ChartEditorHoldNoteSprite extends SustainTrail
{
/**
* The ChartEditorState this note belongs to.
*/
public var parentState:ChartEditorState;
public function new(parent:ChartEditorState)
{
var noteStyle = NoteStyleRegistry.instance.fetchDefault();
super(0, 100, noteStyle);
this.parentState = parent;
zoom = 1.0;
zoom *= noteStyle.fetchHoldNoteScale();
zoom *= 0.7;
zoom *= ChartEditorState.GRID_SIZE / Strumline.STRUMLINE_SIZE;
setup();
}
/**
* Set the height directly, to a value in pixels.
* @param h The desired height in pixels.
*/
public function setHeightDirectly(h:Float)
{
sustainLength = h / (getScrollSpeed() * Constants.PIXELS_PER_MS);
fullSustainLength = sustainLength;
}
function setup():Void
{
strumTime = 999999999;
missedNote = false;
hitNote = false;
active = true;
visible = true;
alpha = 1.0;
width = graphic.width / 8 * zoom; // amount of notes * 2
}
public override function revive():Void
{
super.revive();
setup();
}
public override function kill():Void
{
super.kill();
active = false;
visible = false;
noteData = null;
strumTime = 999999999;
noteDirection = 0;
sustainLength = 0;
fullSustainLength = 0;
}
/**
* Return whether this note is currently visible.
*/
public function isHoldNoteVisible(viewAreaBottom:Float, viewAreaTop:Float):Bool
{
// True if the note is above the view area.
var aboveViewArea = (this.y + this.height < viewAreaTop);
// True if the note is below the view area.
var belowViewArea = (this.y > viewAreaBottom);
return !aboveViewArea && !belowViewArea;
}
/**
* Return whether a hold note, if placed in the scene, would be visible.
*/
public static function wouldHoldNoteBeVisible(viewAreaBottom:Float, viewAreaTop:Float, noteData:SongNoteData, ?origin:FlxObject):Bool
{
var noteHeight:Float = noteData.stepLength * ChartEditorState.GRID_SIZE;
var notePosY:Float = noteData.stepTime * ChartEditorState.GRID_SIZE;
if (origin != null) notePosY += origin.y;
// True if the note is above the view area.
var aboveViewArea = (notePosY + noteHeight < viewAreaTop);
// True if the note is below the view area.
var belowViewArea = (notePosY > viewAreaBottom);
return !aboveViewArea && !belowViewArea;
}
public function updateHoldNotePosition(?origin:FlxObject)
{
var cursorColumn:Int = this.noteData.data;
if (cursorColumn < 0) cursorColumn = 0;
if (cursorColumn >= (ChartEditorState.STRUMLINE_SIZE * 2 + 1))
{
cursorColumn = (ChartEditorState.STRUMLINE_SIZE * 2 + 1);
}
else
{
// Invert player and opponent columns.
if (cursorColumn >= ChartEditorState.STRUMLINE_SIZE)
{
cursorColumn -= ChartEditorState.STRUMLINE_SIZE;
}
else
{
cursorColumn += ChartEditorState.STRUMLINE_SIZE;
}
}
this.x = cursorColumn * ChartEditorState.GRID_SIZE;
// Notes far in the song will start far down, but the group they belong to will have a high negative offset.
if (this.noteData.stepTime >= 0)
{
// noteData.stepTime is a calculated value which accounts for BPM changes
var stepTime:Float = this.noteData.stepTime;
var roundedStepTime:Float = Math.floor(stepTime + 0.01); // Add epsilon to fix rounding issues
this.y = roundedStepTime * ChartEditorState.GRID_SIZE;
}
this.x += ChartEditorState.GRID_SIZE / 2;
this.x -= this.width / 2;
this.y += ChartEditorState.GRID_SIZE / 2;
if (origin != null)
{
this.x += origin.x;
this.y += origin.y;
}
}
}

View file

@ -34,16 +34,6 @@ class ChartEditorNoteSprite extends FlxSprite
*/
public var noteStyle(get, null):String;
/**
* This note is the previous sprite in a sustain chain.
*/
public var parentNoteSprite(default, set):ChartEditorNoteSprite = null;
/**
* This note is the next sprite in a sustain chain.
*/
public var childNoteSprite(default, set):ChartEditorNoteSprite = null;
public function new(parent:ChartEditorState)
{
super();
@ -124,14 +114,6 @@ class ChartEditorNoteSprite extends FlxSprite
if (this.noteData == null)
{
// Disown parent.
this.parentNoteSprite = null;
if (this.childNoteSprite != null)
{
// Kill all children and disown them.
this.childNoteSprite.noteData = null;
this.childNoteSprite = null;
}
this.kill();
return this.noteData;
}
@ -170,68 +152,22 @@ class ChartEditorNoteSprite extends FlxSprite
}
}
if (parentNoteSprite == null)
this.x = cursorColumn * ChartEditorState.GRID_SIZE;
// Notes far in the song will start far down, but the group they belong to will have a high negative offset.
if (this.noteData.stepTime >= 0)
{
this.x = cursorColumn * ChartEditorState.GRID_SIZE;
// Notes far in the song will start far down, but the group they belong to will have a high negative offset.
// TODO: stepTime doesn't account for fluctuating BPMs.
if (this.noteData.stepTime >= 0) this.y = this.noteData.stepTime * ChartEditorState.GRID_SIZE;
if (origin != null)
{
this.x += origin.x;
this.y += origin.y;
}
}
else
{
// If this is a hold note, we need to adjust the position to be centered.
if (parentNoteSprite.parentNoteSprite == null)
{
this.x = parentNoteSprite.x;
this.x += (ChartEditorState.GRID_SIZE / 2);
this.x -= this.width / 2;
}
else
{
this.x = parentNoteSprite.x;
}
this.y = parentNoteSprite.y;
if (parentNoteSprite.parentNoteSprite == null)
{
this.y += parentNoteSprite.height / 2;
}
else
{
this.y += parentNoteSprite.height - 1;
}
}
}
function set_parentNoteSprite(value:ChartEditorNoteSprite):ChartEditorNoteSprite
{
this.parentNoteSprite = value;
if (this.parentNoteSprite != null)
{
this.noteData = this.parentNoteSprite.noteData;
// noteData.stepTime is a calculated value which accounts for BPM changes
var stepTime:Float = this.noteData.stepTime;
var roundedStepTime:Float = Math.floor(stepTime + 0.01); // Add epsilon to fix rounding issues
this.y = roundedStepTime * ChartEditorState.GRID_SIZE;
}
return this.parentNoteSprite;
}
function set_childNoteSprite(value:ChartEditorNoteSprite):ChartEditorNoteSprite
{
this.childNoteSprite = value;
if (this.parentNoteSprite != null)
if (origin != null)
{
this.noteData = this.parentNoteSprite.noteData;
this.x += origin.x;
this.y += origin.y;
}
return this.childNoteSprite;
}
function get_noteStyle():String
@ -244,7 +180,6 @@ class ChartEditorNoteSprite extends FlxSprite
{
// Decide whether to display a note or a sustain.
var baseAnimationName:String = 'tap';
if (this.parentNoteSprite != null) baseAnimationName = (this.childNoteSprite != null) ? 'hold' : 'holdEnd';
// Play the appropriate animation for the type, direction, and skin.
var animationName:String = '${baseAnimationName}${this.noteData.getDirectionName()}${this.noteStyle}';
@ -257,17 +192,6 @@ class ChartEditorNoteSprite extends FlxSprite
{
case 'tap':
this.setGraphicSize(0, ChartEditorState.GRID_SIZE);
case 'hold':
if (parentNoteSprite.parentNoteSprite == null)
{
this.setGraphicSize(Std.int(ChartEditorState.GRID_SIZE / 2), Std.int(ChartEditorState.GRID_SIZE / 2));
}
else
{
this.setGraphicSize(Std.int(ChartEditorState.GRID_SIZE / 2), ChartEditorState.GRID_SIZE);
}
case 'holdEnd':
this.setGraphicSize(Std.int(ChartEditorState.GRID_SIZE / 2), Std.int(ChartEditorState.GRID_SIZE / 2));
}
this.updateHitbox();
@ -280,22 +204,30 @@ class ChartEditorNoteSprite extends FlxSprite
*/
public function isNoteVisible(viewAreaBottom:Float, viewAreaTop:Float):Bool
{
var outsideViewArea = (this.y + this.height < viewAreaTop || this.y > viewAreaBottom);
// True if the note is above the view area.
var aboveViewArea = (this.y + this.height < viewAreaTop);
if (!outsideViewArea)
{
return true;
}
// True if the note is below the view area.
var belowViewArea = (this.y > viewAreaBottom);
// TODO: Check if this note's parent or child is visible.
return false;
return !aboveViewArea && !belowViewArea;
}
public function getBaseNoteSprite()
/**
* Return whether a note, if placed in the scene, would be visible.
*/
public static function wouldNoteBeVisible(viewAreaBottom:Float, viewAreaTop:Float, noteData:SongNoteData, ?origin:FlxObject):Bool
{
if (this.parentNoteSprite == null) return this;
else
return this.parentNoteSprite;
var noteHeight:Float = ChartEditorState.GRID_SIZE;
var notePosY:Float = noteData.stepTime * ChartEditorState.GRID_SIZE;
if (origin != null) notePosY += origin.y;
// True if the note is above the view area.
var aboveViewArea = (notePosY + noteHeight < viewAreaTop);
// True if the note is below the view area.
var belowViewArea = (notePosY > viewAreaBottom);
return !aboveViewArea && !belowViewArea;
}
}

View file

@ -1,11 +1,5 @@
package funkin.ui.debug.charting;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.ui.debug.charting.ChartEditorCommand;
import funkin.play.song.SongData.SongPlayableChar;
import funkin.play.character.BaseCharacter.CharacterType;
import haxe.ui.containers.dialogs.CollapsibleDialog;
import openfl.Assets;
import flixel.addons.display.FlxSliceSprite;
import flixel.addons.display.FlxTiledSprite;
import flixel.FlxSprite;
@ -22,32 +16,41 @@ import flixel.util.FlxSort;
import flixel.util.FlxTimer;
import funkin.audio.visualize.PolygonSpectogram;
import funkin.audio.VoicesGroup;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.input.Cursor;
import funkin.input.TurboKeyHandler;
import funkin.modding.events.ScriptEvent;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.HealthIcon;
import funkin.play.notes.NoteSprite;
import funkin.play.notes.Strumline;
import funkin.play.song.Song;
import funkin.play.song.SongData.SongChartData;
import funkin.play.song.SongData.SongDataParser;
import funkin.play.song.SongData.SongEventData;
import funkin.play.song.SongData.SongMetadata;
import funkin.play.song.SongData.SongNoteData;
import funkin.play.song.SongData.SongPlayableChar;
import funkin.play.song.SongDataUtils;
import funkin.ui.debug.charting.ChartEditorCommand;
import funkin.ui.debug.charting.ChartEditorCommand;
import funkin.ui.debug.charting.ChartEditorThemeHandler.ChartEditorTheme;
import funkin.ui.debug.charting.ChartEditorToolboxHandler.ChartEditorToolMode;
import funkin.ui.haxeui.components.CharacterPlayer;
import funkin.ui.haxeui.HaxeUIState;
import funkin.util.FileUtil;
import funkin.util.Constants;
import funkin.util.DateUtil;
import funkin.util.FileUtil;
import funkin.util.SerializerUtil;
import funkin.util.SortUtil;
import funkin.util.WindowUtil;
import haxe.DynamicAccess;
import haxe.io.Bytes;
import haxe.io.Path;
import haxe.ui.components.Label;
import haxe.ui.components.Slider;
import haxe.ui.containers.dialogs.CollapsibleDialog;
import haxe.ui.containers.menus.MenuItem;
import haxe.ui.containers.TreeView;
import haxe.ui.containers.TreeViewNode;
@ -57,6 +60,7 @@ import haxe.ui.events.DragEvent;
import haxe.ui.events.UIEvent;
import haxe.ui.notifications.NotificationManager;
import haxe.ui.notifications.NotificationType;
import openfl.Assets;
import openfl.display.BitmapData;
import openfl.geom.Rectangle;
@ -121,6 +125,11 @@ class ChartEditorState extends HaxeUIState
*/
static final MENU_BAR_HEIGHT:Int = 32;
/**
* The height of the playbar in the layout.
*/
static final PLAYBAR_HEIGHT:Int = 48;
/**
* Duration to wait before autosaving the chart.
*/
@ -182,29 +191,35 @@ class ChartEditorState extends HaxeUIState
/**
* scrollPosition, converted to steps.
* TODO: Handle BPM changes.
* NOT dependant on BPM, because the size of a grid square does not change with BPM.
*/
var scrollPositionInSteps(get, null):Float;
var scrollPositionInSteps(get, set):Float;
function get_scrollPositionInSteps():Float
{
return scrollPositionInPixels / GRID_SIZE;
}
function set_scrollPositionInSteps(value:Float):Float
{
scrollPositionInPixels = value * GRID_SIZE;
return value;
}
/**
* scrollPosition, converted to milliseconds.
* TODO: Handle BPM changes.
* DEPENDANT on BPM, because the duration of a grid square changes with BPM.
*/
var scrollPositionInMs(get, set):Float;
function get_scrollPositionInMs():Float
{
return scrollPositionInSteps * Conductor.stepLengthMs;
return Conductor.getStepTimeInMs(scrollPositionInSteps);
}
function set_scrollPositionInMs(value:Float):Float
{
scrollPositionInPixels = value / Conductor.stepLengthMs;
scrollPositionInSteps = Conductor.getTimeInSteps(value);
return value;
}
@ -216,11 +231,26 @@ class ChartEditorState extends HaxeUIState
*/
var playheadPositionInPixels(default, set):Float;
var playheadPositionInSteps(get, null):Float;
function set_playheadPositionInPixels(value:Float):Float
{
// Make sure playhead doesn't go outside the song.
if (value + scrollPositionInPixels < 0) value = -scrollPositionInPixels;
if (value + scrollPositionInPixels > songLengthInPixels) value = songLengthInPixels - scrollPositionInPixels;
this.playheadPositionInPixels = value;
// Move the playhead sprite to the correct position.
gridPlayhead.y = this.playheadPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
return this.playheadPositionInPixels;
}
/**
* playheadPosition, converted to steps.
* NOT dependant on BPM, because the size of a grid square does not change with BPM.
*/
var playheadPositionInSteps(get, null):Float;
function get_playheadPositionInSteps():Float
{
return playheadPositionInPixels / GRID_SIZE;
@ -228,60 +258,76 @@ class ChartEditorState extends HaxeUIState
/**
* playheadPosition, converted to milliseconds.
* DEPENDANT on BPM, because the duration of a grid square changes with BPM.
*/
var playheadPositionInMs(get, null):Float;
function get_playheadPositionInMs():Float
{
return playheadPositionInSteps * Conductor.stepLengthMs;
return Conductor.getStepTimeInMs(playheadPositionInSteps);
}
/**
* This is the song's length in PIXELS, same format as scrollPosition.
* songLength, in milliseconds.
*/
var songLengthInPixels(get, default):Int;
@:isVar var songLengthInMs(get, set):Float;
function get_songLengthInPixels():Int
function get_songLengthInMs():Float
{
if (songLengthInPixels <= 0) return 1000;
if (songLengthInMs <= 0) return 1000;
return songLengthInMs;
}
return songLengthInPixels;
function set_songLengthInMs(value:Float):Float
{
this.songLengthInMs = value;
// Make sure playhead doesn't go outside the song.
if (playheadPositionInMs > songLengthInMs) playheadPositionInMs = songLengthInMs;
return this.songLengthInMs;
}
/**
* songLength, converted to steps.
* TODO: Handle BPM changes.
* Dependant on BPM, because the size of a grid square does not change with BPM but the length of a beat does.
*/
var songLengthInSteps(get, set):Float;
function get_songLengthInSteps():Float
{
return songLengthInPixels / GRID_SIZE;
return Conductor.getTimeInSteps(songLengthInMs);
}
function set_songLengthInSteps(value:Float):Float
{
songLengthInPixels = Std.int(value * GRID_SIZE);
// Getting a reasonable result from setting songLengthInSteps requires that Conductor.mapBPMChanges be called first.
songLengthInMs = Conductor.getStepTimeInMs(value);
return value;
}
/**
* songLength, converted to milliseconds.
* TODO: Handle BPM changes.
* This is the song's length in PIXELS, same format as scrollPosition.
* Dependant on BPM, because the size of a grid square does not change with BPM but the length of a beat does.
*/
var songLengthInMs(get, set):Float;
var songLengthInPixels(get, set):Int;
function get_songLengthInMs():Float
function get_songLengthInPixels():Int
{
return songLengthInSteps * Conductor.stepLengthMs;
return Std.int(songLengthInSteps * GRID_SIZE);
}
function set_songLengthInMs(value:Float):Float
function set_songLengthInPixels(value:Int):Int
{
songLengthInSteps = Conductor.getTimeInSteps(audioInstTrack.length);
songLengthInSteps = value / GRID_SIZE;
return value;
}
/**
* The current theme used by the editor.
* Dictates the appearance of many UI elements.
* Currently hardcoded to just Light and Dark.
*/
var currentTheme(default, set):ChartEditorTheme = null;
function set_currentTheme(value:ChartEditorTheme):ChartEditorTheme
@ -1002,6 +1048,13 @@ class ChartEditorState extends HaxeUIState
*/
var renderedNotes:FlxTypedSpriteGroup<ChartEditorNoteSprite>;
/**
* The sprite group containing the hold note graphics.
* Only displays a subset of the data from `currentSongChartNoteData`,
* and kills notes that are off-screen to be recycled later.
*/
var renderedHoldNotes:FlxTypedSpriteGroup<ChartEditorHoldNoteSprite>;
/**
* The sprite group containing the song events.
* Only displays a subset of the data from `currentSongChartEventData`,
@ -1088,7 +1141,7 @@ class ChartEditorState extends HaxeUIState
gridGhostNote = new ChartEditorNoteSprite(this);
gridGhostNote.alpha = 0.6;
gridGhostNote.noteData = new SongNoteData(-1, -1, 0, '');
gridGhostNote.noteData = new SongNoteData(0, 0, 0, "");
gridGhostNote.visible = false;
add(gridGhostNote);
@ -1187,6 +1240,10 @@ class ChartEditorState extends HaxeUIState
*/
function buildNoteGroup():Void
{
renderedHoldNotes = new FlxTypedSpriteGroup<ChartEditorHoldNoteSprite>();
renderedHoldNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y);
add(renderedHoldNotes);
renderedNotes = new FlxTypedSpriteGroup<ChartEditorNoteSprite>();
renderedNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y);
add(renderedNotes);
@ -1812,12 +1869,9 @@ class ChartEditorState extends HaxeUIState
moveSongToScrollPosition();
}
// Cursor position snapped to the grid.
// The song position of the cursor, in steps.
var cursorFractionalStep:Float = cursorY / GRID_SIZE / (16 / noteSnapQuant);
var cursorStep:Int = Std.int(Math.floor(cursorFractionalStep));
var cursorMs:Float = cursorStep * Conductor.stepLengthMs * (16 / noteSnapQuant);
var cursorMs:Float = Conductor.getStepTimeInMs(cursorFractionalStep);
// The direction value for the column at the cursor.
var cursorColumn:Int = Math.floor(cursorX / GRID_SIZE);
if (cursorColumn < 0) cursorColumn = 0;
@ -1855,7 +1909,7 @@ class ChartEditorState extends HaxeUIState
// We released the mouse. Select the notes in the box.
var cursorFractionalStepStart:Float = cursorYStart / GRID_SIZE;
var cursorStepStart:Int = Math.floor(cursorFractionalStepStart);
var cursorMsStart:Float = cursorStepStart * Conductor.stepLengthMs;
var cursorMsStart:Float = Conductor.getStepTimeInMs(cursorStepStart);
var cursorColumnBase:Int = Math.floor(cursorX / GRID_SIZE);
var cursorColumnBaseStart:Int = Math.floor(cursorXStart / GRID_SIZE);
@ -1994,8 +2048,7 @@ class ChartEditorState extends HaxeUIState
{
if (highlightedNote != null)
{
// Handle the case of clicking on a sustain piece.
highlightedNote = highlightedNote.getBaseNoteSprite();
// TODO: Handle the case of clicking on a sustain piece.
// Control click to select/deselect an individual note.
if (isNoteSelected(highlightedNote.noteData))
{
@ -2059,12 +2112,13 @@ class ChartEditorState extends HaxeUIState
{
// Handle extending the note as you drag.
// Since use Math.floor and stepLengthMs here, the hold notes will be beat snapped.
var dragLengthSteps:Float = Math.floor((cursorMs - currentPlaceNoteData.time) / Conductor.stepLengthMs);
// TODO: This should be beat snapped?
var dragLengthSteps:Float = Conductor.getTimeInSteps(cursorMs) - currentPlaceNoteData.stepTime;
// Without this, the newly placed note feels too short compared to the user's input.
var INCREMENT:Float = 1.0;
var dragLengthMs:Float = (dragLengthSteps + INCREMENT) * Conductor.stepLengthMs;
// TODO: Make this not busted with BPM changes
var dragLengthMs:Float = Math.floor(dragLengthSteps + INCREMENT) * Conductor.stepLengthMs;
// TODO: Add and update some sort of preview?
@ -2197,8 +2251,7 @@ class ChartEditorState extends HaxeUIState
if (highlightedNote != null)
{
// Handle the case of clicking on a sustain piece.
highlightedNote = highlightedNote.getBaseNoteSprite();
// TODO: Handle the case of clicking on a sustain piece.
// Remove the note.
performCommand(new RemoveNotesCommand([highlightedNote.noteData]));
}
@ -2286,10 +2339,10 @@ class ChartEditorState extends HaxeUIState
// Update for whether downscroll is enabled.
renderedNotes.flipX = (isViewDownscroll);
// Calculate the view bounds.
var viewAreaTop:Float = this.scrollPositionInPixels - GRID_TOP_PAD;
var viewHeight:Float = (FlxG.height - MENU_BAR_HEIGHT);
var viewAreaBottom:Float = this.scrollPositionInPixels + viewHeight;
// Calculate the top and bottom of the view area.
var viewAreaTopPixels:Float = MENU_BAR_HEIGHT;
var visibleGridHeightPixels:Float = FlxG.height - MENU_BAR_HEIGHT - PLAYBAR_HEIGHT; // The area underneath the menu bar and playbar is not visible.
var viewAreaBottomPixels:Float = viewAreaTopPixels + visibleGridHeightPixels;
// Remove notes that are no longer visible and list the ones that are.
var displayedNoteData:Array<SongNoteData> = [];
@ -2297,7 +2350,7 @@ class ChartEditorState extends HaxeUIState
{
if (noteSprite == null || !noteSprite.exists || !noteSprite.visible) continue;
if (!noteSprite.isNoteVisible(viewAreaBottom, viewAreaTop))
if (!noteSprite.isNoteVisible(viewAreaBottomPixels, viewAreaTopPixels))
{
// This sprite is off-screen.
// Kill the note sprite and recycle it.
@ -2309,18 +2362,6 @@ class ChartEditorState extends HaxeUIState
// Kill the note sprite and recycle it.
noteSprite.noteData = null;
}
else if (noteSprite.noteData.length > 0 && (noteSprite.parentNoteSprite == null && noteSprite.childNoteSprite == null))
{
// Note was extended.
// Kill the note sprite and recycle it.
noteSprite.noteData = null;
}
else if (noteSprite.noteData.length == 0 && (noteSprite.parentNoteSprite != null || noteSprite.childNoteSprite != null))
{
// Note was shortened.
// Kill the note sprite and recycle it.
noteSprite.noteData = null;
}
else
{
// Note is already displayed and should remain displayed.
@ -2331,13 +2372,42 @@ class ChartEditorState extends HaxeUIState
}
}
var displayedHoldNoteData:Array<SongNoteData> = [];
for (holdNoteSprite in renderedHoldNotes.members)
{
if (holdNoteSprite == null || !holdNoteSprite.exists || !holdNoteSprite.visible) continue;
if (!holdNoteSprite.isHoldNoteVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD))
{
holdNoteSprite.kill();
}
else if (currentSongChartNoteData.indexOf(holdNoteSprite.noteData) == -1 || holdNoteSprite.noteData.length == 0)
{
// This hold note was deleted.
// Kill the hold note sprite and recycle it.
holdNoteSprite.kill();
}
else if (displayedHoldNoteData.indexOf(holdNoteSprite.noteData) != -1)
{
// This hold note is a duplicate.
// Kill the hold note sprite and recycle it.
holdNoteSprite.kill();
}
else
{
displayedHoldNoteData.push(holdNoteSprite.noteData);
// Update the event sprite's position.
holdNoteSprite.updateHoldNotePosition(renderedNotes);
}
}
// Remove events that are no longer visible and list the ones that are.
var displayedEventData:Array<SongEventData> = [];
for (eventSprite in renderedEvents.members)
{
if (eventSprite == null || !eventSprite.exists || !eventSprite.visible) continue;
if (!eventSprite.isEventVisible(viewAreaBottom, viewAreaTop))
if (!eventSprite.isEventVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD))
{
// This sprite is off-screen.
// Kill the event sprite and recycle it.
@ -2368,59 +2438,36 @@ class ChartEditorState extends HaxeUIState
continue;
}
// Get the position the note should be at.
var noteTimePixels:Float = noteData.time / Conductor.stepLengthMs * GRID_SIZE;
// Make sure the note appears when scrolling up.
var modifiedViewAreaTop:Float = viewAreaTop - GRID_SIZE;
if (noteTimePixels < modifiedViewAreaTop || noteTimePixels > viewAreaBottom) continue;
// Else, this note is visible and we need to render it!
if (!ChartEditorNoteSprite.wouldNoteBeVisible(viewAreaBottomPixels, viewAreaTopPixels, noteData,
renderedNotes)) continue; // Else, this note is visible and we need to render it!
// Get a note sprite from the pool.
// If we can reuse a deleted note, do so.
// If a new note is needed, call buildNoteSprite.
var noteSprite:ChartEditorNoteSprite = renderedNotes.recycle(() -> new ChartEditorNoteSprite(this));
trace('Creating new Note... (${renderedNotes.members.length})');
noteSprite.parentState = this;
// The note sprite handles animation playback and positioning.
noteSprite.noteData = noteData;
// Setting note data resets position relative to the grid so we fix that.
noteSprite.x += renderedNotes.x;
noteSprite.y += renderedNotes.y;
noteSprite.updateNotePosition(renderedNotes);
if (noteSprite.noteData.length > 0)
// Add hold notes that are now visible (and not already displayed).
if (noteSprite.noteData.length > 0 && displayedHoldNoteData.indexOf(noteData) == -1)
{
// If the note is a hold, we need to make sure it's long enough.
var noteLengthMs:Float = noteSprite.noteData.length;
var noteLengthSteps:Float = (noteLengthMs / Conductor.stepLengthMs);
var lastNoteSprite:ChartEditorNoteSprite = noteSprite;
var holdNoteSprite:ChartEditorHoldNoteSprite = renderedHoldNotes.recycle(() -> new ChartEditorHoldNoteSprite(this));
trace('Creating new HoldNote... (${renderedHoldNotes.members.length})');
while (noteLengthSteps > 0)
{
if (noteLengthSteps <= 1.0)
{
// Last note in the hold.
// TODO: We may need to make it shorter and clip it visually.
}
var noteLengthPixels:Float = noteSprite.noteData.stepLength * GRID_SIZE;
var nextNoteSprite:ChartEditorNoteSprite = renderedNotes.recycle(ChartEditorNoteSprite);
nextNoteSprite.parentState = this;
nextNoteSprite.parentNoteSprite = lastNoteSprite;
lastNoteSprite.childNoteSprite = nextNoteSprite;
holdNoteSprite.noteData = noteSprite.noteData;
holdNoteSprite.noteDirection = noteSprite.noteData.getDirection();
lastNoteSprite = nextNoteSprite;
holdNoteSprite.setHeightDirectly(noteLengthPixels);
noteLengthSteps -= 1;
}
// Make sure the last note sprite shows the end cap properly.
lastNoteSprite.childNoteSprite = null;
// var noteLengthPixels:Float = (noteLengthMs / Conductor.stepLengthMs + 1) * GRID_SIZE;
// add(new FlxSprite(noteSprite.x, noteSprite.y - renderedNotes.y + noteLengthPixels).makeGraphic(40, 2, 0xFFFF0000));
holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
}
}
@ -2433,21 +2480,16 @@ class ChartEditorState extends HaxeUIState
continue;
}
// Get the position the event should be at.
var eventTimePixels:Float = eventData.time / Conductor.stepLengthMs * GRID_SIZE;
// Make sure the event appears when scrolling up.
var modifiedViewAreaTop:Float = viewAreaTop - GRID_SIZE;
if (eventTimePixels < modifiedViewAreaTop || eventTimePixels > viewAreaBottom) continue;
if (!ChartEditorEventSprite.wouldEventBeVisible(viewAreaBottomPixels, viewAreaTopPixels, eventData, renderedNotes)) continue;
// Else, this event is visible and we need to render it!
// Get an event sprite from the pool.
// If we can reuse a deleted event, do so.
// If a new event is needed, call buildEventSprite.
var eventSprite:ChartEditorEventSprite = renderedEvents.recycle(() -> new ChartEditorEventSprite(this));
var eventSprite:ChartEditorEventSprite = renderedEvents.recycle(() -> new ChartEditorEventSprite(this), false, true);
eventSprite.parentState = this;
trace('Creating new Event... (${renderedEvents.members.length})');
// The event sprite handles animation playback and positioning.
eventSprite.eventData = eventData;
@ -2457,6 +2499,37 @@ class ChartEditorState extends HaxeUIState
eventSprite.y += renderedEvents.y;
}
// Add hold notes that have been made visible (but not their parents)
for (noteData in currentSongChartNoteData)
{
// Is the note a hold note?
if (noteData.length <= 0) continue;
// Is the hold note rendered already?
if (displayedHoldNoteData.indexOf(noteData) != -1) continue;
// Is the hold note offscreen?
if (!ChartEditorHoldNoteSprite.wouldHoldNoteBeVisible(viewAreaBottomPixels, viewAreaTopPixels, noteData, renderedHoldNotes)) continue;
// Hold note should be rendered.
var holdNoteFactory = function() {
// TODO: Print some kind of warning if `renderedHoldNotes.members` is too high?
return new ChartEditorHoldNoteSprite(this);
}
var holdNoteSprite:ChartEditorHoldNoteSprite = renderedHoldNotes.recycle(holdNoteFactory);
var noteLengthPixels:Float = noteData.stepLength * GRID_SIZE;
holdNoteSprite.noteData = noteData;
holdNoteSprite.noteDirection = noteData.getDirection();
holdNoteSprite.setHeightDirectly(noteLengthPixels);
holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
displayedHoldNoteData.push(noteData);
}
// Destroy all existing selection squares.
for (member in renderedSelectionSquares.members)
{
@ -2468,7 +2541,8 @@ class ChartEditorState extends HaxeUIState
// Recycle selection squares if possible.
for (noteSprite in renderedNotes.members)
{
if (isNoteSelected(noteSprite.noteData) && noteSprite.parentNoteSprite == null)
// TODO: Handle selection of hold notes.
if (isNoteSelected(noteSprite.noteData))
{
var selectionSquare:FlxSprite = renderedSelectionSquares.recycle(buildSelectionSquare);
@ -2500,6 +2574,12 @@ class ChartEditorState extends HaxeUIState
// Sort the events DESCENDING. This keeps the sustain behind the associated note.
renderedEvents.sort(FlxSort.byY, FlxSort.DESCENDING);
}
// Add a debug value which displays the current size of the note pool.
// The pool will grow as more notes need to be rendered at once.
// If this gets too big, something needs to be optimized somewhere! -Eric
FlxG.watch.addQuick("tapNotesRendered", renderedNotes.members.length);
FlxG.watch.addQuick("holdNotesRendered", renderedHoldNotes.members.length);
}
function buildSelectionSquare():FlxSprite
@ -2882,7 +2962,7 @@ class ChartEditorState extends HaxeUIState
*/
function handleNotePreview():Void
{
//
// TODO: Finish this.
if (notePreviewDirty)
{
notePreviewDirty = false;
@ -3023,11 +3103,13 @@ class ChartEditorState extends HaxeUIState
// Assume notes are sorted by time.
for (noteData in currentSongChartNoteData)
{
// Check for notes between the old and new song positions.
if (noteData.time < oldSongPosition) // Note is in the past.
continue;
if (noteData.time >= newSongPosition) // Note is in the future.
return;
if (noteData.time > newSongPosition) // Note is in the future.
return; // Assume all notes are also in the future.
// Note was just hit.
@ -3160,6 +3242,7 @@ class ChartEditorState extends HaxeUIState
}
// Move the rendered notes to the correct position.
renderedNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y);
renderedHoldNotes.setPosition(gridTiledSprite.x, gridTiledSprite.y);
renderedEvents.setPosition(gridTiledSprite.x, gridTiledSprite.y);
renderedSelectionSquares.setPosition(gridTiledSprite.x, gridTiledSprite.y);
@ -3169,29 +3252,11 @@ class ChartEditorState extends HaxeUIState
return this.scrollPositionInPixels;
}
function get_playheadPositionInPixels():Float
{
return this.playheadPositionInPixels;
}
function set_playheadPositionInPixels(value:Float):Float
{
// Make sure playhead doesn't go outside the song.
if (value + scrollPositionInPixels < 0) value = -scrollPositionInPixels;
if (value + scrollPositionInPixels > songLengthInPixels) value = songLengthInPixels - scrollPositionInPixels;
this.playheadPositionInPixels = value;
// Move the playhead sprite to the correct position.
gridPlayhead.y = this.playheadPositionInPixels + (MENU_BAR_HEIGHT + GRID_TOP_PAD);
return this.playheadPositionInPixels;
}
/**
* Loads an instrumental from an absolute file path, replacing the current instrumental.
*
* @param path The absolute path to the audio file.
*
* @return Success or failure.
*/
public function loadInstrumentalFromPath(path:Path):Bool
@ -3295,7 +3360,16 @@ class ChartEditorState extends HaxeUIState
}
/**
* Loads a vocal track from an OpenFL asset.
* Clear the voices group.
*/
public function clearVocals():Void
{
audioVocalTrackGroup.clear();
}
/**
* Load a vocal track for a given song and character and add it to the voices group.
*
* @param path ID of the asset.
* @param charKey Character to load the vocal track for.
* @return Success or failure.
@ -3346,9 +3420,8 @@ class ChartEditorState extends HaxeUIState
for (metadata in rawSongMetadata)
{
var variation:String = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation;
songMetadata.set(variation, metadata);
var variation = (metadata.variation == null || metadata.variation == '') ? 'default' : metadata.variation;
songMetadata.set(variation, Reflect.copy(metadata));
songChartData.set(variation, SongDataParser.parseSongChartData(songId, metadata.variation));
}
@ -3359,21 +3432,21 @@ class ChartEditorState extends HaxeUIState
audioInstTrack.stop();
audioInstTrack = null;
}
Conductor.forceBPM(null); // Disable the forced BPM.
Conductor.mapTimeChanges(currentSongMetadata.timeChanges);
sortChartData();
clearVocals();
loadInstrumentalFromAsset(Paths.inst(songId));
if (audioVocalTrackGroup != null)
var voiceList:Array<String> = song.getDifficulty(selectedDifficulty).buildVoiceList();
for (voicePath in voiceList)
{
audioVocalTrackGroup.stop();
audioVocalTrackGroup.clear();
loadVocalsFromAsset(voicePath);
}
// Add player vocals.
if (currentSongCharacterPlayer != null) audioVocalTrackGroup.addPlayerVoice(new FlxSound().loadEmbedded(Assets.getSound(Paths.voices(songId,
'-$currentSongCharacterPlayer'))));
// Add opponent vocals.
if (currentSongCharacterOpponent != null) audioVocalTrackGroup.addOpponentVoice(new FlxSound().loadEmbedded(Assets.getSound(Paths.voices(songId,
'-$currentSongCharacterOpponent'))));
postLoadInstrumental();
NotificationManager.instance.addNotification(
{
@ -3421,7 +3494,8 @@ class ChartEditorState extends HaxeUIState
function moveSongToScrollPosition():Void
{
// Update the songPosition in the Conductor.
Conductor.update(scrollPositionInMs);
var targetPos = scrollPositionInMs;
Conductor.update(targetPos);
// Update the songPosition in the audio tracks.
if (audioInstTrack != null) audioInstTrack.time = scrollPositionInMs + playheadPositionInMs;

View file

@ -66,7 +66,7 @@ class StageBuilderState extends MusicBeatState
// snd.addEventListener(SampleDataEvent.SAMPLE_DATA, sineShit);
// snd.__buffer.
// snd = Assets.getSound(Paths.music('freakyMenu'));
// snd = Assets.getSound(Paths.music('freakyMenu/freakyMenu'));
// for (thing in snd.load)
// thing = Std.int(thing / 2);
// snd.play();

View file

@ -122,6 +122,93 @@ class Constants
*/
public static final DEFAULT_VARIATION:String = 'default';
/**
* The default intensity for camera zooms.
*/
public static final DEFAULT_ZOOM_INTENSITY:Float = 0.015;
/**
* The default rate for camera zooms (in beats per zoom).
*/
public static final DEFAULT_ZOOM_RATE:Int = 4;
/**
* The default BPM for charts, so things don't break if none is specified.
*/
public static final DEFAULT_BPM:Int = 100;
/**
* Default numerator for the time signature.
*/
public static final DEFAULT_TIME_SIGNATURE_NUM:Int = 4;
/**
* Default denominator for the time signature.
*/
public static final DEFAULT_TIME_SIGNATURE_DEN:Int = 4;
/**
* TIMING
*/
// ==============================
/**
* A magic number used when calculating scroll speed and note distances.
*/
public static final PIXELS_PER_MS:Float = 0.45;
/**
* The maximum interval within which a note can be hit, in milliseconds.
*/
public static final HIT_WINDOW_MS:Float = 160.0;
/**
* Constant for the number of seconds in a minute.
*/
public static final SECS_PER_MIN:Int = 60;
/**
* Constant for the number of milliseconds in a second.
*/
public static final MS_PER_SEC:Int = 1000;
/**
* The number of microseconds in a millisecond.
*/
public static final US_PER_MS:Int = 1000;
/**
* The number of microseconds in a second.
*/
public static final US_PER_SEC:Int = US_PER_MS * MS_PER_SEC;
/**
* The number of nanoseconds in a microsecond.
*/
public static final NS_PER_US:Int = 1000;
/**
* The number of nanoseconds in a millisecond.
*/
public static final NS_PER_MS:Int = NS_PER_US * US_PER_MS;
/**
* The number of nanoseconds in a second.
*/
public static final NS_PER_SEC:Int = NS_PER_US * US_PER_MS * MS_PER_SEC;
/**
* Number of steps in a beat.
* One step is one 16th note and one beat is one quarter note.
*/
public static final STEPS_PER_BEAT:Int = 4;
/**
* All MP3 decoders introduce a playback delay of `528` samples,
* which at 44,100 Hz (samples per second) is ~12 ms.
*/
public static final MP3_DELAY_MS:Float = 528 / 44100 * Constants.MS_PER_SEC;
/**
* HEALTH VALUES
*/
@ -201,80 +288,14 @@ class Constants
*/
public static final GHOST_TAPPING:Bool = false;
/**
* TIMING
*/
// ==============================
public static final HIT_WINDOW_MS:Int = 160;
public static final PIXELS_PER_MS:Float = 0.45;
/**
* The number of seconds in a minute.
*/
public static final SECS_PER_MIN:Int = 60;
/**
* The number of milliseconds in a second.
*/
public static final MS_PER_SEC:Int = 1000;
/**
* The number of microseconds in a millisecond.
*/
public static final US_PER_MS:Int = 1000;
/**
* The number of microseconds in a second.
*/
public static final US_PER_SEC:Int = US_PER_MS * MS_PER_SEC;
/**
* The number of nanoseconds in a microsecond.
*/
public static final NS_PER_US:Int = 1000;
/**
* The number of nanoseconds in a millisecond.
*/
public static final NS_PER_MS:Int = NS_PER_US * US_PER_MS;
/**
* The number of nanoseconds in a second.
*/
public static final NS_PER_SEC:Int = NS_PER_US * US_PER_MS * MS_PER_SEC;
/**
* All MP3 decoders introduce a playback delay of `528` samples,
* which at 44,100 Hz (samples per second) is ~12 ms.
*/
public static final MP3_DELAY_MS:Float = 528 / 44100 * MS_PER_SEC;
/**
* The default BPM of the conductor.
*/
public static final DEFAULT_BPM:Float = 100.0;
/**
* The default numerator for the time signature.
*/
public static final DEFAULT_TIME_SIGNATURE_NUM:Int = 4;
/**
* The default denominator for the time signature.
*/
public static final DEFAULT_TIME_SIGNATURE_DEN:Int = 4;
/**
* Number of steps in a beat.
* One step is one 16th note and one beat is one quarter note.
*/
public static final STEPS_PER_BEAT:Int = 4;
/**
* OTHER
*/
// ==============================
/**
* The separator between an asset library and the asset path.
*/
public static final LIBRARY_SEPARATOR:String = ':';
/**
@ -287,16 +308,13 @@ class Constants
*/
public static final COUNTDOWN_VOLUME:Float = 0.6;
/**
* The horizontal offset of the strumline from the left edge of the screen.
*/
public static final STRUMLINE_X_OFFSET:Float = 48;
/**
* The vertical offset of the strumline from the top edge of the screen.
*/
public static final STRUMLINE_Y_OFFSET:Float = 24;
/**
* The default intensity for camera zooms.
*/
public static final DEFAULT_ZOOM_INTENSITY:Float = 0.015;
/**
* The default rate for camera zooms (in beats per zoom).
*/
public static final DEFAULT_ZOOM_RATE:Int = 4;
}

View file

@ -28,22 +28,15 @@ class SortUtil
return FlxSort.byValues(order, a.noteData.time, b.noteData.time);
}
public static inline function alphabetically(a:String, b:String)
/**
* Sort predicate for sorting strings alphabetically.
*/
public static function alphabetically(a:String, b:String)
{
a = a.toUpperCase();
b = b.toUpperCase();
if (a < b)
{
return -1;
}
else if (a > b)
{
return 1;
}
else
{
return 0;
}
// Sort alphabetically. Yes that's how this works.
return a == b ? 0 : a > b ? 1 : -1;
}
}