mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2024-11-14 19:25:16 -05:00
Chart editor basic difficulty switching
This commit is contained in:
parent
8a2e4687b2
commit
36a49affee
25 changed files with 3830 additions and 288 deletions
1203
rfc/chart-format/chartformat-7.jsonc
Normal file
1203
rfc/chart-format/chartformat-7.jsonc
Normal file
File diff suppressed because it is too large
Load diff
|
@ -72,19 +72,20 @@ class Conductor
|
|||
return crochet / 4;
|
||||
}
|
||||
|
||||
public static var currentBeat(get, null):Int;
|
||||
/**
|
||||
* Current position in the song, in beats.
|
||||
**/
|
||||
public static var currentBeat(default, null):Int;
|
||||
|
||||
static function get_currentBeat():Int
|
||||
{
|
||||
return currentBeat;
|
||||
}
|
||||
/**
|
||||
* Current position in the song, in steps.
|
||||
*/
|
||||
public static var currentStep(default, null):Int;
|
||||
|
||||
public static var currentStep(get, null):Int;
|
||||
|
||||
static function get_currentStep():Int
|
||||
{
|
||||
return currentStep;
|
||||
}
|
||||
/**
|
||||
* Current position in the song, in steps and fractions of a step.
|
||||
*/
|
||||
public static var currentStepTime(default, null):Float;
|
||||
|
||||
public static var beatHit(default, null):FlxSignal = new FlxSignal();
|
||||
public static var stepHit(default, null):FlxSignal = new FlxSignal();
|
||||
|
@ -94,6 +95,9 @@ class Conductor
|
|||
public static var audioOffset:Float = 0;
|
||||
public static var offset:Float = 0;
|
||||
|
||||
// TODO: Add code to update this.
|
||||
public static var beatsPerMeasure:Int = 4;
|
||||
|
||||
private function new()
|
||||
{
|
||||
}
|
||||
|
@ -116,7 +120,13 @@ class Conductor
|
|||
return lastChange;
|
||||
}
|
||||
|
||||
@:deprecated // Use loadSong with metadata files instead.
|
||||
/**
|
||||
* Forcibly defines the current BPM of the song.
|
||||
* Useful for things like the chart editor that need to manipulate BPM in real time.
|
||||
*
|
||||
* 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)
|
||||
{
|
||||
Conductor.bpmOverride = bpm;
|
||||
|
@ -156,13 +166,15 @@ class Conductor
|
|||
}
|
||||
else if (currentTimeChange != null)
|
||||
{
|
||||
currentStep = Math.floor((currentTimeChange.beatTime * 4) + (songPosition - currentTimeChange.timeStamp) / stepCrochet);
|
||||
currentStepTime = (currentTimeChange.beatTime * 4) + (songPosition - currentTimeChange.timeStamp) / stepCrochet;
|
||||
currentStep = Math.floor(currentStepTime);
|
||||
currentBeat = Math.floor(currentStep / 4);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Assume a constant BPM equal to the forced value.
|
||||
currentStep = Math.floor((songPosition) / stepCrochet);
|
||||
currentStepTime = (songPosition / stepCrochet);
|
||||
currentStep = Math.floor(currentStepTime);
|
||||
currentBeat = Math.floor(currentStep / 4);
|
||||
}
|
||||
|
||||
|
@ -209,23 +221,6 @@ class Conductor
|
|||
|
||||
for (currentTimeChange in songTimeChanges)
|
||||
{
|
||||
// var prevTimeChange:SongTimeChange = timeChanges.length == 0 ? null : timeChanges[timeChanges.length - 1];
|
||||
|
||||
/*
|
||||
if (prevTimeChange != null)
|
||||
{
|
||||
var deltaTime:Float = currentTimeChange.timeStamp - prevTimeChange.timeStamp;
|
||||
var deltaSteps:Int = Math.round(deltaTime / (60 / prevTimeChange.bpm) * 1000 / 4);
|
||||
|
||||
currentTimeChange.stepTime = prevTimeChange.stepTime + deltaSteps;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We know the time and steps of this time change is 0, since this is the first time change.
|
||||
currentTimeChange.stepTime = 0;
|
||||
}
|
||||
*/
|
||||
|
||||
timeChanges.push(currentTimeChange);
|
||||
}
|
||||
|
||||
|
@ -233,4 +228,39 @@ class Conductor
|
|||
|
||||
// Done.
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a time in milliseconds, return a time in steps.
|
||||
*/
|
||||
public static function getTimeInSteps(ms:Float):Int
|
||||
{
|
||||
if (timeChanges.length == 0)
|
||||
{
|
||||
// Assume a constant BPM equal to the forced value.
|
||||
return Math.floor(ms / stepCrochet);
|
||||
}
|
||||
else
|
||||
{
|
||||
var resultStep:Int = 0;
|
||||
|
||||
var lastTimeChange:SongTimeChange = timeChanges[0];
|
||||
for (timeChange in timeChanges)
|
||||
{
|
||||
if (ms >= timeChange.timeStamp)
|
||||
{
|
||||
lastTimeChange = timeChange;
|
||||
resultStep = lastTimeChange.beatTime * 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
// This time change is after the requested time.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
resultStep += Math.floor((ms - lastTimeChange.timeStamp) / stepCrochet);
|
||||
|
||||
return resultStep;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import flixel.system.FlxSound;
|
|||
import flixel.system.debug.stats.StatsGraph;
|
||||
import flixel.text.FlxText;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.audiovis.PolygonSpectogram;
|
||||
import funkin.audio.visualize.PolygonSpectogram;
|
||||
import funkin.ui.CoolStatsGraph;
|
||||
import haxe.Timer;
|
||||
import openfl.events.KeyboardEvent;
|
||||
|
|
|
@ -76,8 +76,6 @@ class MusicBeatState extends FlxUIState
|
|||
FlxG.state.openSubState(new DebugMenuSubState());
|
||||
}
|
||||
|
||||
// Conductor.update(FlxG.sound.music.time + Conductor.offset);
|
||||
|
||||
FlxG.watch.addQuick("songPos", Conductor.songPosition);
|
||||
|
||||
dispatchEvent(new UpdateScriptEvent(elapsed));
|
||||
|
|
|
@ -153,10 +153,10 @@ class Note extends FlxSprite
|
|||
default:
|
||||
frames = Paths.getSparrowAtlas('NOTE_assets');
|
||||
|
||||
animation.addByPrefix('purpleScroll', 'purple instance');
|
||||
animation.addByPrefix('blueScroll', 'blue instance');
|
||||
animation.addByPrefix('greenScroll', 'green instance');
|
||||
animation.addByPrefix('redScroll', 'red instance');
|
||||
animation.addByPrefix('blueScroll', 'blue instance');
|
||||
animation.addByPrefix('purpleScroll', 'purple instance');
|
||||
|
||||
animation.addByPrefix('purpleholdend', 'pruple end hold');
|
||||
animation.addByPrefix('greenholdend', 'green hold end');
|
||||
|
|
|
@ -587,10 +587,13 @@ class TitleState extends MusicBeatState
|
|||
|
||||
danceLeft = !danceLeft;
|
||||
|
||||
if (danceLeft)
|
||||
gfDance.animation.play('danceRight');
|
||||
else
|
||||
gfDance.animation.play('danceLeft');
|
||||
if (gfDance != null && gfDance.animation != null)
|
||||
{
|
||||
if (danceLeft)
|
||||
gfDance.animation.play('danceRight');
|
||||
else
|
||||
gfDance.animation.play('danceLeft');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package funkin.audiovis;
|
||||
package funkin.audio.visualize;
|
||||
|
||||
import funkin.audiovis.VisShit.CurAudioInfo;
|
||||
import flixel.math.FlxMath;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.system.FlxSound;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.audiovis.VisShit;
|
||||
import funkin.graphics.rendering.MeshRender;
|
||||
import lime.utils.Int16Array;
|
||||
import funkin.rendering.MeshRender;
|
||||
|
||||
class PolygonSpectogram extends MeshRender
|
||||
{
|
||||
|
@ -30,7 +30,7 @@ class PolygonSpectogram extends MeshRender
|
|||
{
|
||||
super(0, 0, col);
|
||||
|
||||
vis = new VisShit(daSound);
|
||||
setSound(daSound);
|
||||
|
||||
if (height != null)
|
||||
this.daHeight = height;
|
||||
|
@ -40,6 +40,11 @@ class PolygonSpectogram extends MeshRender
|
|||
// col not in yet
|
||||
}
|
||||
|
||||
public function setSound(daSound:FlxSound)
|
||||
{
|
||||
vis = new VisShit(daSound);
|
||||
}
|
||||
|
||||
override function update(elapsed:Float)
|
||||
{
|
||||
super.update(elapsed);
|
|
@ -7,7 +7,7 @@ import flixel.math.FlxPoint;
|
|||
import flixel.math.FlxVector;
|
||||
import flixel.system.FlxSound;
|
||||
import flixel.util.FlxColor;
|
||||
import funkin.audiovis.PolygonSpectogram.VISTYPE;
|
||||
import funkin.audio.visualize.PolygonSpectogram.VISTYPE;
|
||||
import funkin.audiovis.VisShit.CurAudioInfo;
|
||||
import funkin.audiovis.dsp.FFT;
|
||||
import haxe.Timer;
|
||||
|
|
|
@ -19,13 +19,13 @@ import flixel.util.FlxColor;
|
|||
import funkin.Conductor.BPMChangeEvent;
|
||||
import funkin.Section.SwagSection;
|
||||
import funkin.SongLoad.SwagSong;
|
||||
import funkin.audio.visualize.PolygonSpectogram;
|
||||
import funkin.audiovis.ABotVis;
|
||||
import funkin.audiovis.PolygonSpectogram;
|
||||
import funkin.audiovis.SpectogramSprite;
|
||||
import funkin.graphics.rendering.MeshRender;
|
||||
import funkin.noteStuff.NoteBasic.NoteData;
|
||||
import funkin.play.HealthIcon;
|
||||
import funkin.play.PlayState;
|
||||
import funkin.rendering.MeshRender;
|
||||
import haxe.Json;
|
||||
import lime.media.AudioBuffer;
|
||||
import lime.utils.Assets;
|
||||
|
@ -1021,7 +1021,7 @@ class ChartingState extends MusicBeatState
|
|||
}
|
||||
}
|
||||
|
||||
function recalculateSteps():Int
|
||||
function recalculateSteps():Float
|
||||
{
|
||||
var lastChange:BPMChangeEvent = {
|
||||
stepTime: 0,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package funkin.rendering;
|
||||
package funkin.graphics.rendering;
|
||||
|
||||
import flixel.FlxStrip;
|
||||
import flixel.util.FlxColor;
|
238
source/funkin/graphics/rendering/SustainTrail.hx
Normal file
238
source/funkin/graphics/rendering/SustainTrail.hx
Normal file
|
@ -0,0 +1,238 @@
|
|||
package funkin.graphics.rendering;
|
||||
|
||||
import flixel.FlxSprite;
|
||||
import flixel.graphics.FlxGraphic;
|
||||
import flixel.graphics.tile.FlxDrawTrianglesItem;
|
||||
import flixel.math.FlxMath;
|
||||
|
||||
/**
|
||||
* This is based heavily on the `FlxStrip` class. It uses `drawTriangles()` to clip a sustain note
|
||||
* trail at a certain time.
|
||||
* The whole `FlxGraphic` is used as a texture map. See the `NOTE_hold_assets.fla` file for specifics
|
||||
* on how it should be constructed.
|
||||
*
|
||||
* @author MtH
|
||||
*/
|
||||
class SustainTrail extends FlxSprite
|
||||
{
|
||||
/**
|
||||
* Used to determine which note color/direction to draw for the sustain.
|
||||
*/
|
||||
public var noteData:Int = 0;
|
||||
|
||||
/**
|
||||
* The zoom level to render the sustain at.
|
||||
* Defaults to 1.0, increased to 6.0 for pixel notes.
|
||||
*/
|
||||
public var zoom(default, set):Float = 1;
|
||||
|
||||
/**
|
||||
* The strumtime of the note, in milliseconds.
|
||||
*/
|
||||
public var strumTime:Float = 0; // millis
|
||||
|
||||
/**
|
||||
* The sustain length of the note, in milliseconds.
|
||||
*/
|
||||
public var sustainLength(default, set):Float = 0; // millis
|
||||
|
||||
/**
|
||||
* The scroll speed of the note, as a multiplier.
|
||||
*/
|
||||
public var scrollSpeed(default, set):Float = 1.0; // stand-in for PlayState scroll speed
|
||||
|
||||
/**
|
||||
* Whether the note was missed.
|
||||
*/
|
||||
public var missed:Bool = false; // maybe BlendMode.MULTIPLY if missed somehow, drawTriangles does not support!
|
||||
|
||||
/**
|
||||
* A `Vector` of floats where each pair of numbers is treated as a coordinate location (an x, y pair).
|
||||
*/
|
||||
private var vertices:DrawData<Float> = new DrawData<Float>();
|
||||
|
||||
/**
|
||||
* A `Vector` of integers or indexes, where every three indexes define a triangle.
|
||||
*/
|
||||
private var indices:DrawData<Int> = new DrawData<Int>();
|
||||
|
||||
/**
|
||||
* A `Vector` of normalized coordinates used to apply texture mapping.
|
||||
*/
|
||||
private var uvtData:DrawData<Float> = new DrawData<Float>();
|
||||
|
||||
private var processedGraphic:FlxGraphic;
|
||||
|
||||
/**
|
||||
* What part of the trail's end actually represents the end of the note.
|
||||
* This can be used to have a little bit sticking out.
|
||||
*/
|
||||
public var endOffset:Float = 0.5; // 0.73 is roughly the bottom of the sprite in the normal graphic!
|
||||
|
||||
/**
|
||||
* At what point the bottom for the trail's end should be clipped off.
|
||||
* Used in cases where there's an extra bit of the graphic on the bottom to avoid antialiasing issues with overflow.
|
||||
*/
|
||||
public var bottomClip:Float = 0.9;
|
||||
|
||||
/**
|
||||
* Normally you would take strumTime:Float, noteData:Int, sustainLength:Float, parentNote:Note (?)
|
||||
* @param NoteData
|
||||
* @param SustainLength
|
||||
* @param FileName
|
||||
*/
|
||||
public function new(NoteData:Int, SustainLength:Float, Path:String, ?Alpha:Float = 0.6, ?Pixel:Bool = false)
|
||||
{
|
||||
super(0, 0, Path);
|
||||
|
||||
// BASIC SETUP
|
||||
this.sustainLength = SustainLength;
|
||||
this.noteData = NoteData;
|
||||
|
||||
// CALCULATE SIZE
|
||||
if (Pixel)
|
||||
{
|
||||
this.endOffset = bottomClip = 1;
|
||||
this.antialiasing = false;
|
||||
this.zoom = 6.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.antialiasing = true;
|
||||
this.zoom = 1.0;
|
||||
}
|
||||
// width = graphic.width / 8 * zoom; // amount of notes * 2
|
||||
height = sustainHeight(sustainLength, scrollSpeed);
|
||||
// instead of scrollSpeed, PlayState.SONG.speed
|
||||
|
||||
alpha = Alpha; // setting alpha calls updateColorTransform(), which initializes processedGraphic!
|
||||
|
||||
updateClipping();
|
||||
indices = new DrawData<Int>(12, true, [0, 1, 2, 2, 3, 0, 4, 5, 6, 6, 7, 4]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates height of a sustain note for a given length (milliseconds) and scroll speed.
|
||||
* @param susLength The length of the sustain note in milliseconds.
|
||||
* @param scroll The current scroll speed.
|
||||
*/
|
||||
public static inline function sustainHeight(susLength:Float, scroll:Float)
|
||||
{
|
||||
return (susLength * 0.45 * scroll);
|
||||
}
|
||||
|
||||
function set_zoom(z:Float)
|
||||
{
|
||||
this.zoom = z;
|
||||
width = graphic.width / 8 * z;
|
||||
updateClipping();
|
||||
return this.zoom;
|
||||
}
|
||||
|
||||
function set_sustainLength(s:Float)
|
||||
{
|
||||
height = sustainHeight(s, scrollSpeed);
|
||||
return sustainLength = s;
|
||||
}
|
||||
|
||||
function set_scrollSpeed(s:Float)
|
||||
{
|
||||
height = sustainHeight(sustainLength, s);
|
||||
return scrollSpeed = s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up new vertex and UV data to clip the trail.
|
||||
* If flipY is true, top and bottom bounds swap places.
|
||||
* @param songTime The time to clip the note at, in milliseconds.
|
||||
*/
|
||||
public function updateClipping(songTime:Float = 0):Void
|
||||
{
|
||||
var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), scrollSpeed), 0, height);
|
||||
if (clipHeight == 0)
|
||||
{
|
||||
visible = false;
|
||||
return;
|
||||
}
|
||||
else
|
||||
visible = true;
|
||||
var bottomHeight:Float = graphic.height * zoom * endOffset;
|
||||
var partHeight:Float = clipHeight - bottomHeight;
|
||||
// == HOLD == //
|
||||
// left bound
|
||||
vertices[6] = vertices[0] = 0.0;
|
||||
// top bound
|
||||
vertices[3] = vertices[1] = flipY ? clipHeight : height - clipHeight;
|
||||
// right bound
|
||||
vertices[4] = vertices[2] = width;
|
||||
// bottom bound (also top bound for hold ends)
|
||||
if (partHeight > 0)
|
||||
vertices[7] = vertices[5] = flipY ? 0.0 + bottomHeight : vertices[1] + partHeight;
|
||||
else
|
||||
vertices[7] = vertices[5] = vertices[1];
|
||||
|
||||
// same shit with da bounds, just in relation to the texture
|
||||
uvtData[6] = uvtData[0] = 1 / 4 * (noteData % 4);
|
||||
// height overflows past image bounds so wraps around, looping the texture
|
||||
// flipY bounds are not swapped for UV data, so the graphic is actually flipped
|
||||
// top bound
|
||||
uvtData[3] = uvtData[1] = (-partHeight) / graphic.height / zoom;
|
||||
uvtData[4] = uvtData[2] = uvtData[0] + 1 / 8; // 1
|
||||
// bottom bound
|
||||
uvtData[7] = uvtData[5] = 0.0;
|
||||
|
||||
// == HOLD ENDS == //
|
||||
// left bound
|
||||
vertices[14] = vertices[8] = vertices[0];
|
||||
// top bound
|
||||
vertices[11] = vertices[9] = vertices[5];
|
||||
// right bound
|
||||
vertices[12] = vertices[10] = vertices[2];
|
||||
// bottom bound, mind the bottomClip because it clips off bottom of graphic!!
|
||||
vertices[15] = vertices[13] = flipY ? graphic.height * (-bottomClip + endOffset) : height + graphic.height * (bottomClip - endOffset);
|
||||
|
||||
uvtData[14] = uvtData[8] = uvtData[2];
|
||||
if (partHeight > 0)
|
||||
uvtData[11] = uvtData[9] = 0.0;
|
||||
else
|
||||
uvtData[11] = uvtData[9] = (bottomHeight - clipHeight) / zoom / graphic.height;
|
||||
uvtData[12] = uvtData[10] = uvtData[8] + 1 / 8;
|
||||
// again, clips off bottom !!
|
||||
uvtData[15] = uvtData[13] = bottomClip;
|
||||
}
|
||||
|
||||
@:access(flixel.FlxCamera)
|
||||
override public function draw():Void
|
||||
{
|
||||
if (alpha == 0 || graphic == null || vertices == null)
|
||||
return;
|
||||
|
||||
for (camera in cameras)
|
||||
{
|
||||
if (!camera.visible || !camera.exists || !isOnScreen(camera))
|
||||
continue;
|
||||
|
||||
getScreenPosition(_point, camera).subtractPoint(offset);
|
||||
camera.drawTriangles(processedGraphic, vertices, indices, uvtData, null, _point, blend, true, antialiasing);
|
||||
}
|
||||
}
|
||||
|
||||
override public function destroy():Void
|
||||
{
|
||||
vertices = null;
|
||||
indices = null;
|
||||
uvtData = null;
|
||||
processedGraphic.destroy();
|
||||
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
override function updateColorTransform():Void
|
||||
{
|
||||
super.updateColorTransform();
|
||||
if (processedGraphic != null)
|
||||
processedGraphic.destroy();
|
||||
processedGraphic = FlxGraphic.fromGraphic(graphic, true);
|
||||
processedGraphic.bitmap.colorTransform(processedGraphic.bitmap.rect, colorTransform);
|
||||
}
|
||||
}
|
|
@ -23,11 +23,21 @@ class PolymodHandler
|
|||
*/
|
||||
static final MOD_FOLDER = "mods";
|
||||
|
||||
public static function createModRoot()
|
||||
{
|
||||
if (!sys.FileSystem.exists(MOD_FOLDER))
|
||||
{
|
||||
sys.FileSystem.createDirectory(MOD_FOLDER);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the game with ALL mods enabled with Polymod.
|
||||
*/
|
||||
public static function loadAllMods()
|
||||
{
|
||||
// Create the mod root if it doesn't exist.
|
||||
createModRoot();
|
||||
trace("Initializing Polymod (using all mods)...");
|
||||
loadModsById(getAllModIds());
|
||||
}
|
||||
|
@ -37,6 +47,9 @@ class PolymodHandler
|
|||
*/
|
||||
public static function loadEnabledMods()
|
||||
{
|
||||
// Create the mod root if it doesn't exist.
|
||||
createModRoot();
|
||||
|
||||
trace("Initializing Polymod (using configured mods)...");
|
||||
loadModsById(getEnabledModIds());
|
||||
}
|
||||
|
@ -46,6 +59,9 @@ class PolymodHandler
|
|||
*/
|
||||
public static function loadNoMods()
|
||||
{
|
||||
// Create the mod root if it doesn't exist.
|
||||
createModRoot();
|
||||
|
||||
// We still need to configure the debug print calls etc.
|
||||
trace("Initializing Polymod (using no mods)...");
|
||||
loadModsById([]);
|
||||
|
|
|
@ -43,7 +43,7 @@ class HealthIcon extends FlxSprite
|
|||
/**
|
||||
* Since the `scale` of the sprite dynamically changes over time,
|
||||
* this value allows you to set a relative scale for the icon.
|
||||
* @default 1x scale
|
||||
* @default 1x scale = 150px width and height.
|
||||
*/
|
||||
public var size:FlxPoint = new FlxPoint(1, 1);
|
||||
|
||||
|
@ -87,13 +87,13 @@ class HealthIcon extends FlxSprite
|
|||
* The size of a non-pixel icon when using the legacy format.
|
||||
* Remember, modern icons can be any size.
|
||||
*/
|
||||
static final LEGACY_ICON_SIZE = 150;
|
||||
public static final HEALTH_ICON_SIZE = 150;
|
||||
|
||||
/**
|
||||
* The size of a pixel icon when using the legacy format.
|
||||
* Remember, modern icons can be any size.
|
||||
*/
|
||||
static final LEGACY_PIXEL_SIZE = 32;
|
||||
static final PIXEL_ICON_SIZE = 32;
|
||||
|
||||
/**
|
||||
* shitty hardcoded value for a specific positioning!!!
|
||||
|
@ -145,11 +145,9 @@ class HealthIcon extends FlxSprite
|
|||
{
|
||||
super.update(elapsed);
|
||||
|
||||
if (PlayState.instance == null)
|
||||
return;
|
||||
|
||||
// Auto-update the state of the icon based on the player's health.
|
||||
if (autoUpdate)
|
||||
// Make sure this is false if the health icon is not being used in the PlayState.
|
||||
if (autoUpdate && PlayState.instance != null)
|
||||
{
|
||||
switch (playerId)
|
||||
{
|
||||
|
@ -168,19 +166,22 @@ class HealthIcon extends FlxSprite
|
|||
+ (PlayState.instance.healthBar.width * (FlxMath.remapToRange(PlayState.instance.healthBar.value, 0, 2, 100, 0) * 0.01))
|
||||
- (this.width - POSITION_OFFSET);
|
||||
}
|
||||
}
|
||||
|
||||
if (bumpEvery != 0)
|
||||
{
|
||||
// Lerp the health icon back to its normal size,
|
||||
// while maintaining aspect ratio.
|
||||
if (this.width > this.height)
|
||||
{
|
||||
// Apply linear interpolation while accounting for frame rate.
|
||||
var targetSize = Std.int(CoolUtil.coolLerp(this.width, 150 * this.size.x, 0.15));
|
||||
var targetSize = Std.int(CoolUtil.coolLerp(this.width, HEALTH_ICON_SIZE * this.size.x, 0.15));
|
||||
|
||||
setGraphicSize(targetSize, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
var targetSize = Std.int(CoolUtil.coolLerp(this.height, 150 * this.size.y, 0.15));
|
||||
var targetSize = Std.int(CoolUtil.coolLerp(this.height, HEALTH_ICON_SIZE * this.size.y, 0.15));
|
||||
|
||||
setGraphicSize(0, targetSize);
|
||||
}
|
||||
|
@ -190,18 +191,20 @@ class HealthIcon extends FlxSprite
|
|||
|
||||
public function onStepHit(curStep:Int)
|
||||
{
|
||||
if (curStep % bumpEvery == 0 && isLegacyStyle)
|
||||
if (bumpEvery != 0 && curStep % bumpEvery == 0 && isLegacyStyle)
|
||||
{
|
||||
// Make the health icons bump (the update function causes them to lerp back down).
|
||||
if (this.width > this.height)
|
||||
{
|
||||
var targetSize = Std.int(CoolUtil.coolLerp(this.width + 30, 150, 0.15));
|
||||
var targetSize = Std.int(CoolUtil.coolLerp(this.width + HEALTH_ICON_SIZE * 0.2, HEALTH_ICON_SIZE, 0.15));
|
||||
targetSize = Std.int(Math.min(targetSize, HEALTH_ICON_SIZE * 1.2));
|
||||
|
||||
setGraphicSize(targetSize, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
var targetSize = Std.int(CoolUtil.coolLerp(this.height + 30, 150, 0.15));
|
||||
var targetSize = Std.int(CoolUtil.coolLerp(this.height + HEALTH_ICON_SIZE * 0.2, HEALTH_ICON_SIZE, 0.15));
|
||||
targetSize = Std.int(Math.min(targetSize, HEALTH_ICON_SIZE * 1.2));
|
||||
|
||||
setGraphicSize(0, targetSize);
|
||||
}
|
||||
|
@ -211,7 +214,7 @@ class HealthIcon extends FlxSprite
|
|||
|
||||
inline function initTargetSize()
|
||||
{
|
||||
setGraphicSize(150);
|
||||
setGraphicSize(HEALTH_ICON_SIZE);
|
||||
updateHitbox();
|
||||
}
|
||||
|
||||
|
@ -330,8 +333,7 @@ class HealthIcon extends FlxSprite
|
|||
}
|
||||
else
|
||||
{
|
||||
loadGraphic(Paths.image('icons/icon-$charId'), true, isPixel ? LEGACY_PIXEL_SIZE : LEGACY_ICON_SIZE,
|
||||
isPixel ? LEGACY_PIXEL_SIZE : LEGACY_ICON_SIZE);
|
||||
loadGraphic(Paths.image('icons/icon-$charId'), true, isPixel ? PIXEL_ICON_SIZE : HEALTH_ICON_SIZE, isPixel ? PIXEL_ICON_SIZE : HEALTH_ICON_SIZE);
|
||||
|
||||
loadAnimationOld(charId);
|
||||
}
|
||||
|
|
|
@ -1340,10 +1340,11 @@ class PlayState extends MusicBeatState
|
|||
|
||||
function resyncVocals():Void
|
||||
{
|
||||
if (_exiting)
|
||||
if (_exiting || vocals == null)
|
||||
return;
|
||||
|
||||
vocals.pause();
|
||||
|
||||
FlxG.sound.music.play();
|
||||
Conductor.update(FlxG.sound.music.time + Conductor.offset);
|
||||
|
||||
|
@ -2226,8 +2227,10 @@ class PlayState extends MusicBeatState
|
|||
resyncVocals();
|
||||
}
|
||||
|
||||
iconP1.onStepHit(Conductor.currentStep);
|
||||
iconP2.onStepHit(Conductor.currentStep);
|
||||
if (iconP1 != null)
|
||||
iconP1.onStepHit(Std.int(Conductor.currentStep));
|
||||
if (iconP2 != null)
|
||||
iconP2.onStepHit(Std.int(Conductor.currentStep));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,9 @@ import funkin.util.assets.FlxAnimationUtil;
|
|||
*
|
||||
* BaseCharacter has game logic, SparrowCharacter 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
|
||||
{
|
||||
|
|
|
@ -2,6 +2,7 @@ package funkin.play.event;
|
|||
|
||||
import flixel.FlxSprite;
|
||||
import funkin.play.PlayState;
|
||||
import funkin.play.character.BaseCharacter;
|
||||
import funkin.play.song.SongData.RawSongEventData;
|
||||
import haxe.DynamicAccess;
|
||||
|
||||
|
@ -260,12 +261,24 @@ class VanillaEventCallbacks
|
|||
case 'boyfriend':
|
||||
trace('[EVENT] Playing animation $anim on boyfriend.');
|
||||
target = PlayState.instance.currentStage.getBoyfriend();
|
||||
case 'bf':
|
||||
trace('[EVENT] Playing animation $anim on boyfriend.');
|
||||
target = PlayState.instance.currentStage.getBoyfriend();
|
||||
case 'player':
|
||||
trace('[EVENT] Playing animation $anim on boyfriend.');
|
||||
target = PlayState.instance.currentStage.getBoyfriend();
|
||||
case 'dad':
|
||||
trace('[EVENT] Playing animation $anim on dad.');
|
||||
target = PlayState.instance.currentStage.getDad();
|
||||
case 'opponent':
|
||||
trace('[EVENT] Playing animation $anim on dad.');
|
||||
target = PlayState.instance.currentStage.getDad();
|
||||
case 'girlfriend':
|
||||
trace('[EVENT] Playing animation $anim on girlfriend.');
|
||||
target = PlayState.instance.currentStage.getGirlfriend();
|
||||
case 'gf':
|
||||
trace('[EVENT] Playing animation $anim on girlfriend.');
|
||||
target = PlayState.instance.currentStage.getGirlfriend();
|
||||
default:
|
||||
target = PlayState.instance.currentStage.getNamedProp(targetName);
|
||||
if (target == null)
|
||||
|
@ -276,7 +289,15 @@ class VanillaEventCallbacks
|
|||
|
||||
if (target != null)
|
||||
{
|
||||
target.animation.play(anim, force);
|
||||
if (Std.isOfType(target, BaseCharacter))
|
||||
{
|
||||
var targetChar:BaseCharacter = cast target;
|
||||
targetChar.playAnimation(anim, force, force);
|
||||
}
|
||||
else
|
||||
{
|
||||
target.animation.play(anim, force);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -213,7 +213,7 @@ class SongDataParser
|
|||
}
|
||||
}
|
||||
|
||||
typedef SongMetadata =
|
||||
typedef RawSongMetadata =
|
||||
{
|
||||
/**
|
||||
* A semantic versioning string for the song data format.
|
||||
|
@ -231,11 +231,41 @@ typedef SongMetadata =
|
|||
var generatedBy:String;
|
||||
|
||||
/**
|
||||
* Defaults to ''. Populated later.
|
||||
* Defaults to `default` or `''`. Populated later.
|
||||
*/
|
||||
var variation:String;
|
||||
};
|
||||
|
||||
@:forward
|
||||
abstract SongMetadata(RawSongMetadata)
|
||||
{
|
||||
public function new(songName:String, artist:String, variation:String = 'default')
|
||||
{
|
||||
this = {
|
||||
version: SongMigrator.CHART_VERSION,
|
||||
songName: songName,
|
||||
artist: artist,
|
||||
timeFormat: 'ms',
|
||||
divisions: 96,
|
||||
timeChanges: [new SongTimeChange(-1, 0, 100, 4, 4, [4, 4, 4, 4])],
|
||||
loop: false,
|
||||
playData: {
|
||||
songVariations: [],
|
||||
difficulties: ['normal'],
|
||||
|
||||
playableChars: {
|
||||
bf: new SongPlayableChar('gf', 'dad'),
|
||||
},
|
||||
|
||||
stage: 'mainStage',
|
||||
noteSkin: 'Normal'
|
||||
},
|
||||
generatedBy: SongValidator.DEFAULT_GENERATEDBY,
|
||||
variation: variation
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
typedef SongPlayData =
|
||||
{
|
||||
var songVariations:Array<String>;
|
||||
|
@ -310,6 +340,14 @@ abstract SongNoteData(RawSongNoteData)
|
|||
return this.t = value;
|
||||
}
|
||||
|
||||
public var stepTime(get, never):Float;
|
||||
|
||||
public function get_stepTime():Float
|
||||
{
|
||||
// TODO: Account for changes in BPM.
|
||||
return this.t / Conductor.stepCrochet;
|
||||
}
|
||||
|
||||
/**
|
||||
* The raw data for the note.
|
||||
*/
|
||||
|
@ -336,6 +374,23 @@ abstract SongNoteData(RawSongNoteData)
|
|||
return this.d % strumlineSize;
|
||||
}
|
||||
|
||||
public function getDirectionName(strumlineSize:Int = 4):String
|
||||
{
|
||||
switch (this.d % strumlineSize)
|
||||
{
|
||||
case 0:
|
||||
return 'Left';
|
||||
case 1:
|
||||
return 'Down';
|
||||
case 2:
|
||||
return 'Up';
|
||||
case 3:
|
||||
return 'Right';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The strumline index of the note, if applicable.
|
||||
* Strips the direction from the data.
|
||||
|
@ -543,14 +598,18 @@ typedef RawSongChartData =
|
|||
@:forward
|
||||
abstract SongChartData(RawSongChartData)
|
||||
{
|
||||
public function new(scrollSpeed:DynamicAccess<Float>, events:Array<SongEventData>, notes:DynamicAccess<Array<SongNoteData>>)
|
||||
public function new(scrollSpeed:Float, events:Array<SongEventData>, notes:Array<SongNoteData>)
|
||||
{
|
||||
this = {
|
||||
version: SongMigrator.CHART_VERSION,
|
||||
|
||||
events: events,
|
||||
notes: notes,
|
||||
scrollSpeed: scrollSpeed,
|
||||
notes: {
|
||||
normal: notes
|
||||
},
|
||||
scrollSpeed: {
|
||||
normal: scrollSpeed
|
||||
},
|
||||
generatedBy: SongValidator.DEFAULT_GENERATEDBY
|
||||
}
|
||||
}
|
||||
|
|
216
source/funkin/play/song/SongSerializer.hx
Normal file
216
source/funkin/play/song/SongSerializer.hx
Normal file
|
@ -0,0 +1,216 @@
|
|||
package funkin.play.song;
|
||||
|
||||
import funkin.play.song.SongData.SongChartData;
|
||||
import funkin.play.song.SongData.SongMetadata;
|
||||
import funkin.util.SerializerUtil;
|
||||
import lime.utils.Bytes;
|
||||
import openfl.events.Event;
|
||||
import openfl.events.IOErrorEvent;
|
||||
import openfl.net.FileReference;
|
||||
|
||||
/**
|
||||
* Utilities for exporting a chart to a JSON file.
|
||||
* Primarily used for the chart editor.
|
||||
*/
|
||||
class SongSerializer
|
||||
{
|
||||
/**
|
||||
* Access a SongChartData JSON file from a specific path, then load it.
|
||||
* @param path The file path to read from.
|
||||
*/
|
||||
public static function importSongChartDataSync(path:String):SongChartData
|
||||
{
|
||||
var fileData = readFile(path);
|
||||
|
||||
if (fileData == null)
|
||||
return null;
|
||||
|
||||
var songChartData:SongChartData = SerializerUtil.fromJSON(fileData);
|
||||
|
||||
return songChartData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Access a SongMetadata JSON file from a specific path, then load it.
|
||||
* @param path The file path to read from.
|
||||
*/
|
||||
public static function importSongMetadataSync(path:String):SongMetadata
|
||||
{
|
||||
var fileData = readFile(path);
|
||||
|
||||
if (fileData == null)
|
||||
return null;
|
||||
|
||||
var songMetadata:SongMetadata = SerializerUtil.fromJSON(fileData);
|
||||
|
||||
return songMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user to browse for a SongChartData JSON file path, then load it.
|
||||
* @param callback The function to call when the file is loaded.
|
||||
*/
|
||||
public static function importSongChartDataAsync(callback:SongChartData->Void):Void
|
||||
{
|
||||
browseFileReference(function(fileReference:FileReference)
|
||||
{
|
||||
var data = fileReference.data.toString();
|
||||
|
||||
if (data == null)
|
||||
return;
|
||||
|
||||
var songChartData:SongChartData = SerializerUtil.fromJSON(data);
|
||||
|
||||
if (songChartData != null)
|
||||
callback(songChartData);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user to browse for a SongMetadata JSON file path, then load it.
|
||||
* @param callback The function to call when the file is loaded.
|
||||
*/
|
||||
public static function importSongMetadataAsync(callback:SongMetadata->Void):Void
|
||||
{
|
||||
browseFileReference(function(fileReference:FileReference)
|
||||
{
|
||||
var data = fileReference.data.toString();
|
||||
|
||||
if (data == null)
|
||||
return;
|
||||
|
||||
var songMetadata:SongMetadata = SerializerUtil.fromJSON(data);
|
||||
|
||||
if (songMetadata != null)
|
||||
callback(songMetadata);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a SongChartData object as a JSON file to an automatically generated path.
|
||||
* Works great on HTML5 and desktop.
|
||||
*/
|
||||
public static function exportSongChartData(data:SongChartData)
|
||||
{
|
||||
var path = 'chart.json';
|
||||
exportSongChartDataAs(path, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a SongMetadata object as a JSON file to an automatically generated path.
|
||||
* Works great on HTML5 and desktop.
|
||||
*/
|
||||
public static function exportSongMetadata(data:SongMetadata)
|
||||
{
|
||||
var path = 'metadata.json';
|
||||
exportSongMetadataAs(path, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a SongChartData object as a JSON file to a specified path.
|
||||
* Works great on HTML5 and desktop.
|
||||
*
|
||||
* @param path The file path to save to.
|
||||
*/
|
||||
public static function exportSongChartDataAs(path:String, data:SongChartData)
|
||||
{
|
||||
var dataString = SerializerUtil.toJSON(data);
|
||||
|
||||
writeFileReference(path, dataString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a SongMetadata object as a JSON file to a specified path.
|
||||
* Works great on HTML5 and desktop.
|
||||
*
|
||||
* @param path The file path to save to.
|
||||
*/
|
||||
public static function exportSongMetadataAs(path:String, data:SongMetadata)
|
||||
{
|
||||
var dataString = SerializerUtil.toJSON(data);
|
||||
|
||||
writeFileReference(path, dataString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the string contents of a file.
|
||||
* Only works on desktop platforms.
|
||||
* @param path The file path to read from.
|
||||
*/
|
||||
static function readFile(path:String):String
|
||||
{
|
||||
#if sys
|
||||
var fileBytes:Bytes = sys.io.File.getBytes(path);
|
||||
|
||||
if (fileBytes == null)
|
||||
return null;
|
||||
|
||||
return fileBytes.toString();
|
||||
#end
|
||||
|
||||
trace('ERROR: readFile not implemented for this platform');
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write string contents to a file.
|
||||
* Only works on desktop platforms.
|
||||
* @param path The file path to read from.
|
||||
*/
|
||||
static function writeFile(path:String, data:String):Void
|
||||
{
|
||||
#if sys
|
||||
sys.io.File.saveContent(path, data);
|
||||
return;
|
||||
#end
|
||||
trace('ERROR: writeFile not implemented for this platform');
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Browse for a file to read and execute a callback once we have a file reference.
|
||||
* Works great on HTML5 or desktop.
|
||||
*
|
||||
* @param callback The function to call when the file is loaded.
|
||||
*/
|
||||
static function browseFileReference(callback:FileReference->Void)
|
||||
{
|
||||
var file = new FileReference();
|
||||
|
||||
file.addEventListener(Event.SELECT, function(e)
|
||||
{
|
||||
var selectedFileRef:FileReference = e.target;
|
||||
trace('Selected file: ' + selectedFileRef.name);
|
||||
selectedFileRef.addEventListener(Event.COMPLETE, function(e)
|
||||
{
|
||||
var loadedFileRef:FileReference = e.target;
|
||||
trace('Loaded file: ' + loadedFileRef.name);
|
||||
callback(loadedFileRef);
|
||||
});
|
||||
selectedFileRef.load();
|
||||
});
|
||||
|
||||
file.browse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the user to save a file to their computer.
|
||||
*/
|
||||
static function writeFileReference(path:String, data:String)
|
||||
{
|
||||
var file = new FileReference();
|
||||
file.addEventListener(Event.COMPLETE, function(e:Event)
|
||||
{
|
||||
trace('Successfully wrote file.');
|
||||
});
|
||||
file.addEventListener(Event.CANCEL, function(e:Event)
|
||||
{
|
||||
trace('Cancelled writing file.');
|
||||
});
|
||||
file.addEventListener(IOErrorEvent.IO_ERROR, function(e:IOErrorEvent)
|
||||
{
|
||||
trace('IO error writing file.');
|
||||
});
|
||||
file.save(data, path);
|
||||
}
|
||||
}
|
137
source/funkin/ui/debug/charting/ChartEditorCommand.hx
Normal file
137
source/funkin/ui/debug/charting/ChartEditorCommand.hx
Normal file
|
@ -0,0 +1,137 @@
|
|||
package funkin.ui.debug.charting;
|
||||
|
||||
import funkin.play.song.SongData.SongNoteData;
|
||||
|
||||
/**
|
||||
* Actions in the chart editor are backed by the Command pattern
|
||||
* (see Bob Nystrom's book "Game Programming Patterns" for more info)
|
||||
*
|
||||
* To make a function compatible with the undo/redo history, create a new class
|
||||
* that implements ChartEditorCommand, then call `ChartEditorState.performCommand(new Command())`
|
||||
*/
|
||||
interface ChartEditorCommand
|
||||
{
|
||||
/**
|
||||
* Calling this function should perform the action that this command represents.
|
||||
* @param state The ChartEditorState to perform the action on.
|
||||
*/
|
||||
public function execute(state:ChartEditorState):Void;
|
||||
|
||||
/**
|
||||
* Calling this function should perform the inverse of the action that this command represents,
|
||||
* effectively undoing the action.
|
||||
* @param state The ChartEditorState to undo the action on.
|
||||
*/
|
||||
public function undo(state:ChartEditorState):Void;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
public function toString():String;
|
||||
}
|
||||
|
||||
class AddNoteCommand implements ChartEditorCommand
|
||||
{
|
||||
private var note:SongNoteData;
|
||||
|
||||
public function new(note:SongNoteData)
|
||||
{
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentSongChartNoteData.push(note);
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
|
||||
state.noteDisplayDirty = true;
|
||||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentSongChartNoteData.remove(note);
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
|
||||
state.noteDisplayDirty = true;
|
||||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var dir:String = note.getDirectionName();
|
||||
|
||||
return 'Add $dir Note';
|
||||
}
|
||||
}
|
||||
|
||||
class RemoveNoteCommand implements ChartEditorCommand
|
||||
{
|
||||
private var note:SongNoteData;
|
||||
|
||||
public function new(note:SongNoteData)
|
||||
{
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentSongChartNoteData.remove(note);
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
|
||||
state.noteDisplayDirty = true;
|
||||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
{
|
||||
state.currentSongChartNoteData.push(note);
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
|
||||
state.noteDisplayDirty = true;
|
||||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
var dir:String = note.getDirectionName();
|
||||
|
||||
return 'Remove $dir Note';
|
||||
}
|
||||
}
|
||||
|
||||
class SwitchDifficultyCommand implements ChartEditorCommand
|
||||
{
|
||||
private var prevDifficulty:String;
|
||||
private var newDifficulty:String;
|
||||
private var prevVariation:String;
|
||||
private var newVariation:String;
|
||||
|
||||
public function new(prevDifficulty:String, newDifficulty:String, prevVariation:String, newVariation:String)
|
||||
{
|
||||
this.prevDifficulty = prevDifficulty;
|
||||
this.newDifficulty = newDifficulty;
|
||||
this.prevVariation = prevVariation;
|
||||
this.newVariation = newVariation;
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
state.selectedVariation = newVariation != null ? newVariation : prevVariation;
|
||||
state.selectedDifficulty = newDifficulty != null ? newDifficulty : prevDifficulty;
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
{
|
||||
state.selectedVariation = prevVariation != null ? prevVariation : newVariation;
|
||||
state.selectedDifficulty = prevDifficulty != null ? prevDifficulty : newDifficulty;
|
||||
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
return 'Switch Difficulty';
|
||||
}
|
||||
}
|
173
source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx
Normal file
173
source/funkin/ui/debug/charting/ChartEditorNoteSprite.hx
Normal file
|
@ -0,0 +1,173 @@
|
|||
package funkin.ui.debug.charting;
|
||||
|
||||
import flixel.FlxSprite;
|
||||
import flixel.graphics.frames.FlxFramesCollection;
|
||||
import flixel.graphics.frames.FlxTileFrames;
|
||||
import flixel.math.FlxPoint;
|
||||
import funkin.play.song.SongData.SongNoteData;
|
||||
|
||||
/**
|
||||
* A note sprite that can be used to display a note in a chart.
|
||||
* Designed to be used and reused efficiently. Has no gameplay functionality.
|
||||
*/
|
||||
class ChartEditorNoteSprite extends FlxSprite
|
||||
{
|
||||
/**
|
||||
* The note data that this sprite represents.
|
||||
* You can set this to null to kill the sprite and flag it for recycling.
|
||||
*/
|
||||
public var noteData(default, set):SongNoteData;
|
||||
|
||||
/**
|
||||
* The note skin that this sprite displays.
|
||||
*/
|
||||
public var noteSkin(default, set):String = 'Normal';
|
||||
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
|
||||
if (noteFrameCollection == null)
|
||||
{
|
||||
initFrameCollection();
|
||||
}
|
||||
|
||||
this.frames = noteFrameCollection;
|
||||
|
||||
// Initialize all the animations, not just the one we're going to use immediately,
|
||||
// so that later we can reuse the sprite without having to initialize more animations during scrolling.
|
||||
this.animation.addByPrefix('tapLeftNormal', 'purple instance');
|
||||
this.animation.addByPrefix('tapDownNormal', 'blue instance');
|
||||
this.animation.addByPrefix('tapUpNormal', 'green instance');
|
||||
this.animation.addByPrefix('tapRightNormal', 'red instance');
|
||||
|
||||
this.animation.addByPrefix('holdLeftNormal', 'purple hold piece instance');
|
||||
this.animation.addByPrefix('holdDownNormal', 'blue hold piece instance');
|
||||
this.animation.addByPrefix('holdUpNormal', 'green hold piece instance');
|
||||
this.animation.addByPrefix('holdRightNormal', 'red hold piece instance');
|
||||
|
||||
this.animation.addByPrefix('holdEndLeftNormal', 'pruple end hold instance');
|
||||
this.animation.addByPrefix('holdEndDownNormal', 'blue end hold instance');
|
||||
this.animation.addByPrefix('holdEndUpNormal', 'green end hold instance');
|
||||
this.animation.addByPrefix('holdEndRightNormal', 'red end hold instance');
|
||||
|
||||
this.animation.addByPrefix('tapLeftPixel', 'pixel4');
|
||||
this.animation.addByPrefix('tapDownPixel', 'pixel5');
|
||||
this.animation.addByPrefix('tapUpPixel', 'pixel6');
|
||||
this.animation.addByPrefix('tapRightPixel', 'pixel7');
|
||||
|
||||
resizeNote();
|
||||
}
|
||||
|
||||
static var noteFrameCollection:FlxFramesCollection = null;
|
||||
|
||||
/**
|
||||
* We load all the note frames once, then reuse them.
|
||||
*/
|
||||
static function initFrameCollection():Void
|
||||
{
|
||||
noteFrameCollection = new FlxFramesCollection(null, ATLAS, null);
|
||||
|
||||
// TODO: Automatically iterate over the list of note skins.
|
||||
|
||||
// Normal notes
|
||||
var frameCollectionNormal = Paths.getSparrowAtlas('NOTE_assets');
|
||||
|
||||
for (frame in frameCollectionNormal.frames)
|
||||
{
|
||||
noteFrameCollection.pushFrame(frame);
|
||||
}
|
||||
|
||||
// Pixel notes
|
||||
var graphicPixel = FlxG.bitmap.add(Paths.image('weeb/pixelUI/arrows-pixels', 'week6'), false, null);
|
||||
if (graphicPixel == null)
|
||||
trace('ERROR: Could not load graphic: ' + Paths.image('weeb/pixelUI/arrows-pixels', 'week6'));
|
||||
var frameCollectionPixel = FlxTileFrames.fromGraphic(graphicPixel, new FlxPoint(17, 17));
|
||||
for (i in 0...frameCollectionPixel.frames.length)
|
||||
{
|
||||
var frame = frameCollectionPixel.frames[i];
|
||||
|
||||
frame.name = 'pixel' + i;
|
||||
noteFrameCollection.pushFrame(frame);
|
||||
}
|
||||
}
|
||||
|
||||
function set_noteData(value:SongNoteData):SongNoteData
|
||||
{
|
||||
this.noteData = value;
|
||||
|
||||
if (this.noteData == null)
|
||||
{
|
||||
this.kill();
|
||||
return this.noteData;
|
||||
}
|
||||
|
||||
this.visible = true;
|
||||
|
||||
// Update the position to match the note skin.
|
||||
setNotePosition();
|
||||
|
||||
// Update the animation to match the note skin.
|
||||
playNoteAnimation();
|
||||
|
||||
return this.noteData;
|
||||
}
|
||||
|
||||
function set_noteSkin(value:String):String
|
||||
{
|
||||
// Don't update if the skin hasn't changed.
|
||||
if (value == this.noteSkin)
|
||||
return this.noteSkin;
|
||||
|
||||
this.noteSkin = value;
|
||||
|
||||
// Make sure to update the graphic to match the note skin.
|
||||
playNoteAnimation();
|
||||
|
||||
return this.noteSkin;
|
||||
}
|
||||
|
||||
function setNotePosition()
|
||||
{
|
||||
var cursorColumn:Int = this.noteData.data;
|
||||
|
||||
if (cursorColumn < 0)
|
||||
cursorColumn = 0;
|
||||
if (cursorColumn >= (ChartEditorState.STRUMLINE_SIZE * 2 + 1))
|
||||
{
|
||||
cursorColumn = (ChartEditorState.STRUMLINE_SIZE * 2 + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Invert player and opponent columns.
|
||||
if (cursorColumn >= ChartEditorState.STRUMLINE_SIZE)
|
||||
{
|
||||
cursorColumn -= ChartEditorState.STRUMLINE_SIZE;
|
||||
}
|
||||
else
|
||||
{
|
||||
cursorColumn += ChartEditorState.STRUMLINE_SIZE;
|
||||
}
|
||||
}
|
||||
this.x = cursorColumn * ChartEditorState.GRID_SIZE;
|
||||
|
||||
// Notes far in the song will start far down, but the group they belong to will have a high negative offset.
|
||||
// TODO: stepTime doesn't account for fluctuating BPMs.
|
||||
this.y = this.noteData.stepTime * ChartEditorState.GRID_SIZE;
|
||||
}
|
||||
|
||||
function playNoteAnimation()
|
||||
{
|
||||
var animationName = 'tap${this.noteData.getDirectionName()}${this.noteSkin}';
|
||||
this.animation.play(animationName);
|
||||
}
|
||||
|
||||
function resizeNote()
|
||||
{
|
||||
this.setGraphicSize(ChartEditorState.GRID_SIZE);
|
||||
this.updateHitbox();
|
||||
|
||||
// TODO: Make this an attribute of the note skin.
|
||||
this.antialiasing = (noteSkin != 'Pixel');
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -2,6 +2,9 @@ package funkin.ui.haxeui;
|
|||
|
||||
import haxe.ui.RuntimeComponentBuilder;
|
||||
import haxe.ui.core.Component;
|
||||
import haxe.ui.core.Screen;
|
||||
import haxe.ui.events.MouseEvent;
|
||||
import lime.app.Application;
|
||||
|
||||
class HaxeUIState extends MusicBeatState
|
||||
{
|
||||
|
@ -33,16 +36,77 @@ class HaxeUIState extends MusicBeatState
|
|||
}
|
||||
catch (e)
|
||||
{
|
||||
trace('[ERROR] Failed to build component from asset: ' + assetPath);
|
||||
trace(e);
|
||||
Application.current.window.alert('Error building component "$assetPath": $e', 'HaxeUI Parsing Error');
|
||||
// trace('[ERROR] Failed to build component from asset: ' + assetPath);
|
||||
// trace(e);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The currently active context menu.
|
||||
*/
|
||||
public var contextMenu:Component;
|
||||
|
||||
/**
|
||||
* This function is called when right clicking on a component, to display a context menu.
|
||||
*/
|
||||
function showContextMenu(assetPath:String, xPos:Float, yPos:Float):Component
|
||||
{
|
||||
if (contextMenu != null)
|
||||
contextMenu.destroy();
|
||||
|
||||
contextMenu = buildComponent(assetPath);
|
||||
|
||||
if (contextMenu != null)
|
||||
{
|
||||
// Move the context menu to the mouse position.
|
||||
contextMenu.left = xPos;
|
||||
contextMenu.top = yPos;
|
||||
Screen.instance.addComponent(contextMenu);
|
||||
}
|
||||
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a context menu to display when right clicking.
|
||||
* @param component Only display the menu when clicking this component. If null, display the menu when right clicking anywhere.
|
||||
* @param assetPath The asset path to the context menu XML.
|
||||
*/
|
||||
public function registerContextMenu(target:Null<Component>, assetPath:String):Void
|
||||
{
|
||||
if (target == null)
|
||||
{
|
||||
Screen.instance.registerEvent(MouseEvent.RIGHT_CLICK, function(e:MouseEvent)
|
||||
{
|
||||
showContextMenu(assetPath, e.screenX, e.screenY);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
target.registerEvent(MouseEvent.RIGHT_CLICK, function(e:MouseEvent)
|
||||
{
|
||||
showContextMenu(assetPath, e.screenX, e.screenY);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public function findComponent<T:Component>(criteria:String = null, type:Class<T> = null, recursive:Null<Bool> = null, searchType:String = "id"):Null<T>
|
||||
{
|
||||
if (component == null)
|
||||
return null;
|
||||
|
||||
return component.findComponent(criteria, type, recursive, searchType);
|
||||
}
|
||||
|
||||
override function destroy()
|
||||
{
|
||||
if (component != null)
|
||||
remove(component);
|
||||
component = null;
|
||||
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,101 +0,0 @@
|
|||
package funkin.ui.haxeui.components;
|
||||
|
||||
import haxe.ui.Toolkit;
|
||||
import haxe.ui.containers.SideBar;
|
||||
import haxe.ui.core.Component;
|
||||
import haxe.ui.core.Screen;
|
||||
import haxe.ui.styles.elements.AnimationKeyFrame;
|
||||
import haxe.ui.styles.elements.AnimationKeyFrames;
|
||||
import haxe.ui.styles.elements.Directive;
|
||||
|
||||
class TabSideBar extends SideBar
|
||||
{
|
||||
var closeButton:Component;
|
||||
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
}
|
||||
|
||||
inline function getCloseButton()
|
||||
{
|
||||
if (closeButton == null)
|
||||
{
|
||||
closeButton = findComponent("closeSideBar", Component);
|
||||
}
|
||||
return closeButton;
|
||||
}
|
||||
|
||||
public override function hide()
|
||||
{
|
||||
var animation = Toolkit.styleSheet.findAnimation("sideBarRestoreContent");
|
||||
var first:AnimationKeyFrame = animation.keyFrames[0];
|
||||
var last:AnimationKeyFrame = animation.keyFrames[animation.keyFrames.length - 1];
|
||||
var rootComponent = Screen.instance.rootComponents[0];
|
||||
|
||||
first.set(new Directive("left", Value.VDimension(Dimension.PX(rootComponent.left))));
|
||||
first.set(new Directive("top", Value.VDimension(Dimension.PX(rootComponent.top))));
|
||||
first.set(new Directive("width", Value.VDimension(Dimension.PX(rootComponent.width))));
|
||||
first.set(new Directive("height", Value.VDimension(Dimension.PX(rootComponent.height))));
|
||||
|
||||
last.set(new Directive("left", Value.VDimension(Dimension.PX(0))));
|
||||
last.set(new Directive("top", Value.VDimension(Dimension.PX(0))));
|
||||
last.set(new Directive("width", Value.VDimension(Dimension.PX(Screen.instance.width))));
|
||||
last.set(new Directive("height", Value.VDimension(Dimension.PX(Screen.instance.height))));
|
||||
|
||||
for (r in Screen.instance.rootComponents)
|
||||
{
|
||||
if (r.classes.indexOf("sidebar") == -1)
|
||||
{
|
||||
r.swapClass("sideBarRestoreContent", "sideBarModifyContent");
|
||||
r.onAnimationEnd = function(_)
|
||||
{
|
||||
r.restorePercentSizes();
|
||||
r.onAnimationEnd = null;
|
||||
rootComponent.removeClass("sideBarRestoreContent");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hideSideBar();
|
||||
}
|
||||
|
||||
private override function hideSideBar()
|
||||
{
|
||||
var showSideBarClass = null;
|
||||
var hideSideBarClass = null;
|
||||
if (position == "left")
|
||||
{
|
||||
showSideBarClass = "showSideBarLeft";
|
||||
hideSideBarClass = "hideSideBarLeft";
|
||||
}
|
||||
else if (position == "right")
|
||||
{
|
||||
showSideBarClass = "showSideBarRight";
|
||||
hideSideBarClass = "hideSideBarRight";
|
||||
}
|
||||
else if (position == "top")
|
||||
{
|
||||
showSideBarClass = "showSideBarTop";
|
||||
hideSideBarClass = "hideSideBarTop";
|
||||
}
|
||||
else if (position == "bottom")
|
||||
{
|
||||
showSideBarClass = "showSideBarBottom";
|
||||
hideSideBarClass = "hideSideBarBottom";
|
||||
}
|
||||
|
||||
this.onAnimationEnd = function(_)
|
||||
{
|
||||
this.removeClass(hideSideBarClass);
|
||||
// onHideAnimationEnd();
|
||||
}
|
||||
|
||||
this.swapClass(hideSideBarClass, showSideBarClass);
|
||||
|
||||
if (modal == true)
|
||||
{
|
||||
hideModalOverlay();
|
||||
}
|
||||
}
|
||||
}
|
58
source/funkin/util/SerializerUtil.hx
Normal file
58
source/funkin/util/SerializerUtil.hx
Normal file
|
@ -0,0 +1,58 @@
|
|||
package funkin.util;
|
||||
|
||||
import haxe.Json;
|
||||
import thx.semver.Version;
|
||||
|
||||
class SerializerUtil
|
||||
{
|
||||
static final INDENT_CHAR = "\t";
|
||||
|
||||
/**
|
||||
* Convert a Haxe object to a JSON string.
|
||||
**/
|
||||
public static function toJSON(input:Dynamic, ?pretty:Bool = true):String
|
||||
{
|
||||
return Json.stringify(input, replacer, pretty ? INDENT_CHAR : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a JSON string to a Haxe object of the chosen type.
|
||||
*/
|
||||
public static function fromJSONTyped<T>(input:String, type:Class<T>):T
|
||||
{
|
||||
return cast Json.parse(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a JSON string to a Haxe object.
|
||||
*/
|
||||
public static function fromJSON(input:String):Dynamic
|
||||
{
|
||||
return Json.parse(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Customize how certain types are serialized when converting to JSON.
|
||||
*/
|
||||
static function replacer(key:String, value:Dynamic):Dynamic
|
||||
{
|
||||
// Hacky because you can't use `isOfType` on a struct.
|
||||
if (key == "version")
|
||||
{
|
||||
if (Std.isOfType(value, String))
|
||||
return value;
|
||||
|
||||
// Stringify Version objects.
|
||||
var valueVersion:thx.semver.Version = cast value;
|
||||
var result = '${valueVersion.major}.${valueVersion.minor}.${valueVersion.patch}';
|
||||
if (valueVersion.hasPre)
|
||||
result += '-${valueVersion.pre}';
|
||||
if (valueVersion.hasBuild)
|
||||
result += '+${valueVersion.build}';
|
||||
return result;
|
||||
}
|
||||
|
||||
// Else, return the value as-is.
|
||||
return value;
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ class GitCommit
|
|||
var commitHash:String = process.stdout.readLine();
|
||||
var commitHashSplice:String = commitHash.substr(0, 7);
|
||||
|
||||
trace('Git Commit ID ${commitHashSplice}');
|
||||
trace('Git Commit ID: ${commitHashSplice}');
|
||||
|
||||
// Generates a string expression
|
||||
return macro $v{commitHashSplice};
|
||||
|
@ -46,7 +46,7 @@ class GitCommit
|
|||
}
|
||||
|
||||
var branchName:String = branchProcess.stdout.readLine();
|
||||
trace('Current Working Branch: ${branchName}');
|
||||
trace('Git Branch Name: ${branchName}');
|
||||
|
||||
// Generates a string expression
|
||||
return macro $v{branchName};
|
||||
|
|
Loading…
Reference in a new issue