WIP on new note rendering, inputs.

This commit is contained in:
EliteMasterEric 2023-06-22 01:41:01 -04:00
parent 28ddadffff
commit 25c70564bd
34 changed files with 3578 additions and 2872 deletions

View file

@ -95,8 +95,8 @@
"name": "lime",
"type": "git",
"dir": null,
"ref": "5634ad7",
"url": "https://github.com/openfl/lime"
"ref": "2447ae6",
"url": "https://github.com/elitemastereric/lime"
},
{
"name": "openfl",
@ -123,4 +123,4 @@
"version": null
}
]
}
}

View file

@ -16,7 +16,11 @@ typedef BPMChangeEvent =
*/
class Conductor
{
static final STEPS_PER_BEAT:Int = 4;
public static final PIXELS_PER_MS:Float = 0.45;
public static final HIT_WINDOW_MS:Float = 160;
public static final SECONDS_PER_MINUTE:Float = 60;
public static final MILLIS_PER_SECOND:Float = 1000;
public static final STEPS_PER_BEAT:Int = 4;
// onBeatHit is called every quarter note
// onStepHit is called every sixteenth note
@ -93,7 +97,7 @@ class Conductor
static function get_beatLengthMs():Float
{
// Tied directly to BPM.
return ((60 / bpm) * 1000);
return ((SECONDS_PER_MINUTE / bpm) * MILLIS_PER_SECOND);
}
/**

View file

@ -391,6 +391,26 @@ class Controls extends FlxActionSet
return byName[name].check();
}
public function getKeysForAction(name:Action):Array<FlxKey> {
#if debug
if (!byName.exists(name))
throw 'Invalid name: $name';
#end
return byName[name].inputs.map(function(input) return (input.device == KEYBOARD) ? input.inputID : null)
.filter(function(key) return key != null);
}
public function getButtonsForAction(name:Action):Array<FlxGamepadInputID> {
#if debug
if (!byName.exists(name))
throw 'Invalid name: $name';
#end
return byName[name].inputs.map(function(input) return (input.device == GAMEPAD) ? input.inputID : null)
.filter(function(key) return key != null);
}
public function getDialogueName(action:FlxActionDigital):String
{
var input = action.inputs[0];

View file

@ -192,6 +192,7 @@ abstract Tallies(RawTallies)
bad: 0,
good: 0,
sick: 0,
killer: 0,
totalNotes: 0,
totalNotesHit: 0,
maxCombo: 0,
@ -213,6 +214,7 @@ typedef RawTallies =
var bad:Int;
var good:Int;
var sick:Int;
var killer:Int;
var maxCombo:Int;
var isNewHighscore:Bool;

View file

@ -2,14 +2,15 @@ package funkin;
import flixel.FlxSprite;
import flixel.FlxSubState;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.group.FlxGroup;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.math.FlxMath;
import flixel.sound.FlxSound;
import flixel.system.debug.stats.StatsGraph;
import flixel.text.FlxText;
import flixel.util.FlxColor;
import funkin.audio.visualize.PolygonSpectogram;
import funkin.play.notes.NoteSprite;
import funkin.ui.CoolStatsGraph;
import haxe.Timer;
import openfl.events.KeyboardEvent;
@ -17,7 +18,7 @@ import openfl.events.KeyboardEvent;
class LatencyState extends MusicBeatSubState
{
var offsetText:FlxText;
var noteGrp:FlxTypedGroup<Note>;
var noteGrp:FlxTypedGroup<NoteSprite>;
var strumLine:FlxSprite;
var blocks:FlxTypedGroup<FlxSprite>;
@ -74,7 +75,7 @@ class LatencyState extends MusicBeatSubState
Conductor.forceBPM(60);
noteGrp = new FlxTypedGroup<Note>();
noteGrp = new FlxTypedGroup<NoteSprite>();
add(noteGrp);
diffGrp = new FlxTypedGroup<FlxText>();
@ -127,7 +128,7 @@ class LatencyState extends MusicBeatSubState
for (i in 0...32)
{
var note:Note = new Note(Conductor.beatLengthMs * i, 1);
var note:NoteSprite = new NoteSprite(Conductor.beatLengthMs * i);
noteGrp.add(note);
}
@ -246,8 +247,8 @@ class LatencyState extends MusicBeatSubState
FlxG.resetState();
}*/
noteGrp.forEach(function(daNote:Note) {
daNote.y = (strumLine.y - ((Conductor.songPosition - Conductor.audioOffset) - daNote.data.strumTime) * 0.45);
noteGrp.forEach(function(daNote:NoteSprite) {
daNote.y = (strumLine.y - ((Conductor.songPosition - Conductor.audioOffset) - daNote.noteData.time) * 0.45);
daNote.x = strumLine.x + 30;
if (daNote.y < strumLine.y) daNote.alpha = 0.5;

View file

@ -1,301 +0,0 @@
package funkin;
import funkin.play.Strumline.StrumlineArrow;
import flixel.FlxSprite;
import flixel.math.FlxMath;
import funkin.noteStuff.NoteBasic.NoteData;
import funkin.noteStuff.NoteBasic.NoteType;
import funkin.play.PlayState;
import funkin.play.Strumline.StrumlineStyle;
import funkin.shaderslmfao.ColorSwap;
import funkin.ui.PreferencesMenu;
import funkin.util.Constants;
class Note extends FlxSprite
{
public var data = new NoteData();
/**
* code colors for.... code....
* i think goes in order of left to right
*
* left 0
* down 1
* up 2
* right 3
*/
public static var codeColors:Array<Int> = [0xFFFF22AA, 0xFF00EEFF, 0xFF00CC00, 0xFFCC1111];
public var mustPress:Bool = false;
public var followsTime:Bool = true; // used if you want the note to follow the time shit!
public var canBeHit:Bool = false;
public var tooLate:Bool = false;
public var wasGoodHit:Bool = false;
public var prevNote:Note;
var willMiss:Bool = false;
public var invisNote:Bool = false;
public var isSustainNote:Bool = false;
public var colorSwap:ColorSwap;
/** the lowercase name of the note, for anim control, i.e. left right up down */
public var dirName(get, never):String;
inline function get_dirName()
return data.dirName;
/** the uppercase name of the note, for anim control, i.e. left right up down */
public var dirNameUpper(get, never):String;
inline function get_dirNameUpper()
return data.dirNameUpper;
/** the lowercase name of the note's color, for anim control, i.e. purple blue green red */
public var colorName(get, never):String;
inline function get_colorName()
return data.colorName;
/** the lowercase name of the note's color, for anim control, i.e. purple blue green red */
public var colorNameUpper(get, never):String;
inline function get_colorNameUpper()
return data.colorNameUpper;
public var highStakes(get, never):Bool;
inline function get_highStakes()
return data.highStakes;
public var lowStakes(get, never):Bool;
inline function get_lowStakes()
return data.lowStakes;
public static var swagWidth:Float = 160 * 0.7;
public static var PURP_NOTE:Int = 0;
public static var GREEN_NOTE:Int = 2;
public static var BLUE_NOTE:Int = 1;
public static var RED_NOTE:Int = 3;
// SCORING STUFF
public static var HIT_WINDOW:Float = (10 / 60) * 1000; // 166.67 ms hit window (10 frames at 60fps)
// thresholds are fractions of HIT_WINDOW ^^
// anything above bad threshold is shit
public static var BAD_THRESHOLD:Float = 0.8; // 125ms , 8 frames
public static var GOOD_THRESHOLD:Float = 0.55; // 91.67ms , 5.5 frames
public static var SICK_THRESHOLD:Float = 0.2; // 33.33ms , 2 frames
public var noteSpeedMulti:Float = 1;
public var pastHalfWay:Bool = false;
// anything below sick threshold is sick
public static var arrowColors:Array<Float> = [1, 1, 1, 1];
// Which note asset to load?
public var style:StrumlineStyle = NORMAL;
public function new(strumTime:Float = 0, noteData:NoteType, ?prevNote:Note, ?sustainNote:Bool = false, ?style:StrumlineStyle = NORMAL)
{
super();
if (prevNote == null) prevNote = this;
this.prevNote = prevNote;
isSustainNote = sustainNote;
x += 50;
// MAKE SURE ITS DEFINITELY OFF SCREEN?
y -= 2000;
data.strumTime = strumTime;
data.noteData = noteData;
this.style = style;
if (this.style == null) this.style = StrumlineStyle.NORMAL;
// TODO: Make this logic more generic
switch (this.style)
{
case PIXEL:
loadGraphic(Paths.image('weeb/pixelUI/arrows-pixels'), true, 17, 17);
animation.add('greenScroll', [6]);
animation.add('redScroll', [7]);
animation.add('blueScroll', [5]);
animation.add('purpleScroll', [4]);
if (isSustainNote)
{
loadGraphic(Paths.image('weeb/pixelUI/arrowEnds'), true, 7, 6);
animation.add('purpleholdend', [4]);
animation.add('greenholdend', [6]);
animation.add('redholdend', [7]);
animation.add('blueholdend', [5]);
animation.add('purplehold', [0]);
animation.add('greenhold', [2]);
animation.add('redhold', [3]);
animation.add('bluehold', [1]);
}
setGraphicSize(Std.int(width * Constants.PIXEL_ART_SCALE));
updateHitbox();
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('purpleholdend', 'pruple end hold');
animation.addByPrefix('greenholdend', 'green hold end');
animation.addByPrefix('redholdend', 'red hold end');
animation.addByPrefix('blueholdend', 'blue hold end');
animation.addByPrefix('purplehold', 'purple hold piece');
animation.addByPrefix('greenhold', 'green hold piece');
animation.addByPrefix('redhold', 'red hold piece');
animation.addByPrefix('bluehold', 'blue hold piece');
setGraphicSize(Std.int(width * 0.7));
updateHitbox();
antialiasing = true;
// colorSwap.colorToReplace = 0xFFF9393F;
// colorSwap.newColor = 0xFF00FF00;
// color = FlxG.random.color();
// color.saturation *= 4;
// replaceColor(0xFFC1C1C1, FlxColor.RED);
}
colorSwap = new ColorSwap();
shader = colorSwap.shader;
updateColors();
x += swagWidth * data.int;
animation.play(data.colorName + 'Scroll');
// trace(prevNote);
if (isSustainNote && prevNote != null)
{
alpha = 0.6;
if (PreferencesMenu.getPref('downscroll')) angle = 180;
x += width / 2;
animation.play(data.colorName + 'holdend');
updateHitbox();
x -= width / 2;
if (PlayState.instance.currentStageId.startsWith('school')) x += 30;
if (prevNote.isSustainNote)
{
prevNote.animation.play(prevNote.colorName + 'hold');
prevNote.updateHitbox();
var scaleThing:Float = Math.round((Conductor.stepLengthMs) * (0.45 * FlxMath.roundDecimal(PlayState.instance.currentChart.scrollSpeed, 2)));
// get them a LIL closer together cuz the antialiasing blurs the edges
if (antialiasing) scaleThing *= 1.0 + (1.0 / prevNote.frameHeight);
prevNote.scale.y = scaleThing / prevNote.frameHeight;
prevNote.updateHitbox();
}
}
}
public function alignToSturmlineArrow(arrow:StrumlineArrow):Void
{
x = arrow.x;
if (isSustainNote && prevNote != null)
{
if (prevNote.isSustainNote)
{
x = prevNote.x;
}
else
{
x += prevNote.width / 2;
x -= width / 2;
}
}
}
override function destroy()
{
prevNote = null;
super.destroy();
}
public function updateColors():Void
{
colorSwap.update(arrowColors[data.noteData]);
}
override function update(elapsed:Float)
{
super.update(elapsed);
// mustPress indicates the player is the one pressing the key
if (mustPress)
{
// miss on the NEXT frame so lag doesnt make u miss notes
if (willMiss && !wasGoodHit)
{
tooLate = true;
canBeHit = false;
}
else
{
if (!pastHalfWay && data.strumTime <= Conductor.songPosition)
{
pastHalfWay = true;
noteSpeedMulti *= 2;
}
if (data.strumTime > Conductor.songPosition - HIT_WINDOW)
{
// * 0.5 if sustain note, so u have to keep holding it closer to all the way thru!
if (data.strumTime < Conductor.songPosition + (HIT_WINDOW * (isSustainNote ? 0.5 : 1))) canBeHit = true;
}
else
{
canBeHit = true;
willMiss = true;
}
}
}
else
{
canBeHit = false;
if (data.strumTime <= Conductor.songPosition) wasGoodHit = true;
}
if (tooLate)
{
if (alpha > 0.3) alpha = 0.3;
}
}
static public function fromData(data:NoteData, prevNote:Note, isSustainNote = false)
{
var result = new Note(data.strumTime, data.noteData, prevNote, isSustainNote);
result.data = data;
return result;
}
}

View file

@ -96,14 +96,14 @@ class Paths
return getPath('music/$key.$SOUND_EXT', MUSIC, library);
}
inline static public function voices(song:String, ?suffix:String)
inline static public function voices(song:String, ?suffix:String = '')
{
if (suffix == null) suffix = ""; // no suffix, for a sorta backwards compatibility with older-ish voice files
return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.$SOUND_EXT';
}
inline static public function inst(song:String, ?suffix:String)
inline static public function inst(song:String, ?suffix:String = '')
{
return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.$SOUND_EXT';
}

View file

@ -2,6 +2,7 @@ package funkin;
import funkin.Controls;
import flixel.FlxCamera;
import funkin.input.PreciseInputManager;
import flixel.input.actions.FlxActionInput;
import flixel.input.gamepad.FlxGamepad;
import flixel.util.FlxSignal;
@ -52,6 +53,9 @@ class PlayerSettings
}
if (useDefault) controls.setKeyboardScheme(Solo);
// Apply loaded settings.
PreciseInputManager.instance.initializeKeys(controls);
}
function addGamepad(gamepad:FlxGamepad)

View file

@ -1,33 +0,0 @@
package funkin;
import funkin.noteStuff.NoteBasic.NoteData;
typedef SwagSection =
{
var sectionNotes:Array<NoteData>;
var lengthInSteps:Int;
var typeOfSection:Int;
var mustHitSection:Bool;
var bpm:Float;
var changeBPM:Bool;
var altAnim:Bool;
}
class Section
{
public var sectionNotes:Array<Dynamic> = [];
public var lengthInSteps:Int = 16;
public var typeOfSection:Int = 0;
public var mustHitSection:Bool = true;
/**
* Copies the first section into the second section!
*/
public static var COPYCAT:Int = 0;
public function new(lengthInSteps:Int = 16)
{
this.lengthInSteps = lengthInSteps;
}
}

View file

@ -1,325 +0,0 @@
package funkin;
import funkin.Section.SwagSection;
import funkin.noteStuff.NoteBasic.NoteData;
import funkin.play.PlayState;
import haxe.Json;
import lime.utils.Assets;
typedef SwagSong =
{
var song:String;
var notes:FunnyNotes;
var difficulties:Array<String>;
var noteMap:Map<String, Array<SwagSection>>;
var bpm:Float;
var needsVoices:Bool;
var voiceList:Array<String>;
var speed:FunnySpeed;
var speedMap:Map<String, Float>;
var player1:String;
var player2:String;
var validScore:Bool;
var extraNotes:Map<String, Array<SwagSection>>;
}
typedef FunnySpeed =
{
var ?easy:Float;
var ?normal:Float;
var ?hard:Float;
}
typedef FunnyNotes =
{
var ?easy:Array<SwagSection>;
var ?normal:Array<SwagSection>;
var ?hard:Array<SwagSection>;
}
class SongLoad
{
public static var curDiff:String = 'normal';
public static var curNotes:Array<SwagSection>;
public static var songData:SwagSong;
public static function loadFromJson(jsonInput:String, ?folder:String):SwagSong
{
var rawJson:String = null;
try
{
rawJson = Assets.getText(Paths.json('songs/${folder.toLowerCase()}/${jsonInput.toLowerCase()}')).trim();
}
catch (e)
{
trace('Failed to load song data: ${e}');
rawJson = null;
}
if (rawJson == null)
{
return null;
}
while (!rawJson.endsWith("}"))
{
rawJson = rawJson.substr(0, rawJson.length - 1);
}
return parseJSONshit(rawJson);
}
public static function getSong(?diff:String):Array<SwagSection>
{
if (diff == null) diff = SongLoad.curDiff;
var songShit:Array<SwagSection> = [];
// THIS IS OVERWRITTEN, WILL BE DEPRECTATED AND REPLACED SOOOOON
if (songData != null)
{
switch (diff)
{
case 'easy':
songShit = songData.notes.easy;
case 'normal':
songShit = songData.notes.normal;
case 'hard':
songShit = songData.notes.hard;
}
}
checkAndCreateNotemap(curDiff);
songShit = songData.noteMap[diff];
return songShit;
}
public static function checkAndCreateNotemap(diff:String):Void
{
if (songData == null || songData.noteMap == null) return;
if (songData.noteMap[diff] == null) songData.noteMap[diff] = [];
}
public static function getSpeed(?diff:String):Float
{
if (PlayState.instance != null && PlayState.instance.currentChart != null)
{
return getSpeed_NEW(diff);
}
if (diff == null) diff = SongLoad.curDiff;
var speedShit:Float = 1;
// all this shit is overridden by the thing that loads it from speedMap Map object!!!
// replace and delete later!
switch (diff)
{
case 'easy':
speedShit = songData?.speed?.easy ?? 1.0;
case 'normal':
speedShit = songData?.speed?.normal ?? 1.0;
case 'hard':
speedShit = songData?.speed?.hard ?? 1.0;
}
if (songData?.speedMap == null || songData?.speedMap[diff] == null)
{
speedShit = 1;
}
else
{
speedShit = songData.speedMap[diff];
}
return speedShit;
}
public static function getSpeed_NEW(?diff:String):Float
{
if (PlayState.instance == null
|| PlayState.instance.currentChart == null
|| PlayState.instance.currentChart.scrollSpeed == 0.0) return 1.0;
return PlayState.instance.currentChart.scrollSpeed;
}
public static function getDefaultSwagSong():SwagSong
{
return {
song: 'Test',
notes: {easy: [], normal: [], hard: []},
difficulties: ["easy", "normal", "hard"],
noteMap: new Map(),
speedMap: new Map(),
bpm: 150,
needsVoices: true,
player1: 'bf',
player2: 'dad',
speed:
{
easy: 1,
normal: 1,
hard: 1
},
validScore: false,
voiceList: ["BF", "BF-pixel"],
extraNotes: []
};
}
public static function getDefaultNoteData():NoteData
{
return new NoteData();
}
/**
* Casts the an array to NOTE data (for LOADING shit from json usually)
*/
public static function castArrayToNoteData(noteStuff:Array<SwagSection>)
{
if (noteStuff == null) return;
for (sectionIndex => section in noteStuff)
{
if (section == null || section.sectionNotes == null) continue;
for (noteIndex => noteDataArray in section.sectionNotes)
{
var arrayDipshit:Array<Dynamic> = cast noteDataArray; // crackhead
if (arrayDipshit != null) // array isnt null, that means it loaded it as an array and needs to be manually parsed?
{
// at this point noteStuff[sectionIndex].sectionNotes[noteIndex] is an array because of the cast from the first line in this function
// so this line right here turns it back into the NoteData typedef type because of another bastard cast
noteStuff[sectionIndex].sectionNotes[noteIndex] = cast SongLoad.getDefaultNoteData(); // turn it from an array (because of the cast), back to noteData? yeah that works
noteStuff[sectionIndex].sectionNotes[noteIndex].strumTime = arrayDipshit[0];
noteStuff[sectionIndex].sectionNotes[noteIndex].noteData = arrayDipshit[1];
noteStuff[sectionIndex].sectionNotes[noteIndex].sustainLength = arrayDipshit[2];
if (arrayDipshit.length > 3)
{
noteStuff[sectionIndex].sectionNotes[noteIndex].noteKind = arrayDipshit[3];
}
}
else if (noteDataArray != null)
{
// array is NULL, so it checks if noteDataArray (doesnt exactly NEED to be an 'array' is also null or not.)
// At this point it should be an OBJECT that can be easily casted!!!
noteStuff[sectionIndex].sectionNotes[noteIndex] = cast noteDataArray;
}
else
throw "shit brokey"; // i actually dont know how throw works lol
}
}
}
/**
* Cast notedata to ARRAY (usually used for level SAVING)
*/
public static function castNoteDataToArray(noteStuff:Array<SwagSection>)
{
if (noteStuff == null) return;
for (sectionIndex => section in noteStuff)
{
for (noteIndex => noteTypeDefShit in section.sectionNotes)
{
var dipshitArray:Array<Dynamic> = [
noteTypeDefShit.strumTime,
noteTypeDefShit.noteData,
noteTypeDefShit.sustainLength,
noteTypeDefShit.noteKind
];
noteStuff[sectionIndex].sectionNotes[noteIndex] = cast dipshitArray;
}
}
}
public static function castNoteDataToNoteData(noteStuff:Array<SwagSection>)
{
if (noteStuff == null) return;
for (sectionIndex => section in noteStuff)
{
for (noteIndex => noteTypedefShit in section.sectionNotes)
{
trace(noteTypedefShit);
noteStuff[sectionIndex].sectionNotes[noteIndex] = noteTypedefShit;
}
}
}
public static function parseJSONshit(rawJson:String):SwagSong
{
var songParsed:Dynamic;
try
{
songParsed = Json.parse(rawJson);
}
catch (e)
{
FlxG.log.warn("Error parsing JSON: " + e.message);
trace("Error parsing JSON: " + e.message);
return null;
}
var swagShit:SwagSong = cast songParsed.song;
swagShit.difficulties = []; // reset it to default before load
swagShit.noteMap = new Map();
swagShit.speedMap = new Map();
for (diff in Reflect.fields(songParsed.song.notes))
{
swagShit.difficulties.push(diff);
swagShit.noteMap[diff] = cast Reflect.field(songParsed.song.notes, diff);
castArrayToNoteData(swagShit.noteMap[diff]);
// castNoteDataToNoteData(swagShit.noteMap[diff]);
/*
switch (diff)
{
case "easy":
castArrayToNoteData(swagShit.notes.hard);
case "normal":
castArrayToNoteData(swagShit.notes.normal);
case "hard":
castArrayToNoteData(swagShit.notes.hard);
}
*/
}
for (diff in swagShit.difficulties)
{
swagShit.speedMap[diff] = cast Reflect.field(songParsed.song.speed, diff);
}
// trace(swagShit.noteMap.toString());
// trace(swagShit.speedMap.toString());
// trace('that was just notemap string lol');
swagShit.validScore = true;
trace("SONG SHIT ABOUTTA WEEK AGOOO");
for (field in Reflect.fields(Json.parse(rawJson).song.speed))
{
// swagShit.speed[field] = Reflect.field(Json.parse(rawJson).song.speed, field);
// swagShit.notes[field] = Reflect.field(Json.parse(rawJson).song.notes, field);
// trace(swagShit.notes[field]);
}
// swagShit.notes = cast Json.parse(rawJson).song.notes[SongLoad.curDiff]; // by default uses
trace('THAT SHIT WAS JUST THE NORMAL NOTES!!!');
songData = swagShit;
// curNotes = songData.notes.get('normal');
return swagShit;
}
}

View file

@ -9,6 +9,7 @@ import flixel.FlxG; // This one in particular causes a compile error if you're u
using Lambda;
using StringTools;
using funkin.util.tools.ArrayTools;
using funkin.util.tools.ArraySortTools;
using funkin.util.tools.IteratorTools;
using funkin.util.tools.MapTools;
using funkin.util.tools.StringTools;

View file

@ -0,0 +1,303 @@
package funkin.input;
import openfl.ui.Keyboard;
import funkin.play.notes.NoteDirection;
import flixel.input.keyboard.FlxKeyboard.FlxKeyInput;
import openfl.events.KeyboardEvent;
import flixel.FlxG;
import flixel.input.FlxInput.FlxInputState;
import flixel.input.FlxKeyManager;
import flixel.input.keyboard.FlxKey;
import flixel.input.keyboard.FlxKeyList;
import flixel.util.FlxSignal.FlxTypedSignal;
import haxe.Int64;
import lime.ui.KeyCode;
import lime.ui.KeyModifier;
/**
* A precise input manager that:
* - Records the exact timestamp of when a key was pressed or released
* - Only records key presses for keys bound to game inputs (up/down/left/right)
*/
class PreciseInputManager extends FlxKeyManager<FlxKey, PreciseInputList>
{
public static var instance(get, null):PreciseInputManager;
static function get_instance():PreciseInputManager
{
return instance ?? (instance = new PreciseInputManager());
}
static final MS_TO_US:Int64 = 1000;
static final US_TO_NS:Int64 = 1000;
static final MS_TO_NS:Int64 = MS_TO_US * US_TO_NS;
static final DIRECTIONS:Array<NoteDirection> = [NoteDirection.LEFT, NoteDirection.DOWN, NoteDirection.UP, NoteDirection.RIGHT];
public var onInputPressed:FlxTypedSignal<PreciseInputEvent->Void>;
public var onInputReleased:FlxTypedSignal<PreciseInputEvent->Void>;
/**
* The list of keys that are bound to game inputs (up/down/left/right).
*/
var _keyList:Array<FlxKey>;
/**
* The direction that a given key is bound to.
*/
var _keyListDir:Map<FlxKey, NoteDirection>;
/**
* The timestamp at which a given note direction was last pressed.
*/
var _dirPressTimestamps:Map<NoteDirection, Int64>;
/**
* The timestamp at which a given note direction was last released.
*/
var _dirReleaseTimestamps:Map<NoteDirection, Int64>;
public function new()
{
super(PreciseInputList.new);
_keyList = [];
_dirPressTimestamps = new Map<NoteDirection, Int64>();
_dirReleaseTimestamps = new Map<NoteDirection, Int64>();
_keyListDir = new Map<FlxKey, NoteDirection>();
FlxG.stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
FlxG.stage.removeEventListener(KeyboardEvent.KEY_UP, onKeyUp);
FlxG.stage.application.window.onKeyDownPrecise.add(handleKeyDown);
FlxG.stage.application.window.onKeyUpPrecise.add(handleKeyUp);
preventDefaultKeys = getPreventDefaultKeys();
onInputPressed = new FlxTypedSignal<PreciseInputEvent->Void>();
onInputReleased = new FlxTypedSignal<PreciseInputEvent->Void>();
}
public static function getKeysForDirection(controls:Controls, noteDirection:NoteDirection)
{
return switch (noteDirection)
{
case NoteDirection.LEFT: controls.getKeysForAction(NOTE_LEFT);
case NoteDirection.DOWN: controls.getKeysForAction(NOTE_DOWN);
case NoteDirection.UP: controls.getKeysForAction(NOTE_UP);
case NoteDirection.RIGHT: controls.getKeysForAction(NOTE_RIGHT);
};
}
/**
* Returns a precise timestamp, measured in nanoseconds.
* Timestamp is only useful for comparing against other timestamps.
*
* @return Int64
*/
@:access(lime._internal.backend.native.NativeCFFI)
public static function getCurrentTimestamp():Int64
{
#if html5
// NOTE: This timestamp isn't that precise on standard HTML5 builds.
// This is because of browser safeguards against timing attacks.
// See https://web.dev/coop-coep to enable headers which allow for more precise timestamps.
return js.Browser.window.performance.now() * MS_TO_NS;
#elseif cpp
// NOTE: If the game hard crashes on this line, rebuild Lime!
// `lime rebuild windows -clean`
return lime._internal.backend.native.NativeCFFI.lime_sdl_get_ticks() * MS_TO_NS;
#else
throw "Eric didn't implement precise timestamps on this platform!";
#end
}
static function getPreventDefaultKeys():Array<FlxKey>
{
return [];
}
/**
* Call this whenever the user's inputs change.
*/
public function initializeKeys(controls:Controls):Void
{
clearKeys();
for (noteDirection in DIRECTIONS)
{
var keys = getKeysForDirection(controls, noteDirection);
for (key in keys)
{
var input = new FlxKeyInput(key);
_keyList.push(key);
_keyListArray.push(input);
_keyListMap.set(key, input);
_keyListDir.set(key, noteDirection);
}
}
}
/**
* Get the time, in nanoseconds, since the given note direction was last pressed.
* @param noteDirection The note direction to check.
* @return An Int64 representing the time since the given note direction was last pressed.
*/
public function getTimeSincePressed(noteDirection:NoteDirection):Int64
{
return getCurrentTimestamp() - _dirPressTimestamps.get(noteDirection);
}
/**
* Get the time, in nanoseconds, since the given note direction was last released.
* @param noteDirection The note direction to check.
* @return An Int64 representing the time since the given note direction was last released.
*/
public function getTimeSinceReleased(noteDirection:NoteDirection):Int64
{
return getCurrentTimestamp() - _dirReleaseTimestamps.get(noteDirection);
}
// TODO: Why doesn't this work?
// @:allow(funkin.input.PreciseInputManager.PreciseInputList)
public function getInputByKey(key:FlxKey):FlxKeyInput
{
return _keyListMap.get(key);
}
public function getDirectionForKey(key:FlxKey):NoteDirection
{
return _keyListDir.get(key);
}
function handleKeyDown(keyCode:KeyCode, _:KeyModifier, timestamp:Int64):Void
{
var key:FlxKey = convertKeyCode(keyCode);
if (_keyList.indexOf(key) == -1) return;
// TODO: Remove this line with SDL3 when timestamps change meaning.
// This is because SDL3's timestamps are measured in nanoseconds, not milliseconds.
timestamp *= MS_TO_NS;
updateKeyStates(key, true);
if (getInputByKey(key) ?.justPressed ?? false)
{
onInputPressed.dispatch(
{
noteDirection: getDirectionForKey(key),
timestamp: timestamp
});
_dirPressTimestamps.set(getDirectionForKey(key), timestamp);
}
}
function handleKeyUp(keyCode:KeyCode, _:KeyModifier, timestamp:Int64):Void
{
var key:FlxKey = convertKeyCode(keyCode);
if (_keyList.indexOf(key) == -1) return;
// TODO: Remove this line with SDL3 when timestamps change meaning.
// This is because SDL3's timestamps are measured in nanoseconds, not milliseconds.
timestamp *= MS_TO_NS;
updateKeyStates(key, false);
if (getInputByKey(key) ?.justReleased ?? false)
{
onInputReleased.dispatch(
{
noteDirection: getDirectionForKey(key),
timestamp: timestamp
});
_dirReleaseTimestamps.set(getDirectionForKey(key), timestamp);
}
}
static function convertKeyCode(input:KeyCode):FlxKey
{
@:privateAccess
{
return Keyboard.__convertKeyCode(input);
}
}
function clearKeys():Void
{
_keyListArray = [];
_keyListMap.clear();
_keyListDir.clear();
}
}
class PreciseInputList extends FlxKeyList
{
var _preciseInputManager:PreciseInputManager;
public function new(state:FlxInputState, preciseInputManager:FlxKeyManager<Dynamic, Dynamic>)
{
super(state, preciseInputManager);
_preciseInputManager = cast preciseInputManager;
}
static function getKeysForDir(noteDir:NoteDirection):Array<FlxKey>
{
return PreciseInputManager.getKeysForDirection(PlayerSettings.player1.controls, noteDir);
}
function isKeyValid(key:FlxKey):Bool
{
@:privateAccess
{
return _preciseInputManager._keyListMap.exists(key);
}
}
public function checkFlxKey(key:FlxKey):Bool
{
if (isKeyValid(key)) return check(cast key);
return false;
}
public function checkDir(noteDir:NoteDirection):Bool
{
for (key in getKeysForDir(noteDir))
{
if (check(_preciseInputManager.getInputByKey(key) ?.ID)) return true;
}
return false;
}
public var NOTE_LEFT(get, never):Bool;
function get_NOTE_LEFT():Bool
return checkDir(NoteDirection.LEFT);
public var NOTE_DOWN(get, never):Bool;
function get_NOTE_DOWN():Bool
return checkDir(NoteDirection.DOWN);
public var NOTE_UP(get, never):Bool;
function get_NOTE_UP():Bool
return checkDir(NoteDirection.UP);
public var NOTE_RIGHT(get, never):Bool;
function get_NOTE_RIGHT():Bool
return checkDir(NoteDirection.RIGHT);
}
typedef PreciseInputEvent =
{
/**
* The direction of the input.
*/
noteDirection:NoteDirection,
/**
* The timestamp of the input. Measured in nanoseconds.
*/
timestamp:Int64,
};

View file

@ -1,10 +1,12 @@
package funkin.modding.events;
import funkin.play.song.SongData.SongNoteData;
import flixel.FlxState;
import flixel.FlxSubState;
import funkin.noteStuff.NoteBasic.NoteDir;
import funkin.play.notes.NoteSprite;
import funkin.play.cutscene.dialogue.Conversation;
import funkin.play.Countdown.CountdownStep;
import funkin.play.notes.NoteDirection;
import openfl.events.EventType;
import openfl.events.KeyboardEvent;
@ -344,7 +346,7 @@ class NoteScriptEvent extends ScriptEvent
* The note associated with this event.
* You cannot replace it, but you can edit it.
*/
public var note(default, null):Note;
public var note(default, null):NoteSprite;
/**
* The combo count as it is with this event.
@ -357,7 +359,7 @@ class NoteScriptEvent extends ScriptEvent
*/
public var playSound(default, default):Bool;
public function new(type:ScriptEventType, note:Note, comboCount:Int = 0, cancelable:Bool = false):Void
public function new(type:ScriptEventType, note:NoteSprite, comboCount:Int = 0, cancelable:Bool = false):Void
{
super(type, cancelable);
this.note = note;
@ -379,7 +381,7 @@ class GhostMissNoteScriptEvent extends ScriptEvent
/**
* The direction that was mistakenly pressed.
*/
public var dir(default, null):NoteDir;
public var dir(default, null):NoteDirection;
/**
* Whether there was a note within judgement range when this ghost note was pressed.
@ -407,7 +409,7 @@ class GhostMissNoteScriptEvent extends ScriptEvent
*/
public var playAnim(default, default):Bool;
public function new(dir:NoteDir, hasPossibleNotes:Bool, healthChange:Float, scoreChange:Int):Void
public function new(dir:NoteDirection, hasPossibleNotes:Bool, healthChange:Float, scoreChange:Int):Void
{
super(ScriptEvent.NOTE_GHOST_MISS, true);
this.dir = dir;
@ -575,19 +577,19 @@ class SongLoadScriptEvent extends ScriptEvent
* The note associated with this event.
* You cannot replace it, but you can edit it.
*/
public var notes(default, set):Array<Note>;
public var notes(default, set):Array<SongNoteData>;
public var id(default, null):String;
public var difficulty(default, null):String;
function set_notes(notes:Array<Note>):Array<Note>
function set_notes(notes:Array<SongNoteData>):Array<SongNoteData>
{
this.notes = notes;
return this.notes;
}
public function new(id:String, difficulty:String, notes:Array<Note>):Void
public function new(id:String, difficulty:String, notes:Array<SongNoteData>):Void
{
super(ScriptEvent.SONG_LOADED, false);
this.id = id;

View file

@ -1,197 +0,0 @@
package funkin.noteStuff;
import flixel.FlxSprite;
import flixel.text.FlxText;
typedef RawNoteData =
{
var strumTime:Float;
var noteData:NoteType;
var sustainLength:Float;
var altNote:String;
var noteKind:NoteKind;
}
@:forward
abstract NoteData(RawNoteData)
{
public function new(strumTime = 0.0, noteData:NoteType = 0, sustainLength = 0.0, altNote = "", noteKind = NORMAL)
{
this =
{
strumTime: strumTime,
noteData: noteData,
sustainLength: sustainLength,
altNote: altNote,
noteKind: noteKind
}
}
public var note(get, never):NoteType;
inline function get_note()
return this.noteData.value;
public var int(get, never):Int;
inline function get_int()
return this.noteData.int;
public var dir(get, never):NoteDir;
inline function get_dir()
return this.noteData.value;
public var dirName(get, never):String;
inline function get_dirName()
return dir.name;
public var dirNameUpper(get, never):String;
inline function get_dirNameUpper()
return dir.nameUpper;
public var color(get, never):NoteColor;
inline function get_color()
return this.noteData.value;
public var colorName(get, never):String;
inline function get_colorName()
return color.name;
public var colorNameUpper(get, never):String;
inline function get_colorNameUpper()
return color.nameUpper;
public var highStakes(get, never):Bool;
inline function get_highStakes()
return this.noteData.highStakes;
public var lowStakes(get, never):Bool;
inline function get_lowStakes()
return this.noteData.lowStakes;
}
enum abstract NoteType(Int) from Int to Int
{
// public var raw(get, never):Int;
// inline function get_raw() return this;
public var int(get, never):Int;
inline function get_int()
return this < 0 ? -this : this % 4;
public var value(get, never):NoteType;
inline function get_value()
return int;
public var highStakes(get, never):Bool;
inline function get_highStakes()
return this > 3;
public var lowStakes(get, never):Bool;
inline function get_lowStakes()
return this < 0;
}
@:forward
enum abstract NoteDir(NoteType) from Int to Int from NoteType
{
var LEFT = 0;
var DOWN = 1;
var UP = 2;
var RIGHT = 3;
var value(get, never):NoteDir;
inline function get_value()
return this.value;
public var name(get, never):String;
function get_name()
{
return switch (value)
{
case LEFT: "left";
case DOWN: "down";
case UP: "up";
case RIGHT: "right";
}
}
public var nameUpper(get, never):String;
function get_nameUpper()
{
return switch (value)
{
case LEFT: "LEFT";
case DOWN: "DOWN";
case UP: "UP";
case RIGHT: "RIGHT";
}
}
}
@:forward
enum abstract NoteColor(NoteType) from Int to Int from NoteType
{
var PURPLE = 0;
var BLUE = 1;
var GREEN = 2;
var RED = 3;
var value(get, never):NoteColor;
inline function get_value()
return this.value;
public var name(get, never):String;
function get_name()
{
return switch (value)
{
case PURPLE: "purple";
case BLUE: "blue";
case GREEN: "green";
case RED: "red";
}
}
public var nameUpper(get, never):String;
function get_nameUpper()
{
return switch (value)
{
case PURPLE: "PURPLE";
case BLUE: "BLUE";
case GREEN: "GREEN";
case RED: "RED";
}
}
}
enum abstract NoteKind(String) from String to String
{
/**
* The default note type.
*/
var NORMAL = "normal";
// Testing shiz
var PYRO_LIGHT = "pyro_light";
var PYRO_KICK = "pyro_kick";
var PYRO_TOSS = "pyro_toss";
var PYRO_COCK = "pyro_cock"; // lol
var PYRO_SHOOT = "pyro_shoot";
}

View file

@ -1,12 +0,0 @@
package funkin.noteStuff;
import funkin.noteStuff.NoteBasic.NoteType;
import funkin.play.Strumline.StrumlineStyle;
class NoteEvent extends Note
{
public function new(strumTime:Float = 0, noteData:NoteType, ?prevNote:Note, ?sustainNote:Bool = false, ?style:StrumlineStyle = NORMAL)
{
super(strumTime, noteData, prevNote, sustainNote, style);
}
}

View file

@ -1,98 +0,0 @@
package funkin.noteStuff;
import haxe.Json;
import openfl.Assets;
/**
* Just various functions that IDK where to put em!!!
* Semi-temp for now? the note stuff is super clutter-y right now
* so I am putting this new stuff here right now XDD
*
* A lot of this stuff can probably be moved to where appropriate!
* i dont care about NoteUtil.hx at all!!!
*/
class NoteUtil
{
/**
* IDK THING FOR BOTH LOL! DIS SHIT HACK-Y
* @param jsonPath
* @return Map<Int, Array<SongEventInfo>>
*/
public static function loadSongEvents(jsonPath:String):Map<Int, Array<SongEventInfo>>
{
return parseSongEvents(loadSongEventFromJson(jsonPath));
}
public static function loadSongEventFromJson(jsonPath:String):Array<SongEvent>
{
var daEvents:Array<SongEvent>;
daEvents = cast Json.parse(Assets.getText(jsonPath)).events; // DUMB LIL DETAIL HERE: MAKE SURE THAT .events IS THERE??
trace('GET JSON SONG EVENTS:');
trace(daEvents);
return daEvents;
}
/**
* Parses song event json stuff into a neater lil map grouping?
* @param songEvents
*/
public static function parseSongEvents(songEvents:Array<SongEvent>):Map<Int, Array<SongEventInfo>>
{
var songData:Map<Int, Array<SongEventInfo>> = new Map();
for (songEvent in songEvents)
{
trace(songEvent);
if (songData[songEvent.t] == null) songData[songEvent.t] = [];
songData[songEvent.t].push({songEventType: songEvent.e, value: songEvent.v, activated: false});
}
trace("FINISH SONG EVENTS!");
trace(songData);
return songData;
}
public static function checkSongEvents(songData:Map<Int, Array<SongEventInfo>>, time:Float)
{
for (eventGrp in songData.keys())
{
if (time >= eventGrp)
{
for (events in songData[eventGrp])
{
if (!events.activated)
{
// TURN TO NICER SWITCH STATEMENT CHECKER OF EVENT TYPES!!
trace(events.value);
trace(eventGrp);
trace(Conductor.songPosition);
events.activated = true;
}
}
}
}
}
}
typedef SongEventInfo =
{
var songEventType:SongEventType;
var value:Dynamic;
var activated:Bool;
}
typedef SongEvent =
{
var t:Int;
var e:SongEventType;
var v:Dynamic;
}
enum abstract SongEventType(String)
{
var FocusCamera;
var PlayCharAnim;
var Trace;
}

File diff suppressed because it is too large Load diff

View file

@ -1,253 +0,0 @@
package funkin.play;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxPoint;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import funkin.noteStuff.NoteBasic.NoteColor;
import funkin.noteStuff.NoteBasic.NoteDir;
import funkin.noteStuff.NoteBasic.NoteType;
import funkin.ui.PreferencesMenu;
import funkin.util.Constants;
/**
* A group controlling the individual notes of the strumline for a given player.
*
* FUN FACT: Setting the X and Y of a FlxSpriteGroup will move all the sprites in the group.
*/
class Strumline extends FlxTypedSpriteGroup<StrumlineArrow>
{
/**
* The style of the strumline.
* Options are normal and pixel.
*/
var style:StrumlineStyle;
/**
* The player this strumline belongs to.
* 0 is Player 1, etc.
*/
var playerId:Int;
/**
* The number of notes in the strumline.
*/
var size:Int;
public function new(playerId:Int = 0, style:StrumlineStyle = NORMAL, size:Int = 4)
{
super(0);
this.playerId = playerId;
this.style = style;
this.size = size;
generateStrumline();
}
function generateStrumline():Void
{
for (index in 0...size)
{
createStrumlineArrow(index);
}
}
function createStrumlineArrow(index:Int):Void
{
var arrow:StrumlineArrow = new StrumlineArrow(index, style);
add(arrow);
}
/**
* 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(arrow:FlxSprite):Void
{
arrow.y -= 10;
arrow.alpha = 0;
FlxTween.tween(arrow, {y: arrow.y + 10, alpha: 1}, 1, {ease: FlxEase.circOut, startDelay: 0.5 + (0.2 * arrow.ID)});
}
public function fadeInArrows():Void
{
for (arrow in this.members)
{
fadeInArrow(arrow);
}
}
function updatePositions()
{
for (arrow in members)
{
arrow.x = Note.swagWidth * arrow.ID;
arrow.x += offset.x;
arrow.y = 0;
arrow.y += offset.y;
}
}
/**
* Retrieves the arrow at the given position in the strumline.
* @param index The index to retrieve.
* @return The corresponding FlxSprite.
*/
public inline function getArrow(value:Int):StrumlineArrow
{
// members maintains the order that the arrows were added.
return this.members[value];
}
public inline function getArrowByNoteType(value:NoteType):StrumlineArrow
{
return getArrow(value.int);
}
public inline function getArrowByNoteDir(value:NoteDir):StrumlineArrow
{
return getArrow(value.int);
}
public inline function getArrowByNoteColor(value:funkin.noteStuff.NoteBasic.NoteColor):StrumlineArrow
{
return getArrow(value.int);
}
/**
* Get the default Y offset of the strumline.
* @return Int
*/
public static inline function getYPos():Int
{
return PreferencesMenu.getPref('downscroll') ? (FlxG.height - 150) : 50;
}
}
class StrumlineArrow extends FlxSprite
{
var style:StrumlineStyle;
public function new(id:Int, style:StrumlineStyle)
{
super(0, 0);
this.ID = id;
this.style = style;
// TODO: Unhardcode this. Maybe use a note style system>
switch (style)
{
case PIXEL:
buildPixelGraphic();
case NORMAL:
buildNormalGraphic();
}
this.updateHitbox();
scrollFactor.set(0, 0);
animation.play('static');
}
public function playAnimation(anim:String, force:Bool = false)
{
animation.play(anim, force);
centerOffsets();
centerOrigin();
}
/**
* Applies the default note style to an arrow.
*/
function buildNormalGraphic():Void
{
this.frames = Paths.getSparrowAtlas('NOTE_assets');
this.animation.addByPrefix('green', 'arrowUP');
this.animation.addByPrefix('blue', 'arrowDOWN');
this.animation.addByPrefix('purple', 'arrowLEFT');
this.animation.addByPrefix('red', 'arrowRIGHT');
this.setGraphicSize(Std.int(this.width * 0.7));
this.antialiasing = true;
this.x += Note.swagWidth * this.ID;
switch (Math.abs(this.ID))
{
case 0:
this.animation.addByPrefix('static', 'arrow static instance 1');
this.animation.addByPrefix('pressed', 'left press', 24, false);
this.animation.addByPrefix('confirm', 'left confirm', 24, false);
case 1:
this.animation.addByPrefix('static', 'arrow static instance 2');
this.animation.addByPrefix('pressed', 'down press', 24, false);
this.animation.addByPrefix('confirm', 'down confirm', 24, false);
case 2:
this.animation.addByPrefix('static', 'arrow static instance 4');
this.animation.addByPrefix('pressed', 'up press', 24, false);
this.animation.addByPrefix('confirm', 'up confirm', 24, false);
case 3:
this.animation.addByPrefix('static', 'arrow static instance 3');
this.animation.addByPrefix('pressed', 'right press', 24, false);
this.animation.addByPrefix('confirm', 'right confirm', 24, false);
}
}
/**
* Applies the pixel note style to an arrow.
*/
function buildPixelGraphic():Void
{
this.loadGraphic(Paths.image('weeb/pixelUI/arrows-pixels'), true, 17, 17);
this.animation.add('purplel', [4]);
this.animation.add('blue', [5]);
this.animation.add('green', [6]);
this.animation.add('red', [7]);
this.setGraphicSize(Std.int(this.width * Constants.PIXEL_ART_SCALE));
this.updateHitbox();
// Forcibly disable anti-aliasing on pixel graphics to stop blur.
this.antialiasing = false;
this.x += Note.swagWidth * this.ID;
// TODO: Seems weird that these are hardcoded like this... no XML?
switch (Math.abs(this.ID))
{
case 0:
this.animation.add('static', [0]);
this.animation.add('pressed', [4, 8], 12, false);
this.animation.add('confirm', [12, 16], 24, false);
case 1:
this.animation.add('static', [1]);
this.animation.add('pressed', [5, 9], 12, false);
this.animation.add('confirm', [13, 17], 24, false);
case 2:
this.animation.add('static', [2]);
this.animation.add('pressed', [6, 10], 12, false);
this.animation.add('confirm', [14, 18], 12, false);
case 3:
this.animation.add('static', [3]);
this.animation.add('pressed', [7, 11], 12, false);
this.animation.add('confirm', [15, 19], 24, false);
}
}
}
/**
* TODO: Unhardcode this and make it part of the note style system.
*/
enum StrumlineStyle
{
NORMAL;
PIXEL;
}

View file

@ -2,10 +2,10 @@ package funkin.play.character;
import flixel.math.FlxPoint;
import funkin.modding.events.ScriptEvent;
import funkin.noteStuff.NoteBasic.NoteDir;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.character.CharacterData.CharacterRenderType;
import funkin.play.stage.Bopper;
import funkin.play.notes.NoteDirection;
/**
* A Character is a stage prop which bops to the music as well as controlled by the strumlines.
@ -488,16 +488,16 @@ class BaseCharacter extends Bopper
{
super.onNoteHit(event);
if (event.note.mustPress && characterType == BF)
if (event.note.noteData.getMustHitNote() && characterType == BF)
{
// If the note is from the same strumline, play the sing animation.
this.playSingAnimation(event.note.data.dir, false);
this.playSingAnimation(event.note.noteData.getDirection(), false);
holdTimer = 0;
}
else if (!event.note.mustPress && characterType == DAD)
else if (!event.note.noteData.getMustHitNote() && characterType == DAD)
{
// If the note is from the same strumline, play the sing animation.
this.playSingAnimation(event.note.data.dir, false);
this.playSingAnimation(event.note.noteData.getDirection(), false);
holdTimer = 0;
}
}
@ -510,17 +510,17 @@ class BaseCharacter extends Bopper
{
super.onNoteMiss(event);
if (event.note.mustPress && characterType == BF)
if (event.note.noteData.getMustHitNote() && characterType == BF)
{
// If the note is from the same strumline, play the sing animation.
this.playSingAnimation(event.note.data.dir, true);
this.playSingAnimation(event.note.noteData.getDirection(), true);
}
else if (!event.note.mustPress && characterType == DAD)
else if (!event.note.noteData.getMustHitNote() && characterType == DAD)
{
// If the note is from the same strumline, play the sing animation.
this.playSingAnimation(event.note.data.dir, true);
this.playSingAnimation(event.note.noteData.getDirection(), true);
}
else if (event.note.mustPress && characterType == GF)
else if (event.note.noteData.getMustHitNote() && characterType == GF)
{
var dropAnim = '';
@ -575,7 +575,7 @@ class BaseCharacter extends Bopper
* @param miss If true, play the miss animation instead of the sing animation.
* @param suffix A suffix to append to the animation name, like `alt`.
*/
public function playSingAnimation(dir:NoteDir, ?miss:Bool = false, ?suffix:String = ''):Void
public function playSingAnimation(dir:NoteDirection, ?miss:Bool = false, ?suffix:String = ''):Void
{
var anim:String = 'sing${dir.nameUpper}${miss ? 'miss' : ''}${suffix != '' ? '-${suffix}' : ''}';

View file

@ -0,0 +1,82 @@
package funkin.play.notes;
import funkin.util.Constants;
import flixel.util.FlxColor;
/**
* The direction of a note.
* This has implicit casting set up, so you can use this as an integer.
*/
enum abstract NoteDirection(Int) from Int to Int
{
var LEFT = 0;
var DOWN = 1;
var UP = 2;
var RIGHT = 3;
public var name(get, never):String;
public var nameUpper(get, never):String;
public var color(get, never):FlxColor;
public var colorName(get, never):String;
@:from
public static function fromInt(value:Int):NoteDirection
{
return switch (value % 4)
{
case 0: LEFT;
case 1: DOWN;
case 2: UP;
case 3: RIGHT;
default: LEFT;
}
}
function get_name():String
{
return switch (abstract)
{
case LEFT:
'left';
case DOWN:
'down';
case UP:
'up';
case RIGHT:
'right';
default:
'unknown';
}
}
function get_nameUpper():String
{
return abstract.name.toUpperCase();
}
function get_color():FlxColor
{
return Constants.COLOR_NOTES[this];
}
function get_colorName():String
{
return switch (abstract)
{
case LEFT:
'purple';
case DOWN:
'blue';
case UP:
'green';
case RIGHT:
'red';
default:
'unknown';
}
}
public function toString():String
{
return abstract.name;
}
}

View file

@ -0,0 +1,90 @@
package funkin.play.notes;
import funkin.play.notes.NoteDirection;
import flixel.graphics.frames.FlxFramesCollection;
import flixel.FlxG;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.FlxSprite;
class NoteSplash extends FlxSprite
{
static final ALPHA:Float = 0.6;
static final FRAMERATE_DEFAULT:Int = 24;
static final FRAMERATE_VARIANCE:Int = 2;
static var frameCollection:FlxFramesCollection;
public static function preloadFrames():Void
{
frameCollection = Paths.getSparrowAtlas('noteSplashes');
}
public function new()
{
super(0, 0);
setup();
this.alpha = ALPHA;
this.antialiasing = true;
this.animation.finishCallback = this.onAnimationFinished;
}
/**
* Add ALL the animations to this sprite. We will recycle and reuse the FlxSprite multiple times.
*/
function setup():Void
{
if (frameCollection == null) preloadFrames();
this.frames = frameCollection;
this.animation.addByPrefix('splash1Left', 'note impact 1 purple0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash1Down', 'note impact 1 blue0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash1Up', 'note impact 1 green0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash1Right', 'note impact 1 red0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash2Left', 'note impact 2 purple0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash2Down', 'note impact 2 blue0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash2Up', 'note impact 2 green0', FRAMERATE_DEFAULT, false, false, false);
this.animation.addByPrefix('splash2Right', 'note impact 2 red0', FRAMERATE_DEFAULT, false, false, false);
if (this.animation.getAnimationList().length < 8)
{
trace('WARNING: NoteSplash failed to initialize all animations.');
}
}
public function playAnimation(name:String, force:Bool = false, reversed:Bool = false, startFrame:Int = 0):Void
{
this.animation.play(name, force, reversed, startFrame);
}
public function play(direction:NoteDirection, variant:Int = null):Void
{
if (variant == null) variant = FlxG.random.int(1, 2);
switch (direction)
{
case NoteDirection.LEFT:
this.playAnimation('splash${variant}Left');
case NoteDirection.DOWN:
this.playAnimation('splash${variant}Down');
case NoteDirection.UP:
this.playAnimation('splash${variant}Up');
case NoteDirection.RIGHT:
this.playAnimation('splash${variant}Right');
}
// Vary the speed of the animation a bit.
animation.curAnim.frameRate = FRAMERATE_DEFAULT + FlxG.random.int(-FRAMERATE_VARIANCE, FRAMERATE_VARIANCE);
// Center the animation on the note splash.
offset.set(width * 0.3, height * 0.3);
}
public function onAnimationFinished(animationName:String):Void
{
// *lightning* *zap* *crackle*
this.kill();
}
}

View file

@ -0,0 +1,178 @@
package funkin.play.notes;
import funkin.play.song.SongData.SongNoteData;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.FlxSprite;
class NoteSprite extends FlxSprite
{
static final DIRECTION_COLORS:Array<String> = ['purple', 'blue', 'green', 'red'];
public var holdNoteSprite:SustainTrail;
/**
* The time at which the note should be hit, in milliseconds.
*/
public var strumTime(default, set):Float;
function set_strumTime(value:Float):Float
{
this.strumTime = value;
return this.strumTime;
}
/**
* The length of the note's sustain, in milliseconds.
* If 0, the note is a tap note.
*/
public var length(default, set):Float;
function set_length(value:Float):Float
{
this.length = value;
this.isSustainNote = (this.length > 0);
return this.length;
}
/**
* The time at which the note should be hit, in steps.
*/
public var stepTime(get, never):Float;
function get_stepTime():Float
{
// TODO: Account for changes in BPM.
return this.strumTime / Conductor.stepLengthMs;
}
/**
* An extra attribute for the note.
* For example, whether the note is an "alt" note, or whether it has custom behavior on hit.
*/
public var kind(default, set):String;
function set_kind(value:String):String
{
this.kind = value;
return this.kind;
}
/**
* The data of the note (i.e. the direction.)
*/
public var direction(default, set):NoteDirection;
function set_direction(value:Int):Int
{
if (frames == null) return value;
animation.play(DIRECTION_COLORS[value] + 'Scroll');
this.direction = value;
return this.direction;
}
public var noteData:SongNoteData;
public var isSustainNote:Bool = false;
/**
* Set this flag to true when hitting the note to avoid scoring it multiple times.
*/
public var hasBeenHit:Bool = false;
/**
* Register this note as hit only after any other notes
*/
public var lowPriority:Bool = false;
/**
* This is true if the note has been fully missed by the player.
* It will be destroyed immediately.
*/
public var hasMissed:Bool;
/**
* This is true if the note is earlier than 10 frames within the strumline.
* and thus can't be hit by the player.
* Managed by PlayState.
*/
public var tooEarly:Bool;
/**
* This is true if the note is within 10 frames of the strumline,
* and thus may be hit by the player.
* Managed by PlayState.
*/
public var mayHit:Bool;
/**
* This is true if the note is earlier than 10 frames after the strumline,
* and thus can't be hit by the player.
* Managed by PlayState.
*/
public var tooLate:Bool;
public function new(strumTime:Float = 0, direction:Int = 0)
{
super(0, -9999);
this.strumTime = strumTime;
this.direction = direction;
if (this.strumTime < 0) this.strumTime = 0;
setupNoteGraphic();
// Disables the update() function for performance.
this.active = false;
}
public static function buildNoteFrames(force:Bool = false):FlxAtlasFrames
{
// static variables inside functions are a cool of Haxe 4.3.0.
static var noteFrames:FlxAtlasFrames = null;
if (noteFrames != null && !force) return noteFrames;
noteFrames = Paths.getSparrowAtlas('NOTE_assets');
noteFrames.parent.persist = true;
return noteFrames;
}
function setupNoteGraphic():Void
{
this.frames = buildNoteFrames();
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');
animation.addByPrefix('redholdend', 'red hold end');
animation.addByPrefix('blueholdend', 'blue hold end');
animation.addByPrefix('purplehold', 'purple hold piece');
animation.addByPrefix('greenhold', 'green hold piece');
animation.addByPrefix('redhold', 'red hold piece');
animation.addByPrefix('bluehold', 'blue hold piece');
setGraphicSize(Strumline.STRUMLINE_SIZE);
updateHitbox();
antialiasing = true;
}
public override function revive():Void
{
super.revive();
this.active = false;
this.tooEarly = false;
this.hasBeenHit = false;
this.mayHit = false;
this.tooLate = false;
this.hasMissed = false;
}
}

View file

@ -0,0 +1,565 @@
package funkin.play.notes;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import funkin.ui.PreferencesMenu;
import funkin.play.notes.NoteSprite;
import flixel.util.FlxSort;
import funkin.play.notes.SustainTrail;
import funkin.util.SortUtil;
import funkin.play.song.SongData.SongNoteData;
import flixel.FlxG;
import flixel.group.FlxSpriteGroup;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
/**
* 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 = 112;
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, null):Float;
static function get_RENDER_DISTANCE_MS():Float
{
return FlxG.height / 0.45;
}
public var isPlayer:Bool;
/**
* 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>;
var strumlineNotes:FlxTypedSpriteGroup<StrumlineNote>;
var noteSplashes:FlxTypedSpriteGroup<NoteSplash>;
var sustainSplashes:FlxTypedSpriteGroup<NoteSplash>;
var noteData:Array<SongNoteData> = [];
var nextNoteIndex:Int = -1;
public function new(isPlayer:Bool)
{
super();
this.isPlayer = isPlayer;
this.strumlineNotes = new FlxTypedSpriteGroup<StrumlineNote>();
this.add(this.strumlineNotes);
// Hold notes are added first so they render behind regular notes.
this.holdNotes = new FlxTypedSpriteGroup<SustainTrail>();
this.add(this.holdNotes);
this.notes = new FlxTypedSpriteGroup<NoteSprite>();
this.add(this.notes);
this.noteSplashes = new FlxTypedSpriteGroup<NoteSplash>(0, 0, NOTE_SPLASH_CAP);
this.add(this.noteSplashes);
for (i in 0...DIRECTIONS.length)
{
var child:StrumlineNote = new StrumlineNote(isPlayer, DIRECTIONS[i]);
child.x = getXPos(DIRECTIONS[i]);
child.x += INITIAL_OFFSET;
child.y = 0;
this.strumlineNotes.add(child);
}
// This MUST be true for children to update!
this.active = true;
}
override function get_width():Float
{
return 4 * Strumline.NOTE_SPACING;
}
public override function update(elapsed:Float):Void
{
super.update(elapsed);
updateNotes();
}
/**
* Get a list of notes within + or - the given strumtime.
* @param strumTime The current time.
* @param hitWindow The hit window to check.
*/
public function getNotesInRange(strumTime:Float, hitWindow:Float):Array<NoteSprite>
{
var hitWindowStart:Float = strumTime - hitWindow;
var hitWindowEnd:Float = strumTime + hitWindow;
return notes.members.filter(function(note:NoteSprite) {
return note != null && note.alive && !note.hasBeenHit && note.strumTime >= hitWindowStart && note.strumTime <= hitWindowEnd;
});
}
public function getHoldNotesInRange(strumTime:Float, hitWindow:Float):Array<SustainTrail>
{
var hitWindowStart:Float = strumTime - hitWindow;
var hitWindowEnd:Float = strumTime + hitWindow;
return holdNotes.members.filter(function(note:SustainTrail) {
return note != null
&& note.alive
&& note.strumTime >= hitWindowStart
&& (note.strumTime + note.fullSustainLength) <= hitWindowEnd;
});
}
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;
}
/**
* 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
*/
static function calculateNoteYPos(strumTime:Float):Float
{
// Make the note move faster visually as it moves offscreen.
var vwoosh:Float = (strumTime < Conductor.songPosition) ? 2.0 : 1.0;
var scrollSpeed:Float = PlayState.instance?.currentChart?.scrollSpeed ?? 1.0;
return Conductor.PIXELS_PER_MS * (Conductor.songPosition - strumTime) * scrollSpeed * vwoosh * (PreferencesMenu.getPref('downscroll') ? 1 : -1);
}
function updateNotes():Void
{
if (noteData.length == 0) return;
var renderWindowStart:Float = Conductor.songPosition + RENDER_DISTANCE_MS;
for (noteIndex in nextNoteIndex...noteData.length)
{
var note:Null<SongNoteData> = noteData[noteIndex];
if (note == null) continue;
if (note.time > renderWindowStart) break;
buildNoteSprite(note);
if (note.length > 0)
{
buildHoldNoteSprite(note);
}
nextNoteIndex++; // Increment the nextNoteIndex rather than splicing the array, because splicing is slow.
}
// Update rendering of notes.
for (note in notes.members)
{
if (note == null || note.hasBeenHit) continue;
note.y = this.y - INITIAL_OFFSET + calculateNoteYPos(note.strumTime);
// Check if the note is outside the hit window, and if so, mark it as missed.
// TODO: Check to make sure this doesn't happen when the note is on screen because it'll probably get deleted.
if (Conductor.songPosition > (note.noteData.time + Conductor.HIT_WINDOW_MS))
{
note.visible = false;
note.hasMissed = true;
if (note.holdNoteSprite != null) note.holdNoteSprite.missed = true;
}
else
{
note.visible = true;
note.hasMissed = false;
if (note.holdNoteSprite != null) note.holdNoteSprite.missed = false;
}
}
// Update rendering of hold notes.
for (holdNote in holdNotes.members)
{
if (holdNote == null || !holdNote.alive) continue;
var renderWindowEnd = holdNote.strumTime + holdNote.fullSustainLength + Conductor.HIT_WINDOW_MS + RENDER_DISTANCE_MS / 8;
if (Conductor.songPosition >= renderWindowEnd || holdNote.sustainLength <= 0)
{
// Hold note is offscreen, kill it.
holdNote.visible = false;
holdNote.kill(); // Do not destroy! Recycling is faster.
}
else if (holdNote.sustainLength <= 0)
{
// Hold note is completed, kill it.
playStatic(holdNote.noteDirection);
holdNote.visible = false;
holdNote.kill();
}
else if (holdNote.sustainLength <= 10)
{
// TODO: Better handle the weird edge case where the hold note is almost completed.
holdNote.visible = false;
}
else if (Conductor.songPosition > holdNote.strumTime && !holdNote.missed)
{
// Hold note is currently being hit, clip it off.
holdConfirm(holdNote.noteDirection);
holdNote.visible = true;
holdNote.sustainLength = (holdNote.strumTime + holdNote.fullSustainLength) - Conductor.songPosition;
if (PreferencesMenu.getPref('downscroll'))
{
holdNote.y = this.y - holdNote.height + STRUMLINE_SIZE / 2;
}
else
{
holdNote.y = this.y - INITIAL_OFFSET + STRUMLINE_SIZE / 2;
}
}
else if (holdNote.missed && (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) * Conductor.PIXELS_PER_MS;
trace('yOffset: ' + yOffset);
trace('holdNote.fullSustainLength: ' + holdNote.fullSustainLength);
trace('holdNote.sustainLength: ' + holdNote.sustainLength);
if (PreferencesMenu.getPref('downscroll'))
{
holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime) - holdNote.height + STRUMLINE_SIZE / 2;
}
else
{
holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime) + yOffset + STRUMLINE_SIZE / 2;
}
}
else
{
// Hold note is new, render it normally.
holdNote.visible = true;
if (PreferencesMenu.getPref('downscroll'))
{
holdNote.y = this.y + calculateNoteYPos(holdNote.strumTime) - holdNote.height + STRUMLINE_SIZE / 2;
}
else
{
holdNote.y = this.y - INITIAL_OFFSET + calculateNoteYPos(holdNote.strumTime) + STRUMLINE_SIZE / 2;
}
}
}
}
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 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));
}
public function hitNote(note:NoteSprite):Void
{
playConfirm(note.direction);
killNote(note);
}
public function killNote(note:NoteSprite):Void
{
note.visible = false;
notes.remove(note, false);
note.kill();
if (note.holdNoteSprite != null)
{
holdNoteSprite.missed = true;
holdNoteSprite.alpha = 0.6;
}
}
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
{
// TODO: Add a setting to disable note splashes.
// if (Settings.noSplash) 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 buildNoteSprite(note:SongNoteData):Void
{
var noteSprite:NoteSprite = constructNoteSprite();
if (noteSprite != null)
{
noteSprite.strumTime = note.time;
noteSprite.direction = note.getDirection();
noteSprite.noteData = note;
noteSprite.x = this.x;
noteSprite.x += getXPos(DIRECTIONS[note.getDirection() % KEY_COUNT]);
noteSprite.x -= NUDGE;
// noteSprite.x += INITIAL_OFFSET;
noteSprite.y = -9999;
}
}
public function buildHoldNoteSprite(note:SongNoteData):Void
{
var holdNoteSprite:SustainTrail = constructHoldNoteSprite();
if (holdNoteSprite != null)
{
holdNoteSprite.noteData = note;
holdNoteSprite.strumTime = note.time;
holdNoteSprite.noteDirection = note.getDirection();
holdNoteSprite.fullSustainLength = note.length;
holdNoteSprite.sustainLength = note.length;
holdNoteSprite.missed = false;
holdNoteSprite.x = this.x;
holdNoteSprite.x += getXPos(DIRECTIONS[note.getDirection() % KEY_COUNT]);
// holdNoteSprite.x += INITIAL_OFFSET;
holdNoteSprite.x += STRUMLINE_SIZE / 2;
holdNoteSprite.x -= holdNoteSprite.width / 2;
holdNoteSprite.y = -9999;
}
}
/**
* 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 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();
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, 100, Paths.image("NOTE_hold_assets"));
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(arrow:StrumlineNote):Void
{
arrow.y -= 10;
arrow.alpha = 0;
FlxTween.tween(arrow, {y: arrow.y + 10, alpha: 1}, 1, {ease: FlxEase.circOut, startDelay: 0.5 + (0.2 * arrow.ID)});
}
public function fadeInArrows():Void
{
for (arrow in this.strumlineNotes)
{
fadeInArrow(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);
}
}

View file

@ -0,0 +1,187 @@
package funkin.play.notes;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.FlxSprite;
import funkin.play.notes.NoteSprite;
/**
* The actual receptor that you see on screen.
*/
class StrumlineNote extends FlxSprite
{
public var isPlayer(default, null):Bool;
public var direction(default, set):NoteDirection;
public function updatePosition(parentNote:NoteSprite)
{
this.x = parentNote.x;
this.x += parentNote.width / 2;
this.x -= this.width / 2;
this.y = parentNote.y;
this.y += parentNote.height / 2;
}
function set_direction(value:NoteDirection):NoteDirection
{
this.direction = value;
setup();
return this.direction;
}
public function new(isPlayer:Bool, direction:NoteDirection)
{
super(0, 0);
this.isPlayer = isPlayer;
this.direction = direction;
this.animation.callback = onAnimationFrame;
this.animation.finishCallback = onAnimationFinished;
this.active = true;
}
function onAnimationFrame(name:String, frameNumber:Int, frameIndex:Int):Void {}
function onAnimationFinished(name:String):Void
{
if (!isPlayer && name.startsWith('confirm'))
{
playStatic();
}
}
override function update(elapsed:Float)
{
super.update(elapsed);
centerOrigin();
}
function setup():Void
{
this.frames = Paths.getSparrowAtlas('StrumlineNotes');
switch (this.direction)
{
case NoteDirection.LEFT:
this.animation.addByIndices('static', 'left confirm', [6, 7], '', 24, false, false, false);
this.animation.addByPrefix('press', 'left press', 24, false, false, false);
this.animation.addByIndices('confirm', 'left confirm', [0, 1, 2, 3], '', 24, false, false, false);
this.animation.addByIndices('confirm-hold', 'left confirm', [2, 3, 4, 5], '', 24, true, false, false);
case NoteDirection.DOWN:
this.animation.addByIndices('static', 'down confirm', [6, 7], '', 24, false, false, false);
this.animation.addByPrefix('press', 'down press', 24, false, false, false);
this.animation.addByIndices('confirm', 'down confirm', [0, 1, 2, 3], '', 24, false, false, false);
this.animation.addByIndices('confirm-hold', 'down confirm', [2, 3, 4, 5], '', 24, true, false, false);
case NoteDirection.UP:
this.animation.addByIndices('static', 'up confirm', [6, 7], '', 24, false, false, false);
this.animation.addByPrefix('press', 'up press', 24, false, false, false);
this.animation.addByIndices('confirm', 'up confirm', [0, 1, 2, 3], '', 24, false, false, false);
this.animation.addByIndices('confirm-hold', 'up confirm', [2, 3, 4, 5], '', 24, true, false, false);
case NoteDirection.RIGHT:
this.animation.addByIndices('static', 'right confirm', [6, 7], '', 24, false, false, false);
this.animation.addByPrefix('press', 'right press', 24, false, false, false);
this.animation.addByIndices('confirm', 'right confirm', [0, 1, 2, 3], '', 24, false, false, false);
this.animation.addByIndices('confirm-hold', 'right confirm', [2, 3, 4, 5], '', 24, true, false, false);
}
this.antialiasing = true;
this.setGraphicSize(Std.int(Strumline.STRUMLINE_SIZE * 1.55));
this.updateHitbox();
this.playStatic();
}
public function playAnimation(name:String = 'static', force:Bool = false, reversed:Bool = false, startFrame:Int = 0):Void
{
this.animation.play(name, force, reversed, startFrame);
centerOffsets();
centerOrigin();
}
public function playStatic():Void
{
this.active = false;
this.playAnimation('static', true);
}
public function playPress():Void
{
this.active = true;
this.playAnimation('press', true);
}
public function playConfirm():Void
{
this.active = true;
this.playAnimation('confirm', true);
}
public function isConfirm():Bool
{
return getCurrentAnimation().startsWith('confirm');
}
public function holdConfirm():Void
{
this.active = true;
if (getCurrentAnimation() == "confirm-hold") return;
if (getCurrentAnimation() == "confirm")
{
if (isAnimationFinished())
{
this.playAnimation('confirm-hold', true, false);
}
return;
}
this.playAnimation('confirm', false, false);
}
/**
* Returns the name of the animation that is currently playing.
* If no animation is playing (usually this means the sprite is BROKEN!),
* returns an empty string to prevent NPEs.
*/
public function getCurrentAnimation():String
{
if (this.animation == null || this.animation.curAnim == null) return "";
return this.animation.curAnim.name;
}
public function isAnimationFinished():Bool
{
return this.animation.finished;
}
static final DEFAULT_OFFSET:Int = 13;
/**
* Adjusts the position of the sprite's graphic relative to the hitbox.
*/
function fixOffsets():Void
{
// Automatically center the bounding box within the graphic.
this.centerOffsets();
if (getCurrentAnimation() == "confirm")
{
// Move the graphic down and to the right to compensate for
// the "glow" effect on the strumline note.
this.offset.x -= DEFAULT_OFFSET;
this.offset.y -= DEFAULT_OFFSET;
}
else
{
this.centerOrigin();
}
}
}

View file

@ -0,0 +1,272 @@
package funkin.play.notes;
import funkin.play.notes.NoteDirection;
import funkin.play.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.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:SongNoteData;
/**
* Set to `true` if the user missed the note.
* The trail should be made transparent, with clipping and effects disabled
*/
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).
*/
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;
/**
* 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, fileName:String)
{
super(0, 0, fileName);
antialiasing = true;
if (fileName == "arrowEnds")
{
endOffset = bottomClip = 1;
antialiasing = false;
zoom = 6;
}
// 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, PlayState.instance.currentChart.scrollSpeed);
// instead of scrollSpeed, PlayState.SONG.speed
flipY = PreferencesMenu.getPref('downscroll');
// alpha = 0.6;
alpha = 1.0;
// calls updateColorTransform(), which initializes processedGraphic!
updateColorTransform();
updateClipping();
indices = new DrawData<Int>(12, true, TRIANGLE_VERTEX_INDICES);
}
/**
* 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)
{
if (s < 0) s = 0;
height = sustainHeight(s, PlayState.instance.currentChart.scrollSpeed);
updateColorTransform();
updateClipping();
return sustainLength = 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), PlayState.instance.currentChart.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 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);
}
}
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);
}
}

View file

@ -298,9 +298,16 @@ class SongDifficulty
return cast events;
}
public inline function cacheInst():Void
public inline function cacheInst(?currentPlayerId:String = null):Void
{
FlxG.sound.cache(Paths.inst(this.song.songId));
if (currentPlayerId != null)
{
FlxG.sound.cache(Paths.inst(this.song.songId, getPlayableChar(currentPlayerId).inst));
}
else
{
FlxG.sound.cache(Paths.inst(this.song.songId));
}
}
public inline function playInst(volume:Float = 1.0, looped:Bool = false):Void

View file

@ -427,6 +427,12 @@ abstract SongNoteData(RawSongNoteData)
return Math.floor(this.d / strumlineSize);
}
/**
* Returns true if the note is one that Boyfriend should try to hit (i.e. it's on his side).
* TODO: The name of this function is a little misleading; what about mines?
* @param strumlineSize Defaults to 4.
* @return True if it's Boyfriend's note.
*/
public inline function getMustHitNote(strumlineSize:Int = 4):Bool
{
return getStrumlineIndex(strumlineSize) == 0;

View file

@ -5,23 +5,24 @@ import flixel.addons.effects.chainable.FlxOutlineEffect;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.util.FlxColor;
import funkin.ui.OptionsState.Page;
import funkin.play.notes.NoteSprite;
class ColorsMenu extends Page
{
var curSelected:Int = 0;
var grpNotes:FlxTypedGroup<Note>;
var grpNotes:FlxTypedGroup<NoteSprite>;
public function new()
{
super();
grpNotes = new FlxTypedGroup<Note>();
grpNotes = new FlxTypedGroup<NoteSprite>();
add(grpNotes);
for (i in 0...4)
{
var note:Note = new Note(0, i);
var note:NoteSprite = new NoteSprite(0, i);
note.x = (100 * i) + i;
note.screenCenter(Y);
@ -52,14 +53,14 @@ class ColorsMenu extends Page
if (controls.UI_UP)
{
grpNotes.members[curSelected].colorSwap.update(elapsed * 0.3);
Note.arrowColors[curSelected] += elapsed * 0.3;
// grpNotes.members[curSelected].colorSwap.update(elapsed * 0.3);
// Note.arrowColors[curSelected] += elapsed * 0.3;
}
if (controls.UI_DOWN)
{
grpNotes.members[curSelected].colorSwap.update(-elapsed * 0.3);
Note.arrowColors[curSelected] += -elapsed * 0.3;
// grpNotes.members[curSelected].colorSwap.update(-elapsed * 0.3);
// Note.arrowColors[curSelected] += -elapsed * 0.3;
}
super.update(elapsed);

View file

@ -22,6 +22,7 @@ import funkin.audio.VoicesGroup;
import funkin.input.Cursor;
import funkin.modding.events.ScriptEvent;
import funkin.play.HealthIcon;
import funkin.play.notes.NoteSprite;
import funkin.play.song.Song;
import funkin.play.song.SongData.SongChartData;
import funkin.play.song.SongData.SongDataParser;
@ -2803,11 +2804,9 @@ class ChartEditorState extends HaxeUIState
// Character preview.
// Why does NOTESCRIPTEVENT TAKE A SPRITE AAAAA
var tempNote:Note = new Note(noteData.time, noteData.data, null, false, NORMAL);
tempNote.mustPress = noteData.getMustHitNote();
tempNote.data.sustainLength = noteData.length;
tempNote.data.noteKind = noteData.kind;
// NoteScriptEvent takes a sprite, ehe. Need to rework that.
var tempNote:NoteSprite = new NoteSprite();
tempNote.noteData = noteData;
tempNote.scrollFactor.set(0, 0);
var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, tempNote, 1, true);
dispatchEvent(event);

View file

@ -88,9 +88,9 @@ class Constants
public static final COLOR_HEALTH_BAR_GREEN:FlxColor = 0xFF66FF33;
/**
* Default variation for charts.
* The base colors of the notes.
*/
public static final DEFAULT_VARIATION:String = 'default';
public static final COLOR_NOTES:Array<FlxColor> = [0xFFFF22AA, 0xFF00EEFF, 0xFF00CC00, 0xFFCC1111];
/**
* STAGE DEFAULTS
@ -117,6 +117,11 @@ class Constants
*/
public static final DEFAULT_SONG:String = 'tutorial';
/**
* Default variation for charts.
*/
public static final DEFAULT_VARIATION:String = 'default';
/**
* OTHER
*/
@ -144,6 +149,9 @@ class Constants
*/
public static final COUNTDOWN_VOLUME:Float = 0.6;
public static final STRUMLINE_X_OFFSET:Float = 48;
public static final STRUMLINE_Y_OFFSET:Float = 24;
/**
* The default intensity for camera zooms.
*/

View file

@ -4,6 +4,7 @@ package funkin.util;
import flixel.FlxBasic;
import flixel.util.FlxSort;
#end
import funkin.play.notes.NoteSprite;
class SortUtil
{
@ -22,8 +23,8 @@ class SortUtil
*
* @param order Either `FlxSort.ASCENDING` or `FlxSort.DESCENDING`
*/
public static inline function byStrumtime(order:Int, a:Note, b:Note)
public static inline function byStrumtime(order:Int, a:NoteSprite, b:NoteSprite)
{
return FlxSort.byValues(order, a.data.strumTime, b.data.strumTime);
return FlxSort.byValues(order, a.noteData.time, b.noteData.time);
}
}

View file

@ -51,4 +51,13 @@ class WindowUtil
// Do nothing.
#end
}
/**
* Sets the title of the application window.
* @param value The title to use.
*/
public static function setWindowTitle(value:String):Void
{
lime.app.Application.current.window.title = value;
}
}

View file

@ -0,0 +1,154 @@
package funkin.util.tools;
/**
* Contains code for sorting arrays using various algorithms.
* @see https://algs4.cs.princeton.edu/20sorting/
*/
class ArraySortTools
{
/**
* Sorts the input array using the merge sort algorithm.
* Stable and guaranteed to run in linearithmic time `O(n log n)`,
* but less efficient in "best-case" situations.
*
* @param input The array to sort in-place.
* @param compare The comparison function to use.
*/
public static function mergeSort<T>(input:Array<T>, compare:CompareFunction<T>):Void
{
if (input == null || input.length <= 1) return;
if (compare == null) throw 'No comparison function provided.';
// Haxe implements merge sort by default.
haxe.ds.ArraySort.sort(input, compare);
}
/**
* Sorts the input array using the quick sort algorithm.
* More efficient on smaller arrays, but is inefficient `O(n^2)` in "worst-case" situations.
* Not stable; relative order of equal elements is not preserved.
*
* @see https://stackoverflow.com/questions/33884057/quick-sort-stackoverflow-error-for-large-arrays
* Fix for stack overflow issues.
* @param input The array to sort in-place.
* @param compare The comparison function to use.
*/
public static function quickSort<T>(input:Array<T>, compare:CompareFunction<T>):Void
{
if (input == null || input.length <= 1) return;
if (compare == null) throw 'No comparison function provided.';
quickSortInner(input, 0, input.length - 1, compare);
}
/**
* Internal recursive function for the quick sort algorithm.
* Written with ChatGPT!
*/
static function quickSortInner<T>(input:Array<T>, low:Int, high:Int, compare:CompareFunction<T>):Void
{
// When low == high, the array is empty or too small to sort.
// EDIT: Recurse on the smaller partition, and loop for the larger partition.
while (low < high)
{
// Designate the first element in the array as the pivot, then partition the array around it.
// Elements less than the pivot will be to the left, and elements greater than the pivot will be to the right.
// Return the index of the pivot.
var pivot:Int = quickSortPartition(input, low, high, compare);
if ((pivot) - low <= high - (pivot + 1))
{
quickSortInner(input, low, pivot, compare);
low = pivot + 1;
}
else
{
quickSortInner(input, pivot + 1, high, compare);
high = pivot;
}
}
}
/**
* Internal function for sorting a partition of an array in the quick sort algorithm.
* Written with ChatGPT!
*/
static function quickSortPartition<T>(input:Array<T>, low:Int, high:Int, compare:CompareFunction<T>):Int
{
// Designate the first element in the array as the pivot.
var pivot:T = input[low];
// Designate two pointers, used to divide the array into two partitions.
var i:Int = low - 1;
var j:Int = high + 1;
while (true)
{
// Move the left pointer to the right until it finds an element greater than the pivot.
do
{
i++;
}
while (compare(input[i], pivot) < 0);
// Move the right pointer to the left until it finds an element less than the pivot.
do
{
j--;
}
while (compare(input[j], pivot) > 0);
// If i and j have crossed, the array has been partitioned, and the pivot will be at the index j.
if (i >= j) return j;
// Else, swap the elements at i and j, and start over.
// This slowly moves the pivot towards the middle of the partition,
// while moving elements less than the pivot to the left and elements greater than the pivot to the right.
var temp:T = input[i];
input[i] = input[j];
input[j] = temp;
}
}
/**
* Sorts the input array using the insertion sort algorithm.
* Stable and is very fast on nearly-sorted arrays,
* but is inefficient `O(n^2)` in "worst-case" situations.
*
* @param input The array to sort in-place.
* @param compare The comparison function to use.
*/
public static function insertionSort<T>(input:Array<T>, compare:CompareFunction<T>):Void
{
if (input == null || input.length <= 1) return;
if (compare == null) throw 'No comparison function provided.';
// Iterate through the array, starting at the second element.
for (i in 1...input.length)
{
// Store the current element.
var current:T = input[i];
// Store the index of the previous element.
var j:Int = i - 1;
// While the previous element is greater than the current element,
// move the previous element to the right and move the index to the left.
while (j >= 0 && compare(input[j], current) > 0)
{
input[j + 1] = input[j];
j--;
}
// Insert the current element into the array.
input[j + 1] = current;
}
}
}
/**
* A comparison function.
* Returns a negative number if the first argument is less than the second,
* a positive number if the first argument is greater than the second,
* or zero if the two arguments are equal.
*/
typedef CompareFunction<T> = T->T->Int;

View file

@ -22,4 +22,19 @@ class ArrayTools
}
return result;
}
/**
* Return the first element of the array that satisfies the predicate, or null if none do.
* @param input The array to search
* @param predicate The predicate to call
* @return The result
*/
public static function find<T>(input:Array<T>, predicate:T->Bool):Null<T>
{
for (element in input)
{
if (predicate(element)) return element;
}
return null;
}
}