Funkin/source/funkin/play/notes/SustainTrail.hx

327 lines
10 KiB
Haxe

package funkin.play.notes;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.notes.NoteDirection;
import funkin.data.song.SongData.SongNoteData;
import flixel.util.FlxDirectionFlags;
import flixel.FlxSprite;
import flixel.graphics.FlxGraphic;
import flixel.graphics.tile.FlxDrawTrianglesItem;
import flixel.math.FlxMath;
import funkin.ui.options.PreferencesMenu;
/**
* 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
{
/**
* The triangles corresponding to the hold, followed by the endcap.
* `top left, top right, bottom left`
* `top left, bottom left, bottom right`
*/
static final TRIANGLE_VERTEX_INDICES:Array<Int> = [0, 1, 2, 1, 2, 3, 4, 5, 6, 5, 6, 7];
public var strumTime:Float = 0; // millis
public var noteDirection:NoteDirection = 0;
public var sustainLength(default, set):Float = 0; // millis
public var fullSustainLength:Float = 0;
public var noteData:Null<SongNoteData>;
public var cover:NoteHoldCover = null;
/**
* Set to `true` if the user hit the note and is currently holding the sustain.
* Should display associated effects.
*/
public var hitNote:Bool = false;
/**
* Set to `true` if the user missed the note or released the sustain.
* Should make the trail transparent.
*/
public var missedNote: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).
*/
public var vertices:DrawData<Float> = new DrawData<Float>();
/**
* A `Vector` of integers or indexes, where every three indexes define a triangle.
*/
public var indices:DrawData<Int> = new DrawData<Int>();
/**
* A `Vector` of normalized coordinates used to apply texture mapping.
*/
public var uvtData:DrawData<Float> = new DrawData<Float>();
private var processedGraphic:FlxGraphic;
private var zoom:Float = 1;
/**
* 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;
public var isPixel:Bool;
/**
* Normally you would take strumTime:Float, noteData:Int, sustainLength:Float, parentNote:Note (?)
* @param NoteData
* @param SustainLength Length in milliseconds.
* @param fileName
*/
public function new(noteDirection:NoteDirection, sustainLength:Float, noteStyle:NoteStyle)
{
super(0, 0, noteStyle.getHoldNoteAssetPath());
antialiasing = true;
this.isPixel = noteStyle.isHoldNotePixel();
if (isPixel)
{
endOffset = bottomClip = 1;
antialiasing = false;
}
zoom *= noteStyle.fetchHoldNoteScale();
// BASIC SETUP
this.sustainLength = sustainLength;
this.fullSustainLength = sustainLength;
this.noteDirection = noteDirection;
zoom *= 0.7;
// CALCULATE SIZE
width = graphic.width / 8 * zoom; // amount of notes * 2
height = sustainHeight(sustainLength, getScrollSpeed());
// instead of scrollSpeed, PlayState.SONG.speed
flipY = Preferences.downscroll;
// alpha = 0.6;
alpha = 1.0;
// calls updateColorTransform(), which initializes processedGraphic!
updateColorTransform();
updateClipping();
indices = new DrawData<Int>(12, true, TRIANGLE_VERTEX_INDICES);
this.active = true; // This NEEDS to be true for the note to be drawn!
}
function getScrollSpeed():Float
{
return PlayState?.instance?.currentChart?.scrollSpeed ?? 1.0;
}
/**
* 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_sustainLength(s:Float):Float
{
if (s < 0.0) s = 0.0;
if (sustainLength == s) return s;
height = sustainHeight(s, getScrollSpeed());
this.sustainLength = s;
updateClipping();
return this.sustainLength;
}
/**
* 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), getScrollSpeed()), 0, height);
if (clipHeight <= 0.1)
{
visible = false;
return;
}
else
{
visible = true;
}
var bottomHeight:Float = graphic.height * zoom * endOffset;
var partHeight:Float = clipHeight - bottomHeight;
// ===HOLD VERTICES==
// Top left
vertices[0 * 2] = 0.0; // Inline with left side
vertices[0 * 2 + 1] = flipY ? clipHeight : height - clipHeight;
// Top right
vertices[1 * 2] = width;
vertices[1 * 2 + 1] = vertices[0 * 2 + 1]; // Inline with top left vertex
// Bottom left
vertices[2 * 2] = 0.0; // Inline with left side
vertices[2 * 2 + 1] = if (partHeight > 0)
{
// flipY makes the sustain render upside down.
flipY ? 0.0 + bottomHeight : vertices[1] + partHeight;
}
else
{
vertices[0 * 2 + 1]; // Inline with top left vertex (no partHeight available)
}
// Bottom right
vertices[3 * 2] = width;
vertices[3 * 2 + 1] = vertices[2 * 2 + 1]; // Inline with bottom left vertex
// ===HOLD UVs===
// The UVs are a bit more complicated.
// UV coordinates are normalized, so they range from 0 to 1.
// We are expecting an image containing 8 horizontal segments, each representing a different colored hold note followed by its end cap.
uvtData[0 * 2] = 1 / 4 * (noteDirection % 4); // 0%/25%/50%/75% of the way through the image
uvtData[0 * 2 + 1] = (-partHeight) / graphic.height / zoom; // top bound
// Top left
// Top right
uvtData[1 * 2] = uvtData[0 * 2] + 1 / 8; // 12.5%/37.5%/62.5%/87.5% of the way through the image (1/8th past the top left)
uvtData[1 * 2 + 1] = uvtData[0 * 2 + 1]; // top bound
// Bottom left
uvtData[2 * 2] = uvtData[0 * 2]; // 0%/25%/50%/75% of the way through the image
uvtData[2 * 2 + 1] = 0.0; // bottom bound
// Bottom right
uvtData[3 * 2] = uvtData[1 * 2]; // 12.5%/37.5%/62.5%/87.5% of the way through the image (1/8th past the top left)
uvtData[3 * 2 + 1] = uvtData[2 * 2 + 1]; // bottom bound
// === END CAP VERTICES ===
// Top left
vertices[4 * 2] = vertices[2 * 2]; // Inline with bottom left vertex of hold
vertices[4 * 2 + 1] = vertices[2 * 2 + 1]; // Inline with bottom left vertex of hold
// Top right
vertices[5 * 2] = vertices[3 * 2]; // Inline with bottom right vertex of hold
vertices[5 * 2 + 1] = vertices[3 * 2 + 1]; // Inline with bottom right vertex of hold
// 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);
// Bottom right
vertices[7 * 2] = vertices[3 * 2]; // Inline with right side
vertices[7 * 2 + 1] = vertices[6 * 2 + 1]; // Inline with bottom of end cap
// === END CAP UVs ===
// Top left
uvtData[4 * 2] = uvtData[2 * 2] + 1 / 8; // 12.5%/37.5%/62.5%/87.5% of the way through the image (1/8th past the top left of hold)
uvtData[4 * 2 + 1] = if (partHeight > 0)
{
0;
}
else
{
(bottomHeight - clipHeight) / zoom / graphic.height;
};
// Top right
uvtData[5 * 2] = uvtData[4 * 2] + 1 / 8; // 25%/50%/75%/100% of the way through the image (1/8th past the top left of cap)
uvtData[5 * 2 + 1] = uvtData[4 * 2 + 1]; // top bound
// Bottom left
uvtData[6 * 2] = uvtData[4 * 2]; // 12.5%/37.5%/62.5%/87.5% of the way through the image (1/8th past the top left of hold)
uvtData[6 * 2 + 1] = bottomClip; // bottom bound
// Bottom right
uvtData[7 * 2] = uvtData[5 * 2]; // 25%/50%/75%/100% of the way through the image (1/8th past the top left of cap)
uvtData[7 * 2 + 1] = uvtData[6 * 2 + 1]; // bottom bound
}
@: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) continue;
// if (!isOnScreen(camera)) continue; // TODO: Update this code to make it work properly.
getScreenPosition(_point, camera).subtractPoint(offset);
camera.drawTriangles(processedGraphic, vertices, indices, uvtData, null, _point, blend, true, antialiasing);
}
}
public override function kill():Void
{
super.kill();
strumTime = 0;
noteDirection = 0;
sustainLength = 0;
fullSustainLength = 0;
noteData = null;
hitNote = false;
missedNote = false;
}
public override function revive():Void
{
super.revive();
strumTime = 0;
noteDirection = 0;
sustainLength = 0;
fullSustainLength = 0;
noteData = null;
hitNote = false;
missedNote = false;
}
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);
}
}