mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2024-11-14 19:25:16 -05:00
WIP on new note rendering, inputs.
This commit is contained in:
parent
28ddadffff
commit
25c70564bd
34 changed files with 3578 additions and 2872 deletions
6
hmm.json
6
hmm.json
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
303
source/funkin/input/PreciseInputManager.hx
Normal file
303
source/funkin/input/PreciseInputManager.hx
Normal 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,
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -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";
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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;
|
||||
}
|
|
@ -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}' : ''}';
|
||||
|
||||
|
|
82
source/funkin/play/notes/NoteDirection.hx
Normal file
82
source/funkin/play/notes/NoteDirection.hx
Normal 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;
|
||||
}
|
||||
}
|
90
source/funkin/play/notes/NoteSplash.hx
Normal file
90
source/funkin/play/notes/NoteSplash.hx
Normal 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();
|
||||
}
|
||||
}
|
178
source/funkin/play/notes/NoteSprite.hx
Normal file
178
source/funkin/play/notes/NoteSprite.hx
Normal 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;
|
||||
}
|
||||
}
|
565
source/funkin/play/notes/Strumline.hx
Normal file
565
source/funkin/play/notes/Strumline.hx
Normal 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);
|
||||
}
|
||||
}
|
187
source/funkin/play/notes/StrumlineNote.hx
Normal file
187
source/funkin/play/notes/StrumlineNote.hx
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
272
source/funkin/play/notes/SustainTrail.hx
Normal file
272
source/funkin/play/notes/SustainTrail.hx
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
154
source/funkin/util/tools/ArraySortTools.hx
Normal file
154
source/funkin/util/tools/ArraySortTools.hx
Normal 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;
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue