mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2025-07-19 02:24:18 +00:00
* Custom render distance strumline * Custom note positions * Custom vertices for holds * Song Retry Event Song retry event for pausesubstate.hx Song retry event for playstate.hx Song retry event for stage.hx Song retry event for bopper.hx Song retry event for song.hx Scripted class event Module event * this should be cast * Show notesplash toggle * Formatting and EOF
1014 lines
27 KiB
Haxe
1014 lines
27 KiB
Haxe
package funkin.play.notes;
|
|
|
|
import flixel.util.FlxSignal.FlxTypedSignal;
|
|
import flixel.FlxG;
|
|
import funkin.play.notes.notestyle.NoteStyle;
|
|
import flixel.group.FlxSpriteGroup;
|
|
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
|
|
import flixel.tweens.FlxEase;
|
|
import flixel.tweens.FlxTween;
|
|
import flixel.util.FlxSort;
|
|
import funkin.play.notes.NoteHoldCover;
|
|
import funkin.play.notes.NoteSplash;
|
|
import funkin.play.notes.NoteSprite;
|
|
import funkin.play.notes.SustainTrail;
|
|
import funkin.data.song.SongData.SongNoteData;
|
|
import funkin.ui.options.PreferencesMenu;
|
|
import funkin.util.SortUtil;
|
|
import funkin.modding.events.ScriptEvent;
|
|
import funkin.play.notes.notekind.NoteKindManager;
|
|
|
|
/**
|
|
* A group of sprites which handles the receptor, the note splashes, and the notes (with sustains) for a given player.
|
|
*/
|
|
class Strumline extends FlxSpriteGroup
|
|
{
|
|
public static final DIRECTIONS:Array<NoteDirection> = [NoteDirection.LEFT, NoteDirection.DOWN, NoteDirection.UP, NoteDirection.RIGHT];
|
|
public static final STRUMLINE_SIZE:Int = 104;
|
|
public static final NOTE_SPACING:Int = STRUMLINE_SIZE + 8;
|
|
|
|
// Positional fixes for new strumline graphics.
|
|
static final INITIAL_OFFSET = -0.275 * STRUMLINE_SIZE;
|
|
static final NUDGE:Float = 2.0;
|
|
|
|
static final KEY_COUNT:Int = 4;
|
|
static final NOTE_SPLASH_CAP:Int = 6;
|
|
|
|
static var RENDER_DISTANCE_MS(get, never):Float;
|
|
|
|
/**
|
|
* The custom render distance for the strumline.
|
|
* This should be in miliseconds only! Not pixels.
|
|
*/
|
|
public static var CUSTOM_RENDER_DISTANCE_MS:Float = 0.0;
|
|
|
|
/**
|
|
* Whether to use the custom render distance.
|
|
* If false, the render distance will be calculated based on the screen height.
|
|
*/
|
|
public static var USE_CUSTOM_RENDER_DISTANCE:Bool = false;
|
|
|
|
static function get_RENDER_DISTANCE_MS():Float
|
|
{
|
|
if (USE_CUSTOM_RENDER_DISTANCE) return CUSTOM_RENDER_DISTANCE_MS;
|
|
return FlxG.height / Constants.PIXELS_PER_MS;
|
|
}
|
|
|
|
/**
|
|
* Whether to play note splashes or not
|
|
* TODO: Make this a setting!
|
|
* IE: Settings.noSplash
|
|
*/
|
|
public var showNotesplash:Bool = true;
|
|
|
|
/**
|
|
* Whether this strumline is controlled by the player's inputs.
|
|
* False means it's controlled by the opponent or Bot Play.
|
|
*/
|
|
public var isPlayer:Bool;
|
|
|
|
/**
|
|
* Usually you want to keep this as is, but if you are using a Strumline and
|
|
* playing a sound that has it's own conductor, set this (LatencyState for example)
|
|
*/
|
|
public var conductorInUse(get, set):Conductor;
|
|
|
|
// Used in-game to control the scroll speed within a song
|
|
public var scrollSpeed:Float = 1.0;
|
|
|
|
public function resetScrollSpeed():Void
|
|
{
|
|
scrollSpeed = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0;
|
|
}
|
|
|
|
var _conductorInUse:Null<Conductor>;
|
|
|
|
function get_conductorInUse():Conductor
|
|
{
|
|
if (_conductorInUse == null) return Conductor.instance;
|
|
return _conductorInUse;
|
|
}
|
|
|
|
function set_conductorInUse(value:Conductor):Conductor
|
|
{
|
|
return _conductorInUse = value;
|
|
}
|
|
|
|
/**
|
|
* Whether the game should auto position notes.
|
|
*/
|
|
public var customPositionData:Bool = false;
|
|
|
|
/**
|
|
* The notes currently being rendered on the strumline.
|
|
* This group iterates over this every frame to update note positions.
|
|
* The PlayState also iterates over this to calculate user inputs.
|
|
*/
|
|
public var notes:FlxTypedSpriteGroup<NoteSprite>;
|
|
|
|
public var holdNotes:FlxTypedSpriteGroup<SustainTrail>;
|
|
|
|
public var onNoteIncoming:FlxTypedSignal<NoteSprite->Void>;
|
|
|
|
var strumlineNotes:FlxTypedSpriteGroup<StrumlineNote>;
|
|
var noteSplashes:FlxTypedSpriteGroup<NoteSplash>;
|
|
var noteHoldCovers:FlxTypedSpriteGroup<NoteHoldCover>;
|
|
|
|
var notesVwoosh:FlxTypedSpriteGroup<NoteSprite>;
|
|
var holdNotesVwoosh:FlxTypedSpriteGroup<SustainTrail>;
|
|
|
|
final noteStyle:NoteStyle;
|
|
|
|
#if FEATURE_GHOST_TAPPING
|
|
var ghostTapTimer:Float = 0.0;
|
|
#end
|
|
|
|
/**
|
|
* The note data for the song. Should NOT be altered after the song starts,
|
|
* so we can easily rewind.
|
|
*/
|
|
var noteData:Array<SongNoteData> = [];
|
|
|
|
var nextNoteIndex:Int = -1;
|
|
|
|
var heldKeys:Array<Bool> = [];
|
|
|
|
public function new(noteStyle:NoteStyle, isPlayer:Bool)
|
|
{
|
|
super();
|
|
|
|
this.isPlayer = isPlayer;
|
|
this.noteStyle = noteStyle;
|
|
|
|
this.strumlineNotes = new FlxTypedSpriteGroup<StrumlineNote>();
|
|
this.strumlineNotes.zIndex = 10;
|
|
this.add(this.strumlineNotes);
|
|
|
|
// Hold notes are added first so they render behind regular notes.
|
|
this.holdNotes = new FlxTypedSpriteGroup<SustainTrail>();
|
|
this.holdNotes.zIndex = 20;
|
|
this.add(this.holdNotes);
|
|
|
|
this.holdNotesVwoosh = new FlxTypedSpriteGroup<SustainTrail>();
|
|
this.holdNotesVwoosh.zIndex = 21;
|
|
this.add(this.holdNotesVwoosh);
|
|
|
|
this.notes = new FlxTypedSpriteGroup<NoteSprite>();
|
|
this.notes.zIndex = 30;
|
|
this.add(this.notes);
|
|
|
|
this.notesVwoosh = new FlxTypedSpriteGroup<NoteSprite>();
|
|
this.notesVwoosh.zIndex = 31;
|
|
this.add(this.notesVwoosh);
|
|
|
|
this.noteHoldCovers = new FlxTypedSpriteGroup<NoteHoldCover>(0, 0, 4);
|
|
this.noteHoldCovers.zIndex = 40;
|
|
this.add(this.noteHoldCovers);
|
|
|
|
this.noteSplashes = new FlxTypedSpriteGroup<NoteSplash>(0, 0, NOTE_SPLASH_CAP);
|
|
this.noteSplashes.zIndex = 50;
|
|
this.add(this.noteSplashes);
|
|
|
|
this.refresh();
|
|
|
|
this.onNoteIncoming = new FlxTypedSignal<NoteSprite->Void>();
|
|
resetScrollSpeed();
|
|
|
|
for (i in 0...KEY_COUNT)
|
|
{
|
|
var child:StrumlineNote = new StrumlineNote(noteStyle, isPlayer, DIRECTIONS[i]);
|
|
child.x = getXPos(DIRECTIONS[i]);
|
|
child.x += INITIAL_OFFSET;
|
|
child.y = 0;
|
|
noteStyle.applyStrumlineOffsets(child);
|
|
this.strumlineNotes.add(child);
|
|
}
|
|
|
|
for (i in 0...KEY_COUNT)
|
|
{
|
|
heldKeys.push(false);
|
|
}
|
|
|
|
// This MUST be true for children to update!
|
|
this.active = true;
|
|
}
|
|
|
|
public function refresh():Void
|
|
{
|
|
sort(SortUtil.byZIndex, FlxSort.ASCENDING);
|
|
}
|
|
|
|
override function get_width():Float
|
|
{
|
|
return KEY_COUNT * Strumline.NOTE_SPACING;
|
|
}
|
|
|
|
public override function update(elapsed:Float):Void
|
|
{
|
|
super.update(elapsed);
|
|
|
|
updateNotes();
|
|
|
|
#if FEATURE_GHOST_TAPPING
|
|
updateGhostTapTimer(elapsed);
|
|
#end
|
|
}
|
|
|
|
#if FEATURE_GHOST_TAPPING
|
|
/**
|
|
* Returns `true` if no notes are in range of the strumline and the player can spam without penalty.
|
|
*/
|
|
public function mayGhostTap():Bool
|
|
{
|
|
// Any notes in range of the strumline.
|
|
if (getNotesMayHit().length > 0)
|
|
{
|
|
return false;
|
|
}
|
|
// Any hold notes in range of the strumline.
|
|
if (getHoldNotesHitOrMissed().length > 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Note has been hit recently.
|
|
if (ghostTapTimer > 0.0) return false;
|
|
|
|
// **yippee**
|
|
return true;
|
|
}
|
|
#end
|
|
|
|
/**
|
|
* Return notes that are within `Constants.HIT_WINDOW` ms of the strumline.
|
|
* @return An array of `NoteSprite` objects.
|
|
*/
|
|
public function getNotesMayHit():Array<NoteSprite>
|
|
{
|
|
return notes.members.filter(function(note:NoteSprite) {
|
|
return note != null && note.alive && !note.hasBeenHit && note.mayHit;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Return hold notes that are within `Constants.HIT_WINDOW` ms of the strumline.
|
|
* @return An array of `SustainTrail` objects.
|
|
*/
|
|
public function getHoldNotesHitOrMissed():Array<SustainTrail>
|
|
{
|
|
return holdNotes.members.filter(function(holdNote:SustainTrail) {
|
|
return holdNote != null && holdNote.alive && (holdNote.hitNote || holdNote.missedNote);
|
|
});
|
|
}
|
|
|
|
public function getNoteSprite(noteData:SongNoteData):NoteSprite
|
|
{
|
|
if (noteData == null) return null;
|
|
|
|
for (note in notes.members)
|
|
{
|
|
if (note == null) continue;
|
|
if (note.alive) continue;
|
|
|
|
if (note.noteData == noteData) return note;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public function getHoldNoteSprite(noteData:SongNoteData):SustainTrail
|
|
{
|
|
if (noteData == null || ((noteData.length ?? 0.0) <= 0.0)) return null;
|
|
|
|
for (holdNote in holdNotes.members)
|
|
{
|
|
if (holdNote == null) continue;
|
|
if (holdNote.alive) continue;
|
|
|
|
if (holdNote.noteData == noteData) return holdNote;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Call this when resetting the playstate.
|
|
*/
|
|
public function vwooshNotes():Void
|
|
{
|
|
for (note in notes.members)
|
|
{
|
|
if (note == null) continue;
|
|
if (!note.alive) continue;
|
|
|
|
notes.remove(note);
|
|
notesVwoosh.add(note);
|
|
|
|
var targetY:Float = FlxG.height + note.y;
|
|
if (Preferences.downscroll) targetY = 0 - note.height;
|
|
FlxTween.tween(note, {y: targetY}, 0.5,
|
|
{
|
|
ease: FlxEase.expoIn,
|
|
onComplete: function(twn) {
|
|
note.kill();
|
|
notesVwoosh.remove(note, true);
|
|
note.destroy();
|
|
}
|
|
});
|
|
}
|
|
|
|
for (holdNote in holdNotes.members)
|
|
{
|
|
if (holdNote == null) continue;
|
|
if (!holdNote.alive) continue;
|
|
|
|
holdNotes.remove(holdNote);
|
|
holdNotesVwoosh.add(holdNote);
|
|
|
|
var targetY:Float = FlxG.height + holdNote.y;
|
|
if (Preferences.downscroll) targetY = 0 - holdNote.height;
|
|
FlxTween.tween(holdNote, {y: targetY}, 0.5,
|
|
{
|
|
ease: FlxEase.expoIn,
|
|
onComplete: function(twn) {
|
|
holdNote.kill();
|
|
holdNotesVwoosh.remove(holdNote, true);
|
|
holdNote.destroy();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* For a note's strumTime, calculate its Y position relative to the strumline.
|
|
* NOTE: Assumes Conductor and PlayState are both initialized.
|
|
* @param strumTime
|
|
* @return Float
|
|
*/
|
|
public function calculateNoteYPos(strumTime:Float, vwoosh:Bool = true):Float
|
|
{
|
|
// Make the note move faster visually as it moves offscreen.
|
|
// var vwoosh:Float = (strumTime < Conductor.songPosition) && vwoosh ? 2.0 : 1.0;
|
|
// ^^^ commented this out... do NOT make it move faster as it moves offscreen!
|
|
var vwoosh:Float = 1.0;
|
|
|
|
return
|
|
Constants.PIXELS_PER_MS * (conductorInUse.songPosition - strumTime - Conductor.instance.inputOffset) * scrollSpeed * vwoosh * (Preferences.downscroll ? 1 : -1);
|
|
}
|
|
|
|
function updateNotes():Void
|
|
{
|
|
if (noteData.length == 0) return;
|
|
|
|
// Ensure note data gets reset if the song happens to loop.
|
|
// NOTE: I had to remove this line because it was causing notes visible during the countdown to be placed multiple times.
|
|
// I don't remember what bug I was trying to fix by adding this.
|
|
// if (conductorInUse.currentStep == 0) nextNoteIndex = 0;
|
|
|
|
var songStart:Float = PlayState.instance?.startTimestamp ?? 0.0;
|
|
var hitWindowStart:Float = conductorInUse.songPosition - Constants.HIT_WINDOW_MS;
|
|
var renderWindowStart:Float = conductorInUse.songPosition + RENDER_DISTANCE_MS;
|
|
|
|
for (noteIndex in nextNoteIndex...noteData.length)
|
|
{
|
|
var note:Null<SongNoteData> = noteData[noteIndex];
|
|
|
|
if (note == null) continue; // Note is blank
|
|
if (note.time < songStart || note.time < hitWindowStart)
|
|
{
|
|
// Note is in the past, skip it.
|
|
nextNoteIndex = noteIndex + 1;
|
|
continue;
|
|
}
|
|
if (note.time > renderWindowStart) break; // Note is too far ahead to render
|
|
|
|
var noteSprite = buildNoteSprite(note);
|
|
|
|
if (note.length > 0)
|
|
{
|
|
noteSprite.holdNoteSprite = buildHoldNoteSprite(note);
|
|
}
|
|
|
|
nextNoteIndex = noteIndex + 1; // Increment the nextNoteIndex rather than splicing the array, because splicing is slow.
|
|
|
|
onNoteIncoming.dispatch(noteSprite);
|
|
}
|
|
|
|
// Update rendering of notes.
|
|
for (note in notes.members)
|
|
{
|
|
if (note == null || !note.alive) continue;
|
|
|
|
var vwoosh:Bool = note.holdNoteSprite == null;
|
|
// Set the note's position.
|
|
if (!customPositionData) note.y = this.y - INITIAL_OFFSET + calculateNoteYPos(note.strumTime, vwoosh);
|
|
|
|
// If the note is miss
|
|
var isOffscreen = Preferences.downscroll ? note.y > FlxG.height : note.y < -note.height;
|
|
if (note.handledMiss && isOffscreen)
|
|
{
|
|
killNote(note);
|
|
}
|
|
}
|
|
|
|
// Update rendering of hold notes.
|
|
for (holdNote in holdNotes.members)
|
|
{
|
|
if (holdNote == null || !holdNote.alive) continue;
|
|
|
|
if (conductorInUse.songPosition > holdNote.strumTime && holdNote.hitNote && !holdNote.missedNote)
|
|
{
|
|
if (isPlayer && !isKeyHeld(holdNote.noteDirection))
|
|
{
|
|
// Stopped pressing the hold note.
|
|
playStatic(holdNote.noteDirection);
|
|
holdNote.missedNote = true;
|
|
holdNote.visible = true;
|
|
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 && conductorInUse.songPosition >= renderWindowEnd)
|
|
{
|
|
// Hold note is offscreen, kill it.
|
|
holdNote.visible = false;
|
|
holdNote.kill(); // Do not destroy! Recycling is faster.
|
|
}
|
|
else if (holdNote.hitNote && holdNote.sustainLength <= 0)
|
|
{
|
|
// Hold note is completed, kill it.
|
|
if (isKeyHeld(holdNote.noteDirection))
|
|
{
|
|
playPress(holdNote.noteDirection);
|
|
}
|
|
else
|
|
{
|
|
playStatic(holdNote.noteDirection);
|
|
}
|
|
|
|
if (holdNote.cover != null && isPlayer)
|
|
{
|
|
holdNote.cover.playEnd();
|
|
}
|
|
else if (holdNote.cover != null)
|
|
{
|
|
// *lightning* *zap* *crackle*
|
|
holdNote.cover.visible = false;
|
|
holdNote.cover.kill();
|
|
}
|
|
|
|
holdNote.visible = false;
|
|
holdNote.kill();
|
|
}
|
|
else if (holdNote.missedNote && (holdNote.fullSustainLength > holdNote.sustainLength))
|
|
{
|
|
// Hold note was dropped before completing, keep it in its clipped state.
|
|
holdNote.visible = true;
|
|
|
|
var yOffset:Float = (holdNote.fullSustainLength - holdNote.sustainLength) * Constants.PIXELS_PER_MS;
|
|
|
|
var vwoosh:Bool = false;
|
|
|
|
if (!customPositionData)
|
|
{
|
|
if (Preferences.downscroll)
|
|
{
|
|
holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
|
|
}
|
|
else
|
|
{
|
|
holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) + yOffset + STRUMLINE_SIZE / 2;
|
|
}
|
|
}
|
|
|
|
// Clean up the cover.
|
|
if (holdNote.cover != null)
|
|
{
|
|
holdNote.cover.visible = false;
|
|
holdNote.cover.kill();
|
|
}
|
|
}
|
|
else if (conductorInUse.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) - conductorInUse.songPosition;
|
|
|
|
if (holdNote.sustainLength <= 10)
|
|
{
|
|
holdNote.visible = false;
|
|
}
|
|
|
|
if (!customPositionData)
|
|
{
|
|
if (Preferences.downscroll)
|
|
{
|
|
holdNote.y = this.y - INITIAL_OFFSET - holdNote.height + STRUMLINE_SIZE / 2;
|
|
}
|
|
else
|
|
{
|
|
holdNote.y = this.y - INITIAL_OFFSET + STRUMLINE_SIZE / 2;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Hold note is new, render it normally.
|
|
holdNote.visible = true;
|
|
var vwoosh:Bool = false;
|
|
|
|
if (!customPositionData)
|
|
{
|
|
if (Preferences.downscroll)
|
|
{
|
|
holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) - holdNote.height + STRUMLINE_SIZE / 2;
|
|
}
|
|
else
|
|
{
|
|
holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime, vwoosh) + STRUMLINE_SIZE / 2;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update rendering of pressed keys.
|
|
for (dir in DIRECTIONS)
|
|
{
|
|
if (isKeyHeld(dir) && getByDirection(dir).getCurrentAnimation() == "static")
|
|
{
|
|
playPress(dir);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return notes that are within, or way after, `Constants.HIT_WINDOW` ms of the strumline.
|
|
* @return An array of `NoteSprite` objects.
|
|
*/
|
|
public function getNotesOnScreen():Array<NoteSprite>
|
|
{
|
|
return notes.members.filter(function(note:NoteSprite) {
|
|
return note != null && note.alive && !note.hasBeenHit;
|
|
});
|
|
}
|
|
|
|
#if FEATURE_GHOST_TAPPING
|
|
function updateGhostTapTimer(elapsed:Float):Void
|
|
{
|
|
// If it's still our turn, don't update the ghost tap timer.
|
|
if (getNotesOnScreen().length > 0) return;
|
|
|
|
ghostTapTimer -= elapsed;
|
|
|
|
if (ghostTapTimer <= 0)
|
|
{
|
|
ghostTapTimer = 0;
|
|
}
|
|
}
|
|
#end
|
|
|
|
/**
|
|
* Called when the PlayState skips a large amount of time forward or backward.
|
|
*/
|
|
public function handleSkippedNotes():Void
|
|
{
|
|
// By calling clean(), we remove all existing notes so they can be re-added.
|
|
clean();
|
|
// By setting noteIndex to 0, the next update will skip past all the notes that are in the past.
|
|
nextNoteIndex = 0;
|
|
}
|
|
|
|
public function onBeatHit():Void
|
|
{
|
|
if (notes.members.length > 1) notes.members.insertionSort(compareNoteSprites.bind(FlxSort.ASCENDING));
|
|
|
|
if (holdNotes.members.length > 1) holdNotes.members.insertionSort(compareHoldNoteSprites.bind(FlxSort.ASCENDING));
|
|
}
|
|
|
|
public function pressKey(dir:NoteDirection):Void
|
|
{
|
|
heldKeys[dir] = true;
|
|
}
|
|
|
|
public function releaseKey(dir:NoteDirection):Void
|
|
{
|
|
heldKeys[dir] = false;
|
|
}
|
|
|
|
public function isKeyHeld(dir:NoteDirection):Bool
|
|
{
|
|
return heldKeys[dir];
|
|
}
|
|
|
|
/**
|
|
* Called when the song is reset.
|
|
* Removes any special animations and the like.
|
|
* Doesn't reset the notes from the chart, that's handled by the PlayState.
|
|
*/
|
|
public function clean():Void
|
|
{
|
|
for (note in notes.members)
|
|
{
|
|
if (note == null) continue;
|
|
killNote(note);
|
|
}
|
|
|
|
for (holdNote in holdNotes.members)
|
|
{
|
|
if (holdNote == null) continue;
|
|
holdNote.kill();
|
|
}
|
|
|
|
for (splash in noteSplashes)
|
|
{
|
|
if (splash == null) continue;
|
|
splash.kill();
|
|
}
|
|
|
|
for (cover in noteHoldCovers)
|
|
{
|
|
if (cover == null) continue;
|
|
cover.kill();
|
|
}
|
|
|
|
heldKeys = [false, false, false, false];
|
|
|
|
for (dir in DIRECTIONS)
|
|
{
|
|
playStatic(dir);
|
|
}
|
|
resetScrollSpeed();
|
|
|
|
#if FEATURE_GHOST_TAPPING
|
|
ghostTapTimer = 0;
|
|
#end
|
|
}
|
|
|
|
public function applyNoteData(data:Array<SongNoteData>):Void
|
|
{
|
|
this.notes.clear();
|
|
|
|
this.noteData = data.copy();
|
|
this.nextNoteIndex = 0;
|
|
|
|
// Sort the notes by strumtime.
|
|
this.noteData.insertionSort(compareNoteData.bind(FlxSort.ASCENDING));
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
|
|
if (removeNote)
|
|
{
|
|
killNote(note);
|
|
}
|
|
else
|
|
{
|
|
note.alpha = 0.5;
|
|
note.desaturate();
|
|
}
|
|
|
|
if (note.holdNoteSprite != null)
|
|
{
|
|
note.holdNoteSprite.hitNote = true;
|
|
note.holdNoteSprite.missedNote = false;
|
|
|
|
note.holdNoteSprite.sustainLength = (note.holdNoteSprite.strumTime + note.holdNoteSprite.fullSustainLength) - conductorInUse.songPosition;
|
|
}
|
|
|
|
#if FEATURE_GHOST_TAPPING
|
|
ghostTapTimer = Constants.GHOST_TAP_DELAY;
|
|
#end
|
|
}
|
|
|
|
public function killNote(note:NoteSprite):Void
|
|
{
|
|
if (note == null) return;
|
|
note.visible = false;
|
|
notes.remove(note, false);
|
|
note.kill();
|
|
|
|
if (note.holdNoteSprite != null)
|
|
{
|
|
note.holdNoteSprite.missedNote = true;
|
|
note.holdNoteSprite.visible = false;
|
|
}
|
|
}
|
|
|
|
public function getByIndex(index:Int):StrumlineNote
|
|
{
|
|
return this.strumlineNotes.members[index];
|
|
}
|
|
|
|
public function getByDirection(direction:NoteDirection):StrumlineNote
|
|
{
|
|
return getByIndex(DIRECTIONS.indexOf(direction));
|
|
}
|
|
|
|
public function playStatic(direction:NoteDirection):Void
|
|
{
|
|
getByDirection(direction).playStatic();
|
|
}
|
|
|
|
public function playPress(direction:NoteDirection):Void
|
|
{
|
|
getByDirection(direction).playPress();
|
|
}
|
|
|
|
public function playConfirm(direction:NoteDirection):Void
|
|
{
|
|
getByDirection(direction).playConfirm();
|
|
}
|
|
|
|
public function holdConfirm(direction:NoteDirection):Void
|
|
{
|
|
getByDirection(direction).holdConfirm();
|
|
}
|
|
|
|
public function isConfirm(direction:NoteDirection):Bool
|
|
{
|
|
return getByDirection(direction).isConfirm();
|
|
}
|
|
|
|
public function playNoteSplash(direction:NoteDirection):Void
|
|
{
|
|
if (!showNotesplash) return;
|
|
if (!noteStyle.isNoteSplashEnabled()) return;
|
|
|
|
var splash:NoteSplash = this.constructNoteSplash();
|
|
|
|
if (splash != null)
|
|
{
|
|
splash.play(direction);
|
|
|
|
splash.x = this.x;
|
|
splash.x += getXPos(direction);
|
|
splash.x += INITIAL_OFFSET;
|
|
splash.y = this.y;
|
|
splash.y -= INITIAL_OFFSET;
|
|
splash.y += 0;
|
|
}
|
|
}
|
|
|
|
public function playNoteHoldCover(holdNote:SustainTrail):Void
|
|
{
|
|
if (!showNotesplash) return;
|
|
if (!noteStyle.isHoldNoteCoverEnabled()) return;
|
|
|
|
var cover:NoteHoldCover = this.constructNoteHoldCover();
|
|
|
|
if (cover != null)
|
|
{
|
|
cover.holdNote = holdNote;
|
|
holdNote.cover = cover;
|
|
cover.visible = true;
|
|
|
|
cover.playStart();
|
|
|
|
cover.x = this.x;
|
|
cover.x += getXPos(holdNote.noteDirection);
|
|
cover.x += STRUMLINE_SIZE / 2;
|
|
cover.x -= cover.width / 2;
|
|
cover.x += -12; // Manual tweaking because fuck.
|
|
|
|
cover.y = this.y;
|
|
cover.y += INITIAL_OFFSET;
|
|
cover.y += STRUMLINE_SIZE / 2;
|
|
cover.y += -96; // Manual tweaking because fuck.
|
|
}
|
|
}
|
|
|
|
public function buildNoteSprite(note:SongNoteData):NoteSprite
|
|
{
|
|
var noteSprite:NoteSprite = constructNoteSprite();
|
|
|
|
if (noteSprite != null)
|
|
{
|
|
var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind, this.noteStyle.id) ?? this.noteStyle;
|
|
noteSprite.setupNoteGraphic(noteKindStyle);
|
|
|
|
noteSprite.direction = note.getDirection();
|
|
noteSprite.noteData = note;
|
|
|
|
noteSprite.x = this.x;
|
|
noteSprite.x += getXPos(DIRECTIONS[note.getDirection() % KEY_COUNT]);
|
|
noteSprite.x -= (noteSprite.width - Strumline.STRUMLINE_SIZE) / 2; // Center it
|
|
noteSprite.x -= NUDGE;
|
|
// noteSprite.x += INITIAL_OFFSET;
|
|
noteSprite.y = -9999;
|
|
}
|
|
|
|
return noteSprite;
|
|
}
|
|
|
|
public function buildHoldNoteSprite(note:SongNoteData):SustainTrail
|
|
{
|
|
var holdNoteSprite:SustainTrail = constructHoldNoteSprite();
|
|
|
|
if (holdNoteSprite != null)
|
|
{
|
|
var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind, this.noteStyle.id) ?? this.noteStyle;
|
|
holdNoteSprite.setupHoldNoteGraphic(noteKindStyle);
|
|
|
|
holdNoteSprite.parentStrumline = this;
|
|
holdNoteSprite.noteData = note;
|
|
holdNoteSprite.strumTime = note.time;
|
|
holdNoteSprite.noteDirection = note.getDirection();
|
|
holdNoteSprite.fullSustainLength = note.length;
|
|
holdNoteSprite.sustainLength = note.length;
|
|
holdNoteSprite.missedNote = false;
|
|
holdNoteSprite.hitNote = false;
|
|
holdNoteSprite.visible = true;
|
|
holdNoteSprite.alpha = 1.0;
|
|
|
|
holdNoteSprite.x = this.x;
|
|
holdNoteSprite.x += getXPos(DIRECTIONS[note.getDirection() % KEY_COUNT]);
|
|
holdNoteSprite.x += STRUMLINE_SIZE / 2;
|
|
holdNoteSprite.x -= holdNoteSprite.width / 2;
|
|
holdNoteSprite.y = -9999;
|
|
}
|
|
|
|
return holdNoteSprite;
|
|
}
|
|
|
|
/**
|
|
* Custom recycling behavior.
|
|
*/
|
|
function constructNoteSplash():NoteSplash
|
|
{
|
|
var result:NoteSplash = null;
|
|
|
|
// If we haven't filled the pool yet...
|
|
if (noteSplashes.length < noteSplashes.maxSize)
|
|
{
|
|
// Create a new note splash.
|
|
result = new NoteSplash();
|
|
this.noteSplashes.add(result);
|
|
}
|
|
else
|
|
{
|
|
// Else, find a note splash which is inactive so we can revive it.
|
|
result = this.noteSplashes.getFirstAvailable();
|
|
|
|
if (result != null)
|
|
{
|
|
result.revive();
|
|
}
|
|
else
|
|
{
|
|
// The note splash pool is full and all note splashes are active,
|
|
// so we just pick one at random to destroy and restart.
|
|
result = FlxG.random.getObject(this.noteSplashes.members);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Custom recycling behavior.
|
|
*/
|
|
function constructNoteHoldCover():NoteHoldCover
|
|
{
|
|
var result:NoteHoldCover = null;
|
|
|
|
// If we haven't filled the pool yet...
|
|
if (noteHoldCovers.length < noteHoldCovers.maxSize)
|
|
{
|
|
// Create a new note hold cover.
|
|
result = new NoteHoldCover();
|
|
this.noteHoldCovers.add(result);
|
|
}
|
|
else
|
|
{
|
|
// Else, find a note splash which is inactive so we can revive it.
|
|
result = this.noteHoldCovers.getFirstAvailable();
|
|
|
|
if (result != null)
|
|
{
|
|
result.revive();
|
|
}
|
|
else
|
|
{
|
|
// The note hold cover pool is full and all note hold covers are active,
|
|
// so we just pick one at random to destroy and restart.
|
|
result = FlxG.random.getObject(this.noteHoldCovers.members);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Custom recycling behavior.
|
|
*/
|
|
function constructNoteSprite():NoteSprite
|
|
{
|
|
var result:NoteSprite = null;
|
|
|
|
// Else, find a note which is inactive so we can revive it.
|
|
result = this.notes.getFirstAvailable();
|
|
|
|
if (result != null)
|
|
{
|
|
// Revive and reuse the note.
|
|
result.revive();
|
|
}
|
|
else
|
|
{
|
|
// The note sprite pool is full and all note splashes are active.
|
|
// We have to create a new note.
|
|
result = new NoteSprite(noteStyle);
|
|
this.notes.add(result);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Custom recycling behavior.
|
|
*/
|
|
function constructHoldNoteSprite():SustainTrail
|
|
{
|
|
var result:SustainTrail = null;
|
|
|
|
// Else, find a note which is inactive so we can revive it.
|
|
result = this.holdNotes.getFirstAvailable();
|
|
|
|
if (result != null)
|
|
{
|
|
// Revive and reuse the note.
|
|
result.revive();
|
|
}
|
|
else
|
|
{
|
|
// The note sprite pool is full and all note splashes are active.
|
|
// We have to create a new note.
|
|
result = new SustainTrail(0, 0, noteStyle);
|
|
this.holdNotes.add(result);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function getXPos(direction:NoteDirection):Float
|
|
{
|
|
return switch (direction)
|
|
{
|
|
case NoteDirection.LEFT: 0;
|
|
case NoteDirection.DOWN: 0 + (1 * Strumline.NOTE_SPACING);
|
|
case NoteDirection.UP: 0 + (2 * Strumline.NOTE_SPACING);
|
|
case NoteDirection.RIGHT: 0 + (3 * Strumline.NOTE_SPACING);
|
|
default: 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Apply a small animation which moves the arrow down and fades it in.
|
|
* Only plays at the start of Free Play songs.
|
|
*
|
|
* Note that modifying the offset of the whole strumline won't have the
|
|
* @param arrow The arrow to animate.
|
|
* @param index The index of the arrow in the strumline.
|
|
*/
|
|
function fadeInArrow(index:Int, arrow:StrumlineNote):Void
|
|
{
|
|
arrow.y -= 10;
|
|
arrow.alpha = 0.0;
|
|
FlxTween.tween(arrow, {y: arrow.y + 10, alpha: 1}, 1, {ease: FlxEase.circOut, startDelay: 0.5 + (0.2 * index)});
|
|
}
|
|
|
|
public function fadeInArrows():Void
|
|
{
|
|
for (index => arrow in this.strumlineNotes.members.keyValueIterator())
|
|
{
|
|
fadeInArrow(index, arrow);
|
|
}
|
|
}
|
|
|
|
function compareNoteData(order:Int, a:SongNoteData, b:SongNoteData):Int
|
|
{
|
|
return FlxSort.byValues(order, a.time, b.time);
|
|
}
|
|
|
|
function compareNoteSprites(order:Int, a:NoteSprite, b:NoteSprite):Int
|
|
{
|
|
return FlxSort.byValues(order, a?.strumTime, b?.strumTime);
|
|
}
|
|
|
|
function compareHoldNoteSprites(order:Int, a:SustainTrail, b:SustainTrail):Int
|
|
{
|
|
return FlxSort.byValues(order, a?.strumTime, b?.strumTime);
|
|
}
|
|
}
|