mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2025-04-21 19:31:52 -04:00
Merge branch 'rewrite/master' into feature/chart-editor-event-tooltips
This commit is contained in:
commit
210088a25d
119 changed files with 4446 additions and 2359 deletions
Project.xmlhmm.json
source
Main.hx
funkin
Conductor.hxHighscore.hxInitState.hx
audio
data
character
conversation
dialogue
event
level
song
speaker
stage
graphics
modding
play
Countdown.hxGameOverSubState.hxPlayState.hx
character
components
event
FocusCameraSongEvent.hxPlayAnimationSongEvent.hxSetCameraBopSongEvent.hxSongEvent.hxZoomCameraSongEvent.hx
notes
scoring
song
stage
save
ui
AtlasText.hxMusicBeatState.hxMusicBeatSubState.hx
debug
charting
ChartEditorState.hx
commands
AddEventsCommand.hxAddNotesCommand.hxChangeStartingBPMCommand.hxChartEditorCommand.hxCopyItemsCommand.hxCutItemsCommand.hxDeselectAllItemsCommand.hxDeselectItemsCommand.hxExtendNoteLengthCommand.hxFlipNotesCommand.hxInvertSelectedItemsCommand.hxMoveEventsCommand.hxMoveItemsCommand.hxMoveNotesCommand.hxPasteItemsCommand.hxRemoveEventsCommand.hxRemoveItemsCommand.hxRemoveNotesCommand.hxSelectAllItemsCommand.hxSelectItemsCommand.hxSetItemSelectionCommand.hxSwitchDifficultyCommand.hx
components
ChartEditorEventSprite.hxChartEditorHoldNoteSprite.hxChartEditorMeasureTicks.hxChartEditorNotePreview.hxChartEditorSelectionSquareSprite.hx
contextmenus
ChartEditorBaseContextMenu.hxChartEditorDefaultContextMenu.hxChartEditorEventContextMenu.hxChartEditorHoldNoteContextMenu.hxChartEditorNoteContextMenu.hxChartEditorSelectionContextMenu.hx
handlers
ChartEditorAudioHandler.hxChartEditorContextMenuHandler.hxChartEditorDialogHandler.hxChartEditorImportExportHandler.hxChartEditorShortcutHandler.hxChartEditorThemeHandler.hxChartEditorToolboxHandler.hx
import.hxtoolboxes
util
latency
stage
freeplay
|
@ -111,6 +111,7 @@
|
|||
<haxelib name="tink_json" /> <!-- JSON parsing (DEPRECATED) -->
|
||||
<haxelib name="thx.semver" /> <!-- Version string handling -->
|
||||
|
||||
<haxelib name="hmm" /> <!-- Read library version data at compile time so it can be baked into logs -->
|
||||
<haxelib name="hxcpp-debug-server" if="desktop debug" /> <!-- VSCode debug support -->
|
||||
|
||||
<!--Disable the Flixel core focus lost screen-->
|
||||
|
@ -127,11 +128,11 @@
|
|||
<haxeflag name="-w" value="-WDeprecated" />
|
||||
|
||||
<!-- Haxe 4.3.0+: Enable pretty syntax errors and stuff. -->
|
||||
<haxedef name="message-reporting" value="pretty" />
|
||||
<haxedef name="message.reporting" value="pretty" />
|
||||
|
||||
<!-- _________________________________ Custom _______________________________ -->
|
||||
<!-- Disable trace() calls in release builds to bump up performance. -->
|
||||
<haxeflag name="--no-traces" unless="debug" />
|
||||
<!-- Disable trace() calls in release builds to bump up performance.
|
||||
<haxeflag name="- -no-traces" unless="debug" />-->
|
||||
<!-- HScript relies heavily on Reflection, which means we can't use DCE. -->
|
||||
<haxeflag name="-dce no" />
|
||||
<!-- Ensure all Funkin' classes are available at runtime. -->
|
||||
|
|
12
hmm.json
12
hmm.json
|
@ -11,7 +11,7 @@
|
|||
"name": "flixel",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "a83738673e7edbf8acba3a1426af284dfe6719fe",
|
||||
"ref": "07c6018008801972d12275690fc144fcc22e3de6",
|
||||
"url": "https://github.com/FunkinCrew/flixel"
|
||||
},
|
||||
{
|
||||
|
@ -37,7 +37,7 @@
|
|||
"name": "flxanimate",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "d7c5621be742e2c98d523dfe5af7528835eaff1e",
|
||||
"ref": "9bacdd6ea39f5e3a33b0f5dfb7bc583fe76060d4",
|
||||
"url": "https://github.com/FunkinCrew/flxanimate"
|
||||
},
|
||||
{
|
||||
|
@ -54,14 +54,14 @@
|
|||
"name": "haxeui-core",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "67c5700e253ff8892589a95945a7799f34ae4df0",
|
||||
"ref": "bb904f8b4b205755a310c23ff25219f9dcd62711",
|
||||
"url": "https://github.com/haxeui/haxeui-core"
|
||||
},
|
||||
{
|
||||
"name": "haxeui-flixel",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "2b9cff727999b53ed292b1675ac1c9089ac77600",
|
||||
"ref": "1ec470c297afd7758a90dc9399aa1e3a4ea6ca0b",
|
||||
"url": "https://github.com/haxeui/haxeui-flixel"
|
||||
},
|
||||
{
|
||||
|
@ -107,7 +107,7 @@
|
|||
"name": "lime",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "737b86f121cdc90358d59e2e527934f267c94a2c",
|
||||
"ref": "fff39ba6fc64969cd51987ef7491d9345043dc5d",
|
||||
"url": "https://github.com/FunkinCrew/lime"
|
||||
},
|
||||
{
|
||||
|
@ -149,7 +149,7 @@
|
|||
"name": "polymod",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "80d1d309803c1b111866524f9769325e3b8b0b1b",
|
||||
"ref": "cb11a95d0159271eb3587428cf4b9602e46dc469",
|
||||
"url": "https://github.com/larsiusprime/polymod"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -112,5 +112,6 @@ class Main extends Sprite
|
|||
Toolkit.theme = 'dark'; // don't be cringe
|
||||
Toolkit.autoScale = false;
|
||||
funkin.input.Cursor.registerHaxeUICursors();
|
||||
haxe.ui.tooltips.ToolTipManager.defaultDelay = 200;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import funkin.data.song.SongDataUtils;
|
|||
* A core class which handles musical timing throughout the game,
|
||||
* both in gameplay and in menus.
|
||||
*/
|
||||
@:nullSafety
|
||||
class Conductor
|
||||
{
|
||||
// onBeatHit is called every quarter note
|
||||
|
@ -28,29 +29,53 @@ class Conductor
|
|||
// 60 BPM = 240 sixteenth notes per minute = 4 onStepHit per second
|
||||
// 7/8 = 3.5 beats per measure = 14 steps per measure
|
||||
|
||||
/**
|
||||
* The current instance of the Conductor.
|
||||
* If one doesn't currently exist, a new one will be created.
|
||||
*
|
||||
* You can also do stuff like store a reference to the Conductor and pass it around or temporarily replace it,
|
||||
* or have a second Conductor running at the same time, or other weird stuff like that if you need to.
|
||||
*/
|
||||
public static var instance:Conductor = new Conductor();
|
||||
|
||||
/**
|
||||
* Signal fired when the current Conductor instance advances to a new measure.
|
||||
*/
|
||||
public static var measureHit(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* Signal fired when the current Conductor instance advances to a new beat.
|
||||
*/
|
||||
public static var beatHit(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* Signal fired when the current Conductor instance advances to a new step.
|
||||
*/
|
||||
public static var stepHit(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* The list of time changes in the song.
|
||||
* There should be at least one time change (at the beginning of the song) to define the BPM.
|
||||
*/
|
||||
static var timeChanges:Array<SongTimeChange> = [];
|
||||
var timeChanges:Array<SongTimeChange> = [];
|
||||
|
||||
/**
|
||||
* The most recent time change for the current song position.
|
||||
*/
|
||||
public static var currentTimeChange(default, null):Null<SongTimeChange>;
|
||||
public var currentTimeChange(default, null):Null<SongTimeChange>;
|
||||
|
||||
/**
|
||||
* The current position in the song in milliseconds.
|
||||
* Update this every frame based on the audio position using `Conductor.update()`.
|
||||
* Update this every frame based on the audio position using `Conductor.instance.update()`.
|
||||
*/
|
||||
public static var songPosition(default, null):Float = 0;
|
||||
public var songPosition(default, null):Float = 0;
|
||||
|
||||
/**
|
||||
* Beats per minute of the current song at the current time.
|
||||
*/
|
||||
public static var bpm(get, never):Float;
|
||||
public var bpm(get, never):Float;
|
||||
|
||||
static function get_bpm():Float
|
||||
function get_bpm():Float
|
||||
{
|
||||
if (bpmOverride != null) return bpmOverride;
|
||||
|
||||
|
@ -62,9 +87,9 @@ class Conductor
|
|||
/**
|
||||
* Beats per minute of the current song at the start time.
|
||||
*/
|
||||
public static var startingBPM(get, never):Float;
|
||||
public var startingBPM(get, never):Float;
|
||||
|
||||
static function get_startingBPM():Float
|
||||
function get_startingBPM():Float
|
||||
{
|
||||
if (bpmOverride != null) return bpmOverride;
|
||||
|
||||
|
@ -78,14 +103,14 @@ class Conductor
|
|||
* The current value set by `forceBPM`.
|
||||
* If false, BPM is determined by time changes.
|
||||
*/
|
||||
static var bpmOverride:Null<Float> = null;
|
||||
var bpmOverride:Null<Float> = null;
|
||||
|
||||
/**
|
||||
* Duration of a measure in milliseconds. Calculated based on bpm.
|
||||
*/
|
||||
public static var measureLengthMs(get, never):Float;
|
||||
public var measureLengthMs(get, never):Float;
|
||||
|
||||
static function get_measureLengthMs():Float
|
||||
function get_measureLengthMs():Float
|
||||
{
|
||||
return beatLengthMs * timeSignatureNumerator;
|
||||
}
|
||||
|
@ -93,9 +118,9 @@ class Conductor
|
|||
/**
|
||||
* Duration of a beat (quarter note) in milliseconds. Calculated based on bpm.
|
||||
*/
|
||||
public static var beatLengthMs(get, never):Float;
|
||||
public var beatLengthMs(get, never):Float;
|
||||
|
||||
static function get_beatLengthMs():Float
|
||||
function get_beatLengthMs():Float
|
||||
{
|
||||
// Tied directly to BPM.
|
||||
return ((Constants.SECS_PER_MIN / bpm) * Constants.MS_PER_SEC);
|
||||
|
@ -104,25 +129,25 @@ class Conductor
|
|||
/**
|
||||
* Duration of a step (sixtennth note) in milliseconds. Calculated based on bpm.
|
||||
*/
|
||||
public static var stepLengthMs(get, never):Float;
|
||||
public var stepLengthMs(get, never):Float;
|
||||
|
||||
static function get_stepLengthMs():Float
|
||||
function get_stepLengthMs():Float
|
||||
{
|
||||
return beatLengthMs / timeSignatureNumerator;
|
||||
}
|
||||
|
||||
public static var timeSignatureNumerator(get, never):Int;
|
||||
public var timeSignatureNumerator(get, never):Int;
|
||||
|
||||
static function get_timeSignatureNumerator():Int
|
||||
function get_timeSignatureNumerator():Int
|
||||
{
|
||||
if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_NUM;
|
||||
|
||||
return currentTimeChange.timeSignatureNum;
|
||||
}
|
||||
|
||||
public static var timeSignatureDenominator(get, never):Int;
|
||||
public var timeSignatureDenominator(get, never):Int;
|
||||
|
||||
static function get_timeSignatureDenominator():Int
|
||||
function get_timeSignatureDenominator():Int
|
||||
{
|
||||
if (currentTimeChange == null) return Constants.DEFAULT_TIME_SIGNATURE_DEN;
|
||||
|
||||
|
@ -132,44 +157,44 @@ class Conductor
|
|||
/**
|
||||
* Current position in the song, in measures.
|
||||
*/
|
||||
public static var currentMeasure(default, null):Int = 0;
|
||||
public var currentMeasure(default, null):Int = 0;
|
||||
|
||||
/**
|
||||
* Current position in the song, in beats.
|
||||
*/
|
||||
public static var currentBeat(default, null):Int = 0;
|
||||
public var currentBeat(default, null):Int = 0;
|
||||
|
||||
/**
|
||||
* Current position in the song, in steps.
|
||||
*/
|
||||
public static var currentStep(default, null):Int = 0;
|
||||
public var currentStep(default, null):Int = 0;
|
||||
|
||||
/**
|
||||
* Current position in the song, in measures and fractions of a measure.
|
||||
*/
|
||||
public static var currentMeasureTime(default, null):Float = 0;
|
||||
public var currentMeasureTime(default, null):Float = 0;
|
||||
|
||||
/**
|
||||
* Current position in the song, in beats and fractions of a measure.
|
||||
*/
|
||||
public static var currentBeatTime(default, null):Float = 0;
|
||||
public var currentBeatTime(default, null):Float = 0;
|
||||
|
||||
/**
|
||||
* Current position in the song, in steps and fractions of a step.
|
||||
*/
|
||||
public static var currentStepTime(default, null):Float = 0;
|
||||
public var currentStepTime(default, null):Float = 0;
|
||||
|
||||
/**
|
||||
* An offset tied to the current chart file to compensate for a delay in the instrumental.
|
||||
*/
|
||||
public static var instrumentalOffset:Float = 0;
|
||||
public var instrumentalOffset:Float = 0;
|
||||
|
||||
/**
|
||||
* The instrumental offset, in terms of steps.
|
||||
*/
|
||||
public static var instrumentalOffsetSteps(get, never):Float;
|
||||
public var instrumentalOffsetSteps(get, never):Float;
|
||||
|
||||
static function get_instrumentalOffsetSteps():Float
|
||||
function get_instrumentalOffsetSteps():Float
|
||||
{
|
||||
var startingStepLengthMs:Float = ((Constants.SECS_PER_MIN / startingBPM) * Constants.MS_PER_SEC) / timeSignatureNumerator;
|
||||
|
||||
|
@ -179,19 +204,19 @@ class Conductor
|
|||
/**
|
||||
* An offset tied to the file format of the audio file being played.
|
||||
*/
|
||||
public static var formatOffset:Float = 0;
|
||||
public var formatOffset:Float = 0;
|
||||
|
||||
/**
|
||||
* An offset set by the user to compensate for input lag.
|
||||
*/
|
||||
public static var inputOffset:Float = 0;
|
||||
public var inputOffset:Float = 0;
|
||||
|
||||
/**
|
||||
* The number of beats in a measure. May be fractional depending on the time signature.
|
||||
*/
|
||||
public static var beatsPerMeasure(get, never):Float;
|
||||
public var beatsPerMeasure(get, never):Float;
|
||||
|
||||
static function get_beatsPerMeasure():Float
|
||||
function get_beatsPerMeasure():Float
|
||||
{
|
||||
// NOTE: Not always an integer, for example 7/8 is 3.5 beats per measure
|
||||
return stepsPerMeasure / Constants.STEPS_PER_BEAT;
|
||||
|
@ -201,30 +226,15 @@ class Conductor
|
|||
* The number of steps in a measure.
|
||||
* TODO: I don't think this can be fractional?
|
||||
*/
|
||||
public static var stepsPerMeasure(get, never):Int;
|
||||
public var stepsPerMeasure(get, never):Int;
|
||||
|
||||
static function get_stepsPerMeasure():Int
|
||||
function get_stepsPerMeasure():Int
|
||||
{
|
||||
// TODO: Is this always an integer?
|
||||
return Std.int(timeSignatureNumerator / timeSignatureDenominator * Constants.STEPS_PER_BEAT * Constants.STEPS_PER_BEAT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Signal fired when the Conductor advances to a new measure.
|
||||
*/
|
||||
public static var measureHit(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* Signal fired when the Conductor advances to a new beat.
|
||||
*/
|
||||
public static var beatHit(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
/**
|
||||
* Signal fired when the Conductor advances to a new step.
|
||||
*/
|
||||
public static var stepHit(default, null):FlxSignal = new FlxSignal();
|
||||
|
||||
function new() {}
|
||||
public function new() {}
|
||||
|
||||
/**
|
||||
* Forcibly defines the current BPM of the song.
|
||||
|
@ -235,7 +245,7 @@ class Conductor
|
|||
* WARNING: Avoid this for things like setting the BPM of the title screen music,
|
||||
* you should have a metadata file for it instead.
|
||||
*/
|
||||
public static function forceBPM(?bpm:Float = null)
|
||||
public function forceBPM(?bpm:Float = null)
|
||||
{
|
||||
if (bpm != null)
|
||||
{
|
||||
|
@ -246,7 +256,7 @@ class Conductor
|
|||
// trace('[CONDUCTOR] Resetting BPM to default');
|
||||
}
|
||||
|
||||
Conductor.bpmOverride = bpm;
|
||||
this.bpmOverride = bpm;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -256,29 +266,29 @@ class Conductor
|
|||
* @param songPosition The current position in the song in milliseconds.
|
||||
* Leave blank to use the FlxG.sound.music position.
|
||||
*/
|
||||
public static function update(?songPosition:Float)
|
||||
public function update(?songPos:Float)
|
||||
{
|
||||
if (songPosition == null)
|
||||
if (songPos == null)
|
||||
{
|
||||
// Take into account instrumental and file format song offsets.
|
||||
songPosition = (FlxG.sound.music != null) ? (FlxG.sound.music.time + instrumentalOffset + formatOffset) : 0.0;
|
||||
songPos = (FlxG.sound.music != null) ? (FlxG.sound.music.time + instrumentalOffset + formatOffset) : 0.0;
|
||||
}
|
||||
|
||||
var oldMeasure = currentMeasure;
|
||||
var oldBeat = currentBeat;
|
||||
var oldStep = currentStep;
|
||||
var oldMeasure = this.currentMeasure;
|
||||
var oldBeat = this.currentBeat;
|
||||
var oldStep = this.currentStep;
|
||||
|
||||
// Set the song position we are at (for purposes of calculating note positions, etc).
|
||||
Conductor.songPosition = songPosition;
|
||||
this.songPosition = songPos;
|
||||
|
||||
currentTimeChange = timeChanges[0];
|
||||
if (Conductor.songPosition > 0.0)
|
||||
if (this.songPosition > 0.0)
|
||||
{
|
||||
for (i in 0...timeChanges.length)
|
||||
{
|
||||
if (songPosition >= timeChanges[i].timeStamp) currentTimeChange = timeChanges[i];
|
||||
if (this.songPosition >= timeChanges[i].timeStamp) currentTimeChange = timeChanges[i];
|
||||
|
||||
if (songPosition < timeChanges[i].timeStamp) break;
|
||||
if (this.songPosition < timeChanges[i].timeStamp) break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -286,45 +296,49 @@ class Conductor
|
|||
{
|
||||
trace('WARNING: Conductor is broken, timeChanges is empty.');
|
||||
}
|
||||
else if (currentTimeChange != null && Conductor.songPosition > 0.0)
|
||||
else if (currentTimeChange != null && this.songPosition > 0.0)
|
||||
{
|
||||
// roundDecimal prevents representing 8 as 7.9999999
|
||||
currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) + (songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6);
|
||||
currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
|
||||
currentMeasureTime = currentStepTime / stepsPerMeasure;
|
||||
currentStep = Math.floor(currentStepTime);
|
||||
currentBeat = Math.floor(currentBeatTime);
|
||||
currentMeasure = Math.floor(currentMeasureTime);
|
||||
this.currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) + (this.songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6);
|
||||
this.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
|
||||
this.currentMeasureTime = currentStepTime / stepsPerMeasure;
|
||||
this.currentStep = Math.floor(currentStepTime);
|
||||
this.currentBeat = Math.floor(currentBeatTime);
|
||||
this.currentMeasure = Math.floor(currentMeasureTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Assume a constant BPM equal to the forced value.
|
||||
currentStepTime = FlxMath.roundDecimal((songPosition / stepLengthMs), 4);
|
||||
currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
|
||||
currentMeasureTime = currentStepTime / stepsPerMeasure;
|
||||
currentStep = Math.floor(currentStepTime);
|
||||
currentBeat = Math.floor(currentBeatTime);
|
||||
currentMeasure = Math.floor(currentMeasureTime);
|
||||
this.currentStepTime = FlxMath.roundDecimal((songPosition / stepLengthMs), 4);
|
||||
this.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
|
||||
this.currentMeasureTime = currentStepTime / stepsPerMeasure;
|
||||
this.currentStep = Math.floor(currentStepTime);
|
||||
this.currentBeat = Math.floor(currentBeatTime);
|
||||
this.currentMeasure = Math.floor(currentMeasureTime);
|
||||
}
|
||||
|
||||
// FlxSignals are really cool.
|
||||
if (currentStep != oldStep)
|
||||
// Only fire the signal if we are THE Conductor.
|
||||
if (this == Conductor.instance)
|
||||
{
|
||||
stepHit.dispatch();
|
||||
}
|
||||
// FlxSignals are really cool.
|
||||
if (currentStep != oldStep)
|
||||
{
|
||||
Conductor.stepHit.dispatch();
|
||||
}
|
||||
|
||||
if (currentBeat != oldBeat)
|
||||
{
|
||||
beatHit.dispatch();
|
||||
}
|
||||
if (currentBeat != oldBeat)
|
||||
{
|
||||
Conductor.beatHit.dispatch();
|
||||
}
|
||||
|
||||
if (currentMeasure != oldMeasure)
|
||||
{
|
||||
measureHit.dispatch();
|
||||
if (currentMeasure != oldMeasure)
|
||||
{
|
||||
Conductor.measureHit.dispatch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function mapTimeChanges(songTimeChanges:Array<SongTimeChange>)
|
||||
public function mapTimeChanges(songTimeChanges:Array<SongTimeChange>)
|
||||
{
|
||||
timeChanges = [];
|
||||
|
||||
|
@ -338,24 +352,21 @@ class Conductor
|
|||
// Without any custom handling, `currentStepTime` becomes non-zero at `songPosition = 0`.
|
||||
if (currentTimeChange.timeStamp < 0.0) currentTimeChange.timeStamp = 0.0;
|
||||
|
||||
if (currentTimeChange.beatTime == null)
|
||||
if (currentTimeChange.timeStamp <= 0.0)
|
||||
{
|
||||
if (currentTimeChange.timeStamp <= 0.0)
|
||||
{
|
||||
currentTimeChange.beatTime = 0.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Calculate the beat time of this timestamp.
|
||||
currentTimeChange.beatTime = 0.0;
|
||||
currentTimeChange.beatTime = 0.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Calculate the beat time of this timestamp.
|
||||
currentTimeChange.beatTime = 0.0;
|
||||
|
||||
if (currentTimeChange.timeStamp > 0.0 && timeChanges.length > 0)
|
||||
{
|
||||
var prevTimeChange:SongTimeChange = timeChanges[timeChanges.length - 1];
|
||||
currentTimeChange.beatTime = FlxMath.roundDecimal(prevTimeChange.beatTime
|
||||
+ ((currentTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC),
|
||||
4);
|
||||
}
|
||||
if (currentTimeChange.timeStamp > 0.0 && timeChanges.length > 0)
|
||||
{
|
||||
var prevTimeChange:SongTimeChange = timeChanges[timeChanges.length - 1];
|
||||
currentTimeChange.beatTime = FlxMath.roundDecimal(prevTimeChange.beatTime
|
||||
+ ((currentTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC),
|
||||
4);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -368,13 +379,13 @@ class Conductor
|
|||
}
|
||||
|
||||
// Update currentStepTime
|
||||
Conductor.update(Conductor.songPosition);
|
||||
this.update(Conductor.instance.songPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a time in milliseconds, return a time in steps.
|
||||
*/
|
||||
public static function getTimeInSteps(ms:Float):Float
|
||||
public function getTimeInSteps(ms:Float):Float
|
||||
{
|
||||
if (timeChanges.length == 0)
|
||||
{
|
||||
|
@ -411,7 +422,7 @@ class Conductor
|
|||
/**
|
||||
* Given a time in steps and fractional steps, return a time in milliseconds.
|
||||
*/
|
||||
public static function getStepTimeInMs(stepTime:Float):Float
|
||||
public function getStepTimeInMs(stepTime:Float):Float
|
||||
{
|
||||
if (timeChanges.length == 0)
|
||||
{
|
||||
|
@ -447,7 +458,7 @@ class Conductor
|
|||
/**
|
||||
* Given a time in beats and fractional beats, return a time in milliseconds.
|
||||
*/
|
||||
public static function getBeatTimeInMs(beatTime:Float):Float
|
||||
public function getBeatTimeInMs(beatTime:Float):Float
|
||||
{
|
||||
if (timeChanges.length == 0)
|
||||
{
|
||||
|
@ -480,13 +491,20 @@ class Conductor
|
|||
}
|
||||
}
|
||||
|
||||
public static function watchQuick():Void
|
||||
{
|
||||
FlxG.watch.addQuick("songPosition", Conductor.instance.songPosition);
|
||||
FlxG.watch.addQuick("bpm", Conductor.instance.bpm);
|
||||
FlxG.watch.addQuick("currentMeasureTime", Conductor.instance.currentMeasureTime);
|
||||
FlxG.watch.addQuick("currentBeatTime", Conductor.instance.currentBeatTime);
|
||||
FlxG.watch.addQuick("currentStepTime", Conductor.instance.currentStepTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the Conductor, replacing the current instance with a fresh one.
|
||||
*/
|
||||
public static function reset():Void
|
||||
{
|
||||
beatHit.removeAll();
|
||||
stepHit.removeAll();
|
||||
|
||||
mapTimeChanges([]);
|
||||
forceBPM(null);
|
||||
update(0);
|
||||
Conductor.instance = new Conductor();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ abstract Tallies(RawTallies)
|
|||
bad: 0,
|
||||
good: 0,
|
||||
sick: 0,
|
||||
killer: 0,
|
||||
totalNotes: 0,
|
||||
totalNotesHit: 0,
|
||||
maxCombo: 0,
|
||||
|
@ -43,7 +42,6 @@ typedef RawTallies =
|
|||
var bad:Int;
|
||||
var good:Int;
|
||||
var sick:Int;
|
||||
var killer:Int;
|
||||
var maxCombo:Int;
|
||||
var isNewHighscore:Bool;
|
||||
|
||||
|
|
|
@ -19,12 +19,12 @@ import funkin.play.PlayStatePlaylist;
|
|||
import openfl.display.BitmapData;
|
||||
import funkin.data.level.LevelRegistry;
|
||||
import funkin.data.notestyle.NoteStyleRegistry;
|
||||
import funkin.data.event.SongEventData.SongEventParser;
|
||||
import funkin.data.event.SongEventRegistry;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
import funkin.play.cutscene.dialogue.ConversationDataParser;
|
||||
import funkin.play.cutscene.dialogue.DialogueBoxDataParser;
|
||||
import funkin.play.cutscene.dialogue.SpeakerDataParser;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.play.stage.StageData.StageDataParser;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.modding.module.ModuleHandler;
|
||||
import funkin.ui.title.TitleState;
|
||||
|
@ -197,6 +197,13 @@ class InitState extends FlxState
|
|||
FlxG.android.preventDefaultKeys = [flixel.input.android.FlxAndroidKey.BACK];
|
||||
#end
|
||||
|
||||
//
|
||||
// FLIXEL PLUGINS
|
||||
//
|
||||
funkin.util.plugins.EvacuateDebugPlugin.initialize();
|
||||
funkin.util.plugins.ReloadAssetsDebugPlugin.initialize();
|
||||
funkin.util.plugins.WatchPlugin.initialize();
|
||||
|
||||
//
|
||||
// GAME DATA PARSING
|
||||
//
|
||||
|
@ -206,12 +213,13 @@ class InitState extends FlxState
|
|||
SongRegistry.instance.loadEntries();
|
||||
LevelRegistry.instance.loadEntries();
|
||||
NoteStyleRegistry.instance.loadEntries();
|
||||
SongEventParser.loadEventCache();
|
||||
SongEventRegistry.loadEventCache();
|
||||
ConversationDataParser.loadConversationCache();
|
||||
DialogueBoxDataParser.loadDialogueBoxCache();
|
||||
SpeakerDataParser.loadSpeakerCache();
|
||||
StageDataParser.loadStageCache();
|
||||
StageRegistry.instance.loadEntries();
|
||||
CharacterDataParser.loadCharacterCache();
|
||||
|
||||
ModuleHandler.buildModuleCallbacks();
|
||||
ModuleHandler.loadModuleCache();
|
||||
|
||||
|
|
|
@ -107,6 +107,26 @@ class FunkinSound extends FlxSound
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user clicks to focus on the window.
|
||||
*/
|
||||
override function onFocus():Void
|
||||
{
|
||||
if (!_alreadyPaused && this._shouldPlay)
|
||||
{
|
||||
resume();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the user tabs away from the window.
|
||||
*/
|
||||
override function onFocusLost():Void
|
||||
{
|
||||
_alreadyPaused = _paused;
|
||||
pause();
|
||||
}
|
||||
|
||||
public override function resume():FunkinSound
|
||||
{
|
||||
if (this._time < 0)
|
||||
|
|
|
@ -132,6 +132,12 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
|
|||
});
|
||||
}
|
||||
|
||||
public override function destroy()
|
||||
{
|
||||
stop();
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all sounds from the group.
|
||||
*/
|
||||
|
|
|
@ -64,7 +64,7 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
|
|||
|
||||
if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, vis.numSamples));
|
||||
else
|
||||
remappedShit = Std.int(FlxMath.remapToRange(Conductor.songPosition, 0, vis.snd.length, 0, vis.numSamples));
|
||||
remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, vis.numSamples));
|
||||
|
||||
var fftSamples:Array<Float> = [];
|
||||
|
||||
|
|
|
@ -164,7 +164,7 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
|
|||
|
||||
if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, numSamples));
|
||||
else
|
||||
remappedShit = Std.int(FlxMath.remapToRange(Conductor.songPosition, 0, vis.snd.length, 0, numSamples));
|
||||
remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, numSamples));
|
||||
|
||||
var fftSamples:Array<Float> = [];
|
||||
var i = remappedShit;
|
||||
|
@ -235,15 +235,15 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
|
|||
if (vis.snd.playing) remappedShit = Std.int(FlxMath.remapToRange(vis.snd.time, 0, vis.snd.length, 0, numSamples));
|
||||
else
|
||||
{
|
||||
if (curTime == Conductor.songPosition)
|
||||
if (curTime == Conductor.instance.songPosition)
|
||||
{
|
||||
wavOptimiz = 3;
|
||||
return; // already did shit, so finishes function early
|
||||
}
|
||||
|
||||
curTime = Conductor.songPosition;
|
||||
curTime = Conductor.instance.songPosition;
|
||||
|
||||
remappedShit = Std.int(FlxMath.remapToRange(Conductor.songPosition, 0, vis.snd.length, 0, numSamples));
|
||||
remappedShit = Std.int(FlxMath.remapToRange(Conductor.instance.songPosition, 0, vis.snd.length, 0, numSamples));
|
||||
}
|
||||
|
||||
wavOptimiz = 8;
|
||||
|
|
0
source/funkin/data/character/TODO.md
Normal file
0
source/funkin/data/character/TODO.md
Normal file
0
source/funkin/data/conversation/TODO.md
Normal file
0
source/funkin/data/conversation/TODO.md
Normal file
0
source/funkin/data/dialogue/TODO.md
Normal file
0
source/funkin/data/dialogue/TODO.md
Normal file
|
@ -1,7 +1,7 @@
|
|||
package funkin.data.event;
|
||||
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.data.event.SongEventData.SongEventSchema;
|
||||
import funkin.data.event.SongEventSchema;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.util.macro.ClassMacro;
|
||||
import funkin.play.event.ScriptedSongEvent;
|
||||
|
@ -9,7 +9,7 @@ import funkin.play.event.ScriptedSongEvent;
|
|||
/**
|
||||
* This class statically handles the parsing of internal and scripted song event handlers.
|
||||
*/
|
||||
class SongEventParser
|
||||
class SongEventRegistry
|
||||
{
|
||||
/**
|
||||
* Every built-in event class must be added to this list.
|
||||
|
@ -160,147 +160,3 @@ class SongEventParser
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@:forward(name, title, type, keys, min, max, step, defaultValue, iterator)
|
||||
abstract SongEventSchema(SongEventSchemaRaw)
|
||||
{
|
||||
public function new(?fields:Array<SongEventSchemaField>)
|
||||
{
|
||||
this = fields;
|
||||
}
|
||||
|
||||
@:arrayAccess
|
||||
public function getByName(name:String):SongEventSchemaField
|
||||
{
|
||||
for (field in this)
|
||||
{
|
||||
if (field.name == name) return field;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getFirstField():SongEventSchemaField
|
||||
{
|
||||
return this[0];
|
||||
}
|
||||
|
||||
public function stringifyFieldValue(name:String, value:Dynamic):String
|
||||
{
|
||||
var field:SongEventSchemaField = getByName(name);
|
||||
if (field == null) return 'Unknown';
|
||||
|
||||
switch (field.type)
|
||||
{
|
||||
case SongEventFieldType.STRING:
|
||||
return Std.string(value);
|
||||
case SongEventFieldType.INTEGER:
|
||||
return Std.string(value);
|
||||
case SongEventFieldType.FLOAT:
|
||||
return Std.string(value);
|
||||
case SongEventFieldType.BOOL:
|
||||
return Std.string(value);
|
||||
case SongEventFieldType.ENUM:
|
||||
for (key in field.keys.keys())
|
||||
{
|
||||
if (field.keys.get(key) == value) return key;
|
||||
}
|
||||
return Std.string(value);
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
@:arrayAccess
|
||||
public inline function get(key:Int)
|
||||
{
|
||||
return this[key];
|
||||
}
|
||||
|
||||
@:arrayAccess
|
||||
public inline function arrayWrite(k:Int, v:SongEventSchemaField):SongEventSchemaField
|
||||
{
|
||||
return this[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
typedef SongEventSchemaRaw = Array<SongEventSchemaField>;
|
||||
|
||||
typedef SongEventSchemaField =
|
||||
{
|
||||
/**
|
||||
* The name of the property as it should be saved in the event data.
|
||||
*/
|
||||
name:String,
|
||||
|
||||
/**
|
||||
* The title of the field to display in the UI.
|
||||
*/
|
||||
title:String,
|
||||
|
||||
/**
|
||||
* The type of the field.
|
||||
*/
|
||||
type:SongEventFieldType,
|
||||
|
||||
/**
|
||||
* Used only for ENUM values.
|
||||
* The key is the display name and the value is the actual value.
|
||||
*/
|
||||
?keys:Map<String, Dynamic>,
|
||||
|
||||
/**
|
||||
* Used for INTEGER and FLOAT values.
|
||||
* The minimum value that can be entered.
|
||||
* @default No minimum
|
||||
*/
|
||||
?min:Float,
|
||||
|
||||
/**
|
||||
* Used for INTEGER and FLOAT values.
|
||||
* The maximum value that can be entered.
|
||||
* @default No maximum
|
||||
*/
|
||||
?max:Float,
|
||||
|
||||
/**
|
||||
* Used for INTEGER and FLOAT values.
|
||||
* The step value that will be used when incrementing/decrementing the value.
|
||||
* @default `0.1`
|
||||
*/
|
||||
?step:Float,
|
||||
|
||||
/**
|
||||
* An optional default value for the field.
|
||||
*/
|
||||
?defaultValue:Dynamic,
|
||||
}
|
||||
|
||||
enum abstract SongEventFieldType(String) from String to String
|
||||
{
|
||||
/**
|
||||
* The STRING type will display as a text field.
|
||||
*/
|
||||
var STRING = "string";
|
||||
|
||||
/**
|
||||
* The INTEGER type will display as a text field that only accepts numbers.
|
||||
*/
|
||||
var INTEGER = "integer";
|
||||
|
||||
/**
|
||||
* The FLOAT type will display as a text field that only accepts numbers.
|
||||
*/
|
||||
var FLOAT = "float";
|
||||
|
||||
/**
|
||||
* The BOOL type will display as a checkbox.
|
||||
*/
|
||||
var BOOL = "bool";
|
||||
|
||||
/**
|
||||
* The ENUM type will display as a dropdown.
|
||||
* Make sure to specify the `keys` field in the schema.
|
||||
*/
|
||||
var ENUM = "enum";
|
||||
}
|
125
source/funkin/data/event/SongEventSchema.hx
Normal file
125
source/funkin/data/event/SongEventSchema.hx
Normal file
|
@ -0,0 +1,125 @@
|
|||
package funkin.data.event;
|
||||
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.data.event.SongEventSchema;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.util.macro.ClassMacro;
|
||||
import funkin.play.event.ScriptedSongEvent;
|
||||
|
||||
@:forward(name, tittlte, type, keys, min, max, step, defaultValue, iterator)
|
||||
abstract SongEventSchema(SongEventSchemaRaw)
|
||||
{
|
||||
public function new(?fields:Array<SongEventSchemaField>)
|
||||
{
|
||||
this = fields;
|
||||
}
|
||||
|
||||
@:arrayAccess
|
||||
public inline function getByName(name:String):SongEventSchemaField
|
||||
{
|
||||
for (field in this)
|
||||
{
|
||||
if (field.name == name) return field;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getFirstField():SongEventSchemaField
|
||||
{
|
||||
return this[0];
|
||||
}
|
||||
|
||||
@:arrayAccess
|
||||
public inline function get(key:Int)
|
||||
{
|
||||
return this[key];
|
||||
}
|
||||
|
||||
@:arrayAccess
|
||||
public inline function arrayWrite(k:Int, v:SongEventSchemaField):SongEventSchemaField
|
||||
{
|
||||
return this[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
typedef SongEventSchemaRaw = Array<SongEventSchemaField>;
|
||||
|
||||
typedef SongEventSchemaField =
|
||||
{
|
||||
/**
|
||||
* The name of the property as it should be saved in the event data.
|
||||
*/
|
||||
name:String,
|
||||
|
||||
/**
|
||||
* The title of the field to display in the UI.
|
||||
*/
|
||||
title:String,
|
||||
|
||||
/**
|
||||
* The type of the field.
|
||||
*/
|
||||
type:SongEventFieldType,
|
||||
|
||||
/**
|
||||
* Used only for ENUM values.
|
||||
* The key is the display name and the value is the actual value.
|
||||
*/
|
||||
?keys:Map<String, Dynamic>,
|
||||
|
||||
/**
|
||||
* Used for INTEGER and FLOAT values.
|
||||
* The minimum value that can be entered.
|
||||
* @default No minimum
|
||||
*/
|
||||
?min:Float,
|
||||
|
||||
/**
|
||||
* Used for INTEGER and FLOAT values.
|
||||
* The maximum value that can be entered.
|
||||
* @default No maximum
|
||||
*/
|
||||
?max:Float,
|
||||
|
||||
/**
|
||||
* Used for INTEGER and FLOAT values.
|
||||
* The step value that will be used when incrementing/decrementing the value.
|
||||
* @default `0.1`
|
||||
*/
|
||||
?step:Float,
|
||||
|
||||
/**
|
||||
* An optional default value for the field.
|
||||
*/
|
||||
?defaultValue:Dynamic,
|
||||
}
|
||||
|
||||
enum abstract SongEventFieldType(String) from String to String
|
||||
{
|
||||
/**
|
||||
* The STRING type will display as a text field.
|
||||
*/
|
||||
var STRING = "string";
|
||||
|
||||
/**
|
||||
* The INTEGER type will display as a text field that only accepts numbers.
|
||||
*/
|
||||
var INTEGER = "integer";
|
||||
|
||||
/**
|
||||
* The FLOAT type will display as a text field that only accepts numbers.
|
||||
*/
|
||||
var FLOAT = "float";
|
||||
|
||||
/**
|
||||
* The BOOL type will display as a checkbox.
|
||||
*/
|
||||
var BOOL = "bool";
|
||||
|
||||
/**
|
||||
* The ENUM type will display as a dropdown.
|
||||
* Make sure to specify the `keys` field in the schema.
|
||||
*/
|
||||
var ENUM = "enum";
|
||||
}
|
|
@ -7,9 +7,9 @@ import funkin.ui.story.ScriptedLevel;
|
|||
class LevelRegistry extends BaseRegistry<Level, LevelData>
|
||||
{
|
||||
/**
|
||||
* The current version string for the stage data format.
|
||||
* The current version string for the level data format.
|
||||
* Handle breaking changes by incrementing this value
|
||||
* and adding migration to the `migrateStageData()` function.
|
||||
* and adding migration to the `migrateLevelData()` function.
|
||||
*/
|
||||
public static final LEVEL_DATA_VERSION:thx.semver.Version = "1.0.0";
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
package funkin.data.song;
|
||||
|
||||
import funkin.data.event.SongEventRegistry;
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.data.event.SongEventData.SongEventParser;
|
||||
import funkin.data.event.SongEventData.SongEventSchema;
|
||||
import funkin.data.event.SongEventSchema;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import thx.semver.Version;
|
||||
import funkin.util.tools.ICloneable;
|
||||
|
||||
/**
|
||||
* Data containing information about a song.
|
||||
|
@ -12,7 +13,7 @@ import thx.semver.Version;
|
|||
* Data which is only necessary in-game should be stored in the SongChartData.
|
||||
*/
|
||||
@:nullSafety
|
||||
class SongMetadata
|
||||
class SongMetadata implements ICloneable<SongMetadata>
|
||||
{
|
||||
/**
|
||||
* A semantic versioning string for the song data format.
|
||||
|
@ -38,10 +39,11 @@ class SongMetadata
|
|||
public var looped:Bool;
|
||||
|
||||
/**
|
||||
* Instrumental and vocal offsets. Optional, defaults to 0.
|
||||
* Instrumental and vocal offsets.
|
||||
* Defaults to an empty SongOffsets object.
|
||||
*/
|
||||
@:optional
|
||||
public var offsets:SongOffsets;
|
||||
public var offsets:Null<SongOffsets>;
|
||||
|
||||
/**
|
||||
* Data relating to the song's gameplay.
|
||||
|
@ -87,16 +89,16 @@ class SongMetadata
|
|||
* @param newVariation Set to a new variation ID to change the new metadata.
|
||||
* @return The cloned SongMetadata
|
||||
*/
|
||||
public function clone(?newVariation:String = null):SongMetadata
|
||||
public function clone():SongMetadata
|
||||
{
|
||||
var result:SongMetadata = new SongMetadata(this.songName, this.artist, newVariation == null ? this.variation : newVariation);
|
||||
var result:SongMetadata = new SongMetadata(this.songName, this.artist, this.variation);
|
||||
result.version = this.version;
|
||||
result.timeFormat = this.timeFormat;
|
||||
result.divisions = this.divisions;
|
||||
result.offsets = this.offsets;
|
||||
result.timeChanges = this.timeChanges;
|
||||
result.offsets = this.offsets != null ? this.offsets.clone() : new SongOffsets(); // if no song offsets found (aka null), so just create new ones
|
||||
result.timeChanges = this.timeChanges.deepClone();
|
||||
result.looped = this.looped;
|
||||
result.playData = this.playData;
|
||||
result.playData = this.playData.clone();
|
||||
result.generatedBy = this.generatedBy;
|
||||
|
||||
return result;
|
||||
|
@ -131,7 +133,7 @@ enum abstract SongTimeFormat(String) from String to String
|
|||
var MILLISECONDS = 'ms';
|
||||
}
|
||||
|
||||
class SongTimeChange
|
||||
class SongTimeChange implements ICloneable<SongTimeChange>
|
||||
{
|
||||
public static final DEFAULT_SONGTIMECHANGE:SongTimeChange = new SongTimeChange(0, 100);
|
||||
|
||||
|
@ -152,7 +154,7 @@ class SongTimeChange
|
|||
*/
|
||||
@:optional
|
||||
@:alias("b")
|
||||
public var beatTime:Null<Float>;
|
||||
public var beatTime:Float;
|
||||
|
||||
/**
|
||||
* Quarter notes per minute (float). Cannot be empty in the first element of the list,
|
||||
|
@ -198,6 +200,11 @@ class SongTimeChange
|
|||
this.beatTuplets = beatTuplets == null ? DEFAULT_BEAT_TUPLETS : beatTuplets;
|
||||
}
|
||||
|
||||
public function clone():SongTimeChange
|
||||
{
|
||||
return new SongTimeChange(this.timeStamp, this.bpm, this.timeSignatureNum, this.timeSignatureDen, this.beatTime, this.beatTuplets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
|
@ -212,7 +219,7 @@ class SongTimeChange
|
|||
* These are intended to correct for issues with the chart, or with the song's audio (for example a 10ms delay before the song starts).
|
||||
* This is independent of the offsets applied in the user's settings, which are applied after these offsets and intended to correct for the user's hardware.
|
||||
*/
|
||||
class SongOffsets
|
||||
class SongOffsets implements ICloneable<SongOffsets>
|
||||
{
|
||||
/**
|
||||
* The offset, in milliseconds, to apply to the song's instrumental relative to the chart.
|
||||
|
@ -282,6 +289,15 @@ class SongOffsets
|
|||
return value;
|
||||
}
|
||||
|
||||
public function clone():SongOffsets
|
||||
{
|
||||
var result:SongOffsets = new SongOffsets(this.instrumental);
|
||||
result.altInstrumentals = this.altInstrumentals.clone();
|
||||
result.vocals = this.vocals.clone();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
|
@ -295,7 +311,7 @@ class SongOffsets
|
|||
* Metadata for a song only used for the music.
|
||||
* For example, the menu music.
|
||||
*/
|
||||
class SongMusicData
|
||||
class SongMusicData implements ICloneable<SongMusicData>
|
||||
{
|
||||
/**
|
||||
* A semantic versioning string for the song data format.
|
||||
|
@ -349,13 +365,13 @@ class SongMusicData
|
|||
this.variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
}
|
||||
|
||||
public function clone(?newVariation:String = null):SongMusicData
|
||||
public function clone():SongMusicData
|
||||
{
|
||||
var result:SongMusicData = new SongMusicData(this.songName, this.artist, newVariation == null ? this.variation : newVariation);
|
||||
var result:SongMusicData = new SongMusicData(this.songName, this.artist, this.variation);
|
||||
result.version = this.version;
|
||||
result.timeFormat = this.timeFormat;
|
||||
result.divisions = this.divisions;
|
||||
result.timeChanges = this.timeChanges;
|
||||
result.timeChanges = this.timeChanges.clone();
|
||||
result.looped = this.looped;
|
||||
result.generatedBy = this.generatedBy;
|
||||
|
||||
|
@ -371,7 +387,7 @@ class SongMusicData
|
|||
}
|
||||
}
|
||||
|
||||
class SongPlayData
|
||||
class SongPlayData implements ICloneable<SongPlayData>
|
||||
{
|
||||
/**
|
||||
* The variations this song has. The associated metadata files should exist.
|
||||
|
@ -420,6 +436,20 @@ class SongPlayData
|
|||
ratings = new Map<String, Int>();
|
||||
}
|
||||
|
||||
public function clone():SongPlayData
|
||||
{
|
||||
var result:SongPlayData = new SongPlayData();
|
||||
result.songVariations = this.songVariations.clone();
|
||||
result.difficulties = this.difficulties.clone();
|
||||
result.characters = this.characters.clone();
|
||||
result.stage = this.stage;
|
||||
result.noteStyle = this.noteStyle;
|
||||
result.ratings = this.ratings.clone();
|
||||
result.album = this.album;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
|
@ -433,7 +463,7 @@ class SongPlayData
|
|||
* Information about the characters used in this variation of the song.
|
||||
* Create a new variation if you want to change the characters.
|
||||
*/
|
||||
class SongCharacterData
|
||||
class SongCharacterData implements ICloneable<SongCharacterData>
|
||||
{
|
||||
@:optional
|
||||
@:default('')
|
||||
|
@ -463,6 +493,14 @@ class SongCharacterData
|
|||
this.instrumental = instrumental;
|
||||
}
|
||||
|
||||
public function clone():SongCharacterData
|
||||
{
|
||||
var result:SongCharacterData = new SongCharacterData(this.player, this.girlfriend, this.opponent, this.instrumental);
|
||||
result.altInstrumentals = this.altInstrumentals.clone();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
|
@ -472,7 +510,7 @@ class SongCharacterData
|
|||
}
|
||||
}
|
||||
|
||||
class SongChartData
|
||||
class SongChartData implements ICloneable<SongChartData>
|
||||
{
|
||||
@:default(funkin.data.song.SongRegistry.SONG_CHART_DATA_VERSION)
|
||||
@:jcustomparse(funkin.data.DataParse.semverVersion)
|
||||
|
@ -542,6 +580,24 @@ class SongChartData
|
|||
return writer.write(this, pretty ? ' ' : null);
|
||||
}
|
||||
|
||||
public function clone():SongChartData
|
||||
{
|
||||
// We have to manually perform the deep clone here because Map.deepClone() doesn't work.
|
||||
var noteDataClone:Map<String, Array<SongNoteData>> = new Map<String, Array<SongNoteData>>();
|
||||
for (key in this.notes.keys())
|
||||
{
|
||||
noteDataClone.set(key, this.notes.get(key).deepClone());
|
||||
}
|
||||
var eventDataClone:Array<SongEventData> = this.events.deepClone();
|
||||
|
||||
var result:SongChartData = new SongChartData(this.scrollSpeed.clone(), eventDataClone, noteDataClone);
|
||||
result.version = this.version;
|
||||
result.generatedBy = this.generatedBy;
|
||||
result.variation = this.variation;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a string representation suitable for debugging.
|
||||
*/
|
||||
|
@ -551,7 +607,7 @@ class SongChartData
|
|||
}
|
||||
}
|
||||
|
||||
class SongEventDataRaw
|
||||
class SongEventDataRaw implements ICloneable<SongEventDataRaw>
|
||||
{
|
||||
/**
|
||||
* The timestamp of the event. The timestamp is in the format of the song's time format.
|
||||
|
@ -605,14 +661,19 @@ class SongEventDataRaw
|
|||
{
|
||||
if (_stepTime != null && !force) return _stepTime;
|
||||
|
||||
return _stepTime = Conductor.getTimeInSteps(this.time);
|
||||
return _stepTime = Conductor.instance.getTimeInSteps(this.time);
|
||||
}
|
||||
|
||||
public function clone():SongEventDataRaw
|
||||
{
|
||||
return new SongEventDataRaw(this.time, this.event, this.value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap SongEventData in an abstract so we can overload operators.
|
||||
*/
|
||||
@:forward(time, event, value, activated, getStepTime)
|
||||
@:forward(time, event, value, activated, getStepTime, clone)
|
||||
abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
|
||||
{
|
||||
public function new(time:Float, event:String, value:Dynamic = null)
|
||||
|
@ -644,12 +705,12 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
|
|||
|
||||
public inline function getHandler():Null<SongEvent>
|
||||
{
|
||||
return SongEventParser.getEvent(this.event);
|
||||
return SongEventRegistry.getEvent(this.event);
|
||||
}
|
||||
|
||||
public inline function getSchema():Null<SongEventSchema>
|
||||
{
|
||||
return SongEventParser.getEventSchema(this.event);
|
||||
return SongEventRegistry.getEventSchema(this.event);
|
||||
}
|
||||
|
||||
public inline function getDynamic(key:String):Null<Dynamic>
|
||||
|
@ -715,7 +776,9 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
|
|||
var value = pair.value;
|
||||
|
||||
var title = eventSchema.getByName(key)?.title ?? 'UnknownField';
|
||||
var valueStr = eventSchema.stringifyFieldValue(key, value);
|
||||
|
||||
if (eventSchema.stringifyFieldValue(key, value) != null) trace(eventSchema.stringifyFieldValue(key, value));
|
||||
var valueStr = eventSchema.stringifyFieldValue(key, value) ?? 'UnknownValue';
|
||||
|
||||
result += '\n- ${title}: ${valueStr}';
|
||||
}
|
||||
|
@ -773,7 +836,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
|
|||
}
|
||||
}
|
||||
|
||||
class SongNoteDataRaw
|
||||
class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
|
||||
{
|
||||
/**
|
||||
* The timestamp of the note. The timestamp is in the format of the song's time format.
|
||||
|
@ -803,7 +866,13 @@ class SongNoteDataRaw
|
|||
@:alias("l")
|
||||
@:default(0)
|
||||
@:optional
|
||||
public var length:Float;
|
||||
public var length(default, set):Float;
|
||||
|
||||
function set_length(value:Float):Float
|
||||
{
|
||||
_stepLength = null;
|
||||
return length = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The kind of the note.
|
||||
|
@ -857,9 +926,14 @@ class SongNoteDataRaw
|
|||
{
|
||||
if (_stepTime != null && !force) return _stepTime;
|
||||
|
||||
return _stepTime = Conductor.getTimeInSteps(this.time);
|
||||
return _stepTime = Conductor.instance.getTimeInSteps(this.time);
|
||||
}
|
||||
|
||||
/**
|
||||
* The length of the note, if applicable, in steps.
|
||||
* Calculated from the length and the BPM.
|
||||
* Cached for performance. Set to `null` to recalculate.
|
||||
*/
|
||||
@:jignored
|
||||
var _stepLength:Null<Float> = null;
|
||||
|
||||
|
@ -873,7 +947,7 @@ class SongNoteDataRaw
|
|||
|
||||
if (_stepLength != null && !force) return _stepLength;
|
||||
|
||||
return _stepLength = Conductor.getTimeInSteps(this.time + this.length) - getStepTime();
|
||||
return _stepLength = Conductor.instance.getTimeInSteps(this.time + this.length) - getStepTime();
|
||||
}
|
||||
|
||||
public function setStepLength(value:Float):Void
|
||||
|
@ -884,11 +958,21 @@ class SongNoteDataRaw
|
|||
}
|
||||
else
|
||||
{
|
||||
var lengthMs:Float = Conductor.getStepTimeInMs(value) - this.time;
|
||||
var endStep:Float = getStepTime() + value;
|
||||
var endMs:Float = Conductor.instance.getStepTimeInMs(endStep);
|
||||
var lengthMs:Float = endMs - this.time;
|
||||
|
||||
this.length = lengthMs;
|
||||
}
|
||||
|
||||
// Recalculate the step length next time it's requested.
|
||||
_stepLength = null;
|
||||
}
|
||||
|
||||
public function clone():SongNoteDataRaw
|
||||
{
|
||||
return new SongNoteDataRaw(this.time, this.data, this.length, this.kind);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -952,6 +1036,10 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
|
|||
@:op(A == B)
|
||||
public function op_equals(other:SongNoteData):Bool
|
||||
{
|
||||
// Handle the case where one value is null.
|
||||
if (this == null) return other == null;
|
||||
if (other == null) return false;
|
||||
|
||||
if (this.kind == '')
|
||||
{
|
||||
if (other.kind != '' && other.kind != 'normal') return false;
|
||||
|
@ -967,6 +1055,10 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
|
|||
@:op(A != B)
|
||||
public function op_notEquals(other:SongNoteData):Bool
|
||||
{
|
||||
// Handle the case where one value is null.
|
||||
if (this == null) return other == null;
|
||||
if (other == null) return false;
|
||||
|
||||
if (this.kind == '')
|
||||
{
|
||||
if (other.kind != '' && other.kind != 'normal') return true;
|
||||
|
@ -982,24 +1074,32 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
|
|||
@:op(A > B)
|
||||
public function op_greaterThan(other:SongNoteData):Bool
|
||||
{
|
||||
if (other == null) return false;
|
||||
|
||||
return this.time > other.time;
|
||||
}
|
||||
|
||||
@:op(A < B)
|
||||
public function op_lessThan(other:SongNoteData):Bool
|
||||
{
|
||||
if (other == null) return false;
|
||||
|
||||
return this.time < other.time;
|
||||
}
|
||||
|
||||
@:op(A >= B)
|
||||
public function op_greaterThanOrEquals(other:SongNoteData):Bool
|
||||
{
|
||||
if (other == null) return false;
|
||||
|
||||
return this.time >= other.time;
|
||||
}
|
||||
|
||||
@:op(A <= B)
|
||||
public function op_lessThanOrEquals(other:SongNoteData):Bool
|
||||
{
|
||||
if (other == null) return false;
|
||||
|
||||
return this.time <= other.time;
|
||||
}
|
||||
|
||||
|
|
|
@ -273,7 +273,7 @@ class SongDataUtils
|
|||
}
|
||||
|
||||
/**
|
||||
* Filter a list of notes to only include notes whose data is within the given range.
|
||||
* Filter a list of notes to only include notes whose data is within the given range, inclusive.
|
||||
*/
|
||||
public static function getNotesInDataRange(notes:Array<SongNoteData>, start:Int, end:Int):Array<SongNoteData>
|
||||
{
|
||||
|
|
|
@ -12,6 +12,7 @@ import funkin.util.VersionUtil;
|
|||
|
||||
using funkin.data.song.migrator.SongDataMigrator;
|
||||
|
||||
@:nullSafety
|
||||
class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
||||
{
|
||||
/**
|
||||
|
@ -31,7 +32,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
|
||||
public static final SONG_MUSIC_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
|
||||
|
||||
public static var DEFAULT_GENERATEDBY(get, null):String;
|
||||
public static var DEFAULT_GENERATEDBY(get, never):String;
|
||||
|
||||
static function get_DEFAULT_GENERATEDBY():String
|
||||
{
|
||||
|
@ -88,7 +89,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
{
|
||||
try
|
||||
{
|
||||
var entry:Song = createEntry(entryId);
|
||||
var entry:Null<Song> = createEntry(entryId);
|
||||
if (entry != null)
|
||||
{
|
||||
trace(' Loaded entry data: ${entry}');
|
||||
|
@ -126,7 +127,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
|
||||
var parser = new json2object.JsonParser<SongMetadata>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
parser.ignoreUnknownVariables = true;
|
||||
|
||||
switch (loadEntryMetadataFile(id, variation))
|
||||
{
|
||||
|
@ -149,7 +150,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
|
||||
var parser = new json2object.JsonParser<SongMetadata>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
parser.ignoreUnknownVariables = true;
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
|
@ -209,7 +210,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
|
||||
var parser = new json2object.JsonParser<SongMetadata_v2_1_0>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
parser.ignoreUnknownVariables = true;
|
||||
|
||||
switch (loadEntryMetadataFile(id, variation))
|
||||
{
|
||||
|
@ -231,7 +232,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
|
||||
var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
parser.ignoreUnknownVariables = true;
|
||||
|
||||
switch (loadEntryMetadataFile(id, variation))
|
||||
{
|
||||
|
@ -251,7 +252,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
function parseEntryMetadataRaw_v2_1_0(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
|
||||
{
|
||||
var parser = new json2object.JsonParser<SongMetadata_v2_1_0>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
parser.ignoreUnknownVariables = true;
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
|
@ -265,7 +266,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
function parseEntryMetadataRaw_v2_0_0(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
|
||||
{
|
||||
var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
parser.ignoreUnknownVariables = true;
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
|
@ -346,7 +347,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
|
||||
var parser = new json2object.JsonParser<SongChartData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
parser.ignoreUnknownVariables = true;
|
||||
|
||||
switch (loadEntryChartFile(id, variation))
|
||||
{
|
||||
|
@ -369,7 +370,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
|
||||
var parser = new json2object.JsonParser<SongChartData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
parser.ignoreUnknownVariables = true;
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
|
@ -455,7 +456,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
{
|
||||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
var entryStr:Null<String> = loadEntryMetadataFile(id, variation)?.contents;
|
||||
var entryVersion:thx.semver.Version = VersionUtil.getVersionFromJSON(entryStr);
|
||||
var entryVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(entryStr);
|
||||
return entryVersion;
|
||||
}
|
||||
|
||||
|
@ -463,7 +464,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
|
|||
{
|
||||
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
|
||||
var entryStr:Null<String> = loadEntryChartFile(id, variation)?.contents;
|
||||
var entryVersion:thx.semver.Version = VersionUtil.getVersionFromJSON(entryStr);
|
||||
var entryVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(entryStr);
|
||||
return entryVersion;
|
||||
}
|
||||
|
||||
|
|
0
source/funkin/data/speaker/TODO.md
Normal file
0
source/funkin/data/speaker/TODO.md
Normal file
199
source/funkin/data/stage/StageData.hx
Normal file
199
source/funkin/data/stage/StageData.hx
Normal file
|
@ -0,0 +1,199 @@
|
|||
package funkin.data.stage;
|
||||
|
||||
import funkin.data.animation.AnimationData;
|
||||
|
||||
@:nullSafety
|
||||
class StageData
|
||||
{
|
||||
/**
|
||||
* The sematic version number of the stage data JSON format.
|
||||
* Supports fancy comparisons like NPM does it's neat.
|
||||
*/
|
||||
@:default(funkin.data.stage.StageRegistry.STAGE_DATA_VERSION)
|
||||
public var version:String;
|
||||
|
||||
public var name:String = 'Unknown';
|
||||
public var props:Array<StageDataProp> = [];
|
||||
public var characters:StageDataCharacters;
|
||||
|
||||
@:default(1.0)
|
||||
@:optional
|
||||
public var cameraZoom:Null<Float>;
|
||||
|
||||
public function new()
|
||||
{
|
||||
this.version = StageRegistry.STAGE_DATA_VERSION;
|
||||
this.characters = makeDefaultCharacters();
|
||||
}
|
||||
|
||||
function makeDefaultCharacters():StageDataCharacters
|
||||
{
|
||||
return {
|
||||
bf:
|
||||
{
|
||||
zIndex: 0,
|
||||
position: [0, 0],
|
||||
cameraOffsets: [-100, -100]
|
||||
},
|
||||
dad:
|
||||
{
|
||||
zIndex: 0,
|
||||
position: [0, 0],
|
||||
cameraOffsets: [100, -100]
|
||||
},
|
||||
gf:
|
||||
{
|
||||
zIndex: 0,
|
||||
position: [0, 0],
|
||||
cameraOffsets: [0, 0]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this StageData into a JSON string.
|
||||
*/
|
||||
public function serialize(pretty:Bool = true):String
|
||||
{
|
||||
var writer = new json2object.JsonWriter<StageData>();
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
}
|
||||
}
|
||||
|
||||
typedef StageDataCharacters =
|
||||
{
|
||||
var bf:StageDataCharacter;
|
||||
var dad:StageDataCharacter;
|
||||
var gf:StageDataCharacter;
|
||||
};
|
||||
|
||||
typedef StageDataProp =
|
||||
{
|
||||
/**
|
||||
* The name of the prop for later lookup by scripts.
|
||||
* Optional; if unspecified, the prop can't be referenced by scripts.
|
||||
*/
|
||||
@:optional
|
||||
var name:String;
|
||||
|
||||
/**
|
||||
* The asset used to display the prop.
|
||||
* NOTE: As of Stage data v1.0.1, you can also use a color here to create a rectangle, like "#ff0000".
|
||||
* In this case, the `scale` property will be used to determine the size of the prop.
|
||||
*/
|
||||
var assetPath:String;
|
||||
|
||||
/**
|
||||
* The position of the prop as an [x, y] array of two floats.
|
||||
*/
|
||||
var position:Array<Float>;
|
||||
|
||||
/**
|
||||
* A number determining the stack order of the prop, relative to other props and the characters in the stage.
|
||||
* Props with lower numbers render below those with higher numbers.
|
||||
* This is just like CSS, it isn't hard.
|
||||
* @default 0
|
||||
*/
|
||||
@:optional
|
||||
@:default(0)
|
||||
var zIndex:Int;
|
||||
|
||||
/**
|
||||
* If set to true, anti-aliasing will be forcibly disabled on the sprite.
|
||||
* This prevents blurry images on pixel-art levels.
|
||||
* @default false
|
||||
*/
|
||||
@:optional
|
||||
@:default(false)
|
||||
var isPixel:Bool;
|
||||
|
||||
/**
|
||||
* Either the scale of the prop as a float, or the [w, h] scale as an array of two floats.
|
||||
* Pro tip: On pixel-art levels, save the sprite small and set this value to 6 or so to save memory.
|
||||
*/
|
||||
@:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats)
|
||||
@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)
|
||||
@:optional
|
||||
var scale:haxe.ds.Either<Float, Array<Float>>;
|
||||
|
||||
/**
|
||||
* The alpha of the prop, as a float.
|
||||
* @default 1.0
|
||||
*/
|
||||
@:optional
|
||||
@:default(1.0)
|
||||
var alpha:Float;
|
||||
|
||||
/**
|
||||
* If not zero, this prop will play an animation every X beats of the song.
|
||||
* This requires animations to be defined. If `danceLeft` and `danceRight` are defined,
|
||||
* they will alternated between, otherwise the `idle` animation will be used.
|
||||
*
|
||||
* @default 0
|
||||
*/
|
||||
@:default(0)
|
||||
@:optional
|
||||
var danceEvery:Int;
|
||||
|
||||
/**
|
||||
* How much the prop scrolls relative to the camera. Used to create a parallax effect.
|
||||
* Represented as an [x, y] array of two floats.
|
||||
* [1, 1] means the prop moves 1:1 with the camera.
|
||||
* [0.5, 0.5] means the prop half as much as the camera.
|
||||
* [0, 0] means the prop is not moved.
|
||||
* @default [0, 0]
|
||||
*/
|
||||
@:optional
|
||||
@:default([0, 0])
|
||||
var scroll:Array<Float>;
|
||||
|
||||
/**
|
||||
* An optional array of animations which the prop can play.
|
||||
* @default Prop has no animations.
|
||||
*/
|
||||
@:optional
|
||||
@:default([])
|
||||
var animations:Array<AnimationData>;
|
||||
|
||||
/**
|
||||
* If animations are used, this is the name of the animation to play first.
|
||||
* @default Don't play an animation.
|
||||
*/
|
||||
@:optional
|
||||
var startingAnimation:Null<String>;
|
||||
|
||||
/**
|
||||
* The animation type to use.
|
||||
* Options: "sparrow", "packer"
|
||||
* @default "sparrow"
|
||||
*/
|
||||
@:default("sparrow")
|
||||
@:optional
|
||||
var animType:String;
|
||||
};
|
||||
|
||||
typedef StageDataCharacter =
|
||||
{
|
||||
/**
|
||||
* A number determining the stack order of the character, relative to props and other characters in the stage.
|
||||
* Again, just like CSS.
|
||||
* @default 0
|
||||
*/
|
||||
@:optional
|
||||
@:default(0)
|
||||
var zIndex:Int;
|
||||
|
||||
/**
|
||||
* The position to render the character at.
|
||||
*/
|
||||
@:optional
|
||||
@:default([0, 0])
|
||||
var position:Array<Float>;
|
||||
|
||||
/**
|
||||
* The camera offsets to apply when focusing on the character on this stage.
|
||||
* @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF
|
||||
*/
|
||||
@:optional
|
||||
var cameraOffsets:Array<Float>;
|
||||
};
|
103
source/funkin/data/stage/StageRegistry.hx
Normal file
103
source/funkin/data/stage/StageRegistry.hx
Normal file
|
@ -0,0 +1,103 @@
|
|||
package funkin.data.stage;
|
||||
|
||||
import funkin.data.stage.StageData;
|
||||
import funkin.play.stage.Stage;
|
||||
import funkin.play.stage.ScriptedStage;
|
||||
|
||||
class StageRegistry extends BaseRegistry<Stage, StageData>
|
||||
{
|
||||
/**
|
||||
* The current version string for the stage data format.
|
||||
* Handle breaking changes by incrementing this value
|
||||
* and adding migration to the `migrateStageData()` function.
|
||||
*/
|
||||
public static final STAGE_DATA_VERSION:thx.semver.Version = "1.0.1";
|
||||
|
||||
public static final STAGE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
|
||||
|
||||
public static final instance:StageRegistry = new StageRegistry();
|
||||
|
||||
public function new()
|
||||
{
|
||||
super('STAGE', 'stages', STAGE_DATA_VERSION_RULE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read, parse, and validate the JSON data and produce the corresponding data object.
|
||||
*/
|
||||
public function parseEntryData(id:String):Null<StageData>
|
||||
{
|
||||
// JsonParser does not take type parameters,
|
||||
// otherwise this function would be in BaseRegistry.
|
||||
var parser = new json2object.JsonParser<StageData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
|
||||
switch (loadEntryFile(id))
|
||||
{
|
||||
case {fileName: fileName, contents: contents}:
|
||||
parser.fromJson(contents, fileName);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, id);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate the JSON data and produce the corresponding data object.
|
||||
*
|
||||
* NOTE: Must be implemented on the implementation class.
|
||||
* @param contents The JSON as a string.
|
||||
* @param fileName An optional file name for error reporting.
|
||||
*/
|
||||
public function parseEntryDataRaw(contents:String, ?fileName:String):Null<StageData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<StageData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, fileName);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
|
||||
function createScriptedEntry(clsName:String):Stage
|
||||
{
|
||||
return ScriptedStage.init(clsName, "unknown");
|
||||
}
|
||||
|
||||
function getScriptedClassNames():Array<String>
|
||||
{
|
||||
return ScriptedStage.listScriptClasses();
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of all the stages from the base game, in order.
|
||||
* TODO: Should this be hardcoded?
|
||||
*/
|
||||
public function listBaseGameStageIds():Array<String>
|
||||
{
|
||||
return [
|
||||
"mainStage", "spookyMansion", "phillyTrain", "limoRide", "mallXmas", "mallEvil", "school", "schoolEvil", "tankmanBattlefield", "phillyStreets",
|
||||
"phillyBlazin",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of all installed story weeks that are not from the base game.
|
||||
*/
|
||||
public function listModdedStageIds():Array<String>
|
||||
{
|
||||
return listEntryIds().filter(function(id:String):Bool {
|
||||
return listBaseGameStageIds().indexOf(id) == -1;
|
||||
});
|
||||
}
|
||||
}
|
53
source/funkin/graphics/FunkinSprite.hx
Normal file
53
source/funkin/graphics/FunkinSprite.hx
Normal file
|
@ -0,0 +1,53 @@
|
|||
package funkin.graphics;
|
||||
|
||||
import flixel.FlxSprite;
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.graphics.FlxGraphic;
|
||||
|
||||
/**
|
||||
* An FlxSprite with additional functionality.
|
||||
*/
|
||||
class FunkinSprite extends FlxSprite
|
||||
{
|
||||
/**
|
||||
* @param x Starting X position
|
||||
* @param y Starting Y position
|
||||
*/
|
||||
public function new(?x:Float = 0, ?y:Float = 0)
|
||||
{
|
||||
super(x, y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Acts similarly to `makeGraphic`, but with improved memory usage,
|
||||
* at the expense of not being able to paint onto the sprite.
|
||||
*
|
||||
* @param width The target width of the sprite.
|
||||
* @param height The target height of the sprite.
|
||||
* @param color The color to fill the sprite with.
|
||||
*/
|
||||
public function makeSolidColor(width:Int, height:Int, color:FlxColor = FlxColor.WHITE):FunkinSprite
|
||||
{
|
||||
var graphic:FlxGraphic = FlxG.bitmap.create(2, 2, color, false, 'solid#${color.toHexString(true, false)}');
|
||||
frames = graphic.imageFrame;
|
||||
scale.set(width / 2, height / 2);
|
||||
updateHitbox();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure scale is applied when cloning a sprite.
|
||||
* The default `clone()` method acts kinda weird TBH.
|
||||
* @return A clone of this sprite.
|
||||
*/
|
||||
public override function clone():FunkinSprite
|
||||
{
|
||||
var result = new FunkinSprite(this.x, this.y);
|
||||
result.frames = this.frames;
|
||||
result.scale.set(this.scale.x, this.scale.y);
|
||||
result.updateHitbox();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -4,11 +4,12 @@ import funkin.util.macro.ClassMacro;
|
|||
import funkin.modding.module.ModuleHandler;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.data.song.SongData;
|
||||
import funkin.play.stage.StageData;
|
||||
import funkin.data.stage.StageData;
|
||||
import polymod.Polymod;
|
||||
import polymod.backends.PolymodAssets.PolymodAssetType;
|
||||
import polymod.format.ParseRules.TextFileFormat;
|
||||
import funkin.data.event.SongEventData.SongEventParser;
|
||||
import funkin.data.event.SongEventRegistry;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
import funkin.util.FileUtil;
|
||||
import funkin.data.level.LevelRegistry;
|
||||
import funkin.data.notestyle.NoteStyleRegistry;
|
||||
|
@ -271,11 +272,11 @@ class PolymodHandler
|
|||
SongRegistry.instance.loadEntries();
|
||||
LevelRegistry.instance.loadEntries();
|
||||
NoteStyleRegistry.instance.loadEntries();
|
||||
SongEventParser.loadEventCache();
|
||||
SongEventRegistry.loadEventCache();
|
||||
ConversationDataParser.loadConversationCache();
|
||||
DialogueBoxDataParser.loadDialogueBoxCache();
|
||||
SpeakerDataParser.loadSpeakerCache();
|
||||
StageDataParser.loadStageCache();
|
||||
StageRegistry.instance.loadEntries();
|
||||
CharacterDataParser.loadCharacterCache();
|
||||
ModuleHandler.loadModuleCache();
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ class Countdown
|
|||
stopCountdown();
|
||||
|
||||
PlayState.instance.isInCountdown = true;
|
||||
Conductor.update(PlayState.instance.startTimestamp + Conductor.beatLengthMs * -5);
|
||||
Conductor.instance.update(PlayState.instance.startTimestamp + Conductor.instance.beatLengthMs * -5);
|
||||
// Handle onBeatHit events manually
|
||||
// @:privateAccess
|
||||
// PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0));
|
||||
|
@ -48,7 +48,7 @@ class Countdown
|
|||
// The timer function gets called based on the beat of the song.
|
||||
countdownTimer = new FlxTimer();
|
||||
|
||||
countdownTimer.start(Conductor.beatLengthMs / 1000, function(tmr:FlxTimer) {
|
||||
countdownTimer.start(Conductor.instance.beatLengthMs / 1000, function(tmr:FlxTimer) {
|
||||
if (PlayState.instance == null)
|
||||
{
|
||||
tmr.cancel();
|
||||
|
@ -158,7 +158,7 @@ class Countdown
|
|||
{
|
||||
stopCountdown();
|
||||
// This will trigger PlayState.startSong()
|
||||
Conductor.update(0);
|
||||
Conductor.instance.update(0);
|
||||
// PlayState.isInCountdown = false;
|
||||
}
|
||||
|
||||
|
@ -225,7 +225,7 @@ class Countdown
|
|||
countdownSprite.screenCenter();
|
||||
|
||||
// Fade sprite in, then out, then destroy it.
|
||||
FlxTween.tween(countdownSprite, {y: countdownSprite.y += 100, alpha: 0}, Conductor.beatLengthMs / 1000,
|
||||
FlxTween.tween(countdownSprite, {y: countdownSprite.y += 100, alpha: 0}, Conductor.instance.beatLengthMs / 1000,
|
||||
{
|
||||
ease: FlxEase.cubeInOut,
|
||||
onComplete: function(twn:FlxTween) {
|
||||
|
|
|
@ -7,6 +7,7 @@ import flixel.sound.FlxSound;
|
|||
import funkin.ui.story.StoryMenuState;
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.util.FlxTimer;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import funkin.ui.MusicBeatSubState;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.modding.events.ScriptEventDispatcher;
|
||||
|
@ -22,6 +23,12 @@ import funkin.play.character.BaseCharacter;
|
|||
*/
|
||||
class GameOverSubState extends MusicBeatSubState
|
||||
{
|
||||
/**
|
||||
* The currently active GameOverSubState.
|
||||
* There should be only one GameOverSubState in existance at a time, we can use a singleton.
|
||||
*/
|
||||
public static var instance:GameOverSubState = null;
|
||||
|
||||
/**
|
||||
* Which alternate animation on the character to use.
|
||||
* You can set this via script.
|
||||
|
@ -87,6 +94,13 @@ class GameOverSubState extends MusicBeatSubState
|
|||
|
||||
override public function create()
|
||||
{
|
||||
if (instance != null)
|
||||
{
|
||||
// TODO: Do something in this case? IDK.
|
||||
trace('WARNING: GameOverSubState instance already exists. This should not happen.');
|
||||
}
|
||||
instance = this;
|
||||
|
||||
super.create();
|
||||
|
||||
//
|
||||
|
@ -94,11 +108,12 @@ class GameOverSubState extends MusicBeatSubState
|
|||
//
|
||||
|
||||
// Add a black background to the screen.
|
||||
var bg = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
|
||||
var bg = new FunkinSprite().makeSolidColor(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
|
||||
// We make this transparent so that we can see the stage underneath during debugging,
|
||||
// but it's normally opaque.
|
||||
bg.alpha = transparent ? 0.25 : 1.0;
|
||||
bg.scrollFactor.set();
|
||||
bg.screenCenter();
|
||||
add(bg);
|
||||
|
||||
// Pluck Boyfriend from the PlayState and place him (in the same position) in the GameOverSubState.
|
||||
|
@ -129,7 +144,7 @@ class GameOverSubState extends MusicBeatSubState
|
|||
gameOverMusic.stop();
|
||||
|
||||
// The conductor now represents the BPM of the game over music.
|
||||
Conductor.update(0);
|
||||
Conductor.instance.update(0);
|
||||
}
|
||||
|
||||
var hasStartedAnimation:Bool = false;
|
||||
|
@ -204,7 +219,7 @@ class GameOverSubState extends MusicBeatSubState
|
|||
{
|
||||
// Match the conductor to the music.
|
||||
// This enables the stepHit and beatHit events.
|
||||
Conductor.update(gameOverMusic.time);
|
||||
Conductor.instance.update(gameOverMusic.time);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -220,6 +235,7 @@ class GameOverSubState extends MusicBeatSubState
|
|||
playJeffQuote();
|
||||
// Start music at lower volume
|
||||
startDeathMusic(0.2, false);
|
||||
boyfriend.playAnimation('deathLoop' + animationSuffix);
|
||||
}
|
||||
default:
|
||||
// Start music at normal volume once the initial death animation finishes.
|
||||
|
@ -280,10 +296,10 @@ class GameOverSubState extends MusicBeatSubState
|
|||
*/
|
||||
function startDeathMusic(?startingVolume:Float = 1, force:Bool = false):Void
|
||||
{
|
||||
var musicPath = Paths.music('gameOver' + musicSuffix);
|
||||
var musicPath = Paths.music('gameplay/gameover/gameOver' + musicSuffix);
|
||||
if (isEnding)
|
||||
{
|
||||
musicPath = Paths.music('gameOverEnd' + musicSuffix);
|
||||
musicPath = Paths.music('gameplay/gameover/gameOverEnd' + musicSuffix);
|
||||
}
|
||||
if (!gameOverMusic.playing || force)
|
||||
{
|
||||
|
@ -303,7 +319,7 @@ class GameOverSubState extends MusicBeatSubState
|
|||
public static function playBlueBalledSFX()
|
||||
{
|
||||
blueballed = true;
|
||||
FlxG.sound.play(Paths.sound('fnf_loss_sfx' + blueBallSuffix));
|
||||
FlxG.sound.play(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix));
|
||||
}
|
||||
|
||||
var playingJeffQuote:Bool = false;
|
||||
|
@ -326,6 +342,11 @@ class GameOverSubState extends MusicBeatSubState
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
public override function toString():String
|
||||
{
|
||||
return "GameOverSubState";
|
||||
}
|
||||
}
|
||||
|
||||
typedef GameOverParams =
|
||||
|
|
|
@ -42,7 +42,7 @@ import funkin.play.cutscene.dialogue.Conversation;
|
|||
import funkin.play.cutscene.dialogue.ConversationDataParser;
|
||||
import funkin.play.cutscene.VanillaCutscenes;
|
||||
import funkin.play.cutscene.VideoCutscene;
|
||||
import funkin.data.event.SongEventData.SongEventParser;
|
||||
import funkin.data.event.SongEventRegistry;
|
||||
import funkin.play.notes.NoteSprite;
|
||||
import funkin.play.notes.NoteDirection;
|
||||
import funkin.play.notes.Strumline;
|
||||
|
@ -50,11 +50,11 @@ import funkin.play.notes.SustainTrail;
|
|||
import funkin.play.scoring.Scoring;
|
||||
import funkin.play.song.Song;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongCharacterData;
|
||||
import funkin.play.stage.Stage;
|
||||
import funkin.play.stage.StageData.StageDataParser;
|
||||
import funkin.ui.transition.LoadingState;
|
||||
import funkin.play.components.PopUpStuff;
|
||||
import funkin.ui.options.PreferencesMenu;
|
||||
|
@ -561,15 +561,15 @@ class PlayState extends MusicBeatSubState
|
|||
}
|
||||
|
||||
// Prepare the Conductor.
|
||||
Conductor.forceBPM(null);
|
||||
Conductor.instance.forceBPM(null);
|
||||
|
||||
if (currentChart.offsets != null)
|
||||
{
|
||||
Conductor.instrumentalOffset = currentChart.offsets.getInstrumentalOffset();
|
||||
Conductor.instance.instrumentalOffset = currentChart.offsets.getInstrumentalOffset();
|
||||
}
|
||||
|
||||
Conductor.mapTimeChanges(currentChart.timeChanges);
|
||||
Conductor.update((Conductor.beatLengthMs * -5) + startTimestamp);
|
||||
Conductor.instance.mapTimeChanges(currentChart.timeChanges);
|
||||
Conductor.instance.update((Conductor.instance.beatLengthMs * -5) + startTimestamp);
|
||||
|
||||
// The song is now loaded. We can continue to initialize the play state.
|
||||
initCameras();
|
||||
|
@ -734,7 +734,7 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
// Reset music properly.
|
||||
|
||||
FlxG.sound.music.time = Math.max(0, startTimestamp - Conductor.instrumentalOffset);
|
||||
FlxG.sound.music.time = Math.max(0, startTimestamp - Conductor.instance.instrumentalOffset);
|
||||
FlxG.sound.music.pause();
|
||||
|
||||
if (!overrideMusic)
|
||||
|
@ -785,22 +785,22 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
if (isInCountdown)
|
||||
{
|
||||
Conductor.update(Conductor.songPosition + elapsed * 1000);
|
||||
if (Conductor.songPosition >= (startTimestamp)) startSong();
|
||||
Conductor.instance.update(Conductor.instance.songPosition + elapsed * 1000);
|
||||
if (Conductor.instance.songPosition >= (startTimestamp)) startSong();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Constants.EXT_SOUND == 'mp3')
|
||||
{
|
||||
Conductor.formatOffset = Constants.MP3_DELAY_MS;
|
||||
Conductor.instance.formatOffset = Constants.MP3_DELAY_MS;
|
||||
}
|
||||
else
|
||||
{
|
||||
Conductor.formatOffset = 0.0;
|
||||
Conductor.instance.formatOffset = 0.0;
|
||||
}
|
||||
|
||||
Conductor.update(); // Normal conductor update.
|
||||
Conductor.instance.update(); // Normal conductor update.
|
||||
}
|
||||
|
||||
var androidPause:Bool = false;
|
||||
|
@ -942,7 +942,7 @@ class PlayState extends MusicBeatSubState
|
|||
// TODO: Check that these work even when songPosition is less than 0.
|
||||
if (songEvents != null && songEvents.length > 0)
|
||||
{
|
||||
var songEventsToActivate:Array<SongEventData> = SongEventParser.queryEvents(songEvents, Conductor.songPosition);
|
||||
var songEventsToActivate:Array<SongEventData> = SongEventRegistry.queryEvents(songEvents, Conductor.instance.songPosition);
|
||||
|
||||
if (songEventsToActivate.length > 0)
|
||||
{
|
||||
|
@ -950,7 +950,7 @@ class PlayState extends MusicBeatSubState
|
|||
for (event in songEventsToActivate)
|
||||
{
|
||||
// If an event is trying to play, but it's over 5 seconds old, skip it.
|
||||
if (event.time - Conductor.songPosition < -5000)
|
||||
if (event.time - Conductor.instance.songPosition < -5000)
|
||||
{
|
||||
event.activated = true;
|
||||
continue;
|
||||
|
@ -961,7 +961,7 @@ class PlayState extends MusicBeatSubState
|
|||
// Calling event.cancelEvent() skips the event. Neat!
|
||||
if (!eventEvent.eventCanceled)
|
||||
{
|
||||
SongEventParser.handleEvent(event);
|
||||
SongEventRegistry.handleEvent(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1052,7 +1052,7 @@ class PlayState extends MusicBeatSubState
|
|||
if (startTimer.finished)
|
||||
{
|
||||
DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC, true,
|
||||
currentSongLengthMs - Conductor.songPosition);
|
||||
currentSongLengthMs - Conductor.instance.songPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -1076,12 +1076,12 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause)
|
||||
{
|
||||
if (Conductor.songPosition > 0.0) DiscordClient.changePresence(detailsText, currentSong.song
|
||||
if (Conductor.instance.songPosition > 0.0) DiscordClient.changePresence(detailsText, currentSong.song
|
||||
+ ' ('
|
||||
+ storyDifficultyText
|
||||
+ ')', iconRPC, true,
|
||||
currentSongLengthMs
|
||||
- Conductor.songPosition);
|
||||
- Conductor.instance.songPosition);
|
||||
else
|
||||
DiscordClient.changePresence(detailsText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC);
|
||||
}
|
||||
|
@ -1154,17 +1154,17 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
if (!startingSong
|
||||
&& FlxG.sound.music != null
|
||||
&& (Math.abs(FlxG.sound.music.time - (Conductor.songPosition + Conductor.instrumentalOffset)) > 200
|
||||
|| Math.abs(vocals.checkSyncError(Conductor.songPosition + Conductor.instrumentalOffset)) > 200))
|
||||
&& (Math.abs(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200
|
||||
|| Math.abs(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200))
|
||||
{
|
||||
trace("VOCALS NEED RESYNC");
|
||||
if (vocals != null) trace(vocals.checkSyncError(Conductor.songPosition + Conductor.instrumentalOffset));
|
||||
trace(FlxG.sound.music.time - (Conductor.songPosition + Conductor.instrumentalOffset));
|
||||
if (vocals != null) trace(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
|
||||
trace(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
|
||||
resyncVocals();
|
||||
}
|
||||
|
||||
if (iconP1 != null) iconP1.onStepHit(Std.int(Conductor.currentStep));
|
||||
if (iconP2 != null) iconP2.onStepHit(Std.int(Conductor.currentStep));
|
||||
if (iconP1 != null) iconP1.onStepHit(Std.int(Conductor.instance.currentStep));
|
||||
if (iconP2 != null) iconP2.onStepHit(Std.int(Conductor.instance.currentStep));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -1185,14 +1185,14 @@ class PlayState extends MusicBeatSubState
|
|||
}
|
||||
|
||||
// Only zoom camera if we are zoomed by less than 35%.
|
||||
if (FlxG.camera.zoom < (1.35 * defaultCameraZoom) && cameraZoomRate > 0 && Conductor.currentBeat % cameraZoomRate == 0)
|
||||
if (FlxG.camera.zoom < (1.35 * defaultCameraZoom) && cameraZoomRate > 0 && Conductor.instance.currentBeat % cameraZoomRate == 0)
|
||||
{
|
||||
// Zoom camera in (1.5%)
|
||||
FlxG.camera.zoom += cameraZoomIntensity * defaultCameraZoom;
|
||||
// Hud zooms double (3%)
|
||||
camHUD.zoom += hudCameraZoomIntensity * defaultHUDCameraZoom;
|
||||
}
|
||||
// trace('Not bopping camera: ${FlxG.camera.zoom} < ${(1.35 * defaultCameraZoom)} && ${cameraZoomRate} > 0 && ${Conductor.currentBeat} % ${cameraZoomRate} == ${Conductor.currentBeat % cameraZoomRate}}');
|
||||
// trace('Not bopping camera: ${FlxG.camera.zoom} < ${(1.35 * defaultCameraZoom)} && ${cameraZoomRate} > 0 && ${Conductor.instance.currentBeat} % ${cameraZoomRate} == ${Conductor.instance.currentBeat % cameraZoomRate}}');
|
||||
|
||||
// That combo milestones that got spoiled that one time.
|
||||
// Comes with NEAT visual and audio effects.
|
||||
|
@ -1205,13 +1205,13 @@ class PlayState extends MusicBeatSubState
|
|||
// TODO: Re-enable combo text (how to do this without sections?).
|
||||
// if (currentSong != null)
|
||||
// {
|
||||
// shouldShowComboText = (Conductor.currentBeat % 8 == 7);
|
||||
// var daSection = .getSong()[Std.int(Conductor.currentBeat / 16)];
|
||||
// shouldShowComboText = (Conductor.instance.currentBeat % 8 == 7);
|
||||
// var daSection = .getSong()[Std.int(Conductor.instance.currentBeat / 16)];
|
||||
// shouldShowComboText = shouldShowComboText && (daSection != null && daSection.mustHitSection);
|
||||
// shouldShowComboText = shouldShowComboText && (Highscore.tallies.combo > 5);
|
||||
//
|
||||
// var daNextSection = .getSong()[Std.int(Conductor.currentBeat / 16) + 1];
|
||||
// var isEndOfSong = .getSong().length < Std.int(Conductor.currentBeat / 16);
|
||||
// var daNextSection = .getSong()[Std.int(Conductor.instance.currentBeat / 16) + 1];
|
||||
// var isEndOfSong = .getSong().length < Std.int(Conductor.instance.currentBeat / 16);
|
||||
// shouldShowComboText = shouldShowComboText && (isEndOfSong || (daNextSection != null && !daNextSection.mustHitSection));
|
||||
// }
|
||||
|
||||
|
@ -1224,7 +1224,7 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
var frameShit:Float = (1 / 24) * 2; // equals 2 frames in the animation
|
||||
|
||||
new FlxTimer().start(((Conductor.beatLengthMs / 1000) * 1.25) - frameShit, function(tmr) {
|
||||
new FlxTimer().start(((Conductor.instance.beatLengthMs / 1000) * 1.25) - frameShit, function(tmr) {
|
||||
animShit.forceFinish();
|
||||
});
|
||||
}
|
||||
|
@ -1261,10 +1261,10 @@ class PlayState extends MusicBeatSubState
|
|||
if (currentStage == null) return;
|
||||
|
||||
// TODO: Add HEY! song events to Tutorial.
|
||||
if (Conductor.currentBeat % 16 == 15
|
||||
if (Conductor.instance.currentBeat % 16 == 15
|
||||
&& currentStage.getDad().characterId == 'gf'
|
||||
&& Conductor.currentBeat > 16
|
||||
&& Conductor.currentBeat < 48)
|
||||
&& Conductor.instance.currentBeat > 16
|
||||
&& Conductor.instance.currentBeat < 48)
|
||||
{
|
||||
currentStage.getBoyfriend().playAnimation('hey', true);
|
||||
currentStage.getDad().playAnimation('cheer', true);
|
||||
|
@ -1353,7 +1353,8 @@ class PlayState extends MusicBeatSubState
|
|||
*/
|
||||
function loadStage(id:String):Void
|
||||
{
|
||||
currentStage = StageDataParser.fetchStage(id);
|
||||
currentStage = StageRegistry.instance.fetchEntry(id);
|
||||
currentStage.revive(); // Stages are killed and props destroyed when the PlayState is destroyed to save memory.
|
||||
|
||||
if (currentStage != null)
|
||||
{
|
||||
|
@ -1575,7 +1576,7 @@ class PlayState extends MusicBeatSubState
|
|||
trace('Song difficulty could not be loaded.');
|
||||
}
|
||||
|
||||
// Conductor.forceBPM(currentChart.getStartingBPM());
|
||||
// Conductor.instance.forceBPM(currentChart.getStartingBPM());
|
||||
|
||||
if (!overrideMusic)
|
||||
{
|
||||
|
@ -1607,7 +1608,7 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
// Reset song events.
|
||||
songEvents = currentChart.getEvents();
|
||||
SongEventParser.resetEvents(songEvents);
|
||||
SongEventRegistry.resetEvents(songEvents);
|
||||
|
||||
// Reset the notes on each strumline.
|
||||
var playerNoteData:Array<SongNoteData> = [];
|
||||
|
@ -1706,7 +1707,7 @@ class PlayState extends MusicBeatSubState
|
|||
FlxG.sound.music.onComplete = endSong;
|
||||
// A negative instrumental offset means the song skips the first few milliseconds of the track.
|
||||
// This just gets added into the startTimestamp behavior so we don't need to do anything extra.
|
||||
FlxG.sound.music.time = startTimestamp - Conductor.instrumentalOffset;
|
||||
FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset;
|
||||
|
||||
trace('Playing vocals...');
|
||||
add(vocals);
|
||||
|
@ -1722,7 +1723,7 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
if (startTimestamp > 0)
|
||||
{
|
||||
// FlxG.sound.music.time = startTimestamp - Conductor.instrumentalOffset;
|
||||
// FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset;
|
||||
handleSkippedNotes();
|
||||
}
|
||||
}
|
||||
|
@ -1796,11 +1797,12 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
if (note == null) continue;
|
||||
|
||||
// TODO: Does this properly account for offsets?
|
||||
var hitWindowStart = note.strumTime - Constants.HIT_WINDOW_MS;
|
||||
var hitWindowCenter = note.strumTime;
|
||||
var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS;
|
||||
|
||||
if (Conductor.songPosition > hitWindowEnd)
|
||||
if (Conductor.instance.songPosition > hitWindowEnd)
|
||||
{
|
||||
if (note.hasMissed) continue;
|
||||
|
||||
|
@ -1810,7 +1812,7 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true;
|
||||
}
|
||||
else if (Conductor.songPosition > hitWindowCenter)
|
||||
else if (Conductor.instance.songPosition > hitWindowCenter)
|
||||
{
|
||||
if (note.hasBeenHit) continue;
|
||||
|
||||
|
@ -1831,7 +1833,7 @@ class PlayState extends MusicBeatSubState
|
|||
opponentStrumline.playNoteHoldCover(note.holdNoteSprite);
|
||||
}
|
||||
}
|
||||
else if (Conductor.songPosition > hitWindowStart)
|
||||
else if (Conductor.instance.songPosition > hitWindowStart)
|
||||
{
|
||||
if (note.hasBeenHit || note.hasMissed) continue;
|
||||
|
||||
|
@ -1864,27 +1866,43 @@ class PlayState extends MusicBeatSubState
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Potential penalty for dropping a hold note?
|
||||
// if (holdNote.missedNote && !holdNote.handledMiss) { holdNote.handledMiss = true; }
|
||||
if (holdNote.missedNote && !holdNote.handledMiss)
|
||||
{
|
||||
// When the opponent drops a hold note.
|
||||
holdNote.handledMiss = true;
|
||||
|
||||
// We dropped a hold note.
|
||||
// Mute vocals and play miss animation, but don't penalize.
|
||||
vocals.opponentVolume = 0;
|
||||
currentStage.getOpponent().playSingAnimation(holdNote.noteData.getDirection(), true);
|
||||
}
|
||||
}
|
||||
|
||||
// Process notes on the player's side.
|
||||
for (note in playerStrumline.notes.members)
|
||||
{
|
||||
if (note == null || note.hasBeenHit) continue;
|
||||
if (note == null) continue;
|
||||
|
||||
if (note.hasBeenHit)
|
||||
{
|
||||
note.tooEarly = false;
|
||||
note.mayHit = false;
|
||||
note.hasMissed = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
var hitWindowStart = note.strumTime - Constants.HIT_WINDOW_MS;
|
||||
var hitWindowCenter = note.strumTime;
|
||||
var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS;
|
||||
|
||||
if (Conductor.songPosition > hitWindowEnd)
|
||||
if (Conductor.instance.songPosition > hitWindowEnd)
|
||||
{
|
||||
note.tooEarly = false;
|
||||
note.mayHit = false;
|
||||
note.hasMissed = true;
|
||||
if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true;
|
||||
}
|
||||
else if (Conductor.songPosition > hitWindowStart)
|
||||
else if (Conductor.instance.songPosition > hitWindowStart)
|
||||
{
|
||||
note.tooEarly = false;
|
||||
note.mayHit = true;
|
||||
|
@ -1934,8 +1952,15 @@ class PlayState extends MusicBeatSubState
|
|||
songScore += Std.int(Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed);
|
||||
}
|
||||
|
||||
// TODO: Potential penalty for dropping a hold note?
|
||||
// if (holdNote.missedNote && !holdNote.handledMiss) { holdNote.handledMiss = true; }
|
||||
if (holdNote.missedNote && !holdNote.handledMiss)
|
||||
{
|
||||
// The player dropped a hold note.
|
||||
holdNote.handledMiss = true;
|
||||
|
||||
// Mute vocals and play miss animation, but don't penalize.
|
||||
vocals.playerVolume = 0;
|
||||
currentStage.getBoyfriend().playSingAnimation(holdNote.noteData.getDirection(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1951,7 +1976,7 @@ class PlayState extends MusicBeatSubState
|
|||
if (note == null || note.hasBeenHit) continue;
|
||||
var hitWindowEnd = note.strumTime + Constants.HIT_WINDOW_MS;
|
||||
|
||||
if (Conductor.songPosition > hitWindowEnd)
|
||||
if (Conductor.instance.songPosition > hitWindowEnd)
|
||||
{
|
||||
// We have passed this note.
|
||||
// Flag the note for deletion without actually penalizing the player.
|
||||
|
@ -2027,8 +2052,6 @@ class PlayState extends MusicBeatSubState
|
|||
trace('Hit note! ${targetNote.noteData}');
|
||||
goodNoteHit(targetNote, input);
|
||||
|
||||
targetNote.visible = false;
|
||||
targetNote.kill();
|
||||
notesInDirection.remove(targetNote);
|
||||
|
||||
// Play the strumline animation.
|
||||
|
@ -2060,15 +2083,8 @@ class PlayState extends MusicBeatSubState
|
|||
// Calling event.cancelEvent() skips all the other logic! Neat!
|
||||
if (event.eventCanceled) return;
|
||||
|
||||
Highscore.tallies.combo++;
|
||||
Highscore.tallies.totalNotesHit++;
|
||||
|
||||
if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo;
|
||||
|
||||
popUpScore(note, input);
|
||||
|
||||
playerStrumline.hitNote(note);
|
||||
|
||||
if (note.isHoldNote && note.holdNoteSprite != null)
|
||||
{
|
||||
playerStrumline.playNoteHoldCover(note.holdNoteSprite);
|
||||
|
@ -2084,8 +2100,6 @@ class PlayState extends MusicBeatSubState
|
|||
function onNoteMiss(note:NoteSprite):Void
|
||||
{
|
||||
// a MISS is when you let a note scroll past you!!
|
||||
Highscore.tallies.missed++;
|
||||
|
||||
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, Highscore.tallies.combo, true);
|
||||
dispatchEvent(event);
|
||||
// Calling event.cancelEvent() skips all the other logic! Neat!
|
||||
|
@ -2115,7 +2129,7 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
inputSpitter.push(
|
||||
{
|
||||
t: Std.int(Conductor.songPosition),
|
||||
t: Std.int(Conductor.instance.songPosition),
|
||||
d: indices[i],
|
||||
l: 20
|
||||
});
|
||||
|
@ -2125,7 +2139,7 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
inputSpitter.push(
|
||||
{
|
||||
t: Std.int(Conductor.songPosition),
|
||||
t: Std.int(Conductor.instance.songPosition),
|
||||
d: -1,
|
||||
l: 20
|
||||
});
|
||||
|
@ -2133,8 +2147,11 @@ class PlayState extends MusicBeatSubState
|
|||
}
|
||||
vocals.playerVolume = 0;
|
||||
|
||||
Highscore.tallies.missed++;
|
||||
|
||||
if (Highscore.tallies.combo != 0)
|
||||
{
|
||||
// Break the combo.
|
||||
Highscore.tallies.combo = comboPopUps.displayCombo(0);
|
||||
}
|
||||
|
||||
|
@ -2186,7 +2203,7 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
inputSpitter.push(
|
||||
{
|
||||
t: Std.int(Conductor.songPosition),
|
||||
t: Std.int(Conductor.instance.songPosition),
|
||||
d: indices[i],
|
||||
l: 20
|
||||
});
|
||||
|
@ -2270,39 +2287,63 @@ class PlayState extends MusicBeatSubState
|
|||
vocals.playerVolume = 1;
|
||||
|
||||
// Calculate the input latency (do this as late as possible).
|
||||
var inputLatencyMs:Float = haxe.Int64.toInt(PreciseInputManager.getCurrentTimestamp() - input.timestamp) / 1000.0 / 1000.0;
|
||||
trace('Input: ${daNote.noteData.getDirectionName()} pressed ${inputLatencyMs}ms ago!');
|
||||
// trace('Compare: ${PreciseInputManager.getCurrentTimestamp()} - ${input.timestamp}');
|
||||
var inputLatencyNs:Int64 = PreciseInputManager.getCurrentTimestamp() - input.timestamp;
|
||||
var inputLatencyMs:Float = inputLatencyNs.toFloat() / Constants.NS_PER_MS;
|
||||
// trace('Input: ${daNote.noteData.getDirectionName()} pressed ${inputLatencyMs}ms ago!');
|
||||
|
||||
// Get the offset and compensate for input latency.
|
||||
// Round inward (trim remainder) for consistency.
|
||||
var noteDiff:Int = Std.int(Conductor.songPosition - daNote.noteData.time - inputLatencyMs);
|
||||
var noteDiff:Int = Std.int(Conductor.instance.songPosition - daNote.noteData.time - inputLatencyMs);
|
||||
|
||||
var score = Scoring.scoreNote(noteDiff, PBOT1);
|
||||
var daRating = Scoring.judgeNote(noteDiff, PBOT1);
|
||||
|
||||
if (daRating == 'miss')
|
||||
{
|
||||
// If daRating is 'miss', that means we made a mistake and should not continue.
|
||||
trace('[WARNING] popUpScore judged a note as a miss!');
|
||||
// TODO: Remove this.
|
||||
comboPopUps.displayRating('miss');
|
||||
return;
|
||||
}
|
||||
|
||||
var isComboBreak = false;
|
||||
switch (daRating)
|
||||
{
|
||||
case 'killer':
|
||||
Highscore.tallies.killer += 1;
|
||||
health += Constants.HEALTH_KILLER_BONUS;
|
||||
case 'sick':
|
||||
Highscore.tallies.sick += 1;
|
||||
health += Constants.HEALTH_SICK_BONUS;
|
||||
isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK;
|
||||
case 'good':
|
||||
Highscore.tallies.good += 1;
|
||||
health += Constants.HEALTH_GOOD_BONUS;
|
||||
isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK;
|
||||
case 'bad':
|
||||
Highscore.tallies.bad += 1;
|
||||
health += Constants.HEALTH_BAD_BONUS;
|
||||
isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK;
|
||||
case 'shit':
|
||||
Highscore.tallies.shit += 1;
|
||||
health += Constants.HEALTH_SHIT_BONUS;
|
||||
case 'miss':
|
||||
Highscore.tallies.missed += 1;
|
||||
health -= Constants.HEALTH_MISS_PENALTY;
|
||||
isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK;
|
||||
}
|
||||
|
||||
if (daRating == "sick" || daRating == "killer")
|
||||
if (isComboBreak)
|
||||
{
|
||||
// Break the combo, but don't increment tallies.misses.
|
||||
Highscore.tallies.combo = comboPopUps.displayCombo(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
Highscore.tallies.combo++;
|
||||
Highscore.tallies.totalNotesHit++;
|
||||
if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo;
|
||||
}
|
||||
|
||||
playerStrumline.hitNote(daNote, !isComboBreak);
|
||||
|
||||
if (daRating == "sick")
|
||||
{
|
||||
playerStrumline.playNoteSplash(daNote.noteData.getDirection());
|
||||
}
|
||||
|
@ -2330,7 +2371,7 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
inputSpitter.push(
|
||||
{
|
||||
t: Std.int(Conductor.songPosition),
|
||||
t: Std.int(Conductor.instance.songPosition),
|
||||
d: indices[i],
|
||||
l: 20
|
||||
});
|
||||
|
@ -2340,7 +2381,7 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
inputSpitter.push(
|
||||
{
|
||||
t: Std.int(Conductor.songPosition),
|
||||
t: Std.int(Conductor.instance.songPosition),
|
||||
d: -1,
|
||||
l: 20
|
||||
});
|
||||
|
@ -2438,7 +2479,6 @@ class PlayState extends MusicBeatSubState
|
|||
score: songScore,
|
||||
tallies:
|
||||
{
|
||||
killer: Highscore.tallies.killer,
|
||||
sick: Highscore.tallies.sick,
|
||||
good: Highscore.tallies.good,
|
||||
bad: Highscore.tallies.bad,
|
||||
|
@ -2489,7 +2529,6 @@ class PlayState extends MusicBeatSubState
|
|||
tallies:
|
||||
{
|
||||
// TODO: Sum up the values for the whole level!
|
||||
killer: 0,
|
||||
sick: 0,
|
||||
good: 0,
|
||||
bad: 0,
|
||||
|
@ -2739,15 +2778,15 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
FlxG.sound.music.pause();
|
||||
|
||||
var targetTimeSteps:Float = Conductor.currentStepTime + (Conductor.timeSignatureNumerator * Constants.STEPS_PER_BEAT * sections);
|
||||
var targetTimeMs:Float = Conductor.getStepTimeInMs(targetTimeSteps);
|
||||
var targetTimeSteps:Float = Conductor.instance.currentStepTime + (Conductor.instance.timeSignatureNumerator * Constants.STEPS_PER_BEAT * sections);
|
||||
var targetTimeMs:Float = Conductor.instance.getStepTimeInMs(targetTimeSteps);
|
||||
|
||||
FlxG.sound.music.time = targetTimeMs;
|
||||
|
||||
handleSkippedNotes();
|
||||
// regenNoteData(FlxG.sound.music.time);
|
||||
|
||||
Conductor.update(FlxG.sound.music.time);
|
||||
Conductor.instance.update(FlxG.sound.music.time);
|
||||
|
||||
resyncVocals();
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import flixel.math.FlxMath;
|
|||
import flixel.math.FlxPoint.FlxCallbackPoint;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.math.FlxRect;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import flixel.system.FlxAssets.FlxGraphicAsset;
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.util.FlxDestroyUtil;
|
||||
|
@ -621,7 +622,7 @@ class AnimateAtlasCharacter extends BaseCharacter
|
|||
* This functionality isn't supported in SpriteGroup
|
||||
* @return this sprite group
|
||||
*/
|
||||
public override function loadGraphicFromSprite(Sprite:FlxSprite):FlxSprite
|
||||
public override function loadGraphicFromSprite(Sprite:FlxSprite):FunkinSprite
|
||||
{
|
||||
#if FLX_DEBUG
|
||||
throw "This function is not supported in FlxSpriteGroup";
|
||||
|
|
|
@ -367,7 +367,7 @@ class BaseCharacter extends Bopper
|
|||
// This lets you add frames to the end of the sing animation to ease back into the idle!
|
||||
|
||||
holdTimer += event.elapsed;
|
||||
var singTimeSec:Float = singTimeSec * (Conductor.beatLengthMs * 0.001); // x beats, to ms.
|
||||
var singTimeSec:Float = singTimeSec * (Conductor.instance.beatLengthMs * 0.001); // x beats, to ms.
|
||||
|
||||
if (getCurrentAnimation().endsWith('miss')) singTimeSec *= 2; // makes it feel more awkward when you miss
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package funkin.play.character;
|
||||
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import flixel.graphics.frames.FlxFramesCollection;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.util.assets.FlxAnimationUtil;
|
||||
|
@ -7,35 +8,17 @@ import funkin.play.character.CharacterData.CharacterRenderType;
|
|||
|
||||
/**
|
||||
* For some characters which use Sparrow atlases, the spritesheets need to be split
|
||||
* into multiple files. This character renderer handles by showing the appropriate sprite.
|
||||
* into multiple files. This character renderer concatenates these together into a single sprite.
|
||||
*
|
||||
* Examples in base game include BF Holding GF (most of the sprites are in one file
|
||||
* but the death animation is in a separate file).
|
||||
* Only example I can think of in mods is Tricky (which has a separate file for each animation).
|
||||
*
|
||||
* BaseCharacter has game logic, SparrowCharacter has only rendering logic.
|
||||
* BaseCharacter has game logic, MultiSparrowCharacter has only rendering logic.
|
||||
* KEEP THEM SEPARATE!
|
||||
*
|
||||
* TODO: Rewrite this to use a single frame collection.
|
||||
* @see https://github.com/HaxeFlixel/flixel/issues/2587#issuecomment-1179620637
|
||||
*/
|
||||
class MultiSparrowCharacter extends BaseCharacter
|
||||
{
|
||||
/**
|
||||
* The actual group which holds all spritesheets this character uses.
|
||||
*/
|
||||
var members:Map<String, FlxFramesCollection> = new Map<String, FlxFramesCollection>();
|
||||
|
||||
/**
|
||||
* A map between animation names and what frame collection the animation should use.
|
||||
*/
|
||||
var animAssetPath:Map<String, String> = new Map<String, String>();
|
||||
|
||||
/**
|
||||
* The current frame collection being used.
|
||||
*/
|
||||
var activeMember:String;
|
||||
|
||||
public function new(id:String)
|
||||
{
|
||||
super(id, CharacterRenderType.MultiSparrow);
|
||||
|
@ -51,7 +34,7 @@ class MultiSparrowCharacter extends BaseCharacter
|
|||
|
||||
function buildSprites():Void
|
||||
{
|
||||
buildSpritesheets();
|
||||
buildSpritesheet();
|
||||
buildAnimations();
|
||||
|
||||
if (_data.isPixel)
|
||||
|
@ -66,95 +49,49 @@ class MultiSparrowCharacter extends BaseCharacter
|
|||
}
|
||||
}
|
||||
|
||||
function buildSpritesheets():Void
|
||||
function buildSpritesheet():Void
|
||||
{
|
||||
// TODO: This currently works by creating like 5 frame collections and switching between them.
|
||||
// It would be better to refactor this to simply concatenate the frame collections together.
|
||||
|
||||
// Build the list of asset paths to use.
|
||||
// Ignore nulls and duplicates.
|
||||
var assetList = [_data.assetPath];
|
||||
var assetList = [];
|
||||
for (anim in _data.animations)
|
||||
{
|
||||
if (anim.assetPath != null && !assetList.contains(anim.assetPath))
|
||||
{
|
||||
assetList.push(anim.assetPath);
|
||||
}
|
||||
animAssetPath.set(anim.name, anim.assetPath);
|
||||
}
|
||||
|
||||
// Load the Sparrow atlas for each path and store them in the members map.
|
||||
var texture:FlxAtlasFrames = Paths.getSparrowAtlas(_data.assetPath, 'shared');
|
||||
|
||||
if (texture == null)
|
||||
{
|
||||
trace('Multi-Sparrow atlas could not load PRIMARY texture: ${_data.assetPath}');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Creating multi-sparrow atlas: ${_data.assetPath}');
|
||||
texture.parent.destroyOnNoUse = false;
|
||||
}
|
||||
|
||||
for (asset in assetList)
|
||||
{
|
||||
var texture:FlxFramesCollection = Paths.getSparrowAtlas(asset, 'shared');
|
||||
var subTexture:FlxAtlasFrames = Paths.getSparrowAtlas(asset, 'shared');
|
||||
// If we don't do this, the unused textures will be removed as soon as they're loaded.
|
||||
|
||||
if (texture == null)
|
||||
if (subTexture == null)
|
||||
{
|
||||
trace('Multi-Sparrow atlas could not load texture: ${asset}');
|
||||
trace('Multi-Sparrow atlas could not load subtexture: ${asset}');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Adding multi-sparrow atlas: ${asset}');
|
||||
texture.parent.destroyOnNoUse = false;
|
||||
members.set(asset, texture);
|
||||
trace('Concatenating multi-sparrow atlas: ${asset}');
|
||||
subTexture.parent.destroyOnNoUse = false;
|
||||
}
|
||||
|
||||
texture.addAtlas(subTexture);
|
||||
}
|
||||
|
||||
// Use the default frame collection to start.
|
||||
loadFramesByAssetPath(_data.assetPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace this sprite's animation frames with the ones at this asset path.
|
||||
*/
|
||||
function loadFramesByAssetPath(assetPath:String):Void
|
||||
{
|
||||
if (_data.assetPath == null)
|
||||
{
|
||||
trace('[ERROR] Multi-Sparrow character has no default asset path!');
|
||||
return;
|
||||
}
|
||||
if (assetPath == null)
|
||||
{
|
||||
// trace('Asset path is null, falling back to default. This is normal!');
|
||||
loadFramesByAssetPath(_data.assetPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.activeMember == assetPath)
|
||||
{
|
||||
// trace('Already using this asset path: ${assetPath}');
|
||||
return;
|
||||
}
|
||||
|
||||
if (members.exists(assetPath))
|
||||
{
|
||||
// Switch to a new set of sprites.
|
||||
// trace('Loading frames from asset path: ${assetPath}');
|
||||
this.frames = members.get(assetPath);
|
||||
this.activeMember = assetPath;
|
||||
this.setScale(_data.scale);
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[WARN] MultiSparrow character ${characterId} could not find asset path: ${assetPath}');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace this sprite's animation frames with the ones needed to play this animation.
|
||||
*/
|
||||
function loadFramesByAnimName(animName)
|
||||
{
|
||||
if (animAssetPath.exists(animName))
|
||||
{
|
||||
loadFramesByAssetPath(animAssetPath.get(animName));
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[WARN] MultiSparrow character ${characterId} could not find animation: ${animName}');
|
||||
}
|
||||
this.frames = texture;
|
||||
this.setScale(_data.scale);
|
||||
}
|
||||
|
||||
function buildAnimations()
|
||||
|
@ -164,7 +101,6 @@ class MultiSparrowCharacter extends BaseCharacter
|
|||
// We need to swap to the proper frame collection before adding the animations, I think?
|
||||
for (anim in _data.animations)
|
||||
{
|
||||
loadFramesByAnimName(anim.name);
|
||||
FlxAnimationUtil.addAtlasAnimation(this, anim);
|
||||
|
||||
if (anim.offsets == null)
|
||||
|
@ -187,37 +123,6 @@ class MultiSparrowCharacter extends BaseCharacter
|
|||
// unless we're forcing a new animation.
|
||||
if (!this.canPlayOtherAnims && !ignoreOther) return;
|
||||
|
||||
loadFramesByAnimName(name);
|
||||
super.playAnimation(name, restart, ignoreOther, reverse);
|
||||
}
|
||||
|
||||
override function set_frames(value:FlxFramesCollection):FlxFramesCollection
|
||||
{
|
||||
// DISABLE THIS SO WE DON'T DESTROY OUR HARD WORK
|
||||
// WE WILL MAKE SURE TO LOAD THE PROPER SPRITESHEET BEFORE PLAYING AN ANIM
|
||||
// if (animation != null)
|
||||
// {
|
||||
// animation.destroyAnimations();
|
||||
// }
|
||||
|
||||
if (value != null)
|
||||
{
|
||||
graphic = value.parent;
|
||||
this.frames = value;
|
||||
this.frame = value.getByIndex(0);
|
||||
// this.numFrames = value.numFrames;
|
||||
resetHelpers();
|
||||
this.bakedRotationAngle = 0;
|
||||
this.animation.frameIndex = 0;
|
||||
graphicLoaded();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.frames = null;
|
||||
this.frame = null;
|
||||
this.graphic = null;
|
||||
}
|
||||
|
||||
return this.frames;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ class ComboMilestone extends FlxTypedSpriteGroup<FlxSprite>
|
|||
{
|
||||
if (onScreenTime < 0.9)
|
||||
{
|
||||
new FlxTimer().start((Conductor.beatLengthMs / 1000) * 0.25, function(tmr) {
|
||||
new FlxTimer().start((Conductor.instance.beatLengthMs / 1000) * 0.25, function(tmr) {
|
||||
forceFinish();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
|
|||
remove(rating, true);
|
||||
rating.destroy();
|
||||
},
|
||||
startDelay: Conductor.beatLengthMs * 0.001
|
||||
startDelay: Conductor.instance.beatLengthMs * 0.001
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -110,7 +110,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
|
|||
remove(comboSpr, true);
|
||||
comboSpr.destroy();
|
||||
},
|
||||
startDelay: Conductor.beatLengthMs * 0.001
|
||||
startDelay: Conductor.instance.beatLengthMs * 0.001
|
||||
});
|
||||
|
||||
var seperatedScore:Array<Int> = [];
|
||||
|
@ -157,7 +157,7 @@ class PopUpStuff extends FlxTypedGroup<FlxSprite>
|
|||
remove(numScore, true);
|
||||
numScore.destroy();
|
||||
},
|
||||
startDelay: Conductor.beatLengthMs * 0.002
|
||||
startDelay: Conductor.instance.beatLengthMs * 0.002
|
||||
});
|
||||
|
||||
daLoop++;
|
||||
|
|
|
@ -5,8 +5,8 @@ import funkin.data.song.SongData;
|
|||
import funkin.data.song.SongData.SongEventData;
|
||||
// Data from the event schema
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.data.event.SongEventData.SongEventSchema;
|
||||
import funkin.data.event.SongEventData.SongEventFieldType;
|
||||
import funkin.data.event.SongEventSchema;
|
||||
import funkin.data.event.SongEventSchema.SongEventFieldType;
|
||||
|
||||
/**
|
||||
* This class represents a handler for a type of song event.
|
||||
|
|
|
@ -7,8 +7,8 @@ import funkin.data.song.SongData;
|
|||
import funkin.data.song.SongData.SongEventData;
|
||||
// Data from the event schema
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.data.event.SongEventData.SongEventSchema;
|
||||
import funkin.data.event.SongEventData.SongEventFieldType;
|
||||
import funkin.data.event.SongEventSchema;
|
||||
import funkin.data.event.SongEventSchema.SongEventFieldType;
|
||||
|
||||
class PlayAnimationSongEvent extends SongEvent
|
||||
{
|
||||
|
|
|
@ -8,8 +8,8 @@ import funkin.data.song.SongData;
|
|||
import funkin.data.song.SongData.SongEventData;
|
||||
// Data from the event schema
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.data.event.SongEventData.SongEventSchema;
|
||||
import funkin.data.event.SongEventData.SongEventFieldType;
|
||||
import funkin.data.event.SongEventSchema;
|
||||
import funkin.data.event.SongEventSchema.SongEventFieldType;
|
||||
|
||||
/**
|
||||
* This class represents a handler for configuring camera bop intensity and rate.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package funkin.play.event;
|
||||
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.data.event.SongEventData.SongEventSchema;
|
||||
import funkin.data.event.SongEventSchema;
|
||||
|
||||
/**
|
||||
* This class represents a handler for a type of song event.
|
||||
|
|
|
@ -8,8 +8,8 @@ import funkin.data.song.SongData;
|
|||
import funkin.data.song.SongData.SongEventData;
|
||||
// Data from the event schema
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.data.event.SongEventData.SongEventFieldType;
|
||||
import funkin.data.event.SongEventData.SongEventSchema;
|
||||
import funkin.data.event.SongEventSchema;
|
||||
import funkin.data.event.SongEventSchema.SongEventFieldType;
|
||||
|
||||
/**
|
||||
* This class represents a handler for camera zoom events.
|
||||
|
@ -79,7 +79,8 @@ class ZoomCameraSongEvent extends SongEvent
|
|||
return;
|
||||
}
|
||||
|
||||
FlxTween.tween(PlayState.instance, {defaultCameraZoom: zoom * FlxCamera.defaultZoom}, (Conductor.stepLengthMs * duration / 1000), {ease: easeFunction});
|
||||
FlxTween.tween(PlayState.instance, {defaultCameraZoom: zoom * FlxCamera.defaultZoom}, (Conductor.instance.stepLengthMs * duration / 1000),
|
||||
{ease: easeFunction});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import funkin.data.song.SongData.SongNoteData;
|
|||
import funkin.play.notes.notestyle.NoteStyle;
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import flixel.FlxSprite;
|
||||
import funkin.graphics.shaders.HSVShader;
|
||||
|
||||
class NoteSprite extends FlxSprite
|
||||
{
|
||||
|
@ -11,6 +12,8 @@ class NoteSprite extends FlxSprite
|
|||
|
||||
public var holdNoteSprite:SustainTrail;
|
||||
|
||||
var hsvShader:HSVShader;
|
||||
|
||||
/**
|
||||
* The time at which the note should be hit, in milliseconds.
|
||||
*/
|
||||
|
@ -102,6 +105,8 @@ class NoteSprite extends FlxSprite
|
|||
this.strumTime = strumTime;
|
||||
this.direction = direction;
|
||||
|
||||
this.hsvShader = new HSVShader();
|
||||
|
||||
if (this.strumTime < 0) this.strumTime = 0;
|
||||
|
||||
setupNoteGraphic(noteStyle);
|
||||
|
@ -116,16 +121,57 @@ class NoteSprite extends FlxSprite
|
|||
|
||||
setGraphicSize(Strumline.STRUMLINE_SIZE);
|
||||
updateHitbox();
|
||||
|
||||
this.shader = hsvShader;
|
||||
}
|
||||
|
||||
#if FLX_DEBUG
|
||||
/**
|
||||
* Call this to override how debug bounding boxes are drawn for this sprite.
|
||||
*/
|
||||
public override function drawDebugOnCamera(camera:flixel.FlxCamera):Void
|
||||
{
|
||||
if (!camera.visible || !camera.exists || !isOnScreen(camera)) return;
|
||||
|
||||
var gfx = beginDrawDebug(camera);
|
||||
|
||||
var rect = getBoundingBox(camera);
|
||||
trace('note sprite bounding box: ' + rect.x + ', ' + rect.y + ', ' + rect.width + ', ' + rect.height);
|
||||
|
||||
gfx.lineStyle(2, 0xFFFF66FF, 0.5); // thickness, color, alpha
|
||||
gfx.drawRect(rect.x, rect.y, rect.width, rect.height);
|
||||
|
||||
gfx.lineStyle(2, 0xFFFFFF66, 0.5); // thickness, color, alpha
|
||||
gfx.drawRect(rect.x, rect.y + rect.height / 2, rect.width, 1);
|
||||
|
||||
endDrawDebug(camera);
|
||||
}
|
||||
#end
|
||||
|
||||
public function desaturate():Void
|
||||
{
|
||||
this.hsvShader.saturation = 0.2;
|
||||
}
|
||||
|
||||
public function setHue(hue:Float):Void
|
||||
{
|
||||
this.hsvShader.hue = hue;
|
||||
}
|
||||
|
||||
public override function revive():Void
|
||||
{
|
||||
super.revive();
|
||||
this.visible = true;
|
||||
this.alpha = 1.0;
|
||||
this.active = false;
|
||||
this.tooEarly = false;
|
||||
this.hasBeenHit = false;
|
||||
this.mayHit = false;
|
||||
this.hasMissed = false;
|
||||
|
||||
this.hsvShader.hue = 1.0;
|
||||
this.hsvShader.saturation = 1.0;
|
||||
this.hsvShader.value = 1.0;
|
||||
}
|
||||
|
||||
public override function kill():Void
|
||||
|
|
|
@ -279,7 +279,7 @@ class Strumline extends FlxSpriteGroup
|
|||
var vwoosh:Float = 1.0;
|
||||
var scrollSpeed:Float = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0;
|
||||
|
||||
return Constants.PIXELS_PER_MS * (Conductor.songPosition - strumTime) * scrollSpeed * vwoosh * (Preferences.downscroll ? 1 : -1);
|
||||
return Constants.PIXELS_PER_MS * (Conductor.instance.songPosition - strumTime) * scrollSpeed * vwoosh * (Preferences.downscroll ? 1 : -1);
|
||||
}
|
||||
|
||||
function updateNotes():Void
|
||||
|
@ -287,8 +287,8 @@ class Strumline extends FlxSpriteGroup
|
|||
if (noteData.length == 0) return;
|
||||
|
||||
var songStart:Float = PlayState.instance?.startTimestamp ?? 0.0;
|
||||
var hitWindowStart:Float = Conductor.songPosition - Constants.HIT_WINDOW_MS;
|
||||
var renderWindowStart:Float = Conductor.songPosition + RENDER_DISTANCE_MS;
|
||||
var hitWindowStart:Float = Conductor.instance.songPosition - Constants.HIT_WINDOW_MS;
|
||||
var renderWindowStart:Float = Conductor.instance.songPosition + RENDER_DISTANCE_MS;
|
||||
|
||||
for (noteIndex in nextNoteIndex...noteData.length)
|
||||
{
|
||||
|
@ -316,7 +316,7 @@ class Strumline extends FlxSpriteGroup
|
|||
// Update rendering of notes.
|
||||
for (note in notes.members)
|
||||
{
|
||||
if (note == null || !note.alive || note.hasBeenHit) continue;
|
||||
if (note == null || !note.alive) continue;
|
||||
|
||||
var vwoosh:Bool = note.holdNoteSprite == null;
|
||||
// Set the note's position.
|
||||
|
@ -335,7 +335,7 @@ class Strumline extends FlxSpriteGroup
|
|||
{
|
||||
if (holdNote == null || !holdNote.alive) continue;
|
||||
|
||||
if (Conductor.songPosition > holdNote.strumTime && holdNote.hitNote && !holdNote.missedNote)
|
||||
if (Conductor.instance.songPosition > holdNote.strumTime && holdNote.hitNote && !holdNote.missedNote)
|
||||
{
|
||||
if (isPlayer && !isKeyHeld(holdNote.noteDirection))
|
||||
{
|
||||
|
@ -343,13 +343,13 @@ class Strumline extends FlxSpriteGroup
|
|||
playStatic(holdNote.noteDirection);
|
||||
holdNote.missedNote = true;
|
||||
holdNote.visible = true;
|
||||
holdNote.alpha = 0.0;
|
||||
holdNote.alpha = 0.0; // Completely hide the dropped hold note.
|
||||
}
|
||||
}
|
||||
|
||||
var renderWindowEnd = holdNote.strumTime + holdNote.fullSustainLength + Constants.HIT_WINDOW_MS + RENDER_DISTANCE_MS / 8;
|
||||
|
||||
if (holdNote.missedNote && Conductor.songPosition >= renderWindowEnd)
|
||||
if (holdNote.missedNote && Conductor.instance.songPosition >= renderWindowEnd)
|
||||
{
|
||||
// Hold note is offscreen, kill it.
|
||||
holdNote.visible = false;
|
||||
|
@ -384,10 +384,6 @@ class Strumline extends FlxSpriteGroup
|
|||
|
||||
var yOffset:Float = (holdNote.fullSustainLength - holdNote.sustainLength) * Constants.PIXELS_PER_MS;
|
||||
|
||||
trace('yOffset: ' + yOffset);
|
||||
trace('holdNote.fullSustainLength: ' + holdNote.fullSustainLength);
|
||||
trace('holdNote.sustainLength: ' + holdNote.sustainLength);
|
||||
|
||||
var vwoosh:Bool = false;
|
||||
|
||||
if (Preferences.downscroll)
|
||||
|
@ -399,13 +395,13 @@ class Strumline extends FlxSpriteGroup
|
|||
holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) + yOffset + STRUMLINE_SIZE / 2;
|
||||
}
|
||||
}
|
||||
else if (Conductor.songPosition > holdNote.strumTime && holdNote.hitNote)
|
||||
else if (Conductor.instance.songPosition > holdNote.strumTime && holdNote.hitNote)
|
||||
{
|
||||
// Hold note is currently being hit, clip it off.
|
||||
holdConfirm(holdNote.noteDirection);
|
||||
holdNote.visible = true;
|
||||
|
||||
holdNote.sustainLength = (holdNote.strumTime + holdNote.fullSustainLength) - Conductor.songPosition;
|
||||
holdNote.sustainLength = (holdNote.strumTime + holdNote.fullSustainLength) - Conductor.instance.songPosition;
|
||||
|
||||
if (holdNote.sustainLength <= 10)
|
||||
{
|
||||
|
@ -531,11 +527,24 @@ class Strumline extends FlxSpriteGroup
|
|||
this.noteData.insertionSort(compareNoteData.bind(FlxSort.ASCENDING));
|
||||
}
|
||||
|
||||
public function hitNote(note:NoteSprite):Void
|
||||
/**
|
||||
* @param note The note to hit.
|
||||
* @param removeNote True to remove the note immediately, false to make it transparent and let it move offscreen.
|
||||
*/
|
||||
public function hitNote(note:NoteSprite, removeNote:Bool = true):Void
|
||||
{
|
||||
playConfirm(note.direction);
|
||||
note.hasBeenHit = true;
|
||||
killNote(note);
|
||||
|
||||
if (removeNote)
|
||||
{
|
||||
killNote(note);
|
||||
}
|
||||
else
|
||||
{
|
||||
note.alpha = 0.5;
|
||||
note.desaturate();
|
||||
}
|
||||
|
||||
if (note.holdNoteSprite != null)
|
||||
{
|
||||
|
|
|
@ -47,6 +47,11 @@ class SustainTrail extends FlxSprite
|
|||
*/
|
||||
public var missedNote:Bool = false;
|
||||
|
||||
/**
|
||||
* Set to `true` after handling additional logic for missing notes.
|
||||
*/
|
||||
public var handledMiss:Bool = false;
|
||||
|
||||
// maybe BlendMode.MULTIPLY if missed somehow, drawTriangles does not support!
|
||||
|
||||
/**
|
||||
|
@ -82,6 +87,9 @@ class SustainTrail extends FlxSprite
|
|||
|
||||
public var isPixel:Bool;
|
||||
|
||||
var graphicWidth:Float = 0;
|
||||
var graphicHeight:Float = 0;
|
||||
|
||||
/**
|
||||
* Normally you would take strumTime:Float, noteData:Int, sustainLength:Float, parentNote:Note (?)
|
||||
* @param NoteData
|
||||
|
@ -110,8 +118,8 @@ class SustainTrail extends FlxSprite
|
|||
zoom *= 0.7;
|
||||
|
||||
// CALCULATE SIZE
|
||||
width = graphic.width / 8 * zoom; // amount of notes * 2
|
||||
height = sustainHeight(sustainLength, getScrollSpeed());
|
||||
graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2
|
||||
graphicHeight = sustainHeight(sustainLength, getScrollSpeed());
|
||||
// instead of scrollSpeed, PlayState.SONG.speed
|
||||
|
||||
flipY = Preferences.downscroll;
|
||||
|
@ -148,12 +156,21 @@ class SustainTrail extends FlxSprite
|
|||
|
||||
if (sustainLength == s) return s;
|
||||
|
||||
height = sustainHeight(s, getScrollSpeed());
|
||||
graphicHeight = sustainHeight(s, getScrollSpeed());
|
||||
this.sustainLength = s;
|
||||
updateClipping();
|
||||
updateHitbox();
|
||||
return this.sustainLength;
|
||||
}
|
||||
|
||||
public override function updateHitbox():Void
|
||||
{
|
||||
width = graphicWidth;
|
||||
height = graphicHeight;
|
||||
offset.set(0, 0);
|
||||
origin.set(width * 0.5, height * 0.5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up new vertex and UV data to clip the trail.
|
||||
* If flipY is true, top and bottom bounds swap places.
|
||||
|
@ -161,7 +178,7 @@ class SustainTrail extends FlxSprite
|
|||
*/
|
||||
public function updateClipping(songTime:Float = 0):Void
|
||||
{
|
||||
var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), getScrollSpeed()), 0, height);
|
||||
var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), getScrollSpeed()), 0, graphicHeight);
|
||||
if (clipHeight <= 0.1)
|
||||
{
|
||||
visible = false;
|
||||
|
@ -178,10 +195,10 @@ class SustainTrail extends FlxSprite
|
|||
// ===HOLD VERTICES==
|
||||
// Top left
|
||||
vertices[0 * 2] = 0.0; // Inline with left side
|
||||
vertices[0 * 2 + 1] = flipY ? clipHeight : height - clipHeight;
|
||||
vertices[0 * 2 + 1] = flipY ? clipHeight : graphicHeight - clipHeight;
|
||||
|
||||
// Top right
|
||||
vertices[1 * 2] = width;
|
||||
vertices[1 * 2] = graphicWidth;
|
||||
vertices[1 * 2 + 1] = vertices[0 * 2 + 1]; // Inline with top left vertex
|
||||
|
||||
// Bottom left
|
||||
|
@ -197,7 +214,7 @@ class SustainTrail extends FlxSprite
|
|||
}
|
||||
|
||||
// Bottom right
|
||||
vertices[3 * 2] = width;
|
||||
vertices[3 * 2] = graphicWidth;
|
||||
vertices[3 * 2 + 1] = vertices[2 * 2 + 1]; // Inline with bottom left vertex
|
||||
|
||||
// ===HOLD UVs===
|
||||
|
@ -233,7 +250,7 @@ class SustainTrail extends FlxSprite
|
|||
|
||||
// Bottom left
|
||||
vertices[6 * 2] = vertices[2 * 2]; // Inline with left side
|
||||
vertices[6 * 2 + 1] = flipY ? (graphic.height * (-bottomClip + endOffset) * zoom) : (height + graphic.height * (bottomClip - endOffset) * zoom);
|
||||
vertices[6 * 2 + 1] = flipY ? (graphic.height * (-bottomClip + endOffset) * zoom) : (graphicHeight + graphic.height * (bottomClip - endOffset) * zoom);
|
||||
|
||||
// Bottom right
|
||||
vertices[7 * 2] = vertices[3 * 2]; // Inline with right side
|
||||
|
@ -277,6 +294,10 @@ class SustainTrail extends FlxSprite
|
|||
getScreenPosition(_point, camera).subtractPoint(offset);
|
||||
camera.drawTriangles(processedGraphic, vertices, indices, uvtData, null, _point, blend, true, antialiasing);
|
||||
}
|
||||
|
||||
#if FLX_DEBUG
|
||||
if (FlxG.debugger.drawDebug) drawDebug();
|
||||
#end
|
||||
}
|
||||
|
||||
public override function kill():Void
|
||||
|
@ -305,6 +326,7 @@ class SustainTrail extends FlxSprite
|
|||
|
||||
hitNote = false;
|
||||
missedNote = false;
|
||||
handledMiss = false;
|
||||
}
|
||||
|
||||
override public function destroy():Void
|
||||
|
|
|
@ -158,8 +158,8 @@ class Scoring
|
|||
|
||||
return switch (absTiming)
|
||||
{
|
||||
case(_ < PBOT1_KILLER_THRESHOLD) => true:
|
||||
'killer';
|
||||
// case(_ < PBOT1_KILLER_THRESHOLD) => true:
|
||||
// 'killer';
|
||||
case(_ < PBOT1_SICK_THRESHOLD) => true:
|
||||
'sick';
|
||||
case(_ < PBOT1_GOOD_THRESHOLD) => true:
|
||||
|
|
|
@ -174,7 +174,10 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
|||
difficulty.timeChanges = metadata.timeChanges;
|
||||
difficulty.looped = metadata.looped;
|
||||
difficulty.generatedBy = metadata.generatedBy;
|
||||
difficulty.offsets = metadata.offsets;
|
||||
difficulty.offsets = metadata?.offsets ?? new SongOffsets();
|
||||
|
||||
difficulty.difficultyRating = metadata.playData.ratings.get(diffId) ?? 0;
|
||||
difficulty.album = metadata.playData.album;
|
||||
|
||||
difficulty.stage = metadata.playData.stage;
|
||||
difficulty.noteStyle = metadata.playData.noteStyle;
|
||||
|
@ -405,6 +408,9 @@ class SongDifficulty
|
|||
|
||||
public var scrollSpeed:Float = Constants.DEFAULT_SCROLLSPEED;
|
||||
|
||||
public var difficultyRating:Int = 0;
|
||||
public var album:Null<String> = null;
|
||||
|
||||
public function new(song:Song, diffId:String, variation:String)
|
||||
{
|
||||
this.song = song;
|
||||
|
|
|
@ -5,13 +5,16 @@ import flixel.group.FlxSpriteGroup;
|
|||
import flixel.math.FlxPoint;
|
||||
import flixel.system.FlxAssets.FlxShader;
|
||||
import flixel.util.FlxSort;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.modding.IScriptedClass;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.modding.events.ScriptEventType;
|
||||
import funkin.modding.events.ScriptEventDispatcher;
|
||||
import funkin.play.character.BaseCharacter;
|
||||
import funkin.play.stage.StageData.StageDataCharacter;
|
||||
import funkin.play.stage.StageData.StageDataParser;
|
||||
import funkin.data.IRegistryEntry;
|
||||
import funkin.data.stage.StageData;
|
||||
import funkin.data.stage.StageData.StageDataCharacter;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
import funkin.play.stage.StageProp;
|
||||
import funkin.util.SortUtil;
|
||||
import funkin.util.assets.FlxAnimationUtil;
|
||||
|
@ -23,14 +26,25 @@ typedef StagePropGroup = FlxTypedSpriteGroup<StageProp>;
|
|||
*
|
||||
* A Stage is comprised of one or more props, each of which is a FlxSprite.
|
||||
*/
|
||||
class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
||||
class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements IRegistryEntry<StageData>
|
||||
{
|
||||
public final stageId:String;
|
||||
public final stageName:String;
|
||||
public final id:String;
|
||||
|
||||
final _data:StageData;
|
||||
public final _data:StageData;
|
||||
|
||||
public var camZoom:Float = 1.0;
|
||||
public var stageName(get, never):String;
|
||||
|
||||
function get_stageName():String
|
||||
{
|
||||
return _data?.name ?? 'Unknown';
|
||||
}
|
||||
|
||||
public var camZoom(get, never):Float;
|
||||
|
||||
function get_camZoom():Float
|
||||
{
|
||||
return _data?.cameraZoom ?? 1.0;
|
||||
}
|
||||
|
||||
var namedProps:Map<String, StageProp> = new Map<String, StageProp>();
|
||||
var characters:Map<String, BaseCharacter> = new Map<String, BaseCharacter>();
|
||||
|
@ -41,21 +55,18 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
|||
* They're used to cache the data needed to build the stage,
|
||||
* then accessed and fleshed out when the stage needs to be built.
|
||||
*
|
||||
* @param stageId
|
||||
* @param id
|
||||
*/
|
||||
public function new(stageId:String)
|
||||
public function new(id:String)
|
||||
{
|
||||
super();
|
||||
|
||||
this.stageId = stageId;
|
||||
_data = StageDataParser.parseStageData(this.stageId);
|
||||
this.id = id;
|
||||
_data = _fetchData(id);
|
||||
|
||||
if (_data == null)
|
||||
{
|
||||
throw 'Could not find stage data for stageId: $stageId';
|
||||
}
|
||||
else
|
||||
{
|
||||
this.stageName = _data.name;
|
||||
throw 'Could not find stage data for stage id: $id';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,9 +140,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
|||
*/
|
||||
function buildStage():Void
|
||||
{
|
||||
trace('Building stage for display: ${this.stageId}');
|
||||
|
||||
this.camZoom = _data.cameraZoom;
|
||||
trace('Building stage for display: ${this.id}');
|
||||
|
||||
this.debugIconGroup = new FlxSpriteGroup();
|
||||
|
||||
|
@ -139,6 +148,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
|||
{
|
||||
trace(' Placing prop: ${dataProp.name} (${dataProp.assetPath})');
|
||||
|
||||
var isSolidColor = dataProp.assetPath.startsWith('#');
|
||||
var isAnimated = dataProp.animations.length > 0;
|
||||
|
||||
var propSprite:StageProp;
|
||||
|
@ -162,6 +172,22 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
|||
propSprite.frames = Paths.getSparrowAtlas(dataProp.assetPath);
|
||||
}
|
||||
}
|
||||
else if (isSolidColor)
|
||||
{
|
||||
var width:Int = 1;
|
||||
var height:Int = 1;
|
||||
switch (dataProp.scale)
|
||||
{
|
||||
case Left(value):
|
||||
width = Std.int(value);
|
||||
height = Std.int(value);
|
||||
|
||||
case Right(values):
|
||||
width = Std.int(values[0]);
|
||||
height = Std.int(values[1]);
|
||||
}
|
||||
propSprite.makeSolidColor(width, height, FlxColor.fromString(dataProp.assetPath));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Initalize static sprite.
|
||||
|
@ -177,13 +203,16 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
|||
continue;
|
||||
}
|
||||
|
||||
switch (dataProp.scale)
|
||||
if (!isSolidColor)
|
||||
{
|
||||
case Left(value):
|
||||
propSprite.scale.set(value);
|
||||
switch (dataProp.scale)
|
||||
{
|
||||
case Left(value):
|
||||
propSprite.scale.set(value);
|
||||
|
||||
case Right(values):
|
||||
propSprite.scale.set(values[0], values[1]);
|
||||
case Right(values):
|
||||
propSprite.scale.set(values[0], values[1]);
|
||||
}
|
||||
}
|
||||
propSprite.updateHitbox();
|
||||
|
||||
|
@ -195,15 +224,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
|||
// If pixel, disable antialiasing.
|
||||
propSprite.antialiasing = !dataProp.isPixel;
|
||||
|
||||
switch (dataProp.scroll)
|
||||
{
|
||||
case Left(value):
|
||||
propSprite.scrollFactor.x = value;
|
||||
propSprite.scrollFactor.y = value;
|
||||
case Right(values):
|
||||
propSprite.scrollFactor.x = values[0];
|
||||
propSprite.scrollFactor.y = values[1];
|
||||
}
|
||||
propSprite.scrollFactor.x = dataProp.scroll[0];
|
||||
propSprite.scrollFactor.y = dataProp.scroll[1];
|
||||
|
||||
propSprite.zIndex = dataProp.zIndex;
|
||||
|
||||
|
@ -731,6 +753,11 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
|
|||
return Sprite;
|
||||
}
|
||||
|
||||
static function _fetchData(id:String):Null<StageData>
|
||||
{
|
||||
return StageRegistry.instance.parseEntryDataWithMigration(id, StageRegistry.instance.fetchEntryVersion(id));
|
||||
}
|
||||
|
||||
public function onScriptEvent(event:ScriptEvent) {}
|
||||
|
||||
public function onPause(event:PauseScriptEvent) {}
|
||||
|
|
|
@ -1,548 +0,0 @@
|
|||
package funkin.play.stage;
|
||||
|
||||
import funkin.data.animation.AnimationData;
|
||||
import funkin.play.stage.ScriptedStage;
|
||||
import funkin.play.stage.Stage;
|
||||
import funkin.util.VersionUtil;
|
||||
import funkin.util.assets.DataAssets;
|
||||
import haxe.Json;
|
||||
import openfl.Assets;
|
||||
|
||||
/**
|
||||
* Contains utilities for loading and parsing stage data.
|
||||
*/
|
||||
class StageDataParser
|
||||
{
|
||||
/**
|
||||
* The current version string for the stage data format.
|
||||
* Handle breaking changes by incrementing this value
|
||||
* and adding migration to the `migrateStageData()` function.
|
||||
*/
|
||||
public static final STAGE_DATA_VERSION:String = "1.0.0";
|
||||
|
||||
/**
|
||||
* The current version rule check for the stage data format.
|
||||
*/
|
||||
public static final STAGE_DATA_VERSION_RULE:String = "1.0.x";
|
||||
|
||||
static final stageCache:Map<String, Stage> = new Map<String, Stage>();
|
||||
|
||||
static final DEFAULT_STAGE_ID = 'UNKNOWN';
|
||||
|
||||
/**
|
||||
* Parses and preloads the game's stage data and scripts when the game starts.
|
||||
*
|
||||
* If you want to force stages to be reloaded, you can just call this function again.
|
||||
*/
|
||||
public static function loadStageCache():Void
|
||||
{
|
||||
// Clear any stages that are cached if there were any.
|
||||
clearStageCache();
|
||||
trace("Loading stage cache...");
|
||||
|
||||
//
|
||||
// SCRIPTED STAGES
|
||||
//
|
||||
var scriptedStageClassNames:Array<String> = ScriptedStage.listScriptClasses();
|
||||
trace(' Instantiating ${scriptedStageClassNames.length} scripted stages...');
|
||||
for (stageCls in scriptedStageClassNames)
|
||||
{
|
||||
var stage:Stage = ScriptedStage.init(stageCls, DEFAULT_STAGE_ID);
|
||||
if (stage != null)
|
||||
{
|
||||
trace(' Loaded scripted stage: ${stage.stageName}');
|
||||
// Disable the rendering logic for stage until it's loaded.
|
||||
// Note that kill() =/= destroy()
|
||||
stage.kill();
|
||||
|
||||
// Then store it.
|
||||
stageCache.set(stage.stageId, stage);
|
||||
}
|
||||
else
|
||||
{
|
||||
trace(' Failed to instantiate scripted stage class: ${stageCls}');
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// UNSCRIPTED STAGES
|
||||
//
|
||||
var stageIdList:Array<String> = DataAssets.listDataFilesInPath('stages/');
|
||||
var unscriptedStageIds:Array<String> = stageIdList.filter(function(stageId:String):Bool {
|
||||
return !stageCache.exists(stageId);
|
||||
});
|
||||
trace(' Instantiating ${unscriptedStageIds.length} non-scripted stages...');
|
||||
for (stageId in unscriptedStageIds)
|
||||
{
|
||||
var stage:Stage;
|
||||
try
|
||||
{
|
||||
stage = new Stage(stageId);
|
||||
if (stage != null)
|
||||
{
|
||||
trace(' Loaded stage data: ${stage.stageName}');
|
||||
stageCache.set(stageId, stage);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace(' An error occurred while loading stage data: ${stageId}');
|
||||
// Assume error was already logged.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
trace(' Successfully loaded ${Lambda.count(stageCache)} stages.');
|
||||
}
|
||||
|
||||
public static function fetchStage(stageId:String):Null<Stage>
|
||||
{
|
||||
if (stageCache.exists(stageId))
|
||||
{
|
||||
trace('Successfully fetch stage: ${stageId}');
|
||||
var stage:Stage = stageCache.get(stageId);
|
||||
stage.revive();
|
||||
return stage;
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Failed to fetch stage, not found in cache: ${stageId}');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static function clearStageCache():Void
|
||||
{
|
||||
if (stageCache != null)
|
||||
{
|
||||
for (stage in stageCache)
|
||||
{
|
||||
stage.destroy();
|
||||
}
|
||||
stageCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a stage's JSON file, parse its data, and return it.
|
||||
*
|
||||
* @param stageId The stage to load.
|
||||
* @return The stage data, or null if validation failed.
|
||||
*/
|
||||
public static function parseStageData(stageId:String):Null<StageData>
|
||||
{
|
||||
var rawJson:String = loadStageFile(stageId);
|
||||
|
||||
var stageData:StageData = migrateStageData(rawJson, stageId);
|
||||
|
||||
return validateStageData(stageId, stageData);
|
||||
}
|
||||
|
||||
public static function listStageIds():Array<String>
|
||||
{
|
||||
return stageCache.keys().array();
|
||||
}
|
||||
|
||||
static function loadStageFile(stagePath:String):String
|
||||
{
|
||||
var stageFilePath:String = Paths.json('stages/${stagePath}');
|
||||
var rawJson = Assets.getText(stageFilePath).trim();
|
||||
|
||||
while (!rawJson.endsWith("}"))
|
||||
{
|
||||
rawJson = rawJson.substr(0, rawJson.length - 1);
|
||||
}
|
||||
|
||||
return rawJson;
|
||||
}
|
||||
|
||||
static function migrateStageData(rawJson:String, stageId:String):Null<StageData>
|
||||
{
|
||||
// If you update the stage data format in a breaking way,
|
||||
// handle migration here by checking the `version` value.
|
||||
|
||||
try
|
||||
{
|
||||
var parser = new json2object.JsonParser<StageData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
parser.fromJson(rawJson, '$stageId.json');
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
trace('[STAGE] Failed to parse stage data');
|
||||
|
||||
for (error in parser.errors)
|
||||
funkin.data.DataError.printError(error);
|
||||
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace(' Error parsing data for stage: ${stageId}');
|
||||
trace(' ${e}');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static final DEFAULT_ANIMTYPE:String = "sparrow";
|
||||
static final DEFAULT_CAMERAZOOM:Float = 1.0;
|
||||
static final DEFAULT_DANCEEVERY:Int = 0;
|
||||
static final DEFAULT_ISPIXEL:Bool = false;
|
||||
static final DEFAULT_NAME:String = "Untitled Stage";
|
||||
static final DEFAULT_OFFSETS:Array<Float> = [0, 0];
|
||||
static final DEFAULT_CAMERA_OFFSETS_BF:Array<Float> = [-100, -100];
|
||||
static final DEFAULT_CAMERA_OFFSETS_DAD:Array<Float> = [150, -100];
|
||||
static final DEFAULT_POSITION:Array<Float> = [0, 0];
|
||||
static final DEFAULT_SCALE:Float = 1.0;
|
||||
static final DEFAULT_ALPHA:Float = 1.0;
|
||||
static final DEFAULT_SCROLL:Array<Float> = [0, 0];
|
||||
static final DEFAULT_ZINDEX:Int = 0;
|
||||
|
||||
static final DEFAULT_CHARACTER_DATA:StageDataCharacter =
|
||||
{
|
||||
zIndex: DEFAULT_ZINDEX,
|
||||
position: DEFAULT_POSITION,
|
||||
cameraOffsets: DEFAULT_OFFSETS,
|
||||
}
|
||||
|
||||
/**
|
||||
* Set unspecified parameters to their defaults.
|
||||
* If the parameter is mandatory, print an error message.
|
||||
* @param id
|
||||
* @param input
|
||||
* @return The validated stage data
|
||||
*/
|
||||
static function validateStageData(id:String, input:StageData):Null<StageData>
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
trace('ERROR: Could not parse stage data for "${id}".');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (input.version == null)
|
||||
{
|
||||
trace('ERROR: Could not load stage data for "$id": missing version');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!VersionUtil.validateVersionStr(input.version, STAGE_DATA_VERSION_RULE))
|
||||
{
|
||||
trace('ERROR: Could not load stage data for "$id": bad version (got ${input.version}, expected ${STAGE_DATA_VERSION_RULE})');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (input.name == null)
|
||||
{
|
||||
trace('WARN: Stage data for "$id" missing name');
|
||||
input.name = DEFAULT_NAME;
|
||||
}
|
||||
|
||||
if (input.cameraZoom == null)
|
||||
{
|
||||
input.cameraZoom = DEFAULT_CAMERAZOOM;
|
||||
}
|
||||
|
||||
if (input.props == null)
|
||||
{
|
||||
input.props = [];
|
||||
}
|
||||
|
||||
for (inputProp in input.props)
|
||||
{
|
||||
// It's fine for inputProp.name to be null
|
||||
|
||||
if (inputProp.assetPath == null)
|
||||
{
|
||||
trace('ERROR: Could not load stage data for "$id": missing assetPath for prop "${inputProp.name}"');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (inputProp.position == null)
|
||||
{
|
||||
inputProp.position = DEFAULT_POSITION;
|
||||
}
|
||||
|
||||
if (inputProp.zIndex == null)
|
||||
{
|
||||
inputProp.zIndex = DEFAULT_ZINDEX;
|
||||
}
|
||||
|
||||
if (inputProp.isPixel == null)
|
||||
{
|
||||
inputProp.isPixel = DEFAULT_ISPIXEL;
|
||||
}
|
||||
|
||||
if (inputProp.danceEvery == null)
|
||||
{
|
||||
inputProp.danceEvery = DEFAULT_DANCEEVERY;
|
||||
}
|
||||
|
||||
if (inputProp.animType == null)
|
||||
{
|
||||
inputProp.animType = DEFAULT_ANIMTYPE;
|
||||
}
|
||||
|
||||
switch (inputProp.scale)
|
||||
{
|
||||
case null:
|
||||
inputProp.scale = Right([DEFAULT_SCALE, DEFAULT_SCALE]);
|
||||
case Left(value):
|
||||
inputProp.scale = Right([value, value]);
|
||||
case Right(_):
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
switch (inputProp.scroll)
|
||||
{
|
||||
case null:
|
||||
inputProp.scroll = Right(DEFAULT_SCROLL);
|
||||
case Left(value):
|
||||
inputProp.scroll = Right([value, value]);
|
||||
case Right(_):
|
||||
// Do nothing
|
||||
}
|
||||
|
||||
if (inputProp.alpha == null)
|
||||
{
|
||||
inputProp.alpha = DEFAULT_ALPHA;
|
||||
}
|
||||
|
||||
if (inputProp.animations == null)
|
||||
{
|
||||
inputProp.animations = [];
|
||||
}
|
||||
|
||||
if (inputProp.animations.length == 0 && inputProp.startingAnimation != null)
|
||||
{
|
||||
trace('ERROR: Could not load stage data for "$id": missing animations for prop "${inputProp.name}"');
|
||||
return null;
|
||||
}
|
||||
|
||||
for (inputAnimation in inputProp.animations)
|
||||
{
|
||||
if (inputAnimation.name == null)
|
||||
{
|
||||
trace('ERROR: Could not load stage data for "$id": missing animation name for prop "${inputProp.name}"');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (inputAnimation.frameRate == null)
|
||||
{
|
||||
inputAnimation.frameRate = 24;
|
||||
}
|
||||
|
||||
if (inputAnimation.offsets == null)
|
||||
{
|
||||
inputAnimation.offsets = DEFAULT_OFFSETS;
|
||||
}
|
||||
|
||||
if (inputAnimation.looped == null)
|
||||
{
|
||||
inputAnimation.looped = true;
|
||||
}
|
||||
|
||||
if (inputAnimation.flipX == null)
|
||||
{
|
||||
inputAnimation.flipX = false;
|
||||
}
|
||||
|
||||
if (inputAnimation.flipY == null)
|
||||
{
|
||||
inputAnimation.flipY = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (input.characters == null)
|
||||
{
|
||||
trace('ERROR: Could not load stage data for "$id": missing characters');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (input.characters.bf == null)
|
||||
{
|
||||
input.characters.bf = DEFAULT_CHARACTER_DATA;
|
||||
}
|
||||
if (input.characters.dad == null)
|
||||
{
|
||||
input.characters.dad = DEFAULT_CHARACTER_DATA;
|
||||
}
|
||||
if (input.characters.gf == null)
|
||||
{
|
||||
input.characters.gf = DEFAULT_CHARACTER_DATA;
|
||||
}
|
||||
|
||||
for (inputCharacter in [input.characters.bf, input.characters.dad, input.characters.gf])
|
||||
{
|
||||
if (inputCharacter.position == null || inputCharacter.position.length != 2)
|
||||
{
|
||||
inputCharacter.position = [0, 0];
|
||||
}
|
||||
}
|
||||
|
||||
// All good!
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
class StageData
|
||||
{
|
||||
/**
|
||||
* The sematic version number of the stage data JSON format.
|
||||
* Supports fancy comparisons like NPM does it's neat.
|
||||
*/
|
||||
public var version:String;
|
||||
|
||||
public var name:String;
|
||||
public var cameraZoom:Null<Float>;
|
||||
public var props:Array<StageDataProp>;
|
||||
public var characters:StageDataCharacters;
|
||||
|
||||
public function new()
|
||||
{
|
||||
this.version = StageDataParser.STAGE_DATA_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this StageData into a JSON string.
|
||||
*/
|
||||
public function serialize(pretty:Bool = true):String
|
||||
{
|
||||
var writer = new json2object.JsonWriter<StageData>();
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
}
|
||||
}
|
||||
|
||||
typedef StageDataCharacters =
|
||||
{
|
||||
var bf:StageDataCharacter;
|
||||
var dad:StageDataCharacter;
|
||||
var gf:StageDataCharacter;
|
||||
};
|
||||
|
||||
typedef StageDataProp =
|
||||
{
|
||||
/**
|
||||
* The name of the prop for later lookup by scripts.
|
||||
* Optional; if unspecified, the prop can't be referenced by scripts.
|
||||
*/
|
||||
@:optional
|
||||
var name:String;
|
||||
|
||||
/**
|
||||
* The asset used to display the prop.
|
||||
*/
|
||||
var assetPath:String;
|
||||
|
||||
/**
|
||||
* The position of the prop as an [x, y] array of two floats.
|
||||
*/
|
||||
var position:Array<Float>;
|
||||
|
||||
/**
|
||||
* A number determining the stack order of the prop, relative to other props and the characters in the stage.
|
||||
* Props with lower numbers render below those with higher numbers.
|
||||
* This is just like CSS, it isn't hard.
|
||||
* @default 0
|
||||
*/
|
||||
@:optional
|
||||
@:default(0)
|
||||
var zIndex:Int;
|
||||
|
||||
/**
|
||||
* If set to true, anti-aliasing will be forcibly disabled on the sprite.
|
||||
* This prevents blurry images on pixel-art levels.
|
||||
* @default false
|
||||
*/
|
||||
@:optional
|
||||
@:default(false)
|
||||
var isPixel:Bool;
|
||||
|
||||
/**
|
||||
* Either the scale of the prop as a float, or the [w, h] scale as an array of two floats.
|
||||
* Pro tip: On pixel-art levels, save the sprite small and set this value to 6 or so to save memory.
|
||||
*/
|
||||
@:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats)
|
||||
@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)
|
||||
@:optional
|
||||
var scale:haxe.ds.Either<Float, Array<Float>>;
|
||||
|
||||
/**
|
||||
* The alpha of the prop, as a float.
|
||||
* @default 1.0
|
||||
*/
|
||||
@:optional
|
||||
@:default(1.0)
|
||||
var alpha:Float;
|
||||
|
||||
/**
|
||||
* If not zero, this prop will play an animation every X beats of the song.
|
||||
* This requires animations to be defined. If `danceLeft` and `danceRight` are defined,
|
||||
* they will alternated between, otherwise the `idle` animation will be used.
|
||||
*
|
||||
* @default 0
|
||||
*/
|
||||
@:default(0)
|
||||
@:optional
|
||||
var danceEvery:Int;
|
||||
|
||||
/**
|
||||
* How much the prop scrolls relative to the camera. Used to create a parallax effect.
|
||||
* Represented as a float or as an [x, y] array of two floats.
|
||||
* [1, 1] means the prop moves 1:1 with the camera.
|
||||
* [0.5, 0.5] means the prop half as much as the camera.
|
||||
* [0, 0] means the prop is not moved.
|
||||
* @default [0, 0]
|
||||
*/
|
||||
@:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats)
|
||||
@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)
|
||||
@:optional
|
||||
var scroll:haxe.ds.Either<Float, Array<Float>>;
|
||||
|
||||
/**
|
||||
* An optional array of animations which the prop can play.
|
||||
* @default Prop has no animations.
|
||||
*/
|
||||
@:optional
|
||||
var animations:Array<AnimationData>;
|
||||
|
||||
/**
|
||||
* If animations are used, this is the name of the animation to play first.
|
||||
* @default Don't play an animation.
|
||||
*/
|
||||
@:optional
|
||||
var startingAnimation:Null<String>;
|
||||
|
||||
/**
|
||||
* The animation type to use.
|
||||
* Options: "sparrow", "packer"
|
||||
* @default "sparrow"
|
||||
*/
|
||||
@:default("sparrow")
|
||||
@:optional
|
||||
var animType:String;
|
||||
};
|
||||
|
||||
typedef StageDataCharacter =
|
||||
{
|
||||
/**
|
||||
* A number determining the stack order of the character, relative to props and other characters in the stage.
|
||||
* Again, just like CSS.
|
||||
* @default 0
|
||||
*/
|
||||
var zIndex:Int;
|
||||
|
||||
/**
|
||||
* The position to render the character at.
|
||||
*/
|
||||
var position:Array<Float>;
|
||||
|
||||
/**
|
||||
* The camera offsets to apply when focusing on the character on this stage.
|
||||
* @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF
|
||||
*/
|
||||
var cameraOffsets:Array<Float>;
|
||||
};
|
|
@ -1,10 +1,10 @@
|
|||
package funkin.play.stage;
|
||||
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import flixel.FlxSprite;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import funkin.modding.IScriptedClass.IStateStageProp;
|
||||
|
||||
class StageProp extends FlxSprite implements IStateStageProp
|
||||
class StageProp extends FunkinSprite implements IStateStageProp
|
||||
{
|
||||
/**
|
||||
* An internal name for this prop.
|
||||
|
|
|
@ -14,8 +14,8 @@ import thx.semver.Version;
|
|||
@:forward(volume, mute)
|
||||
abstract Save(RawSaveData)
|
||||
{
|
||||
// Version 2.0.1 adds attributes to `optionsChartEditor`, that should return default values if they are null.
|
||||
public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.1";
|
||||
// Version 2.0.2 adds attributes to `optionsChartEditor`, that should return default values if they are null.
|
||||
public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.2";
|
||||
public static final SAVE_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
|
||||
|
||||
// We load this version's saves from a new save path, to maintain SOME level of backwards compatibility.
|
||||
|
@ -108,6 +108,7 @@ abstract Save(RawSaveData)
|
|||
metronomeVolume: 1.0,
|
||||
hitsoundsEnabledPlayer: true,
|
||||
hitsoundsEnabledOpponent: true,
|
||||
themeMusic: true,
|
||||
instVolume: 1.0,
|
||||
voicesVolume: 1.0,
|
||||
playbackSpeed: 1.0,
|
||||
|
@ -347,6 +348,23 @@ abstract Save(RawSaveData)
|
|||
return this.optionsChartEditor.hitsoundsEnabledOpponent;
|
||||
}
|
||||
|
||||
public var chartEditorThemeMusic(get, set):Bool;
|
||||
|
||||
function get_chartEditorThemeMusic():Bool
|
||||
{
|
||||
if (this.optionsChartEditor.themeMusic == null) this.optionsChartEditor.themeMusic = true;
|
||||
|
||||
return this.optionsChartEditor.themeMusic;
|
||||
}
|
||||
|
||||
function set_chartEditorThemeMusic(value:Bool):Bool
|
||||
{
|
||||
// Set and apply.
|
||||
this.optionsChartEditor.themeMusic = value;
|
||||
flush();
|
||||
return this.optionsChartEditor.themeMusic;
|
||||
}
|
||||
|
||||
public var chartEditorInstVolume(get, set):Float;
|
||||
|
||||
function get_chartEditorInstVolume():Float
|
||||
|
@ -774,7 +792,6 @@ typedef SaveScoreData =
|
|||
|
||||
typedef SaveScoreTallyData =
|
||||
{
|
||||
var killer:Int;
|
||||
var sick:Int;
|
||||
var good:Int;
|
||||
var bad:Int;
|
||||
|
@ -1027,6 +1044,12 @@ typedef SaveDataChartEditorOptions =
|
|||
*/
|
||||
var ?hitsoundsEnabledOpponent:Bool;
|
||||
|
||||
/**
|
||||
* Theme music in the Chart Editor.
|
||||
* @default `true`
|
||||
*/
|
||||
var ?themeMusic:Bool;
|
||||
|
||||
/**
|
||||
* Instrumental volume in the Chart Editor.
|
||||
* @default `1.0`
|
||||
|
|
|
@ -120,7 +120,6 @@ class SaveDataMigrator
|
|||
accuracy: inputSaveData.songCompletion.get('${levelId}-easy') ?? 0.0,
|
||||
tallies:
|
||||
{
|
||||
killer: 0,
|
||||
sick: 0,
|
||||
good: 0,
|
||||
bad: 0,
|
||||
|
@ -140,7 +139,6 @@ class SaveDataMigrator
|
|||
accuracy: inputSaveData.songCompletion.get('${levelId}') ?? 0.0,
|
||||
tallies:
|
||||
{
|
||||
killer: 0,
|
||||
sick: 0,
|
||||
good: 0,
|
||||
bad: 0,
|
||||
|
@ -160,7 +158,6 @@ class SaveDataMigrator
|
|||
accuracy: inputSaveData.songCompletion.get('${levelId}-hard') ?? 0.0,
|
||||
tallies:
|
||||
{
|
||||
killer: 0,
|
||||
sick: 0,
|
||||
good: 0,
|
||||
bad: 0,
|
||||
|
@ -183,7 +180,6 @@ class SaveDataMigrator
|
|||
accuracy: 0,
|
||||
tallies:
|
||||
{
|
||||
killer: 0,
|
||||
sick: 0,
|
||||
good: 0,
|
||||
bad: 0,
|
||||
|
@ -209,7 +205,6 @@ class SaveDataMigrator
|
|||
accuracy: 0,
|
||||
tallies:
|
||||
{
|
||||
killer: 0,
|
||||
sick: 0,
|
||||
good: 0,
|
||||
bad: 0,
|
||||
|
@ -235,7 +230,6 @@ class SaveDataMigrator
|
|||
accuracy: 0,
|
||||
tallies:
|
||||
{
|
||||
killer: 0,
|
||||
sick: 0,
|
||||
good: 0,
|
||||
bad: 0,
|
||||
|
|
|
@ -274,4 +274,5 @@ enum abstract AtlasFont(String) from String to String
|
|||
{
|
||||
var DEFAULT = "default";
|
||||
var BOLD = "bold";
|
||||
var FREEPLAY_CLEAR = "freeplay-clear";
|
||||
}
|
||||
|
|
|
@ -80,25 +80,11 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
|
|||
if (FlxG.keys.justPressed.F5) debug_refreshModules();
|
||||
}
|
||||
|
||||
function handleQuickWatch():Void
|
||||
{
|
||||
// Display Conductor info in the watch window.
|
||||
FlxG.watch.addQuick("songPosition", Conductor.songPosition);
|
||||
FlxG.watch.addQuick("songPositionNoOffset", Conductor.songPosition + Conductor.instrumentalOffset);
|
||||
FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0);
|
||||
FlxG.watch.addQuick("bpm", Conductor.bpm);
|
||||
FlxG.watch.addQuick("currentMeasureTime", Conductor.currentBeatTime);
|
||||
FlxG.watch.addQuick("currentBeatTime", Conductor.currentBeatTime);
|
||||
FlxG.watch.addQuick("currentStepTime", Conductor.currentStepTime);
|
||||
}
|
||||
|
||||
override function update(elapsed:Float)
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
handleControls();
|
||||
handleFunctionControls();
|
||||
handleQuickWatch();
|
||||
|
||||
dispatchEvent(new UpdateScriptEvent(elapsed));
|
||||
}
|
||||
|
@ -139,7 +125,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
|
|||
|
||||
public function stepHit():Bool
|
||||
{
|
||||
var event = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.currentBeat, Conductor.currentStep);
|
||||
var event = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.instance.currentBeat, Conductor.instance.currentStep);
|
||||
|
||||
dispatchEvent(event);
|
||||
|
||||
|
@ -150,7 +136,7 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
|
|||
|
||||
public function beatHit():Bool
|
||||
{
|
||||
var event = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.currentBeat, Conductor.currentStep);
|
||||
var event = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.instance.currentBeat, Conductor.instance.currentStep);
|
||||
|
||||
dispatchEvent(event);
|
||||
|
||||
|
|
|
@ -65,12 +65,8 @@ class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandl
|
|||
if (FlxG.keys.justPressed.F5) debug_refreshModules();
|
||||
|
||||
// Display Conductor info in the watch window.
|
||||
FlxG.watch.addQuick("songPosition", Conductor.songPosition);
|
||||
FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0);
|
||||
FlxG.watch.addQuick("bpm", Conductor.bpm);
|
||||
FlxG.watch.addQuick("currentMeasureTime", Conductor.currentBeatTime);
|
||||
FlxG.watch.addQuick("currentBeatTime", Conductor.currentBeatTime);
|
||||
FlxG.watch.addQuick("currentStepTime", Conductor.currentStepTime);
|
||||
Conductor.watchQuick();
|
||||
|
||||
dispatchEvent(new UpdateScriptEvent(elapsed));
|
||||
}
|
||||
|
@ -99,7 +95,7 @@ class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandl
|
|||
*/
|
||||
public function stepHit():Bool
|
||||
{
|
||||
var event:ScriptEvent = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.currentBeat, Conductor.currentStep);
|
||||
var event:ScriptEvent = new SongTimeScriptEvent(SONG_STEP_HIT, Conductor.instance.currentBeat, Conductor.instance.currentStep);
|
||||
|
||||
dispatchEvent(event);
|
||||
|
||||
|
@ -115,7 +111,7 @@ class MusicBeatSubState extends FlxTransitionableSubState implements IEventHandl
|
|||
*/
|
||||
public function beatHit():Bool
|
||||
{
|
||||
var event:ScriptEvent = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.currentBeat, Conductor.currentStep);
|
||||
var event:ScriptEvent = new SongTimeScriptEvent(SONG_BEAT_HIT, Conductor.instance.currentBeat, Conductor.instance.currentStep);
|
||||
|
||||
dispatchEvent(event);
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -59,6 +59,12 @@ class AddEventsCommand implements ChartEditorCommand
|
|||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return (events.length > 0);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var len:Int = events.length;
|
||||
|
|
|
@ -59,6 +59,12 @@ class AddNotesCommand implements ChartEditorCommand
|
|||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return (notes.length > 0);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
if (notes.length == 1)
|
||||
|
|
|
@ -34,7 +34,12 @@ class ChangeStartingBPMCommand implements ChartEditorCommand
|
|||
|
||||
state.currentSongMetadata.timeChanges = timeChanges;
|
||||
|
||||
Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges);
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
state.notePreviewViewportBoundsDirty = true;
|
||||
state.scrollPositionInPixels = 0;
|
||||
|
||||
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
|
@ -51,7 +56,18 @@ class ChangeStartingBPMCommand implements ChartEditorCommand
|
|||
|
||||
state.currentSongMetadata.timeChanges = timeChanges;
|
||||
|
||||
Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges);
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
state.notePreviewViewportBoundsDirty = true;
|
||||
state.scrollPositionInPixels = 0;
|
||||
|
||||
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return (targetBPM != previousBPM);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
|
|
|
@ -6,6 +6,8 @@ package funkin.ui.debug.charting.commands;
|
|||
*
|
||||
* To make a functionality compatible with the undo/redo history, create a new class
|
||||
* that implements ChartEditorCommand, then call `ChartEditorState.performCommand(new Command())`
|
||||
*
|
||||
* NOTE: Make the constructor very simple, as it may be called without executing by the command palette.
|
||||
*/
|
||||
interface ChartEditorCommand
|
||||
{
|
||||
|
@ -22,6 +24,15 @@ interface ChartEditorCommand
|
|||
*/
|
||||
public function undo(state:ChartEditorState):Void;
|
||||
|
||||
/**
|
||||
* Return whether or not this command should be appended to the in the undo/redo history.
|
||||
* Generally this should be true, it should only be false if the command is minor and non-destructive,
|
||||
* like copying to the clipboard.
|
||||
*
|
||||
* Called after `execute()` is performed.
|
||||
*/
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool;
|
||||
|
||||
/**
|
||||
* Get a short description of the action (for the UI).
|
||||
* For example, return `Add Left Note` to display `Undo Add Left Note` in the menu.
|
||||
|
|
144
source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx
Normal file
144
source/funkin/ui/debug/charting/commands/CopyItemsCommand.hx
Normal file
|
@ -0,0 +1,144 @@
|
|||
package funkin.ui.debug.charting.commands;
|
||||
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.data.song.SongDataUtils;
|
||||
import flixel.tweens.FlxEase;
|
||||
import flixel.tweens.FlxTween;
|
||||
|
||||
/**
|
||||
* Command that copies a given set of notes and song events to the clipboard,
|
||||
* without deleting them from the chart editor.
|
||||
*/
|
||||
@:nullSafety
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
class CopyItemsCommand implements ChartEditorCommand
|
||||
{
|
||||
var notes:Array<SongNoteData>;
|
||||
var events:Array<SongEventData>;
|
||||
|
||||
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
|
||||
{
|
||||
this.notes = notes;
|
||||
this.events = events;
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
// Calculate a single time offset for all the notes and events.
|
||||
var timeOffset:Null<Int> = state.currentNoteSelection.length > 0 ? Std.int(state.currentNoteSelection[0].time) : null;
|
||||
if (state.currentEventSelection.length > 0)
|
||||
{
|
||||
if (timeOffset == null || state.currentEventSelection[0].time < timeOffset)
|
||||
{
|
||||
timeOffset = Std.int(state.currentEventSelection[0].time);
|
||||
}
|
||||
}
|
||||
|
||||
SongDataUtils.writeItemsToClipboard(
|
||||
{
|
||||
notes: SongDataUtils.buildNoteClipboard(state.currentNoteSelection, timeOffset),
|
||||
events: SongDataUtils.buildEventClipboard(state.currentEventSelection, timeOffset),
|
||||
});
|
||||
|
||||
performVisuals(state);
|
||||
}
|
||||
|
||||
function performVisuals(state:ChartEditorState):Void
|
||||
{
|
||||
if (state.currentNoteSelection.length > 0)
|
||||
{
|
||||
// Display the "Copied Notes" text.
|
||||
if (state.txtCopyNotif != null)
|
||||
{
|
||||
state.txtCopyNotif.visible = true;
|
||||
state.txtCopyNotif.text = "Copied " + state.currentNoteSelection.length + " notes to clipboard";
|
||||
state.txtCopyNotif.x = FlxG.mouse.x - (state.txtCopyNotif.width / 2);
|
||||
state.txtCopyNotif.y = FlxG.mouse.y - 16;
|
||||
FlxTween.tween(state.txtCopyNotif, {y: state.txtCopyNotif.y - 32}, 0.5,
|
||||
{
|
||||
type: FlxTween.ONESHOT,
|
||||
ease: FlxEase.quadOut,
|
||||
onComplete: function(_) {
|
||||
state.txtCopyNotif.visible = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Wiggle the notes.
|
||||
for (note in state.renderedNotes.members)
|
||||
{
|
||||
if (state.isNoteSelected(note.noteData))
|
||||
{
|
||||
FlxTween.globalManager.cancelTweensOf(note);
|
||||
FlxTween.globalManager.cancelTweensOf(note.scale);
|
||||
note.playNoteAnimation();
|
||||
var prevX:Float = note.scale.x;
|
||||
var prevY:Float = note.scale.y;
|
||||
|
||||
note.scale.x *= 1.2;
|
||||
note.scale.y *= 1.2;
|
||||
|
||||
note.angle = FlxG.random.bool() ? -10 : 10;
|
||||
FlxTween.tween(note, {"angle": 0}, 0.8, {ease: FlxEase.elasticOut});
|
||||
|
||||
FlxTween.tween(note.scale, {"y": prevX, "x": prevY}, 0.7,
|
||||
{
|
||||
ease: FlxEase.elasticOut,
|
||||
onComplete: function(_) {
|
||||
note.playNoteAnimation();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Wiggle the events.
|
||||
for (event in state.renderedEvents.members)
|
||||
{
|
||||
if (state.isEventSelected(event.eventData))
|
||||
{
|
||||
FlxTween.globalManager.cancelTweensOf(event);
|
||||
FlxTween.globalManager.cancelTweensOf(event.scale);
|
||||
event.playAnimation();
|
||||
var prevX:Float = event.scale.x;
|
||||
var prevY:Float = event.scale.y;
|
||||
|
||||
event.scale.x *= 1.2;
|
||||
event.scale.y *= 1.2;
|
||||
|
||||
event.angle = FlxG.random.bool() ? -10 : 10;
|
||||
FlxTween.tween(event, {"angle": 0}, 0.8, {ease: FlxEase.elasticOut});
|
||||
|
||||
FlxTween.tween(event.scale, {"y": prevX, "x": prevY}, 0.7,
|
||||
{
|
||||
ease: FlxEase.elasticOut,
|
||||
onComplete: function(_) {
|
||||
event.playAnimation();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
{
|
||||
// This command is not undoable. Do nothing.
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is not undoable. Don't add it to the history.
|
||||
return false;
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var len:Int = notes.length + events.length;
|
||||
|
||||
if (notes.length == 0) return 'Copy $len Events to Clipboard';
|
||||
else if (events.length == 0) return 'Copy $len Notes to Clipboard';
|
||||
else
|
||||
return 'Copy $len Items to Clipboard';
|
||||
}
|
||||
}
|
|
@ -56,6 +56,12 @@ class CutItemsCommand implements ChartEditorCommand
|
|||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Always add it to the history.
|
||||
return (notes.length > 0 || events.length > 0);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var len:Int = notes.length + events.length;
|
||||
|
|
|
@ -10,17 +10,16 @@ import funkin.data.song.SongData.SongEventData;
|
|||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
class DeselectAllItemsCommand implements ChartEditorCommand
|
||||
{
|
||||
var previousNoteSelection:Array<SongNoteData>;
|
||||
var previousEventSelection:Array<SongEventData>;
|
||||
var previousNoteSelection:Array<SongNoteData> = [];
|
||||
var previousEventSelection:Array<SongEventData> = [];
|
||||
|
||||
public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
|
||||
{
|
||||
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
|
||||
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
|
||||
}
|
||||
public function new() {}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
this.previousNoteSelection = state.currentNoteSelection;
|
||||
this.previousEventSelection = state.currentEventSelection;
|
||||
|
||||
state.currentNoteSelection = [];
|
||||
state.currentEventSelection = [];
|
||||
|
||||
|
@ -35,6 +34,12 @@ class DeselectAllItemsCommand implements ChartEditorCommand
|
|||
state.noteDisplayDirty = true;
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return (previousNoteSelection.length > 0 || previousEventSelection.length > 0);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
return 'Deselect All Items';
|
||||
|
|
|
@ -45,16 +45,27 @@ class DeselectItemsCommand implements ChartEditorCommand
|
|||
state.notePreviewDirty = true;
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return (notes.length > 0 || events.length > 0);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var noteCount = notes.length + events.length;
|
||||
var isPlural = (notes.length + events.length) > 1;
|
||||
var notesOnly = (notes.length > 0 && events.length == 0);
|
||||
var eventsOnly = (notes.length == 0 && events.length > 0);
|
||||
|
||||
if (noteCount == 1)
|
||||
if (notesOnly)
|
||||
{
|
||||
var dir:String = notes[0].getDirectionName();
|
||||
return 'Deselect $dir Items';
|
||||
return 'Deselect ${notes.length} ${isPlural ? 'Notes' : 'Note'}';
|
||||
}
|
||||
else if (eventsOnly)
|
||||
{
|
||||
return 'Deselect ${events.length} ${isPlural ? 'Events' : 'Event'}';
|
||||
}
|
||||
|
||||
return 'Deselect ${noteCount} Items';
|
||||
return 'Deselect ${notes.length + events.length} Items';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,17 +13,25 @@ class ExtendNoteLengthCommand implements ChartEditorCommand
|
|||
var note:SongNoteData;
|
||||
var oldLength:Float;
|
||||
var newLength:Float;
|
||||
var unit:Unit;
|
||||
|
||||
public function new(note:SongNoteData, newLength:Float)
|
||||
public function new(note:SongNoteData, newLength:Float, unit:Unit = MILLISECONDS)
|
||||
{
|
||||
this.note = note;
|
||||
this.oldLength = note.length;
|
||||
this.newLength = newLength;
|
||||
this.unit = unit;
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
note.length = newLength;
|
||||
switch (unit)
|
||||
{
|
||||
case MILLISECONDS:
|
||||
this.note.length = newLength;
|
||||
case STEPS:
|
||||
this.note.setStepLength(newLength);
|
||||
}
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
|
@ -36,7 +44,8 @@ class ExtendNoteLengthCommand implements ChartEditorCommand
|
|||
{
|
||||
state.playSound(Paths.sound('chartingSounds/undo'));
|
||||
|
||||
note.length = oldLength;
|
||||
// Always use milliseconds for undoing
|
||||
this.note.length = oldLength;
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
|
@ -45,8 +54,31 @@ class ExtendNoteLengthCommand implements ChartEditorCommand
|
|||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return (oldLength != newLength);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
return 'Extend Note Length';
|
||||
if (oldLength == 0)
|
||||
{
|
||||
return 'Add Hold to Note';
|
||||
}
|
||||
else if (newLength == 0)
|
||||
{
|
||||
return 'Remove Hold from Note';
|
||||
}
|
||||
else
|
||||
{
|
||||
return 'Extend Hold Note Length';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum Unit
|
||||
{
|
||||
MILLISECONDS;
|
||||
STEPS;
|
||||
}
|
||||
|
|
|
@ -51,6 +51,12 @@ class FlipNotesCommand implements ChartEditorCommand
|
|||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return (notes.length > 0);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var len:Int = notes.length;
|
||||
|
|
|
@ -12,19 +12,19 @@ import funkin.data.song.SongDataUtils;
|
|||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
class InvertSelectedItemsCommand implements ChartEditorCommand
|
||||
{
|
||||
var previousNoteSelection:Array<SongNoteData>;
|
||||
var previousEventSelection:Array<SongEventData>;
|
||||
var previousNoteSelection:Array<SongNoteData> = [];
|
||||
var previousEventSelection:Array<SongEventData> = [];
|
||||
|
||||
public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
|
||||
{
|
||||
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
|
||||
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
|
||||
}
|
||||
public function new() {}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
this.previousNoteSelection = state.currentNoteSelection;
|
||||
this.previousEventSelection = state.currentEventSelection;
|
||||
|
||||
state.currentNoteSelection = SongDataUtils.subtractNotes(state.currentSongChartNoteData, previousNoteSelection);
|
||||
state.currentEventSelection = SongDataUtils.subtractEvents(state.currentSongChartEventData, previousEventSelection);
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
}
|
||||
|
||||
|
@ -36,6 +36,12 @@ class InvertSelectedItemsCommand implements ChartEditorCommand
|
|||
state.noteDisplayDirty = true;
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return (previousNoteSelection.length > 0 || previousEventSelection.length > 0);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
return 'Invert Selected Items';
|
||||
|
|
|
@ -33,7 +33,7 @@ class MoveEventsCommand implements ChartEditorCommand
|
|||
{
|
||||
// Clone the notes to prevent editing from affecting the history.
|
||||
var resultEvent = event.clone();
|
||||
resultEvent.time = (resultEvent.time + offset).clamp(0, Conductor.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
|
||||
resultEvent.time = (resultEvent.time + offset).clamp(0, Conductor.instance.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
|
||||
|
||||
movedEvents.push(resultEvent);
|
||||
}
|
||||
|
@ -65,6 +65,12 @@ class MoveEventsCommand implements ChartEditorCommand
|
|||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return (events.length > 0);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var len:Int = events.length;
|
||||
|
|
|
@ -21,8 +21,8 @@ class MoveItemsCommand implements ChartEditorCommand
|
|||
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>, offset:Float, columns:Int)
|
||||
{
|
||||
// Clone the notes to prevent editing from affecting the history.
|
||||
this.notes = [for (note in notes) note.clone()];
|
||||
this.events = [for (event in events) event.clone()];
|
||||
this.notes = notes.clone();
|
||||
this.events = events.clone();
|
||||
this.offset = offset;
|
||||
this.columns = columns;
|
||||
this.movedNotes = [];
|
||||
|
@ -41,7 +41,7 @@ class MoveItemsCommand implements ChartEditorCommand
|
|||
{
|
||||
// Clone the notes to prevent editing from affecting the history.
|
||||
var resultNote = note.clone();
|
||||
resultNote.time = (resultNote.time + offset).clamp(0, Conductor.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
|
||||
resultNote.time = (resultNote.time + offset).clamp(0, Conductor.instance.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
|
||||
resultNote.data = ChartEditorState.gridColumnToNoteData((ChartEditorState.noteDataToGridColumn(resultNote.data) + columns).clamp(0,
|
||||
ChartEditorState.STRUMLINE_SIZE * 2 - 1));
|
||||
|
||||
|
@ -52,7 +52,7 @@ class MoveItemsCommand implements ChartEditorCommand
|
|||
{
|
||||
// Clone the notes to prevent editing from affecting the history.
|
||||
var resultEvent = event.clone();
|
||||
resultEvent.time = (resultEvent.time + offset).clamp(0, Conductor.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
|
||||
resultEvent.time = (resultEvent.time + offset).clamp(0, Conductor.instance.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
|
||||
|
||||
movedEvents.push(resultEvent);
|
||||
}
|
||||
|
@ -88,6 +88,12 @@ class MoveItemsCommand implements ChartEditorCommand
|
|||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return (notes.length > 0 || events.length > 0);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var len:Int = notes.length + events.length;
|
||||
|
|
|
@ -34,7 +34,7 @@ class MoveNotesCommand implements ChartEditorCommand
|
|||
{
|
||||
// Clone the notes to prevent editing from affecting the history.
|
||||
var resultNote = note.clone();
|
||||
resultNote.time = (resultNote.time + offset).clamp(0, Conductor.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
|
||||
resultNote.time = (resultNote.time + offset).clamp(0, Conductor.instance.getStepTimeInMs(state.songLengthInSteps - (1 * state.noteSnapRatio)));
|
||||
resultNote.data = ChartEditorState.gridColumnToNoteData((ChartEditorState.noteDataToGridColumn(resultNote.data) + columns).clamp(0,
|
||||
ChartEditorState.STRUMLINE_SIZE * 2 - 1));
|
||||
|
||||
|
@ -67,6 +67,12 @@ class MoveNotesCommand implements ChartEditorCommand
|
|||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return (notes.length > 0);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var len:Int = notes.length;
|
||||
|
|
|
@ -32,9 +32,9 @@ class PasteItemsCommand implements ChartEditorCommand
|
|||
return;
|
||||
}
|
||||
|
||||
var stepEndOfSong:Float = Conductor.getTimeInSteps(state.songLengthInMs);
|
||||
var stepEndOfSong:Float = Conductor.instance.getTimeInSteps(state.songLengthInMs);
|
||||
var stepCutoff:Float = stepEndOfSong - 1.0;
|
||||
var msCutoff:Float = Conductor.getStepTimeInMs(stepCutoff);
|
||||
var msCutoff:Float = Conductor.instance.getStepTimeInMs(stepCutoff);
|
||||
|
||||
addedNotes = SongDataUtils.offsetSongNoteData(currentClipboard.notes, Std.int(targetTimestamp));
|
||||
addedNotes = SongDataUtils.clampSongNoteData(addedNotes, 0.0, msCutoff);
|
||||
|
@ -71,6 +71,12 @@ class PasteItemsCommand implements ChartEditorCommand
|
|||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return (addedNotes.length > 0 || addedEvents.length > 0);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard();
|
||||
|
|
|
@ -48,6 +48,12 @@ class RemoveEventsCommand implements ChartEditorCommand
|
|||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return (events.length > 0);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
if (events.length == 1 && events[0] != null)
|
||||
|
|
|
@ -62,6 +62,12 @@ class RemoveItemsCommand implements ChartEditorCommand
|
|||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return (notes.length > 0 || events.length > 0);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
return 'Remove ${notes.length + events.length} Items';
|
||||
|
|
|
@ -50,6 +50,12 @@ class RemoveNotesCommand implements ChartEditorCommand
|
|||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return (notes.length > 0);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
if (notes.length == 1 && notes[0] != null)
|
||||
|
|
|
@ -10,19 +10,25 @@ import funkin.data.song.SongData.SongEventData;
|
|||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
class SelectAllItemsCommand implements ChartEditorCommand
|
||||
{
|
||||
var previousNoteSelection:Array<SongNoteData>;
|
||||
var previousEventSelection:Array<SongEventData>;
|
||||
var shouldSelectNotes:Bool;
|
||||
var shouldSelectEvents:Bool;
|
||||
|
||||
public function new(?previousNoteSelection:Array<SongNoteData>, ?previousEventSelection:Array<SongEventData>)
|
||||
var previousNoteSelection:Array<SongNoteData> = [];
|
||||
var previousEventSelection:Array<SongEventData> = [];
|
||||
|
||||
public function new(shouldSelectNotes:Bool, shouldSelectEvents:Bool)
|
||||
{
|
||||
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
|
||||
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
|
||||
this.shouldSelectNotes = shouldSelectNotes;
|
||||
this.shouldSelectEvents = shouldSelectEvents;
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentNoteSelection = state.currentSongChartNoteData;
|
||||
state.currentEventSelection = state.currentSongChartEventData;
|
||||
this.previousNoteSelection = state.currentNoteSelection;
|
||||
this.previousEventSelection = state.currentEventSelection;
|
||||
|
||||
state.currentNoteSelection = shouldSelectNotes ? state.currentSongChartNoteData : [];
|
||||
state.currentEventSelection = shouldSelectEvents ? state.currentSongChartEventData : [];
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
}
|
||||
|
@ -35,8 +41,29 @@ class SelectAllItemsCommand implements ChartEditorCommand
|
|||
state.noteDisplayDirty = true;
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return (state.currentNoteSelection.length > 0 || state.currentEventSelection.length > 0);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
return 'Select All Items';
|
||||
if (shouldSelectNotes && !shouldSelectEvents)
|
||||
{
|
||||
return 'Select All Notes';
|
||||
}
|
||||
else if (shouldSelectEvents && !shouldSelectNotes)
|
||||
{
|
||||
return 'Select All Events';
|
||||
}
|
||||
else if (shouldSelectNotes && shouldSelectEvents)
|
||||
{
|
||||
return 'Select All Notes and Events';
|
||||
}
|
||||
else
|
||||
{
|
||||
return 'Select Nothing (Huh?)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,10 +15,10 @@ class SelectItemsCommand implements ChartEditorCommand
|
|||
var notes:Array<SongNoteData>;
|
||||
var events:Array<SongEventData>;
|
||||
|
||||
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
|
||||
public function new(?notes:Array<SongNoteData>, ?events:Array<SongEventData>)
|
||||
{
|
||||
this.notes = notes;
|
||||
this.events = events;
|
||||
this.notes = notes ?? [];
|
||||
this.events = events ?? [];
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
|
@ -33,6 +33,32 @@ class SelectItemsCommand implements ChartEditorCommand
|
|||
state.currentEventSelection.push(event);
|
||||
}
|
||||
|
||||
// If we just selected one or more events (and no notes), then we should make the event data toolbox display the event data for the selected event.
|
||||
if (this.notes.length == 0 && this.events.length >= 1)
|
||||
{
|
||||
var eventSelected = this.events[0];
|
||||
|
||||
state.eventKindToPlace = eventSelected.event;
|
||||
|
||||
// This code is here to parse event data that's not built as a struct for some reason.
|
||||
// TODO: Clean this up or get rid of it.
|
||||
var eventSchema = eventSelected.getSchema();
|
||||
var defaultKey = null;
|
||||
if (eventSchema == null)
|
||||
{
|
||||
trace('[WARNING] Event schema not found for event ${eventSelected.event}.');
|
||||
}
|
||||
else
|
||||
{
|
||||
defaultKey = eventSchema.getFirstField()?.name;
|
||||
}
|
||||
var eventData = eventSelected.valueAsStruct(defaultKey);
|
||||
|
||||
state.eventDataToPlace = eventData;
|
||||
|
||||
state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT);
|
||||
}
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
}
|
||||
|
@ -46,6 +72,12 @@ class SelectItemsCommand implements ChartEditorCommand
|
|||
state.notePreviewDirty = true;
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return (notes.length > 0 || events.length > 0);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var len:Int = notes.length + events.length;
|
||||
|
|
|
@ -13,23 +13,49 @@ class SetItemSelectionCommand implements ChartEditorCommand
|
|||
{
|
||||
var notes:Array<SongNoteData>;
|
||||
var events:Array<SongEventData>;
|
||||
var previousNoteSelection:Array<SongNoteData>;
|
||||
var previousEventSelection:Array<SongEventData>;
|
||||
var previousNoteSelection:Array<SongNoteData> = [];
|
||||
var previousEventSelection:Array<SongEventData> = [];
|
||||
|
||||
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>, previousNoteSelection:Array<SongNoteData>,
|
||||
previousEventSelection:Array<SongEventData>)
|
||||
public function new(notes:Array<SongNoteData>, events:Array<SongEventData>)
|
||||
{
|
||||
this.notes = notes;
|
||||
this.events = events;
|
||||
this.previousNoteSelection = previousNoteSelection == null ? [] : previousNoteSelection;
|
||||
this.previousEventSelection = previousEventSelection == null ? [] : previousEventSelection;
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
this.previousNoteSelection = state.currentNoteSelection;
|
||||
this.previousEventSelection = state.currentEventSelection;
|
||||
|
||||
state.currentNoteSelection = notes;
|
||||
state.currentEventSelection = events;
|
||||
|
||||
// If we just selected one or more events (and no notes), then we should make the event data toolbox display the event data for the selected event.
|
||||
if (this.notes.length == 0 && this.events.length >= 1)
|
||||
{
|
||||
var eventSelected = this.events[0];
|
||||
|
||||
state.eventKindToPlace = eventSelected.event;
|
||||
|
||||
// This code is here to parse event data that's not built as a struct for some reason.
|
||||
// TODO: Clean this up or get rid of it.
|
||||
var eventSchema = eventSelected.getSchema();
|
||||
var defaultKey = null;
|
||||
if (eventSchema == null)
|
||||
{
|
||||
trace('[WARNING] Event schema not found for event ${eventSelected.event}.');
|
||||
}
|
||||
else
|
||||
{
|
||||
defaultKey = eventSchema.getFirstField()?.name;
|
||||
}
|
||||
var eventData = eventSelected.valueAsStruct(defaultKey);
|
||||
|
||||
state.eventDataToPlace = eventData;
|
||||
|
||||
state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT);
|
||||
}
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
}
|
||||
|
||||
|
@ -41,8 +67,14 @@ class SetItemSelectionCommand implements ChartEditorCommand
|
|||
state.noteDisplayDirty = true;
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// Add to the history if we actually performed an action.
|
||||
return (state.currentNoteSelection != previousNoteSelection && state.currentEventSelection != previousEventSelection);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
return 'Select ${notes.length} Items';
|
||||
return 'Select ${notes.length + events.length} Items';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,6 +38,12 @@ class SwitchDifficultyCommand implements ChartEditorCommand
|
|||
state.notePreviewDirty = true;
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// Add to the history if we actually performed an action.
|
||||
return (prevVariation != newVariation || prevDifficulty != newDifficulty);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
return 'Switch Difficulty';
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package funkin.ui.debug.charting.components;
|
||||
|
||||
import funkin.data.event.SongEventData.SongEventParser;
|
||||
import funkin.data.event.SongEventRegistry;
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import openfl.display.BitmapData;
|
||||
import openfl.utils.Assets;
|
||||
|
@ -91,7 +91,7 @@ class ChartEditorEventSprite extends FlxSprite
|
|||
}
|
||||
|
||||
// Push all the other events as frames.
|
||||
for (eventName in SongEventParser.listEventIds())
|
||||
for (eventName in SongEventRegistry.listEventIds())
|
||||
{
|
||||
var exists:Bool = Assets.exists(Paths.image('ui/chart-editor/events/$eventName'));
|
||||
if (!exists) continue; // No graphic for this event.
|
||||
|
@ -117,7 +117,7 @@ class ChartEditorEventSprite extends FlxSprite
|
|||
|
||||
function buildAnimations():Void
|
||||
{
|
||||
var eventNames:Array<String> = [DEFAULT_EVENT].concat(SongEventParser.listEventIds());
|
||||
var eventNames:Array<String> = [DEFAULT_EVENT].concat(SongEventRegistry.listEventIds());
|
||||
for (eventName in eventNames)
|
||||
{
|
||||
this.animation.addByPrefix(eventName, '${eventName}0', 24, false);
|
||||
|
@ -131,8 +131,10 @@ class ChartEditorEventSprite extends FlxSprite
|
|||
return DEFAULT_EVENT;
|
||||
}
|
||||
|
||||
public function playAnimation(name:String):Void
|
||||
public function playAnimation(?name:String):Void
|
||||
{
|
||||
if (name == null) name = eventData?.event ?? DEFAULT_EVENT;
|
||||
|
||||
var correctedName = correctAnimationName(name);
|
||||
this.animation.play(correctedName);
|
||||
refresh();
|
||||
|
@ -158,8 +160,6 @@ class ChartEditorEventSprite extends FlxSprite
|
|||
else
|
||||
{
|
||||
this.visible = true;
|
||||
// Only play the animation if the event type has changed.
|
||||
// if (this.eventData == null || this.eventData.event != value.event)
|
||||
playAnimation(value.event);
|
||||
this.eventData = value;
|
||||
// Update the position to match the note data.
|
||||
|
|
|
@ -39,6 +39,17 @@ class ChartEditorHoldNoteSprite extends SustainTrail
|
|||
setup();
|
||||
}
|
||||
|
||||
public override function updateHitbox():Void
|
||||
{
|
||||
// Expand the clickable hitbox to the full column width, then nudge to the left to re-center it.
|
||||
width = ChartEditorState.GRID_SIZE;
|
||||
height = graphicHeight;
|
||||
|
||||
var xOffset = (ChartEditorState.GRID_SIZE - graphicWidth) / 2;
|
||||
offset.set(-xOffset, 0);
|
||||
origin.set(width * 0.5, height * 0.5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the height directly, to a value in pixels.
|
||||
* @param h The desired height in pixels.
|
||||
|
@ -52,6 +63,25 @@ class ChartEditorHoldNoteSprite extends SustainTrail
|
|||
fullSustainLength = sustainLength;
|
||||
}
|
||||
|
||||
#if FLX_DEBUG
|
||||
/**
|
||||
* Call this to override how debug bounding boxes are drawn for this sprite.
|
||||
*/
|
||||
public override function drawDebugOnCamera(camera:flixel.FlxCamera):Void
|
||||
{
|
||||
if (!camera.visible || !camera.exists || !isOnScreen(camera)) return;
|
||||
|
||||
var rect = getBoundingBox(camera);
|
||||
trace('hold note bounding box: ' + rect.x + ', ' + rect.y + ', ' + rect.width + ', ' + rect.height);
|
||||
|
||||
var gfx = beginDrawDebug(camera);
|
||||
debugBoundingBoxColor = 0xffFF66FF;
|
||||
gfx.lineStyle(2, color, 0.5); // thickness, color, alpha
|
||||
gfx.drawRect(rect.x, rect.y, rect.width, rect.height);
|
||||
endDrawDebug(camera);
|
||||
}
|
||||
#end
|
||||
|
||||
function setup():Void
|
||||
{
|
||||
strumTime = 999999999;
|
||||
|
@ -60,7 +90,9 @@ class ChartEditorHoldNoteSprite extends SustainTrail
|
|||
active = true;
|
||||
visible = true;
|
||||
alpha = 1.0;
|
||||
width = graphic.width / 8 * zoom; // amount of notes * 2
|
||||
graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2
|
||||
|
||||
updateHitbox();
|
||||
}
|
||||
|
||||
public override function revive():Void
|
||||
|
@ -154,7 +186,7 @@ class ChartEditorHoldNoteSprite extends SustainTrail
|
|||
}
|
||||
|
||||
this.x += ChartEditorState.GRID_SIZE / 2;
|
||||
this.x -= this.width / 2;
|
||||
this.x -= this.graphicWidth / 2;
|
||||
|
||||
this.y += ChartEditorState.GRID_SIZE / 2;
|
||||
|
||||
|
@ -163,5 +195,8 @@ class ChartEditorHoldNoteSprite extends SustainTrail
|
|||
this.x += origin.x;
|
||||
this.y += origin.y;
|
||||
}
|
||||
|
||||
// Account for expanded clickable hitbox.
|
||||
this.x += this.offset.x;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
package funkin.ui.debug.charting.components;
|
||||
|
||||
import flixel.FlxSprite;
|
||||
import flixel.addons.display.FlxTiledSprite;
|
||||
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
|
||||
import flixel.text.FlxText;
|
||||
import flixel.util.FlxColor;
|
||||
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
class ChartEditorMeasureTicks extends FlxTypedSpriteGroup<FlxSprite>
|
||||
{
|
||||
var chartEditorState:ChartEditorState;
|
||||
|
||||
var tickTiledSprite:FlxTiledSprite;
|
||||
var measureNumber:FlxText;
|
||||
|
||||
override function set_y(value:Float):Float
|
||||
{
|
||||
var result = super.set_y(value);
|
||||
|
||||
updateMeasureNumber();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public function new(chartEditorState:ChartEditorState)
|
||||
{
|
||||
super();
|
||||
|
||||
this.chartEditorState = chartEditorState;
|
||||
|
||||
tickTiledSprite = new FlxTiledSprite(chartEditorState.measureTickBitmap, chartEditorState.measureTickBitmap.width, 1000, false, true);
|
||||
add(tickTiledSprite);
|
||||
|
||||
measureNumber = new FlxText(0, 0, ChartEditorState.GRID_SIZE, "1");
|
||||
measureNumber.setFormat(Paths.font('vcr.ttf'), 20, FlxColor.WHITE);
|
||||
measureNumber.borderStyle = FlxTextBorderStyle.OUTLINE;
|
||||
measureNumber.borderColor = FlxColor.BLACK;
|
||||
add(measureNumber);
|
||||
}
|
||||
|
||||
public function reloadTickBitmap():Void
|
||||
{
|
||||
tickTiledSprite.loadGraphic(chartEditorState.measureTickBitmap);
|
||||
}
|
||||
|
||||
/**
|
||||
* At time of writing, we only have to manipulate one measure number because we can only see one measure at a time.
|
||||
*/
|
||||
function updateMeasureNumber()
|
||||
{
|
||||
if (measureNumber == null) return;
|
||||
|
||||
var viewTopPosition = 0 - this.y;
|
||||
var viewHeight = FlxG.height - ChartEditorState.MENU_BAR_HEIGHT - ChartEditorState.PLAYBAR_HEIGHT;
|
||||
var viewBottomPosition = viewTopPosition + viewHeight;
|
||||
|
||||
var measureNumberInViewport = Math.floor(viewTopPosition / ChartEditorState.GRID_SIZE / Conductor.instance.stepsPerMeasure) + 1;
|
||||
var measureNumberPosition = measureNumberInViewport * ChartEditorState.GRID_SIZE * Conductor.instance.stepsPerMeasure;
|
||||
|
||||
measureNumber.text = '${measureNumberInViewport + 1}';
|
||||
measureNumber.y = measureNumberPosition + this.y;
|
||||
|
||||
// trace(measureNumber.text + ' at ' + measureNumber.y);
|
||||
}
|
||||
|
||||
public function setHeight(songLengthInPixels:Float):Void
|
||||
{
|
||||
tickTiledSprite.height = songLengthInPixels;
|
||||
}
|
||||
}
|
|
@ -70,9 +70,9 @@ class ChartEditorNotePreview extends FlxSprite
|
|||
* @param event The data for the event.
|
||||
* @param songLengthInMs The total length of the song in milliseconds.
|
||||
*/
|
||||
public function addEvent(event:SongEventData, songLengthInMs:Int):Void
|
||||
public function addEvent(event:SongEventData, songLengthInMs:Int, ?isSelection:Bool = false):Void
|
||||
{
|
||||
drawNote(-1, false, Std.int(event.time), songLengthInMs);
|
||||
drawNote(-1, false, Std.int(event.time), songLengthInMs, isSelection);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -114,6 +114,19 @@ class ChartEditorNotePreview extends FlxSprite
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an array of selected events to the preview.
|
||||
* @param events The data for the events.
|
||||
* @param songLengthInMs The total length of the song in milliseconds.
|
||||
*/
|
||||
public function addSelectedEvents(events:Array<SongEventData>, songLengthInMs:Int):Void
|
||||
{
|
||||
for (event in events)
|
||||
{
|
||||
addEvent(event, songLengthInMs, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a note on the preview.
|
||||
* @param dir Note data.
|
||||
|
|
|
@ -1,20 +1,33 @@
|
|||
package funkin.ui.debug.charting.components;
|
||||
|
||||
import flixel.addons.display.FlxSliceSprite;
|
||||
import flixel.FlxSprite;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import flixel.math.FlxRect;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.ui.debug.charting.handlers.ChartEditorThemeHandler;
|
||||
|
||||
/**
|
||||
* A sprite that can be used to display a square over a selected note or event in the chart.
|
||||
* Designed to be used and reused efficiently. Has no gameplay functionality.
|
||||
*/
|
||||
class ChartEditorSelectionSquareSprite extends FlxSprite
|
||||
@:nullSafety
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
class ChartEditorSelectionSquareSprite extends FlxSliceSprite
|
||||
{
|
||||
public var noteData:Null<SongNoteData>;
|
||||
public var eventData:Null<SongEventData>;
|
||||
|
||||
public function new()
|
||||
public function new(chartEditorState:ChartEditorState)
|
||||
{
|
||||
super();
|
||||
super(chartEditorState.selectionSquareBitmap,
|
||||
new FlxRect(ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH
|
||||
+ 4, ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH
|
||||
+ 4,
|
||||
ChartEditorState.GRID_SIZE
|
||||
- (2 * ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH + 8),
|
||||
ChartEditorState.GRID_SIZE
|
||||
- (2 * ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH + 8)),
|
||||
32, 32);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package funkin.ui.debug.charting.contextmenus;
|
||||
|
||||
import haxe.ui.containers.menus.Menu;
|
||||
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
class ChartEditorBaseContextMenu extends Menu
|
||||
{
|
||||
var chartEditorState:ChartEditorState;
|
||||
|
||||
public function new(chartEditorState:ChartEditorState, xPos:Float = 0, yPos:Float = 0)
|
||||
{
|
||||
super();
|
||||
|
||||
this.chartEditorState = chartEditorState;
|
||||
|
||||
this.left = xPos;
|
||||
this.top = yPos;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package funkin.ui.debug.charting.contextmenus;
|
||||
|
||||
import haxe.ui.containers.menus.Menu;
|
||||
import haxe.ui.core.Screen;
|
||||
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/default.xml"))
|
||||
class ChartEditorDefaultContextMenu extends ChartEditorBaseContextMenu
|
||||
{
|
||||
public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0)
|
||||
{
|
||||
super(chartEditorState2, xPos2, yPos2);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package funkin.ui.debug.charting.contextmenus;
|
||||
|
||||
import haxe.ui.containers.menus.Menu;
|
||||
import haxe.ui.containers.menus.MenuItem;
|
||||
import haxe.ui.core.Screen;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
import funkin.ui.debug.charting.commands.RemoveEventsCommand;
|
||||
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/event.xml"))
|
||||
class ChartEditorEventContextMenu extends ChartEditorBaseContextMenu
|
||||
{
|
||||
var contextmenuEdit:MenuItem;
|
||||
var contextmenuDelete:MenuItem;
|
||||
|
||||
var data:SongEventData;
|
||||
|
||||
public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0, data:SongEventData)
|
||||
{
|
||||
super(chartEditorState2, xPos2, yPos2);
|
||||
this.data = data;
|
||||
|
||||
initialize();
|
||||
}
|
||||
|
||||
function initialize()
|
||||
{
|
||||
contextmenuEdit.onClick = function(_) {
|
||||
chartEditorState.showToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT);
|
||||
}
|
||||
|
||||
contextmenuDelete.onClick = function(_) {
|
||||
chartEditorState.performCommand(new RemoveEventsCommand([data]));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package funkin.ui.debug.charting.contextmenus;
|
||||
|
||||
import haxe.ui.containers.menus.Menu;
|
||||
import haxe.ui.containers.menus.MenuItem;
|
||||
import haxe.ui.core.Screen;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.ui.debug.charting.commands.FlipNotesCommand;
|
||||
import funkin.ui.debug.charting.commands.RemoveNotesCommand;
|
||||
import funkin.ui.debug.charting.commands.ExtendNoteLengthCommand;
|
||||
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/hold-note.xml"))
|
||||
class ChartEditorHoldNoteContextMenu extends ChartEditorBaseContextMenu
|
||||
{
|
||||
var contextmenuFlip:MenuItem;
|
||||
var contextmenuDelete:MenuItem;
|
||||
|
||||
var data:SongNoteData;
|
||||
|
||||
public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0, data:SongNoteData)
|
||||
{
|
||||
super(chartEditorState2, xPos2, yPos2);
|
||||
this.data = data;
|
||||
|
||||
initialize();
|
||||
}
|
||||
|
||||
function initialize():Void
|
||||
{
|
||||
// NOTE: Remember to use commands here to ensure undo/redo works properly
|
||||
contextmenuFlip.onClick = function(_) {
|
||||
chartEditorState.performCommand(new FlipNotesCommand([data]));
|
||||
}
|
||||
|
||||
contextmenuRemoveHold.onClick = function(_) {
|
||||
chartEditorState.performCommand(new ExtendNoteLengthCommand(data, 0));
|
||||
}
|
||||
|
||||
contextmenuDelete.onClick = function(_) {
|
||||
chartEditorState.performCommand(new RemoveNotesCommand([data]));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package funkin.ui.debug.charting.contextmenus;
|
||||
|
||||
import haxe.ui.containers.menus.Menu;
|
||||
import haxe.ui.containers.menus.MenuItem;
|
||||
import haxe.ui.core.Screen;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.ui.debug.charting.commands.FlipNotesCommand;
|
||||
import funkin.ui.debug.charting.commands.RemoveNotesCommand;
|
||||
import funkin.ui.debug.charting.commands.ExtendNoteLengthCommand;
|
||||
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/note.xml"))
|
||||
class ChartEditorNoteContextMenu extends ChartEditorBaseContextMenu
|
||||
{
|
||||
var contextmenuFlip:MenuItem;
|
||||
var contextmenuDelete:MenuItem;
|
||||
|
||||
var data:SongNoteData;
|
||||
|
||||
public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0, data:SongNoteData)
|
||||
{
|
||||
super(chartEditorState2, xPos2, yPos2);
|
||||
this.data = data;
|
||||
|
||||
initialize();
|
||||
}
|
||||
|
||||
function initialize():Void
|
||||
{
|
||||
// NOTE: Remember to use commands here to ensure undo/redo works properly
|
||||
contextmenuFlip.onClick = function(_) {
|
||||
chartEditorState.performCommand(new FlipNotesCommand([data]));
|
||||
}
|
||||
|
||||
contextmenuAddHold.onClick = function(_) {
|
||||
chartEditorState.performCommand(new ExtendNoteLengthCommand(data, 4, STEPS));
|
||||
}
|
||||
|
||||
contextmenuDelete.onClick = function(_) {
|
||||
chartEditorState.performCommand(new RemoveNotesCommand([data]));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package funkin.ui.debug.charting.contextmenus;
|
||||
|
||||
import haxe.ui.containers.menus.Menu;
|
||||
import haxe.ui.containers.menus.MenuItem;
|
||||
import haxe.ui.core.Screen;
|
||||
import funkin.ui.debug.charting.commands.CutItemsCommand;
|
||||
import funkin.ui.debug.charting.commands.RemoveEventsCommand;
|
||||
import funkin.ui.debug.charting.commands.RemoveItemsCommand;
|
||||
import funkin.ui.debug.charting.commands.RemoveNotesCommand;
|
||||
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/selection.xml"))
|
||||
class ChartEditorSelectionContextMenu extends ChartEditorBaseContextMenu
|
||||
{
|
||||
var contextmenuCut:MenuItem;
|
||||
var contextmenuCopy:MenuItem;
|
||||
var contextmenuPaste:MenuItem;
|
||||
var contextmenuDelete:MenuItem;
|
||||
var contextmenuFlip:MenuItem;
|
||||
var contextmenuSelectAll:MenuItem;
|
||||
var contextmenuSelectInverse:MenuItem;
|
||||
var contextmenuSelectNone:MenuItem;
|
||||
|
||||
public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0)
|
||||
{
|
||||
super(chartEditorState2, xPos2, yPos2);
|
||||
|
||||
initialize();
|
||||
}
|
||||
|
||||
function initialize():Void
|
||||
{
|
||||
contextmenuCut.onClick = (_) -> {
|
||||
chartEditorState.performCommand(new CutItemsCommand(chartEditorState.currentNoteSelection, chartEditorState.currentEventSelection));
|
||||
};
|
||||
contextmenuCopy.onClick = (_) -> {
|
||||
chartEditorState.copySelection();
|
||||
};
|
||||
contextmenuFlip.onClick = (_) -> {
|
||||
if (chartEditorState.currentNoteSelection.length > 0 && chartEditorState.currentEventSelection.length > 0)
|
||||
{
|
||||
chartEditorState.performCommand(new RemoveItemsCommand(chartEditorState.currentNoteSelection, chartEditorState.currentEventSelection));
|
||||
}
|
||||
else if (chartEditorState.currentNoteSelection.length > 0)
|
||||
{
|
||||
chartEditorState.performCommand(new RemoveNotesCommand(chartEditorState.currentNoteSelection));
|
||||
}
|
||||
else if (chartEditorState.currentEventSelection.length > 0)
|
||||
{
|
||||
chartEditorState.performCommand(new RemoveEventsCommand(chartEditorState.currentEventSelection));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Do nothing???
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -185,20 +185,22 @@ class ChartEditorAudioHandler
|
|||
state.audioVocalTrackGroup.addPlayerVoice(vocalTrack);
|
||||
state.audioVisGroup.addPlayerVis(vocalTrack);
|
||||
state.audioVisGroup.playerVis.x = 885;
|
||||
state.audioVisGroup.playerVis.realtimeVisLenght = Conductor.getStepTimeInMs(16) * 0.00195;
|
||||
state.audioVisGroup.playerVis.realtimeVisLenght = Conductor.instance.getStepTimeInMs(16) * 0.00195;
|
||||
state.audioVisGroup.playerVis.daHeight = (ChartEditorState.GRID_SIZE) * 16;
|
||||
state.audioVisGroup.playerVis.detail = 1;
|
||||
state.audioVisGroup.playerVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS - ChartEditorState.GRID_TOP_PAD);
|
||||
|
||||
state.audioVocalTrackGroup.playerVoicesOffset = state.currentSongOffsets.getVocalOffset(charId);
|
||||
return true;
|
||||
case DAD:
|
||||
state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
|
||||
state.audioVisGroup.addOpponentVis(vocalTrack);
|
||||
state.audioVisGroup.opponentVis.x = 435;
|
||||
state.audioVisGroup.opponentVis.x = 405;
|
||||
|
||||
state.audioVisGroup.opponentVis.realtimeVisLenght = Conductor.getStepTimeInMs(16) * 0.00195;
|
||||
state.audioVisGroup.opponentVis.realtimeVisLenght = Conductor.instance.getStepTimeInMs(16) * 0.00195;
|
||||
state.audioVisGroup.opponentVis.daHeight = (ChartEditorState.GRID_SIZE) * 16;
|
||||
state.audioVisGroup.opponentVis.detail = 1;
|
||||
state.audioVisGroup.opponentVis.y = Math.max(state.gridTiledSprite?.y ?? 0.0, ChartEditorState.GRID_INITIAL_Y_POS - ChartEditorState.GRID_TOP_PAD);
|
||||
|
||||
state.audioVocalTrackGroup.opponentVoicesOffset = state.currentSongOffsets.getVocalOffset(charId);
|
||||
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
package funkin.ui.debug.charting.handlers;
|
||||
|
||||
import funkin.ui.debug.charting.contextmenus.ChartEditorDefaultContextMenu;
|
||||
import funkin.ui.debug.charting.contextmenus.ChartEditorEventContextMenu;
|
||||
import funkin.ui.debug.charting.contextmenus.ChartEditorHoldNoteContextMenu;
|
||||
import funkin.ui.debug.charting.contextmenus.ChartEditorNoteContextMenu;
|
||||
import funkin.ui.debug.charting.contextmenus.ChartEditorSelectionContextMenu;
|
||||
import haxe.ui.containers.menus.Menu;
|
||||
import haxe.ui.core.Screen;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongEventData;
|
||||
|
||||
/**
|
||||
* Handles context menus (the little menus that appear when you right click on stuff) for the new Chart Editor.
|
||||
*/
|
||||
@:nullSafety
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
class ChartEditorContextMenuHandler
|
||||
{
|
||||
static var existingMenus:Array<Menu> = [];
|
||||
|
||||
public static function openDefaultContextMenu(state:ChartEditorState, xPos:Float, yPos:Float)
|
||||
{
|
||||
displayMenu(state, new ChartEditorDefaultContextMenu(state, xPos, yPos));
|
||||
}
|
||||
|
||||
/**
|
||||
* Opened when shift+right-clicking a selection of multiple items.
|
||||
*/
|
||||
public static function openSelectionContextMenu(state:ChartEditorState, xPos:Float, yPos:Float)
|
||||
{
|
||||
displayMenu(state, new ChartEditorSelectionContextMenu(state, xPos, yPos));
|
||||
}
|
||||
|
||||
/**
|
||||
* Opened when shift+right-clicking a single note.
|
||||
*/
|
||||
public static function openNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData)
|
||||
{
|
||||
displayMenu(state, new ChartEditorNoteContextMenu(state, xPos, yPos, data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Opened when shift+right-clicking a single hold note.
|
||||
*/
|
||||
public static function openHoldNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData)
|
||||
{
|
||||
displayMenu(state, new ChartEditorHoldNoteContextMenu(state, xPos, yPos, data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Opened when shift+right-clicking a single event.
|
||||
*/
|
||||
public static function openEventContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongEventData)
|
||||
{
|
||||
displayMenu(state, new ChartEditorEventContextMenu(state, xPos, yPos, data));
|
||||
}
|
||||
|
||||
static function displayMenu(state:ChartEditorState, targetMenu:Menu)
|
||||
{
|
||||
// Close any existing menus
|
||||
closeAllMenus(state);
|
||||
|
||||
// Show the new menu
|
||||
Screen.instance.addComponent(targetMenu);
|
||||
existingMenus.push(targetMenu);
|
||||
}
|
||||
|
||||
public static function closeMenu(state:ChartEditorState, targetMenu:Menu)
|
||||
{
|
||||
// targetMenu.close();
|
||||
existingMenus.remove(targetMenu);
|
||||
}
|
||||
|
||||
public static function closeAllMenus(state:ChartEditorState)
|
||||
{
|
||||
for (existingMenu in existingMenus)
|
||||
{
|
||||
closeMenu(state, existingMenu);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ import funkin.play.character.BaseCharacter;
|
|||
import funkin.play.character.CharacterData;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.play.song.Song;
|
||||
import funkin.play.stage.StageData;
|
||||
import funkin.data.stage.StageData;
|
||||
import funkin.ui.debug.charting.dialogs.ChartEditorAboutDialog;
|
||||
import funkin.ui.debug.charting.dialogs.ChartEditorBaseDialog.DialogDropTarget;
|
||||
import funkin.ui.debug.charting.dialogs.ChartEditorCharacterIconSelectorMenu;
|
||||
|
@ -684,8 +684,9 @@ class ChartEditorDialogHandler
|
|||
|
||||
state.songMetadata.set(targetVariation, newSongMetadata);
|
||||
|
||||
Conductor.instrumentalOffset = state.currentInstrumentalOffset; // Loads from the metadata.
|
||||
Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges);
|
||||
Conductor.instance.instrumentalOffset = state.currentInstrumentalOffset; // Loads from the metadata.
|
||||
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
|
||||
state.updateTimeSignature();
|
||||
|
||||
state.selectedVariation = Constants.DEFAULT_VARIATION;
|
||||
state.selectedDifficulty = state.availableDifficulties[0];
|
||||
|
|
|
@ -43,7 +43,8 @@ class ChartEditorImportExportHandler
|
|||
var variation = (metadata.variation == null || metadata.variation == '') ? Constants.DEFAULT_VARIATION : metadata.variation;
|
||||
|
||||
// Clone to prevent modifying the original.
|
||||
var metadataClone:SongMetadata = metadata.clone(variation);
|
||||
var metadataClone:SongMetadata = metadata.clone();
|
||||
metadataClone.variation = variation;
|
||||
if (metadataClone != null) songMetadata.set(variation, metadataClone);
|
||||
|
||||
var chartData:Null<SongChartData> = SongRegistry.instance.parseEntryChartData(songId, metadata.variation);
|
||||
|
@ -114,9 +115,10 @@ class ChartEditorImportExportHandler
|
|||
state.songMetadata = newSongMetadata;
|
||||
state.songChartData = newSongChartData;
|
||||
|
||||
Conductor.forceBPM(null); // Disable the forced BPM.
|
||||
Conductor.instrumentalOffset = state.currentInstrumentalOffset; // Loads from the metadata.
|
||||
Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges);
|
||||
Conductor.instance.forceBPM(null); // Disable the forced BPM.
|
||||
Conductor.instance.instrumentalOffset = state.currentInstrumentalOffset; // Loads from the metadata.
|
||||
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
|
||||
state.updateTimeSignature();
|
||||
|
||||
state.notePreviewDirty = true;
|
||||
state.notePreviewViewportBoundsDirty = true;
|
||||
|
@ -415,16 +417,34 @@ class ChartEditorImportExportHandler
|
|||
]);
|
||||
// We have to force write because the program will die before the save dialog is closed.
|
||||
trace('Force exporting to $targetPath...');
|
||||
FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath, targetMode);
|
||||
if (onSaveCb != null) onSaveCb(targetPath);
|
||||
try
|
||||
{
|
||||
FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath, targetMode);
|
||||
// On success.
|
||||
if (onSaveCb != null) onSaveCb(targetPath);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// On failure.
|
||||
if (onCancelCb != null) onCancelCb();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Force write since we know what file the user wants to overwrite.
|
||||
trace('Force exporting to $targetPath...');
|
||||
FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath, targetMode);
|
||||
state.saveDataDirty = false;
|
||||
if (onSaveCb != null) onSaveCb(targetPath);
|
||||
try
|
||||
{
|
||||
// On success.
|
||||
FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath, targetMode);
|
||||
state.saveDataDirty = false;
|
||||
if (onSaveCb != null) onSaveCb(targetPath);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
// On failure.
|
||||
if (onCancelCb != null) onCancelCb();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
|
|
@ -2,6 +2,10 @@ package funkin.ui.debug.charting.handlers;
|
|||
|
||||
import funkin.util.PlatformUtil;
|
||||
|
||||
/**
|
||||
* Handles modifying the shortcut text of menu items based on the current platform.
|
||||
* On MacOS, `Ctrl`, `Alt`, and `Shift` are replaced with `⌘` (or `^`), `⌥`, and `⇧`, respectively.
|
||||
*/
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
class ChartEditorShortcutHandler
|
||||
{
|
||||
|
@ -18,7 +22,8 @@ class ChartEditorShortcutHandler
|
|||
state.menubarItemCopy.shortcutText = ctrlOrCmd('C');
|
||||
state.menubarItemPaste.shortcutText = ctrlOrCmd('V');
|
||||
|
||||
state.menubarItemSelectAll.shortcutText = ctrlOrCmd('A');
|
||||
state.menubarItemSelectAllNotes.shortcutText = ctrlOrCmd('A');
|
||||
state.menubarItemSelectAllEvents.shortcutText = ctrlOrCmd(alt('A'));
|
||||
state.menubarItemSelectInverse.shortcutText = ctrlOrCmd('I');
|
||||
state.menubarItemSelectNone.shortcutText = ctrlOrCmd('D');
|
||||
state.menubarItemSelectBeforeCursor.shortcutText = shift('Home');
|
||||
|
|
|
@ -52,7 +52,7 @@ class ChartEditorThemeHandler
|
|||
// Border on the square highlighting selected notes.
|
||||
static final SELECTION_SQUARE_BORDER_COLOR_LIGHT:FlxColor = 0xFF339933;
|
||||
static final SELECTION_SQUARE_BORDER_COLOR_DARK:FlxColor = 0xFF339933;
|
||||
static final SELECTION_SQUARE_BORDER_WIDTH:Int = 1;
|
||||
public static final SELECTION_SQUARE_BORDER_WIDTH:Int = 1;
|
||||
|
||||
// Fill on the square highlighting selected notes.
|
||||
// Make sure this is transparent so you can see the notes underneath.
|
||||
|
@ -81,6 +81,7 @@ class ChartEditorThemeHandler
|
|||
{
|
||||
updateBackground(state);
|
||||
updateGridBitmap(state);
|
||||
updateMeasureTicks(state);
|
||||
updateSelectionSquare(state);
|
||||
updateNotePreview(state);
|
||||
}
|
||||
|
@ -125,7 +126,7 @@ class ChartEditorThemeHandler
|
|||
// 2 * (Strumline Size) + 1 grid squares wide, by (4 * quarter notes per measure) grid squares tall.
|
||||
// This gets reused to fill the screen.
|
||||
var gridWidth:Int = Std.int(ChartEditorState.GRID_SIZE * TOTAL_COLUMN_COUNT);
|
||||
var gridHeight:Int = Std.int(ChartEditorState.GRID_SIZE * Conductor.stepsPerMeasure);
|
||||
var gridHeight:Int = Std.int(ChartEditorState.GRID_SIZE * Conductor.instance.stepsPerMeasure);
|
||||
state.gridBitmap = FlxGridOverlay.createGrid(ChartEditorState.GRID_SIZE, ChartEditorState.GRID_SIZE, gridWidth, gridHeight, true, gridColor1, gridColor2);
|
||||
|
||||
// Selection borders
|
||||
|
@ -142,7 +143,7 @@ class ChartEditorThemeHandler
|
|||
selectionBorderColor);
|
||||
|
||||
// Selection borders horizontally along the middle.
|
||||
for (i in 1...(Conductor.stepsPerMeasure))
|
||||
for (i in 1...(Conductor.instance.stepsPerMeasure))
|
||||
{
|
||||
state.gridBitmap.fillRect(new Rectangle(0, (ChartEditorState.GRID_SIZE * i) - (ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2),
|
||||
state.gridBitmap.width, ChartEditorState.GRID_SELECTION_BORDER_WIDTH),
|
||||
|
@ -197,9 +198,9 @@ class ChartEditorThemeHandler
|
|||
};
|
||||
|
||||
// Selection borders horizontally in the middle.
|
||||
for (i in 1...(Conductor.stepsPerMeasure))
|
||||
for (i in 1...(Conductor.instance.stepsPerMeasure))
|
||||
{
|
||||
if ((i % Conductor.beatsPerMeasure) == 0)
|
||||
if ((i % Conductor.instance.beatsPerMeasure) == 0)
|
||||
{
|
||||
state.gridBitmap.fillRect(new Rectangle(0, (ChartEditorState.GRID_SIZE * i) - (GRID_BEAT_DIVIDER_WIDTH / 2), state.gridBitmap.width,
|
||||
GRID_BEAT_DIVIDER_WIDTH),
|
||||
|
@ -207,9 +208,6 @@ class ChartEditorThemeHandler
|
|||
}
|
||||
}
|
||||
|
||||
// Divider at top
|
||||
state.gridBitmap.fillRect(new Rectangle(0, 0, state.gridBitmap.width, GRID_MEASURE_DIVIDER_WIDTH / 2), gridMeasureDividerColor);
|
||||
|
||||
// Draw vertical dividers between the strumlines.
|
||||
|
||||
var gridStrumlineDividerColor:FlxColor = switch (state.currentTheme)
|
||||
|
@ -233,6 +231,61 @@ class ChartEditorThemeHandler
|
|||
// Else, gridTiledSprite will be built later.
|
||||
}
|
||||
|
||||
static function updateMeasureTicks(state:ChartEditorState):Void
|
||||
{
|
||||
var measureTickWidth:Int = 6;
|
||||
var beatTickWidth:Int = 4;
|
||||
var stepTickWidth:Int = 2;
|
||||
|
||||
// Draw the measure ticks.
|
||||
var ticksWidth:Int = Std.int(ChartEditorState.GRID_SIZE); // 1 grid squares wide.
|
||||
var ticksHeight:Int = Std.int(ChartEditorState.GRID_SIZE * Conductor.instance.stepsPerMeasure); // 1 measure tall.
|
||||
state.measureTickBitmap = new BitmapData(ticksWidth, ticksHeight, true);
|
||||
state.measureTickBitmap.fillRect(new Rectangle(0, 0, ticksWidth, ticksHeight), GRID_BEAT_DIVIDER_COLOR_DARK);
|
||||
|
||||
// Draw the measure ticks.
|
||||
state.measureTickBitmap.fillRect(new Rectangle(0, 0, state.measureTickBitmap.width, measureTickWidth / 2), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
var bottomTickY:Float = state.measureTickBitmap.height - (measureTickWidth / 2);
|
||||
state.measureTickBitmap.fillRect(new Rectangle(0, bottomTickY, state.measureTickBitmap.width, measureTickWidth / 2), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
|
||||
// Draw the beat ticks.
|
||||
var beatTick2Y:Float = state.measureTickBitmap.height * 1 / Conductor.instance.beatsPerMeasure - (beatTickWidth / 2);
|
||||
var beatTick3Y:Float = state.measureTickBitmap.height * 2 / Conductor.instance.beatsPerMeasure - (beatTickWidth / 2);
|
||||
var beatTick4Y:Float = state.measureTickBitmap.height * 3 / Conductor.instance.beatsPerMeasure - (beatTickWidth / 2);
|
||||
var beatTickLength:Float = state.measureTickBitmap.width * 2 / 3;
|
||||
state.measureTickBitmap.fillRect(new Rectangle(0, beatTick2Y, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.measureTickBitmap.fillRect(new Rectangle(0, beatTick3Y, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.measureTickBitmap.fillRect(new Rectangle(0, beatTick4Y, beatTickLength, beatTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
|
||||
// Draw the step ticks.
|
||||
// TODO: Make this a loop or something.
|
||||
var stepTick2Y:Float = state.measureTickBitmap.height * 1 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
|
||||
var stepTick3Y:Float = state.measureTickBitmap.height * 2 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
|
||||
var stepTick4Y:Float = state.measureTickBitmap.height * 3 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
|
||||
var stepTick6Y:Float = state.measureTickBitmap.height * 5 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
|
||||
var stepTick7Y:Float = state.measureTickBitmap.height * 6 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
|
||||
var stepTick8Y:Float = state.measureTickBitmap.height * 7 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
|
||||
var stepTick10Y:Float = state.measureTickBitmap.height * 9 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
|
||||
var stepTick11Y:Float = state.measureTickBitmap.height * 10 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
|
||||
var stepTick12Y:Float = state.measureTickBitmap.height * 11 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
|
||||
var stepTick14Y:Float = state.measureTickBitmap.height * 13 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
|
||||
var stepTick15Y:Float = state.measureTickBitmap.height * 14 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
|
||||
var stepTick16Y:Float = state.measureTickBitmap.height * 15 / Conductor.instance.stepsPerMeasure - (stepTickWidth / 2);
|
||||
var stepTickLength:Float = state.measureTickBitmap.width * 1 / 3;
|
||||
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick2Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick3Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick4Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick6Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick7Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick8Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick10Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick11Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick12Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick14Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick15Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
state.measureTickBitmap.fillRect(new Rectangle(0, stepTick16Y, stepTickLength, stepTickWidth), GRID_MEASURE_DIVIDER_COLOR_LIGHT);
|
||||
}
|
||||
|
||||
static function updateSelectionSquare(state:ChartEditorState):Void
|
||||
{
|
||||
var selectionSquareBorderColor:FlxColor = switch (state.currentTheme)
|
||||
|
@ -264,6 +317,12 @@ class ChartEditorThemeHandler
|
|||
ChartEditorState.GRID_SIZE
|
||||
- (2 * SELECTION_SQUARE_BORDER_WIDTH + 8)),
|
||||
32, 32);
|
||||
|
||||
state.selectionBoxSprite.scrollFactor.set(0, 0);
|
||||
state.selectionBoxSprite.zIndex = 30;
|
||||
state.add(state.selectionBoxSprite);
|
||||
|
||||
state.setSelectionBoxBounds();
|
||||
}
|
||||
|
||||
static function updateNotePreview(state:ChartEditorState):Void
|
||||
|
@ -289,14 +348,21 @@ class ChartEditorThemeHandler
|
|||
ChartEditorState.GRID_SIZE - (SELECTION_SQUARE_BORDER_WIDTH * 2), ChartEditorState.GRID_SIZE - (SELECTION_SQUARE_BORDER_WIDTH * 2)),
|
||||
viewportFillColor);
|
||||
|
||||
state.notePreviewViewport = new FlxSliceSprite(state.notePreviewViewportBitmap,
|
||||
new FlxRect(SELECTION_SQUARE_BORDER_WIDTH
|
||||
+ 1, SELECTION_SQUARE_BORDER_WIDTH
|
||||
+ 1, ChartEditorState.GRID_SIZE
|
||||
- (2 * SELECTION_SQUARE_BORDER_WIDTH + 2),
|
||||
ChartEditorState.GRID_SIZE
|
||||
- (2 * SELECTION_SQUARE_BORDER_WIDTH + 2)),
|
||||
32, 32);
|
||||
if (state.notePreviewViewport != null)
|
||||
{
|
||||
state.notePreviewViewport.loadGraphic(state.notePreviewViewportBitmap);
|
||||
}
|
||||
else
|
||||
{
|
||||
state.notePreviewViewport = new FlxSliceSprite(state.notePreviewViewportBitmap,
|
||||
new FlxRect(SELECTION_SQUARE_BORDER_WIDTH
|
||||
+ 1, SELECTION_SQUARE_BORDER_WIDTH
|
||||
+ 1,
|
||||
ChartEditorState.GRID_SIZE
|
||||
- (2 * SELECTION_SQUARE_BORDER_WIDTH + 2), ChartEditorState.GRID_SIZE
|
||||
- (2 * SELECTION_SQUARE_BORDER_WIDTH + 2)),
|
||||
32, 32);
|
||||
}
|
||||
}
|
||||
|
||||
public static function buildPlayheadBlock():FlxSprite
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package funkin.ui.debug.charting.handlers;
|
||||
|
||||
import funkin.play.stage.StageData.StageDataParser;
|
||||
import funkin.play.stage.StageData;
|
||||
import funkin.data.stage.StageData;
|
||||
import funkin.play.character.CharacterData;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import haxe.ui.components.HorizontalSlider;
|
||||
|
@ -9,20 +8,20 @@ import haxe.ui.containers.TreeView;
|
|||
import haxe.ui.containers.TreeViewNode;
|
||||
import funkin.play.character.BaseCharacter.CharacterType;
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.data.event.SongEventData;
|
||||
import funkin.data.event.SongEventSchema;
|
||||
import funkin.data.song.SongData.SongTimeChange;
|
||||
import funkin.play.character.BaseCharacter.CharacterType;
|
||||
import funkin.play.character.CharacterData;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.play.song.SongSerializer;
|
||||
import funkin.play.stage.StageData;
|
||||
import funkin.play.stage.StageData.StageDataParser;
|
||||
import funkin.data.stage.StageData;
|
||||
import haxe.ui.RuntimeComponentBuilder;
|
||||
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
|
||||
import funkin.ui.haxeui.components.CharacterPlayer;
|
||||
import funkin.util.FileUtil;
|
||||
import haxe.ui.components.Button;
|
||||
import haxe.ui.data.ArrayDataSource;
|
||||
import haxe.ui.components.CheckBox;
|
||||
import haxe.ui.components.DropDown;
|
||||
import haxe.ui.components.HorizontalSlider;
|
||||
|
@ -36,12 +35,13 @@ import haxe.ui.containers.dialogs.Dialog.DialogButton;
|
|||
import haxe.ui.containers.dialogs.Dialog.DialogEvent;
|
||||
import funkin.ui.debug.charting.toolboxes.ChartEditorBaseToolbox;
|
||||
import funkin.ui.debug.charting.toolboxes.ChartEditorMetadataToolbox;
|
||||
import funkin.ui.debug.charting.toolboxes.ChartEditorEventDataToolbox;
|
||||
import funkin.ui.debug.charting.toolboxes.ChartEditorDifficultyToolbox;
|
||||
import haxe.ui.containers.Frame;
|
||||
import haxe.ui.containers.Grid;
|
||||
import haxe.ui.containers.TreeView;
|
||||
import haxe.ui.containers.TreeViewNode;
|
||||
import haxe.ui.core.Component;
|
||||
import haxe.ui.data.ArrayDataSource;
|
||||
import haxe.ui.events.UIEvent;
|
||||
|
||||
/**
|
||||
|
@ -79,12 +79,13 @@ class ChartEditorToolboxHandler
|
|||
{
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
|
||||
onShowToolboxNoteData(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:
|
||||
onShowToolboxEventData(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:
|
||||
// TODO: Fix this.
|
||||
cast(toolbox, ChartEditorBaseToolbox).refresh();
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
|
||||
onShowToolboxPlaytestProperties(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:
|
||||
onShowToolboxDifficulty(state, toolbox);
|
||||
cast(toolbox, ChartEditorBaseToolbox).refresh();
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
|
||||
// TODO: Fix this.
|
||||
cast(toolbox, ChartEditorBaseToolbox).refresh();
|
||||
|
@ -119,12 +120,10 @@ class ChartEditorToolboxHandler
|
|||
{
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
|
||||
onHideToolboxNoteData(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:
|
||||
onHideToolboxEventData(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
|
||||
onHideToolboxPlaytestProperties(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:
|
||||
onHideToolboxDifficulty(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
|
||||
onHideToolboxMetadata(state, toolbox);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
|
||||
|
@ -195,7 +194,7 @@ class ChartEditorToolboxHandler
|
|||
{
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT:
|
||||
toolbox = buildToolboxNoteDataLayout(state);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT:
|
||||
toolbox = buildToolboxEventDataLayout(state);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYTEST_PROPERTIES_LAYOUT:
|
||||
toolbox = buildToolboxPlaytestPropertiesLayout(state);
|
||||
|
@ -283,19 +282,19 @@ class ChartEditorToolboxHandler
|
|||
toolboxNotesCustomKindLabel.hidden = false;
|
||||
toolboxNotesCustomKind.hidden = false;
|
||||
|
||||
state.selectedNoteKind = toolboxNotesCustomKind.text;
|
||||
state.noteKindToPlace = toolboxNotesCustomKind.text;
|
||||
}
|
||||
else
|
||||
{
|
||||
toolboxNotesCustomKindLabel.hidden = true;
|
||||
toolboxNotesCustomKind.hidden = true;
|
||||
|
||||
state.selectedNoteKind = event.data.id;
|
||||
state.noteKindToPlace = event.data.id;
|
||||
}
|
||||
}
|
||||
|
||||
toolboxNotesCustomKind.onChange = function(event:UIEvent) {
|
||||
state.selectedNoteKind = toolboxNotesCustomKind.text;
|
||||
state.noteKindToPlace = toolboxNotesCustomKind.text;
|
||||
}
|
||||
|
||||
return toolbox;
|
||||
|
@ -305,159 +304,14 @@ class ChartEditorToolboxHandler
|
|||
|
||||
static function onHideToolboxNoteData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
||||
|
||||
static function buildToolboxEventDataLayout(state:ChartEditorState):Null<CollapsibleDialog>
|
||||
{
|
||||
var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT);
|
||||
|
||||
if (toolbox == null) return null;
|
||||
|
||||
// Starting position.
|
||||
toolbox.x = 100;
|
||||
toolbox.y = 150;
|
||||
|
||||
toolbox.onDialogClosed = function(event:DialogEvent) {
|
||||
state.menubarItemToggleToolboxEvents.selected = false;
|
||||
}
|
||||
|
||||
var toolboxEventsEventKind:Null<DropDown> = toolbox.findComponent('toolboxEventsEventKind', DropDown);
|
||||
if (toolboxEventsEventKind == null) throw 'ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Could not find toolboxEventsEventKind component.';
|
||||
var toolboxEventsDataGrid:Null<Grid> = toolbox.findComponent('toolboxEventsDataGrid', Grid);
|
||||
if (toolboxEventsDataGrid == null) throw 'ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Could not find toolboxEventsDataGrid component.';
|
||||
|
||||
toolboxEventsEventKind.dataSource = new ArrayDataSource();
|
||||
|
||||
var songEvents:Array<SongEvent> = SongEventParser.listEvents();
|
||||
|
||||
for (event in songEvents)
|
||||
{
|
||||
toolboxEventsEventKind.dataSource.add({text: event.getTitle(), value: event.id});
|
||||
}
|
||||
|
||||
toolboxEventsEventKind.onChange = function(event:UIEvent) {
|
||||
var eventType:String = event.data.value;
|
||||
|
||||
trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Event type changed: $eventType');
|
||||
|
||||
state.selectedEventKind = eventType;
|
||||
|
||||
var schema:SongEventSchema = SongEventParser.getEventSchema(eventType);
|
||||
|
||||
if (schema == null)
|
||||
{
|
||||
trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Unknown event kind: $eventType');
|
||||
return;
|
||||
}
|
||||
|
||||
buildEventDataFormFromSchema(state, toolboxEventsDataGrid, schema);
|
||||
}
|
||||
toolboxEventsEventKind.value = state.selectedEventKind;
|
||||
|
||||
return toolbox;
|
||||
}
|
||||
|
||||
static function onShowToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
||||
|
||||
static function onShowToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
||||
static function onHideToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
||||
|
||||
static function onHideToolboxEventData(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
||||
|
||||
static function onShowToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
||||
|
||||
static function onHideToolboxPlaytestProperties(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
||||
|
||||
static function buildEventDataFormFromSchema(state:ChartEditorState, target:Box, schema:SongEventSchema):Void
|
||||
{
|
||||
trace(schema);
|
||||
// Clear the frame.
|
||||
target.removeAllComponents();
|
||||
|
||||
state.selectedEventData = {};
|
||||
|
||||
for (field in schema)
|
||||
{
|
||||
if (field == null) continue;
|
||||
|
||||
// Add a label.
|
||||
var label:Label = new Label();
|
||||
label.text = field.title;
|
||||
label.verticalAlign = "center";
|
||||
target.addComponent(label);
|
||||
|
||||
var input:Component;
|
||||
switch (field.type)
|
||||
{
|
||||
case INTEGER:
|
||||
var numberStepper:NumberStepper = new NumberStepper();
|
||||
numberStepper.id = field.name;
|
||||
numberStepper.step = field.step ?? 1.0;
|
||||
numberStepper.min = field.min ?? 0.0;
|
||||
numberStepper.max = field.max ?? 10.0;
|
||||
if (field.defaultValue != null) numberStepper.value = field.defaultValue;
|
||||
input = numberStepper;
|
||||
case FLOAT:
|
||||
var numberStepper:NumberStepper = new NumberStepper();
|
||||
numberStepper.id = field.name;
|
||||
numberStepper.step = field.step ?? 0.1;
|
||||
if (field.min != null) numberStepper.min = field.min;
|
||||
if (field.max != null) numberStepper.max = field.max;
|
||||
if (field.defaultValue != null) numberStepper.value = field.defaultValue;
|
||||
input = numberStepper;
|
||||
case BOOL:
|
||||
var checkBox:CheckBox = new CheckBox();
|
||||
checkBox.id = field.name;
|
||||
if (field.defaultValue != null) checkBox.selected = field.defaultValue;
|
||||
input = checkBox;
|
||||
case ENUM:
|
||||
var dropDown:DropDown = new DropDown();
|
||||
dropDown.id = field.name;
|
||||
dropDown.width = 200.0;
|
||||
dropDown.dataSource = new ArrayDataSource();
|
||||
|
||||
if (field.keys == null) throw 'Field "${field.name}" is of Enum type but has no keys.';
|
||||
|
||||
// Add entries to the dropdown.
|
||||
|
||||
for (optionName in field.keys.keys())
|
||||
{
|
||||
var optionValue:Null<Dynamic> = field.keys.get(optionName);
|
||||
trace('$optionName : $optionValue');
|
||||
dropDown.dataSource.add({value: optionValue, text: optionName});
|
||||
}
|
||||
|
||||
dropDown.value = field.defaultValue;
|
||||
|
||||
input = dropDown;
|
||||
case STRING:
|
||||
input = new TextField();
|
||||
input.id = field.name;
|
||||
if (field.defaultValue != null) input.text = field.defaultValue;
|
||||
default:
|
||||
// Unknown type. Display a label so we know what it is.
|
||||
input = new Label();
|
||||
input.id = field.name;
|
||||
input.text = field.type;
|
||||
}
|
||||
|
||||
target.addComponent(input);
|
||||
|
||||
input.onChange = function(event:UIEvent) {
|
||||
var value = event.target.value;
|
||||
if (field.type == ENUM)
|
||||
{
|
||||
value = event.target.value.value;
|
||||
}
|
||||
trace('ChartEditorToolboxHandler.buildEventDataFormFromSchema() - ${event.target.id} = ${value}');
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
state.selectedEventData.remove(event.target.id);
|
||||
}
|
||||
else
|
||||
{
|
||||
state.selectedEventData.set(event.target.id, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static function buildToolboxPlaytestPropertiesLayout(state:ChartEditorState):Null<CollapsibleDialog>
|
||||
{
|
||||
// fill with playtest properties
|
||||
|
@ -501,93 +355,15 @@ class ChartEditorToolboxHandler
|
|||
return toolbox;
|
||||
}
|
||||
|
||||
static function buildToolboxDifficultyLayout(state:ChartEditorState):Null<CollapsibleDialog>
|
||||
static function buildToolboxDifficultyLayout(state:ChartEditorState):Null<ChartEditorBaseToolbox>
|
||||
{
|
||||
var toolbox:CollapsibleDialog = cast RuntimeComponentBuilder.fromAsset(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
|
||||
var toolbox:ChartEditorBaseToolbox = ChartEditorDifficultyToolbox.build(state);
|
||||
|
||||
if (toolbox == null) return null;
|
||||
|
||||
// Starting position.
|
||||
toolbox.x = 125;
|
||||
toolbox.y = 200;
|
||||
|
||||
toolbox.onDialogClosed = function(event:UIEvent) {
|
||||
state.menubarItemToggleToolboxDifficulty.selected = false;
|
||||
}
|
||||
|
||||
var difficultyToolboxAddVariation:Null<Button> = toolbox.findComponent('difficultyToolboxAddVariation', Button);
|
||||
if (difficultyToolboxAddVariation == null)
|
||||
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxAddVariation component.';
|
||||
var difficultyToolboxAddDifficulty:Null<Button> = toolbox.findComponent('difficultyToolboxAddDifficulty', Button);
|
||||
if (difficultyToolboxAddDifficulty == null)
|
||||
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxAddDifficulty component.';
|
||||
var difficultyToolboxSaveMetadata:Null<Button> = toolbox.findComponent('difficultyToolboxSaveMetadata', Button);
|
||||
if (difficultyToolboxSaveMetadata == null)
|
||||
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveMetadata component.';
|
||||
var difficultyToolboxSaveChart:Null<Button> = toolbox.findComponent('difficultyToolboxSaveChart', Button);
|
||||
if (difficultyToolboxSaveChart == null)
|
||||
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveChart component.';
|
||||
// var difficultyToolboxSaveAll:Null<Button> = toolbox.findComponent('difficultyToolboxSaveAll', Button);
|
||||
// if (difficultyToolboxSaveAll == null) throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveAll component.';
|
||||
var difficultyToolboxLoadMetadata:Null<Button> = toolbox.findComponent('difficultyToolboxLoadMetadata', Button);
|
||||
if (difficultyToolboxLoadMetadata == null)
|
||||
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxLoadMetadata component.';
|
||||
var difficultyToolboxLoadChart:Null<Button> = toolbox.findComponent('difficultyToolboxLoadChart', Button);
|
||||
if (difficultyToolboxLoadChart == null)
|
||||
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxLoadChart component.';
|
||||
|
||||
difficultyToolboxAddVariation.onClick = function(_:UIEvent) {
|
||||
state.openAddVariationDialog(true);
|
||||
};
|
||||
|
||||
difficultyToolboxAddDifficulty.onClick = function(_:UIEvent) {
|
||||
state.openAddDifficultyDialog(true);
|
||||
};
|
||||
|
||||
difficultyToolboxSaveMetadata.onClick = function(_:UIEvent) {
|
||||
var vari:String = state.selectedVariation != Constants.DEFAULT_VARIATION ? '-${state.selectedVariation}' : '';
|
||||
FileUtil.writeFileReference('${state.currentSongId}$vari-metadata.json', state.currentSongMetadata.serialize());
|
||||
};
|
||||
|
||||
difficultyToolboxSaveChart.onClick = function(_:UIEvent) {
|
||||
var vari:String = state.selectedVariation != Constants.DEFAULT_VARIATION ? '-${state.selectedVariation}' : '';
|
||||
FileUtil.writeFileReference('${state.currentSongId}$vari-chart.json', state.currentSongChartData.serialize());
|
||||
};
|
||||
|
||||
difficultyToolboxLoadMetadata.onClick = function(_:UIEvent) {
|
||||
// Replace metadata for current variation.
|
||||
SongSerializer.importSongMetadataAsync(function(songMetadata) {
|
||||
state.currentSongMetadata = songMetadata;
|
||||
});
|
||||
};
|
||||
|
||||
difficultyToolboxLoadChart.onClick = function(_:UIEvent) {
|
||||
// Replace chart data for current variation.
|
||||
SongSerializer.importSongChartDataAsync(function(songChartData) {
|
||||
state.currentSongChartData = songChartData;
|
||||
state.noteDisplayDirty = true;
|
||||
});
|
||||
};
|
||||
|
||||
state.difficultySelectDirty = true;
|
||||
|
||||
return toolbox;
|
||||
}
|
||||
|
||||
static function onShowToolboxDifficulty(state:ChartEditorState, toolbox:CollapsibleDialog):Void
|
||||
{
|
||||
// Update the selected difficulty when reopening the toolbox.
|
||||
var treeView:Null<TreeView> = toolbox.findComponent('difficultyToolboxTree');
|
||||
if (treeView == null) return;
|
||||
|
||||
var current = state.getCurrentTreeDifficultyNode(treeView);
|
||||
if (current == null) return;
|
||||
treeView.selectedNode = current;
|
||||
trace('selected node: ${treeView.selectedNode}');
|
||||
}
|
||||
|
||||
static function onHideToolboxDifficulty(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
||||
|
||||
static function buildToolboxMetadataLayout(state:ChartEditorState):Null<ChartEditorBaseToolbox>
|
||||
{
|
||||
var toolbox:ChartEditorBaseToolbox = ChartEditorMetadataToolbox.build(state);
|
||||
|
@ -597,7 +373,14 @@ class ChartEditorToolboxHandler
|
|||
return toolbox;
|
||||
}
|
||||
|
||||
static function onHideToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
|
||||
static function buildToolboxEventDataLayout(state:ChartEditorState):Null<ChartEditorBaseToolbox>
|
||||
{
|
||||
var toolbox:ChartEditorBaseToolbox = ChartEditorEventDataToolbox.build(state);
|
||||
|
||||
if (toolbox == null) return null;
|
||||
|
||||
return toolbox;
|
||||
}
|
||||
|
||||
static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):Null<CollapsibleDialog>
|
||||
{
|
||||
|
|
|
@ -3,6 +3,7 @@ package funkin.ui.debug.charting;
|
|||
#if !macro
|
||||
// Apply handlers so they can be called as though they were functions in ChartEditorState
|
||||
using funkin.ui.debug.charting.handlers.ChartEditorAudioHandler;
|
||||
using funkin.ui.debug.charting.handlers.ChartEditorContextMenuHandler;
|
||||
using funkin.ui.debug.charting.handlers.ChartEditorDialogHandler;
|
||||
using funkin.ui.debug.charting.handlers.ChartEditorImportExportHandler;
|
||||
using funkin.ui.debug.charting.handlers.ChartEditorNotificationHandler;
|
||||
|
|
|
@ -0,0 +1,239 @@
|
|||
package funkin.ui.debug.charting.toolboxes;
|
||||
|
||||
import funkin.play.character.BaseCharacter.CharacterType;
|
||||
import funkin.play.character.CharacterData;
|
||||
import funkin.data.stage.StageData;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand;
|
||||
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
|
||||
import haxe.ui.components.Button;
|
||||
import haxe.ui.components.CheckBox;
|
||||
import haxe.ui.containers.dialogs.Dialogs;
|
||||
import haxe.ui.containers.dialogs.Dialog.DialogButton;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import haxe.ui.components.DropDown;
|
||||
import haxe.ui.components.HorizontalSlider;
|
||||
import funkin.util.FileUtil;
|
||||
import haxe.ui.containers.dialogs.MessageBox.MessageBoxType;
|
||||
import funkin.play.song.SongSerializer;
|
||||
import haxe.ui.components.Label;
|
||||
import haxe.ui.components.NumberStepper;
|
||||
import haxe.ui.components.Slider;
|
||||
import haxe.ui.components.TextField;
|
||||
import funkin.play.stage.Stage;
|
||||
import haxe.ui.containers.Box;
|
||||
import haxe.ui.containers.TreeView;
|
||||
import haxe.ui.containers.TreeViewNode;
|
||||
import haxe.ui.containers.Frame;
|
||||
import haxe.ui.events.UIEvent;
|
||||
|
||||
/**
|
||||
* The toolbox which allows viewing the list of difficulties, switching to a specific one,
|
||||
* and adding/removing variations and difficulties.
|
||||
*/
|
||||
// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros.
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/difficulty.xml"))
|
||||
class ChartEditorDifficultyToolbox extends ChartEditorBaseToolbox
|
||||
{
|
||||
var difficultyToolboxTree:TreeView;
|
||||
var difficultyToolboxAddVariation:Button;
|
||||
var difficultyToolboxAddDifficulty:Button;
|
||||
var difficultyToolboxRemoveDifficulty:Button;
|
||||
var difficultyToolboxSaveMetadata:Button;
|
||||
var difficultyToolboxSaveChart:Button;
|
||||
var difficultyToolboxLoadMetadata:Button;
|
||||
var difficultyToolboxLoadChart:Button;
|
||||
|
||||
public function new(chartEditorState2:ChartEditorState)
|
||||
{
|
||||
super(chartEditorState2);
|
||||
|
||||
initialize();
|
||||
|
||||
this.onDialogClosed = onClose;
|
||||
}
|
||||
|
||||
function onClose(event:UIEvent)
|
||||
{
|
||||
chartEditorState.menubarItemToggleToolboxDifficulty.selected = false;
|
||||
}
|
||||
|
||||
function initialize():Void
|
||||
{
|
||||
// Starting position.
|
||||
// TODO: Save and load this.
|
||||
this.x = 150;
|
||||
this.y = 250;
|
||||
|
||||
difficultyToolboxAddVariation.onClick = function(_:UIEvent) {
|
||||
chartEditorState.openAddVariationDialog(true);
|
||||
};
|
||||
|
||||
difficultyToolboxAddDifficulty.onClick = function(_:UIEvent) {
|
||||
chartEditorState.openAddDifficultyDialog(true);
|
||||
};
|
||||
|
||||
difficultyToolboxRemoveDifficulty.onClick = function(_:UIEvent) {
|
||||
var currentVariation:String = chartEditorState.selectedVariation;
|
||||
var currentDifficulty:String = chartEditorState.selectedDifficulty;
|
||||
|
||||
trace('Removing difficulty "$currentVariation:$currentDifficulty"');
|
||||
|
||||
var callback = (button) -> {
|
||||
switch (button)
|
||||
{
|
||||
case DialogButton.YES:
|
||||
// Remove the difficulty.
|
||||
chartEditorState.removeDifficulty(currentVariation, currentDifficulty);
|
||||
refresh();
|
||||
case DialogButton.NO: // Do nothing.
|
||||
default: // Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
Dialogs.messageBox("Are you sure? This cannot be undone.", "Remove Difficulty", MessageBoxType.TYPE_YESNO, callback);
|
||||
};
|
||||
|
||||
difficultyToolboxSaveMetadata.onClick = function(_:UIEvent) {
|
||||
var vari:String = chartEditorState.selectedVariation != Constants.DEFAULT_VARIATION ? '-${chartEditorState.selectedVariation}' : '';
|
||||
FileUtil.writeFileReference('${chartEditorState.currentSongId}$vari-metadata.json', chartEditorState.currentSongMetadata.serialize());
|
||||
};
|
||||
|
||||
difficultyToolboxSaveChart.onClick = function(_:UIEvent) {
|
||||
var vari:String = chartEditorState.selectedVariation != Constants.DEFAULT_VARIATION ? '-${chartEditorState.selectedVariation}' : '';
|
||||
FileUtil.writeFileReference('${chartEditorState.currentSongId}$vari-chart.json', chartEditorState.currentSongChartData.serialize());
|
||||
};
|
||||
|
||||
difficultyToolboxLoadMetadata.onClick = function(_:UIEvent) {
|
||||
// Replace metadata for current variation.
|
||||
SongSerializer.importSongMetadataAsync(function(songMetadata) {
|
||||
chartEditorState.currentSongMetadata = songMetadata;
|
||||
});
|
||||
};
|
||||
|
||||
difficultyToolboxLoadChart.onClick = function(_:UIEvent) {
|
||||
// Replace chart data for current variation.
|
||||
SongSerializer.importSongChartDataAsync(function(songChartData) {
|
||||
chartEditorState.currentSongChartData = songChartData;
|
||||
chartEditorState.noteDisplayDirty = true;
|
||||
});
|
||||
};
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the tree view and rebuild it with the current song metadata (variation and difficulty list).
|
||||
*/
|
||||
public function updateTree():Void
|
||||
{
|
||||
// Clear the tree view so we can rebuild it.
|
||||
difficultyToolboxTree.clearNodes();
|
||||
|
||||
// , icon: 'haxeui-core/styles/default/haxeui_tiny.png'
|
||||
var treeSong:TreeViewNode = difficultyToolboxTree.addNode({id: 'stv_song', text: 'S: ${chartEditorState.currentSongName}'});
|
||||
treeSong.expanded = true;
|
||||
|
||||
for (curVariation in chartEditorState.availableVariations)
|
||||
{
|
||||
var variationMetadata:Null<SongMetadata> = chartEditorState.songMetadata.get(curVariation);
|
||||
if (variationMetadata == null) continue;
|
||||
|
||||
var treeVariation:TreeViewNode = treeSong.addNode(
|
||||
{
|
||||
id: 'stv_variation_$curVariation',
|
||||
text: 'V: ${curVariation.toTitleCase()}'
|
||||
});
|
||||
treeVariation.expanded = true;
|
||||
|
||||
var difficultyList:Array<String> = variationMetadata.playData.difficulties;
|
||||
|
||||
for (difficulty in difficultyList)
|
||||
{
|
||||
var _treeDifficulty:TreeViewNode = treeVariation.addNode(
|
||||
{
|
||||
id: 'stv_difficulty_${curVariation}_$difficulty',
|
||||
text: 'D: ${difficulty.toTitleCase()}'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
difficultyToolboxTree.onChange = onTreeChange;
|
||||
refreshTreeSelection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selected item in the tree to the current variation/difficulty.
|
||||
*
|
||||
* @param targetNode The node to select. If null, the current variation/difficulty will be used.
|
||||
*/
|
||||
public function refreshTreeSelection():Void
|
||||
{
|
||||
var targetNode = getCurrentTreeNode();
|
||||
if (targetNode != null) difficultyToolboxTree.selectedNode = targetNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the node in the tree representing the current variation/difficulty.
|
||||
*/
|
||||
function getCurrentTreeNode():TreeViewNode
|
||||
{
|
||||
return
|
||||
difficultyToolboxTree.findNodeByPath('stv_song/stv_variation_$chartEditorState.selectedVariation/stv_difficulty_${chartEditorState.selectedVariation}_$chartEditorState.selectedDifficulty',
|
||||
'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an item in the tree is selected. Updates the current variation/difficulty.
|
||||
*/
|
||||
function onTreeChange(event:UIEvent):Void
|
||||
{
|
||||
// Get the newly selected node.
|
||||
var treeView:TreeView = cast event.target;
|
||||
var targetNode:TreeViewNode = difficultyToolboxTree.selectedNode;
|
||||
|
||||
if (targetNode == null)
|
||||
{
|
||||
trace('No target node!');
|
||||
// Reset the user's selection.
|
||||
refreshTreeSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (targetNode.data.id.split('_')[1])
|
||||
{
|
||||
case 'difficulty':
|
||||
var variation:String = targetNode.data.id.split('_')[2];
|
||||
var difficulty:String = targetNode.data.id.split('_')[3];
|
||||
|
||||
if (variation != null && difficulty != null)
|
||||
{
|
||||
trace('Changing difficulty to "$variation:$difficulty"');
|
||||
chartEditorState.selectedVariation = variation;
|
||||
chartEditorState.selectedDifficulty = difficulty;
|
||||
chartEditorState.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
|
||||
refreshTreeSelection();
|
||||
}
|
||||
// case 'song':
|
||||
// case 'variation':
|
||||
default:
|
||||
// Reset the user's selection.
|
||||
trace('Selected wrong node type, resetting selection.');
|
||||
refreshTreeSelection();
|
||||
chartEditorState.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
|
||||
}
|
||||
}
|
||||
|
||||
public override function refresh():Void
|
||||
{
|
||||
super.refresh();
|
||||
|
||||
refreshTreeSelection();
|
||||
}
|
||||
|
||||
public static function build(chartEditorState:ChartEditorState):ChartEditorDifficultyToolbox
|
||||
{
|
||||
return new ChartEditorDifficultyToolbox(chartEditorState);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,259 @@
|
|||
package funkin.ui.debug.charting.toolboxes;
|
||||
|
||||
import funkin.play.character.BaseCharacter.CharacterType;
|
||||
import funkin.play.character.CharacterData;
|
||||
import funkin.data.stage.StageData;
|
||||
import funkin.play.event.SongEvent;
|
||||
import funkin.data.event.SongEventSchema;
|
||||
import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand;
|
||||
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
|
||||
import haxe.ui.components.Button;
|
||||
import haxe.ui.components.CheckBox;
|
||||
import haxe.ui.components.DropDown;
|
||||
import haxe.ui.components.HorizontalSlider;
|
||||
import haxe.ui.components.Label;
|
||||
import haxe.ui.components.NumberStepper;
|
||||
import haxe.ui.components.Slider;
|
||||
import haxe.ui.core.Component;
|
||||
import funkin.data.event.SongEventRegistry;
|
||||
import haxe.ui.components.TextField;
|
||||
import haxe.ui.containers.Box;
|
||||
import haxe.ui.containers.Frame;
|
||||
import haxe.ui.events.UIEvent;
|
||||
import haxe.ui.data.ArrayDataSource;
|
||||
import haxe.ui.containers.Grid;
|
||||
import haxe.ui.components.DropDown;
|
||||
import haxe.ui.containers.Frame;
|
||||
|
||||
/**
|
||||
* The toolbox which allows modifying information like Song Title, Scroll Speed, Characters/Stages, and starting BPM.
|
||||
*/
|
||||
// @:nullSafety // TODO: Fix null safety when used with HaxeUI build macros.
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/event-data.xml"))
|
||||
class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
|
||||
{
|
||||
var toolboxEventsEventKind:DropDown;
|
||||
var toolboxEventsDataFrame:Frame;
|
||||
var toolboxEventsDataGrid:Grid;
|
||||
|
||||
var _initializing:Bool = true;
|
||||
|
||||
public function new(chartEditorState2:ChartEditorState)
|
||||
{
|
||||
super(chartEditorState2);
|
||||
|
||||
initialize();
|
||||
|
||||
this.onDialogClosed = onClose;
|
||||
|
||||
this._initializing = false;
|
||||
}
|
||||
|
||||
function onClose(event:UIEvent)
|
||||
{
|
||||
chartEditorState.menubarItemToggleToolboxEventData.selected = false;
|
||||
}
|
||||
|
||||
function initialize():Void
|
||||
{
|
||||
toolboxEventsEventKind.dataSource = new ArrayDataSource();
|
||||
|
||||
var songEvents:Array<SongEvent> = SongEventRegistry.listEvents();
|
||||
|
||||
for (event in songEvents)
|
||||
{
|
||||
toolboxEventsEventKind.dataSource.add({text: event.getTitle(), value: event.id});
|
||||
}
|
||||
|
||||
toolboxEventsEventKind.onChange = function(event:UIEvent) {
|
||||
var eventType:String = event.data.value;
|
||||
|
||||
trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Event type changed: $eventType');
|
||||
|
||||
// Edit the event data to place.
|
||||
chartEditorState.eventKindToPlace = eventType;
|
||||
|
||||
var schema:SongEventSchema = SongEventRegistry.getEventSchema(eventType);
|
||||
|
||||
if (schema == null)
|
||||
{
|
||||
trace('ChartEditorToolboxHandler.buildToolboxEventDataLayout() - Unknown event kind: $eventType');
|
||||
return;
|
||||
}
|
||||
|
||||
buildEventDataFormFromSchema(toolboxEventsDataGrid, schema);
|
||||
|
||||
if (!_initializing && chartEditorState.currentEventSelection.length > 0)
|
||||
{
|
||||
// Edit the event data of any selected events.
|
||||
for (event in chartEditorState.currentEventSelection)
|
||||
{
|
||||
event.event = chartEditorState.eventKindToPlace;
|
||||
event.value = chartEditorState.eventDataToPlace;
|
||||
}
|
||||
chartEditorState.saveDataDirty = true;
|
||||
chartEditorState.noteDisplayDirty = true;
|
||||
chartEditorState.notePreviewDirty = true;
|
||||
}
|
||||
}
|
||||
toolboxEventsEventKind.value = chartEditorState.eventKindToPlace;
|
||||
}
|
||||
|
||||
public override function refresh():Void
|
||||
{
|
||||
super.refresh();
|
||||
|
||||
toolboxEventsEventKind.value = chartEditorState.eventKindToPlace;
|
||||
|
||||
for (pair in chartEditorState.eventDataToPlace.keyValueIterator())
|
||||
{
|
||||
var fieldId:String = pair.key;
|
||||
var value:Null<Dynamic> = pair.value;
|
||||
|
||||
var field:Component = toolboxEventsDataGrid.findComponent(fieldId);
|
||||
|
||||
if (field == null)
|
||||
{
|
||||
throw 'ChartEditorToolboxHandler.refresh() - Field "${fieldId}" does not exist in the event data form.';
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (field)
|
||||
{
|
||||
case Std.isOfType(_, NumberStepper) => true:
|
||||
var numberStepper:NumberStepper = cast field;
|
||||
numberStepper.value = value;
|
||||
case Std.isOfType(_, CheckBox) => true:
|
||||
var checkBox:CheckBox = cast field;
|
||||
checkBox.selected = value;
|
||||
case Std.isOfType(_, DropDown) => true:
|
||||
var dropDown:DropDown = cast field;
|
||||
dropDown.value = value;
|
||||
case Std.isOfType(_, TextField) => true:
|
||||
var textField:TextField = cast field;
|
||||
textField.text = value;
|
||||
default:
|
||||
throw 'ChartEditorToolboxHandler.refresh() - Field "${fieldId}" is of unknown type "${Type.getClassName(Type.getClass(field))}".';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildEventDataFormFromSchema(target:Box, schema:SongEventSchema):Void
|
||||
{
|
||||
trace(schema);
|
||||
// Clear the frame.
|
||||
target.removeAllComponents();
|
||||
|
||||
chartEditorState.eventDataToPlace = {};
|
||||
|
||||
for (field in schema)
|
||||
{
|
||||
if (field == null) continue;
|
||||
|
||||
// Add a label for the data field.
|
||||
var label:Label = new Label();
|
||||
label.text = field.title;
|
||||
label.verticalAlign = "center";
|
||||
target.addComponent(label);
|
||||
|
||||
// Add an input field for the data field.
|
||||
var input:Component;
|
||||
switch (field.type)
|
||||
{
|
||||
case INTEGER:
|
||||
var numberStepper:NumberStepper = new NumberStepper();
|
||||
numberStepper.id = field.name;
|
||||
numberStepper.step = field.step ?? 1.0;
|
||||
numberStepper.min = field.min ?? 0.0;
|
||||
numberStepper.max = field.max ?? 10.0;
|
||||
if (field.defaultValue != null) numberStepper.value = field.defaultValue;
|
||||
input = numberStepper;
|
||||
case FLOAT:
|
||||
var numberStepper:NumberStepper = new NumberStepper();
|
||||
numberStepper.id = field.name;
|
||||
numberStepper.step = field.step ?? 0.1;
|
||||
if (field.min != null) numberStepper.min = field.min;
|
||||
if (field.max != null) numberStepper.max = field.max;
|
||||
if (field.defaultValue != null) numberStepper.value = field.defaultValue;
|
||||
input = numberStepper;
|
||||
case BOOL:
|
||||
var checkBox:CheckBox = new CheckBox();
|
||||
checkBox.id = field.name;
|
||||
if (field.defaultValue != null) checkBox.selected = field.defaultValue;
|
||||
input = checkBox;
|
||||
case ENUM:
|
||||
var dropDown:DropDown = new DropDown();
|
||||
dropDown.id = field.name;
|
||||
dropDown.width = 200.0;
|
||||
dropDown.dataSource = new ArrayDataSource();
|
||||
|
||||
if (field.keys == null) throw 'Field "${field.name}" is of Enum type but has no keys.';
|
||||
|
||||
// Add entries to the dropdown.
|
||||
|
||||
for (optionName in field.keys.keys())
|
||||
{
|
||||
var optionValue:Null<Dynamic> = field.keys.get(optionName);
|
||||
trace('$optionName : $optionValue');
|
||||
dropDown.dataSource.add({value: optionValue, text: optionName});
|
||||
}
|
||||
|
||||
dropDown.value = field.defaultValue;
|
||||
|
||||
input = dropDown;
|
||||
case STRING:
|
||||
input = new TextField();
|
||||
input.id = field.name;
|
||||
if (field.defaultValue != null) input.text = field.defaultValue;
|
||||
default:
|
||||
// Unknown type. Display a label that proclaims the type so we can debug it.
|
||||
input = new Label();
|
||||
input.id = field.name;
|
||||
input.text = field.type;
|
||||
}
|
||||
|
||||
target.addComponent(input);
|
||||
|
||||
// Update the value of the event data.
|
||||
input.onChange = function(event:UIEvent) {
|
||||
var value = event.target.value;
|
||||
if (field.type == ENUM)
|
||||
{
|
||||
value = event.target.value.value;
|
||||
}
|
||||
|
||||
trace('ChartEditorToolboxHandler.buildEventDataFormFromSchema() - ${event.target.id} = ${value}');
|
||||
|
||||
// Edit the event data to place.
|
||||
if (value == null)
|
||||
{
|
||||
chartEditorState.eventDataToPlace.remove(event.target.id);
|
||||
}
|
||||
else
|
||||
{
|
||||
chartEditorState.eventDataToPlace.set(event.target.id, value);
|
||||
}
|
||||
|
||||
// Edit the event data of any existing events.
|
||||
if (!_initializing && chartEditorState.currentEventSelection.length > 0)
|
||||
{
|
||||
for (event in chartEditorState.currentEventSelection)
|
||||
{
|
||||
event.event = chartEditorState.eventKindToPlace;
|
||||
event.value = chartEditorState.eventDataToPlace;
|
||||
}
|
||||
chartEditorState.saveDataDirty = true;
|
||||
chartEditorState.noteDisplayDirty = true;
|
||||
chartEditorState.notePreviewDirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static function build(chartEditorState:ChartEditorState):ChartEditorEventDataToolbox
|
||||
{
|
||||
return new ChartEditorEventDataToolbox(chartEditorState);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,8 @@ package funkin.ui.debug.charting.toolboxes;
|
|||
|
||||
import funkin.play.character.BaseCharacter.CharacterType;
|
||||
import funkin.play.character.CharacterData;
|
||||
import funkin.play.stage.StageData;
|
||||
import funkin.data.stage.StageData;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand;
|
||||
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
|
||||
import haxe.ui.components.Button;
|
||||
|
@ -13,6 +14,7 @@ import haxe.ui.components.Label;
|
|||
import haxe.ui.components.NumberStepper;
|
||||
import haxe.ui.components.Slider;
|
||||
import haxe.ui.components.TextField;
|
||||
import funkin.play.stage.Stage;
|
||||
import haxe.ui.containers.Box;
|
||||
import haxe.ui.containers.Frame;
|
||||
import haxe.ui.events.UIEvent;
|
||||
|
@ -116,13 +118,33 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
|
|||
}
|
||||
};
|
||||
|
||||
inputTimeSignature.onChange = function(event:UIEvent) {
|
||||
var timeSignatureStr:String = event.data.text;
|
||||
var timeSignature = timeSignatureStr.split('/');
|
||||
if (timeSignature.length != 2) return;
|
||||
|
||||
var timeSignatureNum:Int = Std.parseInt(timeSignature[0]);
|
||||
var timeSignatureDen:Int = Std.parseInt(timeSignature[1]);
|
||||
|
||||
var previousTimeSignatureNum:Int = chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureNum;
|
||||
var previousTimeSignatureDen:Int = chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureDen;
|
||||
if (timeSignatureNum == previousTimeSignatureNum && timeSignatureDen == previousTimeSignatureDen) return;
|
||||
|
||||
chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureNum = timeSignatureNum;
|
||||
chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureDen = timeSignatureDen;
|
||||
|
||||
trace('Time signature changed to ${timeSignatureNum}/${timeSignatureDen}');
|
||||
|
||||
chartEditorState.updateTimeSignature();
|
||||
};
|
||||
|
||||
inputOffsetInst.onChange = function(event:UIEvent) {
|
||||
if (event.value == null) return;
|
||||
|
||||
chartEditorState.currentInstrumentalOffset = event.value;
|
||||
Conductor.instrumentalOffset = event.value;
|
||||
Conductor.instance.instrumentalOffset = event.value;
|
||||
// Update song length.
|
||||
chartEditorState.songLengthInMs = (chartEditorState.audioInstTrack?.length ?? 1000.0) + Conductor.instrumentalOffset;
|
||||
chartEditorState.songLengthInMs = (chartEditorState.audioInstTrack?.length ?? 1000.0) + Conductor.instance.instrumentalOffset;
|
||||
};
|
||||
|
||||
inputOffsetVocal.onChange = function(event:UIEvent) {
|
||||
|
@ -162,6 +184,8 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
|
|||
|
||||
public override function refresh():Void
|
||||
{
|
||||
super.refresh();
|
||||
|
||||
inputSongName.value = chartEditorState.currentSongMetadata.songName;
|
||||
inputSongArtist.value = chartEditorState.currentSongMetadata.artist;
|
||||
inputStage.value = chartEditorState.currentSongMetadata.playData.stage;
|
||||
|
@ -172,12 +196,16 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
|
|||
frameVariation.text = 'Variation: ${chartEditorState.selectedVariation.toTitleCase()}';
|
||||
frameDifficulty.text = 'Difficulty: ${chartEditorState.selectedDifficulty.toTitleCase()}';
|
||||
|
||||
var currentTimeSignature = '${chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureNum}/${chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureDen}';
|
||||
trace('Setting time signature to ${currentTimeSignature}');
|
||||
inputTimeSignature.value = {id: currentTimeSignature, text: currentTimeSignature};
|
||||
|
||||
var stageId:String = chartEditorState.currentSongMetadata.playData.stage;
|
||||
var stageData:Null<StageData> = StageDataParser.parseStageData(stageId);
|
||||
var stage:Null<Stage> = StageRegistry.instance.fetchEntry(stageId);
|
||||
if (inputStage != null)
|
||||
{
|
||||
inputStage.value = (stageData != null) ?
|
||||
{id: stageId, text: stageData.name} :
|
||||
inputStage.value = (stage != null) ?
|
||||
{id: stage.id, text: stage.stageName} :
|
||||
{id: "mainStage", text: "Main Stage"};
|
||||
}
|
||||
|
||||
|
|
|
@ -2,10 +2,11 @@ package funkin.ui.debug.charting.util;
|
|||
|
||||
import funkin.data.notestyle.NoteStyleRegistry;
|
||||
import funkin.play.notes.notestyle.NoteStyle;
|
||||
import funkin.play.stage.StageData;
|
||||
import funkin.play.stage.StageData.StageDataParser;
|
||||
import funkin.data.stage.StageData;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
import funkin.play.character.CharacterData;
|
||||
import haxe.ui.components.DropDown;
|
||||
import funkin.play.stage.Stage;
|
||||
import funkin.play.character.BaseCharacter.CharacterType;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
|
||||
|
@ -60,16 +61,16 @@ class ChartEditorDropdowns
|
|||
{
|
||||
dropDown.dataSource.clear();
|
||||
|
||||
var stageIds:Array<String> = StageDataParser.listStageIds();
|
||||
var stageIds:Array<String> = StageRegistry.instance.listEntryIds();
|
||||
|
||||
var returnValue:DropDownEntry = {id: "mainStage", text: "Main Stage"};
|
||||
|
||||
for (stageId in stageIds)
|
||||
{
|
||||
var stage:Null<StageData> = StageDataParser.parseStageData(stageId);
|
||||
var stage:Null<Stage> = StageRegistry.instance.fetchEntry(stageId);
|
||||
if (stage == null) continue;
|
||||
|
||||
var value = {id: stageId, text: stage.name};
|
||||
var value = {id: stage.id, text: stage.stageName};
|
||||
if (startingStageId == stageId) returnValue = value;
|
||||
|
||||
dropDown.dataSource.add(value);
|
||||
|
|
|
@ -75,7 +75,7 @@ class LatencyState extends MusicBeatSubState
|
|||
|
||||
// funnyStatsGraph.hi
|
||||
|
||||
Conductor.forceBPM(60);
|
||||
Conductor.instance.forceBPM(60);
|
||||
|
||||
noteGrp = new FlxTypedGroup<NoteSprite>();
|
||||
add(noteGrp);
|
||||
|
@ -91,14 +91,14 @@ class LatencyState extends MusicBeatSubState
|
|||
// // musSpec.visType = FREQUENCIES;
|
||||
// add(musSpec);
|
||||
|
||||
for (beat in 0...Math.floor(FlxG.sound.music.length / Conductor.beatLengthMs))
|
||||
for (beat in 0...Math.floor(FlxG.sound.music.length / Conductor.instance.beatLengthMs))
|
||||
{
|
||||
var beatTick:FlxSprite = new FlxSprite(songPosToX(beat * Conductor.beatLengthMs), FlxG.height - 15);
|
||||
var beatTick:FlxSprite = new FlxSprite(songPosToX(beat * Conductor.instance.beatLengthMs), FlxG.height - 15);
|
||||
beatTick.makeGraphic(2, 15);
|
||||
beatTick.alpha = 0.3;
|
||||
add(beatTick);
|
||||
|
||||
var offsetTxt:FlxText = new FlxText(songPosToX(beat * Conductor.beatLengthMs), FlxG.height - 26, 0, "swag");
|
||||
var offsetTxt:FlxText = new FlxText(songPosToX(beat * Conductor.instance.beatLengthMs), FlxG.height - 26, 0, "swag");
|
||||
offsetTxt.alpha = 0.5;
|
||||
diffGrp.add(offsetTxt);
|
||||
|
||||
|
@ -130,7 +130,7 @@ class LatencyState extends MusicBeatSubState
|
|||
|
||||
for (i in 0...32)
|
||||
{
|
||||
var note:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault(), Conductor.beatLengthMs * i);
|
||||
var note:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault(), Conductor.instance.beatLengthMs * i);
|
||||
noteGrp.add(note);
|
||||
}
|
||||
|
||||
|
@ -146,9 +146,9 @@ class LatencyState extends MusicBeatSubState
|
|||
|
||||
override function stepHit():Bool
|
||||
{
|
||||
if (Conductor.currentStep % 4 == 2)
|
||||
if (Conductor.instance.currentStep % 4 == 2)
|
||||
{
|
||||
blocks.members[((Conductor.currentBeat % 8) + 1) % 8].alpha = 0.5;
|
||||
blocks.members[((Conductor.instance.currentBeat % 8) + 1) % 8].alpha = 0.5;
|
||||
}
|
||||
|
||||
return super.stepHit();
|
||||
|
@ -156,11 +156,11 @@ class LatencyState extends MusicBeatSubState
|
|||
|
||||
override function beatHit():Bool
|
||||
{
|
||||
if (Conductor.currentBeat % 8 == 0) blocks.forEach(blok -> {
|
||||
if (Conductor.instance.currentBeat % 8 == 0) blocks.forEach(blok -> {
|
||||
blok.alpha = 0;
|
||||
});
|
||||
|
||||
blocks.members[Conductor.currentBeat % 8].alpha = 1;
|
||||
blocks.members[Conductor.instance.currentBeat % 8].alpha = 1;
|
||||
// block.visible = !block.visible;
|
||||
|
||||
return super.beatHit();
|
||||
|
@ -192,17 +192,17 @@ class LatencyState extends MusicBeatSubState
|
|||
|
||||
if (FlxG.keys.pressed.D) FlxG.sound.music.time += 1000 * FlxG.elapsed;
|
||||
|
||||
Conductor.update(swagSong.getTimeWithDiff() - Conductor.inputOffset);
|
||||
// Conductor.songPosition += (Timer.stamp() * 1000) - FlxG.sound.music.prevTimestamp;
|
||||
Conductor.instance.update(swagSong.getTimeWithDiff() - Conductor.instance.inputOffset);
|
||||
// Conductor.instance.songPosition += (Timer.stamp() * 1000) - FlxG.sound.music.prevTimestamp;
|
||||
|
||||
songPosVis.x = songPosToX(Conductor.songPosition);
|
||||
songVisFollowAudio.x = songPosToX(Conductor.songPosition - Conductor.instrumentalOffset);
|
||||
songVisFollowVideo.x = songPosToX(Conductor.songPosition - Conductor.inputOffset);
|
||||
songPosVis.x = songPosToX(Conductor.instance.songPosition);
|
||||
songVisFollowAudio.x = songPosToX(Conductor.instance.songPosition - Conductor.instance.instrumentalOffset);
|
||||
songVisFollowVideo.x = songPosToX(Conductor.instance.songPosition - Conductor.instance.inputOffset);
|
||||
|
||||
offsetText.text = "INST Offset: " + Conductor.instrumentalOffset + "ms";
|
||||
offsetText.text += "\nINPUT Offset: " + Conductor.inputOffset + "ms";
|
||||
offsetText.text += "\ncurrentStep: " + Conductor.currentStep;
|
||||
offsetText.text += "\ncurrentBeat: " + Conductor.currentBeat;
|
||||
offsetText.text = "INST Offset: " + Conductor.instance.instrumentalOffset + "ms";
|
||||
offsetText.text += "\nINPUT Offset: " + Conductor.instance.inputOffset + "ms";
|
||||
offsetText.text += "\ncurrentStep: " + Conductor.instance.currentStep;
|
||||
offsetText.text += "\ncurrentBeat: " + Conductor.instance.currentBeat;
|
||||
|
||||
var avgOffsetInput:Float = 0;
|
||||
|
||||
|
@ -221,24 +221,24 @@ class LatencyState extends MusicBeatSubState
|
|||
{
|
||||
if (FlxG.keys.justPressed.RIGHT)
|
||||
{
|
||||
Conductor.instrumentalOffset += 1.0 * multiply;
|
||||
Conductor.instance.instrumentalOffset += 1.0 * multiply;
|
||||
}
|
||||
|
||||
if (FlxG.keys.justPressed.LEFT)
|
||||
{
|
||||
Conductor.instrumentalOffset -= 1.0 * multiply;
|
||||
Conductor.instance.instrumentalOffset -= 1.0 * multiply;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (FlxG.keys.justPressed.RIGHT)
|
||||
{
|
||||
Conductor.inputOffset += 1.0 * multiply;
|
||||
Conductor.instance.inputOffset += 1.0 * multiply;
|
||||
}
|
||||
|
||||
if (FlxG.keys.justPressed.LEFT)
|
||||
{
|
||||
Conductor.inputOffset -= 1.0 * multiply;
|
||||
Conductor.instance.inputOffset -= 1.0 * multiply;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -250,7 +250,7 @@ class LatencyState extends MusicBeatSubState
|
|||
}*/
|
||||
|
||||
noteGrp.forEach(function(daNote:NoteSprite) {
|
||||
daNote.y = (strumLine.y - ((Conductor.songPosition - Conductor.instrumentalOffset) - daNote.noteData.time) * 0.45);
|
||||
daNote.y = (strumLine.y - ((Conductor.instance.songPosition - Conductor.instance.instrumentalOffset) - daNote.noteData.time) * 0.45);
|
||||
daNote.x = strumLine.x + 30;
|
||||
|
||||
if (daNote.y < strumLine.y) daNote.alpha = 0.5;
|
||||
|
@ -258,7 +258,7 @@ class LatencyState extends MusicBeatSubState
|
|||
if (daNote.y < 0 - daNote.height)
|
||||
{
|
||||
daNote.alpha = 1;
|
||||
// daNote.data.strumTime += Conductor.beatLengthMs * 8;
|
||||
// daNote.data.strumTime += Conductor.instance.beatLengthMs * 8;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -267,14 +267,14 @@ class LatencyState extends MusicBeatSubState
|
|||
|
||||
function generateBeatStuff()
|
||||
{
|
||||
Conductor.update(swagSong.getTimeWithDiff());
|
||||
Conductor.instance.update(swagSong.getTimeWithDiff());
|
||||
|
||||
var closestBeat:Int = Math.round(Conductor.songPosition / Conductor.beatLengthMs) % diffGrp.members.length;
|
||||
var getDiff:Float = Conductor.songPosition - (closestBeat * Conductor.beatLengthMs);
|
||||
getDiff -= Conductor.inputOffset;
|
||||
var closestBeat:Int = Math.round(Conductor.instance.songPosition / Conductor.instance.beatLengthMs) % diffGrp.members.length;
|
||||
var getDiff:Float = Conductor.instance.songPosition - (closestBeat * Conductor.instance.beatLengthMs);
|
||||
getDiff -= Conductor.instance.inputOffset;
|
||||
|
||||
// lil fix for end of song
|
||||
if (closestBeat == 0 && getDiff >= Conductor.beatLengthMs * 2) getDiff -= FlxG.sound.music.length;
|
||||
if (closestBeat == 0 && getDiff >= Conductor.instance.beatLengthMs * 2) getDiff -= FlxG.sound.music.length;
|
||||
|
||||
trace("\tDISTANCE TO CLOSEST BEAT: " + getDiff + "ms");
|
||||
trace("\tCLOSEST BEAT: " + closestBeat);
|
||||
|
|
|
@ -5,15 +5,17 @@ import flixel.input.mouse.FlxMouseEvent;
|
|||
import flixel.math.FlxPoint;
|
||||
import funkin.play.character.BaseCharacter;
|
||||
import funkin.play.PlayState;
|
||||
import funkin.play.stage.StageData;
|
||||
import funkin.data.stage.StageData;
|
||||
import funkin.play.stage.StageProp;
|
||||
import funkin.graphics.shaders.StrokeShader;
|
||||
import funkin.ui.haxeui.HaxeUISubState;
|
||||
import funkin.ui.debug.stage.StageEditorCommand;
|
||||
import funkin.util.SerializerUtil;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
import funkin.util.MouseUtil;
|
||||
import haxe.ui.containers.ListView;
|
||||
import haxe.ui.core.Component;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import haxe.ui.events.UIEvent;
|
||||
import haxe.ui.RuntimeComponentBuilder;
|
||||
import openfl.events.Event;
|
||||
|
@ -354,7 +356,13 @@ class StageOffsetSubState extends HaxeUISubState
|
|||
|
||||
function prepStageStuff():String
|
||||
{
|
||||
var stageLol:StageData = StageDataParser.parseStageData(PlayState.instance.currentStageId);
|
||||
var stageLol:StageData = StageRegistry.instance.fetchEntry(PlayState.instance.currentStageId)?._data;
|
||||
|
||||
if (stageLol == null)
|
||||
{
|
||||
FlxG.log.error("Stage not found in registry!");
|
||||
return "";
|
||||
}
|
||||
|
||||
for (prop in stageLol.props)
|
||||
{
|
||||
|
@ -378,6 +386,6 @@ class StageOffsetSubState extends HaxeUISubState
|
|||
stageLol.characters.gf.position[0] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.x);
|
||||
stageLol.characters.gf.position[1] = Std.int(GF_FEET_SNIIIIIIIIIIIIIFFFF.y);
|
||||
|
||||
return SerializerUtil.toJSON(stageLol);
|
||||
return stageLol.serialize();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package funkin.ui.freeplay;
|
||||
|
||||
import funkin.input.Controls;
|
||||
import flash.text.TextField;
|
||||
import flixel.addons.display.FlxGridOverlay;
|
||||
import flixel.addons.transition.FlxTransitionableState;
|
||||
|
@ -23,33 +22,35 @@ import flixel.tweens.FlxTween;
|
|||
import flixel.util.FlxColor;
|
||||
import flixel.util.FlxSpriteUtil;
|
||||
import flixel.util.FlxTimer;
|
||||
import funkin.input.Controls.Control;
|
||||
import funkin.data.level.LevelRegistry;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.graphics.adobeanimate.FlxAtlasSprite;
|
||||
import funkin.graphics.shaders.AngleMask;
|
||||
import funkin.graphics.shaders.HSVShader;
|
||||
import funkin.graphics.shaders.PureColor;
|
||||
import funkin.util.MathUtil;
|
||||
import funkin.graphics.shaders.StrokeShader;
|
||||
import funkin.input.Controls;
|
||||
import funkin.input.Controls.Control;
|
||||
import funkin.play.components.HealthIcon;
|
||||
import funkin.play.PlayState;
|
||||
import funkin.play.PlayStatePlaylist;
|
||||
import funkin.play.song.Song;
|
||||
import funkin.save.Save;
|
||||
import funkin.save.Save.SaveScoreData;
|
||||
import funkin.ui.AtlasText;
|
||||
import funkin.ui.freeplay.BGScrollingText;
|
||||
import funkin.ui.freeplay.DifficultyStars;
|
||||
import funkin.ui.freeplay.DJBoyfriend;
|
||||
import funkin.ui.freeplay.FreeplayScore;
|
||||
import funkin.ui.freeplay.LetterSort;
|
||||
import funkin.ui.freeplay.SongMenuItem;
|
||||
import funkin.ui.mainmenu.MainMenuState;
|
||||
import funkin.ui.MusicBeatState;
|
||||
import funkin.ui.MusicBeatSubState;
|
||||
import funkin.ui.mainmenu.MainMenuState;
|
||||
import funkin.ui.transition.LoadingState;
|
||||
import funkin.ui.transition.StickerSubState;
|
||||
import funkin.util.MathUtil;
|
||||
import funkin.util.MathUtil;
|
||||
import lime.app.Future;
|
||||
import lime.utils.Assets;
|
||||
|
||||
|
@ -64,7 +65,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
var currentDifficulty:String = Constants.DEFAULT_DIFFICULTY;
|
||||
|
||||
var fp:FreeplayScore;
|
||||
var txtCompletion:FlxText;
|
||||
var txtCompletion:AtlasText;
|
||||
var lerpCompletion:Float = 0;
|
||||
var intendedCompletion:Float = 0;
|
||||
var lerpScore:Float = 0;
|
||||
|
@ -87,6 +88,8 @@ class FreeplayState extends MusicBeatSubState
|
|||
var grpCapsules:FlxTypedGroup<SongMenuItem>;
|
||||
var curCapsule:SongMenuItem;
|
||||
var curPlaying:Bool = false;
|
||||
var ostName:FlxText;
|
||||
var difficultyStars:DifficultyStars;
|
||||
|
||||
var dj:DJBoyfriend;
|
||||
|
||||
|
@ -150,15 +153,10 @@ class FreeplayState extends MusicBeatSubState
|
|||
for (songId in LevelRegistry.instance.parseEntryData(levelId).songs)
|
||||
{
|
||||
var song:Song = SongRegistry.instance.fetchEntry(songId);
|
||||
var songBaseDifficulty:SongDifficulty = song.getDifficulty(Constants.DEFAULT_DIFFICULTY);
|
||||
|
||||
var songName = songBaseDifficulty.songName;
|
||||
var songOpponent = songBaseDifficulty.characters.opponent;
|
||||
var songDifficulties = song.listDifficulties();
|
||||
songs.push(new FreeplaySongData(levelId, songId, song));
|
||||
|
||||
songs.push(new FreeplaySongData(songId, songName, levelId, songOpponent, songDifficulties));
|
||||
|
||||
for (difficulty in songDifficulties)
|
||||
for (difficulty in song.listDifficulties())
|
||||
{
|
||||
diffIdsTotal.pushUnique(difficulty);
|
||||
}
|
||||
|
@ -334,6 +332,8 @@ class FreeplayState extends MusicBeatSubState
|
|||
if (diffSprite.difficultyId == currentDifficulty) diffSprite.visible = true;
|
||||
}
|
||||
|
||||
// NOTE: This is an AtlasSprite because we use an animation to bring it into view.
|
||||
// TODO: Add the ability to select the album graphic.
|
||||
var albumArt:FlxAtlasSprite = new FlxAtlasSprite(640, 360, Paths.animateAtlas("freeplay/albumRoll"));
|
||||
albumArt.visible = false;
|
||||
add(albumArt);
|
||||
|
@ -347,7 +347,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
|
||||
var albumTitle:FlxSprite = new FlxSprite(947, 491).loadGraphic(Paths.image('freeplay/albumTitle-fnfvol1'));
|
||||
var albumArtist:FlxSprite = new FlxSprite(1010, 607).loadGraphic(Paths.image('freeplay/albumArtist-kawaisprite'));
|
||||
var difficultyStars:DifficultyStars = new DifficultyStars(140, 39);
|
||||
difficultyStars = new DifficultyStars(140, 39);
|
||||
|
||||
difficultyStars.stars.visible = false;
|
||||
albumTitle.visible = false;
|
||||
|
@ -382,11 +382,16 @@ class FreeplayState extends MusicBeatSubState
|
|||
add(overhangStuff);
|
||||
FlxTween.tween(overhangStuff, {y: 0}, 0.3, {ease: FlxEase.quartOut});
|
||||
|
||||
var fnfFreeplay:FlxText = new FlxText(0, 12, 0, "FREEPLAY", 48);
|
||||
var fnfFreeplay:FlxText = new FlxText(8, 8, 0, "FREEPLAY", 48);
|
||||
fnfFreeplay.font = "VCR OSD Mono";
|
||||
fnfFreeplay.visible = false;
|
||||
|
||||
exitMovers.set([overhangStuff, fnfFreeplay],
|
||||
ostName = new FlxText(8, 8, FlxG.width - 8 - 8, "OFFICIAL OST", 48);
|
||||
ostName.font = "VCR OSD Mono";
|
||||
ostName.alignment = RIGHT;
|
||||
ostName.visible = false;
|
||||
|
||||
exitMovers.set([overhangStuff, fnfFreeplay, ostName],
|
||||
{
|
||||
y: -overhangStuff.height,
|
||||
x: 0,
|
||||
|
@ -397,8 +402,9 @@ class FreeplayState extends MusicBeatSubState
|
|||
var sillyStroke = new StrokeShader(0xFFFFFFFF, 2, 2);
|
||||
fnfFreeplay.shader = sillyStroke;
|
||||
add(fnfFreeplay);
|
||||
add(ostName);
|
||||
|
||||
var fnfHighscoreSpr:FlxSprite = new FlxSprite(890, 70);
|
||||
var fnfHighscoreSpr:FlxSprite = new FlxSprite(860, 70);
|
||||
fnfHighscoreSpr.frames = Paths.getSparrowAtlas('freeplay/highscore');
|
||||
fnfHighscoreSpr.animation.addByPrefix("highscore", "highscore", 24, false);
|
||||
fnfHighscoreSpr.visible = false;
|
||||
|
@ -415,8 +421,10 @@ class FreeplayState extends MusicBeatSubState
|
|||
fp.visible = false;
|
||||
add(fp);
|
||||
|
||||
txtCompletion = new FlxText(1200, 77, 0, "0", 32);
|
||||
txtCompletion.font = "VCR OSD Mono";
|
||||
var clearBoxSprite:FlxSprite = new FlxSprite(1165, 65).loadGraphic(Paths.image('freeplay/clearBox'));
|
||||
add(clearBoxSprite);
|
||||
|
||||
txtCompletion = new AtlasText(1185, 87, "69", AtlasFont.FREEPLAY_CLEAR);
|
||||
txtCompletion.visible = false;
|
||||
add(txtCompletion);
|
||||
|
||||
|
@ -485,6 +493,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
new FlxTimer().start(1 / 24, function(handShit) {
|
||||
fnfHighscoreSpr.visible = true;
|
||||
fnfFreeplay.visible = true;
|
||||
ostName.visible = true;
|
||||
fp.visible = true;
|
||||
fp.updateScore(0);
|
||||
|
||||
|
@ -674,9 +683,32 @@ class FreeplayState extends MusicBeatSubState
|
|||
lerpScore = MathUtil.coolLerp(lerpScore, intendedScore, 0.2);
|
||||
lerpCompletion = MathUtil.coolLerp(lerpCompletion, intendedCompletion, 0.9);
|
||||
|
||||
if (Math.isNaN(lerpScore))
|
||||
{
|
||||
lerpScore = intendedScore;
|
||||
}
|
||||
|
||||
if (Math.isNaN(lerpCompletion))
|
||||
{
|
||||
lerpCompletion = intendedCompletion;
|
||||
}
|
||||
|
||||
fp.updateScore(Std.int(lerpScore));
|
||||
|
||||
txtCompletion.text = Math.floor(lerpCompletion * 100) + "%";
|
||||
txtCompletion.text = '${Math.floor(lerpCompletion * 100)}';
|
||||
|
||||
// Right align the completion percentage
|
||||
switch (txtCompletion.text.length)
|
||||
{
|
||||
case 3:
|
||||
txtCompletion.x = 1185 - 10;
|
||||
case 2:
|
||||
txtCompletion.x = 1185;
|
||||
case 1:
|
||||
txtCompletion.x = 1185 + 24;
|
||||
default:
|
||||
txtCompletion.x = 1185;
|
||||
}
|
||||
|
||||
handleInputs(elapsed);
|
||||
}
|
||||
|
@ -913,6 +945,11 @@ class FreeplayState extends MusicBeatSubState
|
|||
intendedCompletion = 0.0;
|
||||
}
|
||||
|
||||
if (intendedCompletion == Math.POSITIVE_INFINITY || intendedCompletion == Math.NEGATIVE_INFINITY || Math.isNaN(intendedCompletion))
|
||||
{
|
||||
intendedCompletion = 0;
|
||||
}
|
||||
|
||||
grpDifficulties.group.forEach(function(diffSprite) {
|
||||
diffSprite.visible = false;
|
||||
});
|
||||
|
@ -938,6 +975,27 @@ class FreeplayState extends MusicBeatSubState
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (change != 0)
|
||||
{
|
||||
// Update the song capsules to reflect the new difficulty info.
|
||||
for (songCapsule in grpCapsules.members)
|
||||
{
|
||||
if (songCapsule == null) continue;
|
||||
if (songCapsule.songData != null)
|
||||
{
|
||||
songCapsule.songData.currentDifficulty = currentDifficulty;
|
||||
songCapsule.init(null, null, songCapsule.songData);
|
||||
}
|
||||
else
|
||||
{
|
||||
songCapsule.init(null, null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set the difficulty star count on the right.
|
||||
difficultyStars.difficulty = daSong?.songRating ?? difficultyStars.difficulty; // yay haxe 4.3
|
||||
}
|
||||
|
||||
// Clears the cache of songs, frees up memory, they' ll have to be loaded in later tho function clearDaCache(actualSongTho:String)
|
||||
|
@ -1046,6 +1104,10 @@ class FreeplayState extends MusicBeatSubState
|
|||
{
|
||||
currentDifficulty = rememberedDifficulty;
|
||||
}
|
||||
|
||||
// Set the difficulty star count on the right.
|
||||
var daSong = songs[curSelected];
|
||||
difficultyStars.difficulty = daSong?.songRating ?? 0;
|
||||
}
|
||||
|
||||
function changeSelection(change:Int = 0)
|
||||
|
@ -1176,19 +1238,47 @@ class FreeplaySongData
|
|||
{
|
||||
public var isFav:Bool = false;
|
||||
|
||||
public var songId:String = "";
|
||||
public var songName:String = "";
|
||||
public var levelId:String = "";
|
||||
public var songCharacter:String = "";
|
||||
public var songDifficulties:Array<String> = [];
|
||||
var song:Song;
|
||||
|
||||
public function new(songId:String, songName:String, levelId:String, songCharacter:String, songDifficulties:Array<String>)
|
||||
public var levelId(default, null):String = "";
|
||||
public var songId(default, null):String = "";
|
||||
|
||||
public var songDifficulties(default, null):Array<String> = [];
|
||||
|
||||
public var songName(default, null):String = "";
|
||||
public var songCharacter(default, null):String = "";
|
||||
public var songRating(default, null):Int = 0;
|
||||
|
||||
public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY;
|
||||
|
||||
function set_currentDifficulty(value:String):String
|
||||
{
|
||||
if (currentDifficulty == value) return value;
|
||||
|
||||
currentDifficulty = value;
|
||||
updateValues();
|
||||
return value;
|
||||
}
|
||||
|
||||
public function new(levelId:String, songId:String, song:Song)
|
||||
{
|
||||
this.songId = songId;
|
||||
this.songName = songName;
|
||||
this.levelId = levelId;
|
||||
this.songCharacter = songCharacter;
|
||||
this.songDifficulties = songDifficulties;
|
||||
this.songId = songId;
|
||||
this.song = song;
|
||||
|
||||
updateValues();
|
||||
}
|
||||
|
||||
function updateValues():Void
|
||||
{
|
||||
this.songDifficulties = song.listDifficulties();
|
||||
if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY;
|
||||
|
||||
var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty);
|
||||
if (songDifficulty == null) return;
|
||||
this.songName = songDifficulty.songName;
|
||||
this.songCharacter = songDifficulty.characters.opponent;
|
||||
this.songRating = songDifficulty.difficultyRating;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -35,11 +35,6 @@ class SongMenuItem extends FlxSpriteGroup
|
|||
|
||||
var ranks:Array<String> = ["fail", "average", "great", "excellent", "perfect"];
|
||||
|
||||
// lol...
|
||||
var diffRanks:Array<String> = [
|
||||
"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "14", "15"
|
||||
];
|
||||
|
||||
public var targetPos:FlxPoint = new FlxPoint();
|
||||
public var doLerp:Bool = false;
|
||||
public var doJumpIn:Bool = false;
|
||||
|
@ -47,10 +42,12 @@ class SongMenuItem extends FlxSpriteGroup
|
|||
public var doJumpOut:Bool = false;
|
||||
|
||||
public var onConfirm:Void->Void;
|
||||
public var diffGrayscale:Grayscale;
|
||||
public var grayscaleShader:Grayscale;
|
||||
|
||||
public var hsvShader(default, set):HSVShader;
|
||||
|
||||
var diffRatingSprite:FlxSprite;
|
||||
|
||||
public function new(x:Float, y:Float)
|
||||
{
|
||||
super(x, y);
|
||||
|
@ -75,26 +72,30 @@ class SongMenuItem extends FlxSpriteGroup
|
|||
add(ranking);
|
||||
grpHide.add(ranking);
|
||||
|
||||
diffGrayscale = new Grayscale(1);
|
||||
|
||||
var diffRank = new FlxSprite(145, 90).loadGraphic(Paths.image("freeplay/diffRankings/diff" + FlxG.random.getObject(diffRanks)));
|
||||
diffRank.shader = diffGrayscale;
|
||||
diffRank.visible = false;
|
||||
add(diffRank);
|
||||
diffRank.origin.set(capsule.origin.x - diffRank.x, capsule.origin.y - diffRank.y);
|
||||
grpHide.add(diffRank);
|
||||
|
||||
switch (rank)
|
||||
{
|
||||
case "perfect":
|
||||
ranking.x -= 10;
|
||||
}
|
||||
|
||||
grayscaleShader = new Grayscale(1);
|
||||
|
||||
diffRatingSprite = new FlxSprite(145, 90).loadGraphic(Paths.image("freeplay/diffRatings/diff00"));
|
||||
diffRatingSprite.shader = grayscaleShader;
|
||||
diffRatingSprite.visible = false;
|
||||
add(diffRatingSprite);
|
||||
diffRatingSprite.origin.set(capsule.origin.x - diffRatingSprite.x, capsule.origin.y - diffRatingSprite.y);
|
||||
grpHide.add(diffRatingSprite);
|
||||
|
||||
songText = new CapsuleText(capsule.width * 0.26, 45, 'Random', Std.int(40 * realScaled));
|
||||
add(songText);
|
||||
grpHide.add(songText);
|
||||
|
||||
pixelIcon = new FlxSprite(155, 15);
|
||||
// TODO: Use value from metadata instead of random.
|
||||
updateDifficultyRating(FlxG.random.int(0, 15));
|
||||
|
||||
pixelIcon = new FlxSprite(160, 35);
|
||||
|
||||
pixelIcon.makeGraphic(32, 32, 0x00000000);
|
||||
pixelIcon.antialiasing = false;
|
||||
pixelIcon.active = false;
|
||||
|
@ -113,6 +114,12 @@ class SongMenuItem extends FlxSpriteGroup
|
|||
setVisibleGrp(false);
|
||||
}
|
||||
|
||||
function updateDifficultyRating(newRating:Int)
|
||||
{
|
||||
var ratingPadded:String = newRating < 10 ? '0$newRating' : '$newRating';
|
||||
diffRatingSprite.loadGraphic(Paths.image('freeplay/diffRatings/diff${ratingPadded}'));
|
||||
}
|
||||
|
||||
function set_hsvShader(value:HSVShader):HSVShader
|
||||
{
|
||||
this.hsvShader = value;
|
||||
|
@ -149,16 +156,17 @@ class SongMenuItem extends FlxSpriteGroup
|
|||
updateSelected();
|
||||
}
|
||||
|
||||
public function init(x:Float, y:Float, songData:Null<FreeplaySongData>)
|
||||
public function init(?x:Float, ?y:Float, songData:Null<FreeplaySongData>)
|
||||
{
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
if (x != null) this.x = x;
|
||||
if (y != null) this.y = y;
|
||||
this.songData = songData;
|
||||
|
||||
// Update capsule text.
|
||||
songText.text = songData?.songName ?? 'Random';
|
||||
// Update capsule character.
|
||||
if (songData?.songCharacter != null) setCharacter(songData.songCharacter);
|
||||
updateDifficultyRating(songData?.songRating ?? 0);
|
||||
// Update opacity, offsets, etc.
|
||||
updateSelected();
|
||||
}
|
||||
|
@ -200,7 +208,14 @@ class SongMenuItem extends FlxSpriteGroup
|
|||
|
||||
pixelIcon.loadGraphic(Paths.image(charPath));
|
||||
pixelIcon.scale.x = pixelIcon.scale.y = 2;
|
||||
pixelIcon.origin.x = 100;
|
||||
|
||||
switch (char)
|
||||
{
|
||||
case "parents-christmas":
|
||||
pixelIcon.origin.x = 140;
|
||||
default:
|
||||
pixelIcon.origin.x = 100;
|
||||
}
|
||||
// pixelIcon.origin.x = capsule.origin.x;
|
||||
// pixelIcon.offset.x -= pixelIcon.origin.x;
|
||||
}
|
||||
|
@ -336,7 +351,7 @@ class SongMenuItem extends FlxSpriteGroup
|
|||
|
||||
function updateSelected():Void
|
||||
{
|
||||
diffGrayscale.setAmount(this.selected ? 0 : 0.8);
|
||||
grayscaleShader.setAmount(this.selected ? 0 : 0.8);
|
||||
songText.alpha = this.selected ? 1 : 0.6;
|
||||
songText.blurredText.visible = this.selected ? true : false;
|
||||
capsule.offset.x = this.selected ? 0 : -5;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue