1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-09-07 14:38:06 +00:00
Funkin/source/funkin/play/notes/notestyle/NoteStyle.hx

1196 lines
42 KiB
Haxe
Raw Permalink Normal View History

package funkin.play.notes.notestyle;
import funkin.play.Countdown;
import flixel.graphics.frames.FlxAtlasFrames;
2025-03-12 01:06:28 +00:00
import flixel.graphics.frames.FlxFramesCollection;
import funkin.data.animation.AnimationData;
import funkin.data.IRegistryEntry;
import funkin.graphics.FunkinSprite;
import funkin.data.notestyle.NoteStyleData;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.util.assets.FlxAnimationUtil;
using funkin.data.animation.AnimationData.AnimationDataUtil;
/**
* Holds the data for what assets to use for a note style,
* and provides convenience methods for building sprites based on them.
*/
@:nullSafety
class NoteStyle implements IRegistryEntry<NoteStyleData>
{
/**
* Note style data as parsed from the JSON file.
*/
public final _data:NoteStyleData;
/**
* The note style to use if this one doesn't have a certain asset.
* This can be recursive, ehe.
*/
var fallback(get, never):Null<NoteStyle>;
2025-02-13 01:35:37 +00:00
function get_fallback():Null<NoteStyle>
{
if (_data == null || _data.fallback == null) return null;
return NoteStyleRegistry.instance.fetchEntry(_data.fallback);
}
/**
* @param id The ID of the JSON file to parse.
*/
2025-05-27 07:20:50 +00:00
public function new(id:String, ?params:Dynamic)
{
this.id = id;
_data = _fetchData(id);
}
/**
* Get the readable name of the note style.
* @return String
*/
public function getName():String
{
return _data.name;
}
/**
* Get the author of the note style.
* @return String
*/
public function getAuthor():String
{
return _data.author;
}
/**
* Get the note style ID of the parent note style.
* @return The string ID, or `null` if there is no parent.
*/
2024-07-17 20:19:18 +00:00
public function getFallbackID():Null<String>
{
return _data.fallback;
}
public function buildNoteSprite(target:NoteSprite):Void
{
// Apply the note sprite frames.
var atlas:Null<FlxAtlasFrames> = buildNoteFrames(false);
if (atlas == null)
{
throw 'Could not load spritesheet for note style: $id';
}
target.frames = atlas;
target.antialiasing = !(_data.assets?.note?.isPixel ?? false);
// Apply the animations.
buildNoteAnimations(target);
// Set the scale.
2024-09-17 20:47:16 +00:00
var scale = getNoteScale();
target.scale.set(scale, scale);
target.updateHitbox();
}
2025-03-03 02:44:41 +00:00
function getNoteAssetLibrary():Null<String>
{
// library:path
var parts = getNoteAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length == 0) return null;
if (parts.length == 1) return null;
return parts[0];
}
var noteFrames:Null<FlxAtlasFrames> = null;
function buildNoteFrames(force:Bool = false):Null<FlxAtlasFrames>
{
var noteAssetPath = getNoteAssetPath();
if (noteAssetPath == null)
{
FlxG.log.warn('Note asset path not found: ${id}');
return null;
}
if (!FunkinSprite.isTextureCached(Paths.image(noteAssetPath)))
{
FlxG.log.warn('Note texture is not cached: ${noteAssetPath}');
}
// Purge the note frames if the cached atlas is invalid.
@:nullSafety(Off)
{
if (noteFrames?.parent?.isDestroyed ?? true) noteFrames = null;
}
if (noteFrames != null && !force) return noteFrames;
2025-03-03 02:44:41 +00:00
noteFrames = Paths.getSparrowAtlas(noteAssetPath, getAssetLibrary(getNoteAssetPath(true)));
if (noteFrames == null)
{
throw 'Could not load note frames for note style: $id';
}
return noteFrames;
}
public function getNoteAssetPath(raw:Bool = false):Null<String>
{
if (raw)
{
var rawPath:Null<String> = _data?.assets?.note?.assetPath;
if (rawPath == null) return fallback?.getNoteAssetPath(true);
return rawPath;
}
// library:path
var parts = getNoteAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length == 0) return null;
if (parts.length == 1) return getNoteAssetPath(true);
return parts[1];
}
function buildNoteAnimations(target:NoteSprite):Void
{
var leftData:Null<AnimationData> = fetchNoteAnimationData(LEFT);
if (leftData != null) target.animation.addByPrefix('purpleScroll', leftData.prefix ?? '', leftData.frameRate ?? 24, leftData.looped ?? false,
leftData.flipX, leftData.flipY);
var downData:Null<AnimationData> = fetchNoteAnimationData(DOWN);
if (downData != null) target.animation.addByPrefix('blueScroll', downData.prefix ?? '', downData.frameRate ?? 24, downData.looped ?? false,
downData.flipX, downData.flipY);
var upData:Null<AnimationData> = fetchNoteAnimationData(UP);
if (upData != null) target.animation.addByPrefix('greenScroll', upData.prefix ?? '', upData.frameRate ?? 24, upData.looped ?? false, upData.flipX,
upData.flipY);
var rightData:Null<AnimationData> = fetchNoteAnimationData(RIGHT);
if (rightData != null) target.animation.addByPrefix('redScroll', rightData.prefix ?? '', rightData.frameRate ?? 24, rightData.looped ?? false,
rightData.flipX, rightData.flipY);
}
public function isNoteAnimated():Bool
{
return _data.assets?.note?.animated ?? fallback?.isNoteAnimated() ?? false;
}
public function getNoteScale():Float
{
return _data.assets?.note?.scale ?? fallback?.getNoteScale() ?? 1.0;
}
function fetchNoteAnimationData(dir:NoteDirection):Null<AnimationData>
{
var result:Null<AnimationData> = switch (dir)
{
case LEFT: _data.assets?.note?.data?.left?.toNamed();
case DOWN: _data.assets?.note?.data?.down?.toNamed();
case UP: _data.assets?.note?.data?.up?.toNamed();
case RIGHT: _data.assets?.note?.data?.right?.toNamed();
};
return result ?? fallback?.fetchNoteAnimationData(dir);
}
public function getHoldNoteAssetPath(raw:Bool = false):Null<String>
{
if (raw)
{
var rawPath:Null<String> = _data?.assets?.holdNote?.assetPath;
return rawPath ?? fallback?.getHoldNoteAssetPath(true);
}
// library:path
var parts = getHoldNoteAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length == 0) return null;
if (parts.length == 1) return Paths.image(parts[0]);
return Paths.image(parts[1], parts[0]);
}
public function isHoldNotePixel():Bool
{
return _data?.assets?.holdNote?.isPixel ?? fallback?.isHoldNotePixel() ?? false;
}
public function fetchHoldNoteScale():Float
{
return _data?.assets?.holdNote?.scale ?? fallback?.fetchHoldNoteScale() ?? 1.0;
}
2024-09-17 20:47:16 +00:00
public function getHoldNoteOffsets():Array<Float>
{
return _data?.assets?.holdNote?.offsets ?? fallback?.getHoldNoteOffsets() ?? [0.0, 0.0];
2024-09-17 20:47:16 +00:00
}
public function applyStrumlineFrames(target:StrumlineNote):Void
{
// TODO: Add support for multi-Sparrow.
// Will be less annoying after this is merged: https://github.com/HaxeFlixel/flixel/pull/2772
2025-03-03 02:44:41 +00:00
var atlas:FlxAtlasFrames = Paths.getSparrowAtlas(getStrumlineAssetPath() ?? '', getAssetLibrary(getStrumlineAssetPath(true)));
if (atlas == null)
{
throw 'Could not load spritesheet for note style: $id';
}
target.frames = atlas;
target.scale.set(_data.assets.noteStrumline?.scale ?? 1.0);
target.antialiasing = !(_data.assets.noteStrumline?.isPixel ?? false);
}
public function getStrumlineAssetPath(raw:Bool = false):Null<String>
{
if (raw)
{
return _data?.assets?.noteStrumline?.assetPath ?? fallback?.getStrumlineAssetPath(true);
}
// library:path
var parts = getStrumlineAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length <= 1) return getStrumlineAssetPath(true);
return parts[1];
}
public function applyStrumlineAnimations(target:StrumlineNote, dir:NoteDirection):Void
{
FlxAnimationUtil.addAtlasAnimations(target, getStrumlineAnimationData(dir));
}
/**
* Fetch the animation data for the strumline.
* NOTE: This function only queries the fallback note style if all the animations are missing for a given direction.
*
* @param dir The direction to fetch the animation data for.
* @return The animation data for the strumline in that direction.
*/
function getStrumlineAnimationData(dir:NoteDirection):Array<AnimationData>
{
var result:Array<Null<AnimationData>> = switch (dir)
{
case NoteDirection.LEFT:
[
_data.assets.noteStrumline?.data?.leftStatic?.toNamed('static'),
_data.assets.noteStrumline?.data?.leftPress?.toNamed('press'),
_data.assets.noteStrumline?.data?.leftConfirm?.toNamed('confirm'),
_data.assets.noteStrumline?.data?.leftConfirmHold?.toNamed('confirm-hold'),
];
case NoteDirection.DOWN: [
_data.assets.noteStrumline?.data?.downStatic?.toNamed('static'),
_data.assets.noteStrumline?.data?.downPress?.toNamed('press'),
_data.assets.noteStrumline?.data?.downConfirm?.toNamed('confirm'),
_data.assets.noteStrumline?.data?.downConfirmHold?.toNamed('confirm-hold'),
];
case NoteDirection.UP: [
_data.assets.noteStrumline?.data?.upStatic?.toNamed('static'),
_data.assets.noteStrumline?.data?.upPress?.toNamed('press'),
_data.assets.noteStrumline?.data?.upConfirm?.toNamed('confirm'),
_data.assets.noteStrumline?.data?.upConfirmHold?.toNamed('confirm-hold'),
];
case NoteDirection.RIGHT: [
_data.assets.noteStrumline?.data?.rightStatic?.toNamed('static'),
_data.assets.noteStrumline?.data?.rightPress?.toNamed('press'),
_data.assets.noteStrumline?.data?.rightConfirm?.toNamed('confirm'),
_data.assets.noteStrumline?.data?.rightConfirmHold?.toNamed('confirm-hold'),
];
default: [];
};
// New variable so we can change the type.
var filteredResult:Array<AnimationData> = thx.Arrays.filterNull(result);
if (filteredResult.length == 0) return fallback?.getStrumlineAnimationData(dir) ?? [];
return filteredResult;
}
2024-09-17 20:47:16 +00:00
public function getStrumlineOffsets():Array<Float>
{
return _data?.assets?.noteStrumline?.offsets ?? fallback?.getStrumlineOffsets() ?? [0.0, 0.0];
2024-09-17 20:47:16 +00:00
}
public function applyStrumlineOffsets(target:StrumlineNote):Void
{
2024-09-17 20:47:16 +00:00
var offsets = getStrumlineOffsets();
target.x += offsets[0];
target.y += offsets[1];
}
public function getStrumlineScale():Float
{
return _data?.assets?.noteStrumline?.scale ?? fallback?.getStrumlineScale() ?? 1.0;
}
public function isNoteSplashEnabled():Bool
{
return _data?.assets?.noteSplash?.data?.enabled ?? fallback?.isNoteSplashEnabled() ?? false;
}
public function isHoldNoteCoverEnabled():Bool
{
return _data?.assets?.holdNoteCover?.data?.enabled ?? fallback?.isHoldNoteCoverEnabled() ?? false;
}
/**
* Build a sprite for the given step of the countdown.
* @param step
* @return A `FunkinSprite`, or `null` if no graphic is available for this step.
*/
public function buildCountdownSprite(step:Countdown.CountdownStep):Null<FunkinSprite>
{
var result = new FunkinSprite();
switch (step)
{
case THREE:
if (_data.assets.countdownThree == null) return fallback?.buildCountdownSprite(step);
var assetPath = buildCountdownSpritePath(step);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.countdownThree?.scale ?? 1.0;
result.scale.y = _data.assets.countdownThree?.scale ?? 1.0;
case TWO:
if (_data.assets.countdownTwo == null) return fallback?.buildCountdownSprite(step);
var assetPath = buildCountdownSpritePath(step);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.countdownTwo?.scale ?? 1.0;
result.scale.y = _data.assets.countdownTwo?.scale ?? 1.0;
case ONE:
if (_data.assets.countdownOne == null) return fallback?.buildCountdownSprite(step);
var assetPath = buildCountdownSpritePath(step);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.countdownOne?.scale ?? 1.0;
result.scale.y = _data.assets.countdownOne?.scale ?? 1.0;
case GO:
if (_data.assets.countdownGo == null) return fallback?.buildCountdownSprite(step);
var assetPath = buildCountdownSpritePath(step);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.countdownGo?.scale ?? 1.0;
result.scale.y = _data.assets.countdownGo?.scale ?? 1.0;
default:
// TODO: Do something here?
return null;
}
result.scrollFactor.set(0, 0);
result.antialiasing = !isCountdownSpritePixel(step);
result.updateHitbox();
return result;
}
public function buildCountdownSpritePath(step:Countdown.CountdownStep):Null<String>
{
var basePath:Null<String> = null;
switch (step)
{
case THREE:
basePath = _data.assets.countdownThree?.assetPath;
case TWO:
basePath = _data.assets.countdownTwo?.assetPath;
case ONE:
basePath = _data.assets.countdownOne?.assetPath;
case GO:
basePath = _data.assets.countdownGo?.assetPath;
default:
basePath = null;
}
if (basePath == null) return fallback?.buildCountdownSpritePath(step);
var parts = basePath?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length < 1) return null;
if (parts.length == 1) return parts[0];
return parts[1];
}
function buildCountdownSpriteLibrary(step:Countdown.CountdownStep):Null<String>
{
var basePath:Null<String> = null;
switch (step)
{
case THREE:
basePath = _data.assets.countdownThree?.assetPath;
case TWO:
basePath = _data.assets.countdownTwo?.assetPath;
case ONE:
basePath = _data.assets.countdownOne?.assetPath;
case GO:
basePath = _data.assets.countdownGo?.assetPath;
default:
basePath = null;
}
if (basePath == null) return fallback?.buildCountdownSpriteLibrary(step);
var parts = basePath?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length <= 1) return null;
return parts[0];
}
public function isCountdownSpritePixel(step:Countdown.CountdownStep):Bool
{
switch (step)
{
case THREE:
var result = _data.assets.countdownThree?.isPixel;
if (result == null) result = fallback?.isCountdownSpritePixel(step) ?? false;
return result;
case TWO:
var result = _data.assets.countdownTwo?.isPixel;
if (result == null) result = fallback?.isCountdownSpritePixel(step) ?? false;
return result;
case ONE:
var result = _data.assets.countdownOne?.isPixel;
if (result == null) result = fallback?.isCountdownSpritePixel(step) ?? false;
return result;
case GO:
var result = _data.assets.countdownGo?.isPixel;
if (result == null) result = fallback?.isCountdownSpritePixel(step) ?? false;
return result;
default:
return false;
}
}
public function getCountdownSpriteOffsets(step:Countdown.CountdownStep):Array<Float>
{
switch (step)
{
case THREE:
var result = _data.assets.countdownThree?.offsets;
if (result == null) result = fallback?.getCountdownSpriteOffsets(step) ?? [0, 0];
return result;
case TWO:
var result = _data.assets.countdownTwo?.offsets;
if (result == null) result = fallback?.getCountdownSpriteOffsets(step) ?? [0, 0];
return result;
case ONE:
var result = _data.assets.countdownOne?.offsets;
if (result == null) result = fallback?.getCountdownSpriteOffsets(step) ?? [0, 0];
return result;
case GO:
var result = _data.assets.countdownGo?.offsets;
if (result == null) result = fallback?.getCountdownSpriteOffsets(step) ?? [0, 0];
return result;
default:
return [0, 0];
}
}
public function getCountdownSoundPath(step:Countdown.CountdownStep, raw:Bool = false):Null<String>
{
if (raw)
{
// TODO: figure out why ?. didn't work here
var rawPath:Null<String> = switch (step)
{
case Countdown.CountdownStep.THREE:
_data.assets.countdownThree?.data?.audioPath;
case Countdown.CountdownStep.TWO:
_data.assets.countdownTwo?.data?.audioPath;
case Countdown.CountdownStep.ONE:
_data.assets.countdownOne?.data?.audioPath;
case Countdown.CountdownStep.GO:
_data.assets.countdownGo?.data?.audioPath;
default:
null;
}
return (rawPath == null) ? fallback?.getCountdownSoundPath(step, true) : rawPath;
}
// library:path
var parts = getCountdownSoundPath(step, true)?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length == 0) return null;
if (parts.length == 1) return Paths.image(parts[0]);
return Paths.sound(parts[1], parts[0]);
}
public function buildJudgementSprite(rating:String):Null<FunkinSprite>
{
var result = new FunkinSprite();
switch (rating)
{
case "sick":
if (_data.assets.judgementSick == null) return fallback?.buildJudgementSprite(rating);
var assetPath = buildJudgementSpritePath(rating);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.judgementSick?.scale ?? 1.0;
result.scale.y = _data.assets.judgementSick?.scale ?? 1.0;
case "good":
if (_data.assets.judgementGood == null) return fallback?.buildJudgementSprite(rating);
var assetPath = buildJudgementSpritePath(rating);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.judgementGood?.scale ?? 1.0;
result.scale.y = _data.assets.judgementGood?.scale ?? 1.0;
case "bad":
if (_data.assets.judgementBad == null) return fallback?.buildJudgementSprite(rating);
var assetPath = buildJudgementSpritePath(rating);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.judgementBad?.scale ?? 1.0;
result.scale.y = _data.assets.judgementBad?.scale ?? 1.0;
case "shit":
if (_data.assets.judgementShit == null) return fallback?.buildJudgementSprite(rating);
var assetPath = buildJudgementSpritePath(rating);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.judgementShit?.scale ?? 1.0;
result.scale.y = _data.assets.judgementShit?.scale ?? 1.0;
default:
return null;
}
result.scrollFactor.set(0.2, 0.2);
var isPixel = isJudgementSpritePixel(rating);
result.antialiasing = !isPixel;
result.pixelPerfectRender = isPixel;
result.pixelPerfectPosition = isPixel;
result.updateHitbox();
return result;
}
public function isJudgementSpritePixel(rating:String):Bool
{
switch (rating)
{
case "sick":
var result = _data.assets.judgementSick?.isPixel;
if (result == null) result = fallback?.isJudgementSpritePixel(rating) ?? false;
return result;
case "good":
var result = _data.assets.judgementGood?.isPixel;
if (result == null) result = fallback?.isJudgementSpritePixel(rating) ?? false;
return result;
case "bad":
var result = _data.assets.judgementBad?.isPixel;
if (result == null) result = fallback?.isJudgementSpritePixel(rating) ?? false;
return result;
case "shit":
var result = _data.assets.judgementShit?.isPixel;
if (result == null) result = fallback?.isJudgementSpritePixel(rating) ?? false;
return result;
default:
return false;
}
}
public function buildJudgementSpritePath(rating:String):Null<String>
{
var basePath:Null<String> = null;
switch (rating)
{
case "sick":
basePath = _data.assets.judgementSick?.assetPath;
case "good":
basePath = _data.assets.judgementGood?.assetPath;
case "bad":
basePath = _data.assets.judgementBad?.assetPath;
case "shit":
basePath = _data.assets.judgementShit?.assetPath;
default:
basePath = null;
}
if (basePath == null) return fallback?.buildJudgementSpritePath(rating);
var parts = basePath?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length < 1) return null;
if (parts.length == 1) return parts[0];
return parts[1];
}
public function getJudgementSpriteOffsets(rating:String):Array<Float>
{
switch (rating)
{
case "sick":
var result = _data.assets.judgementSick?.offsets;
if (result == null) result = fallback?.getJudgementSpriteOffsets(rating) ?? [0, 0];
return result;
case "good":
var result = _data.assets.judgementGood?.offsets;
if (result == null) result = fallback?.getJudgementSpriteOffsets(rating) ?? [0, 0];
return result;
case "bad":
var result = _data.assets.judgementBad?.offsets;
if (result == null) result = fallback?.getJudgementSpriteOffsets(rating) ?? [0, 0];
return result;
case "shit":
var result = _data.assets.judgementShit?.offsets;
if (result == null) result = fallback?.getJudgementSpriteOffsets(rating) ?? [0, 0];
return result;
default:
return [0, 0];
}
}
public function buildComboNumSprite(digit:Int):Null<FunkinSprite>
{
var result = new FunkinSprite();
switch (digit)
{
case 0:
if (_data.assets.comboNumber0 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber0?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber0?.scale ?? 1.0;
case 1:
if (_data.assets.comboNumber1 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber1?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber1?.scale ?? 1.0;
case 2:
if (_data.assets.comboNumber2 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber2?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber2?.scale ?? 1.0;
case 3:
if (_data.assets.comboNumber3 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber3?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber3?.scale ?? 1.0;
case 4:
if (_data.assets.comboNumber4 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber4?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber4?.scale ?? 1.0;
case 5:
if (_data.assets.comboNumber5 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber5?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber5?.scale ?? 1.0;
case 6:
if (_data.assets.comboNumber6 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber6?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber6?.scale ?? 1.0;
case 7:
if (_data.assets.comboNumber7 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber7?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber7?.scale ?? 1.0;
case 8:
if (_data.assets.comboNumber8 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber8?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber8?.scale ?? 1.0;
case 9:
if (_data.assets.comboNumber9 == null) return fallback?.buildComboNumSprite(digit);
var assetPath = buildComboNumSpritePath(digit);
if (assetPath == null) return null;
result.loadTexture(assetPath);
result.scale.x = _data.assets.comboNumber9?.scale ?? 1.0;
result.scale.y = _data.assets.comboNumber9?.scale ?? 1.0;
default:
return null;
}
var isPixel = isComboNumSpritePixel(digit);
result.antialiasing = !isPixel;
result.pixelPerfectRender = isPixel;
result.pixelPerfectPosition = isPixel;
result.updateHitbox();
return result;
}
public function isComboNumSpritePixel(digit:Int):Bool
{
switch (digit)
{
case 0:
var result = _data.assets.comboNumber0?.isPixel;
if (result == null) result = fallback?.isComboNumSpritePixel(digit) ?? false;
return result;
case 1:
var result = _data.assets.comboNumber1?.isPixel;
if (result == null) result = fallback?.isComboNumSpritePixel(digit) ?? false;
return result;
case 2:
var result = _data.assets.comboNumber2?.isPixel;
if (result == null) result = fallback?.isComboNumSpritePixel(digit) ?? false;
return result;
case 3:
var result = _data.assets.comboNumber3?.isPixel;
if (result == null) result = fallback?.isComboNumSpritePixel(digit) ?? false;
return result;
case 4:
var result = _data.assets.comboNumber4?.isPixel;
if (result == null) result = fallback?.isComboNumSpritePixel(digit) ?? false;
return result;
case 5:
var result = _data.assets.comboNumber5?.isPixel;
if (result == null) result = fallback?.isComboNumSpritePixel(digit) ?? false;
return result;
case 6:
var result = _data.assets.comboNumber6?.isPixel;
if (result == null) result = fallback?.isComboNumSpritePixel(digit) ?? false;
return result;
case 7:
var result = _data.assets.comboNumber7?.isPixel;
if (result == null) result = fallback?.isComboNumSpritePixel(digit) ?? false;
return result;
case 8:
var result = _data.assets.comboNumber8?.isPixel;
if (result == null) result = fallback?.isComboNumSpritePixel(digit) ?? false;
return result;
case 9:
var result = _data.assets.comboNumber9?.isPixel;
if (result == null) result = fallback?.isComboNumSpritePixel(digit) ?? false;
return result;
default:
return false;
}
}
public function buildComboNumSpritePath(digit:Int):Null<String>
{
var basePath:Null<String> = null;
switch (digit)
{
case 0:
basePath = _data.assets.comboNumber0?.assetPath;
case 1:
basePath = _data.assets.comboNumber1?.assetPath;
case 2:
basePath = _data.assets.comboNumber2?.assetPath;
case 3:
basePath = _data.assets.comboNumber3?.assetPath;
case 4:
basePath = _data.assets.comboNumber4?.assetPath;
case 5:
basePath = _data.assets.comboNumber5?.assetPath;
case 6:
basePath = _data.assets.comboNumber6?.assetPath;
case 7:
basePath = _data.assets.comboNumber7?.assetPath;
case 8:
basePath = _data.assets.comboNumber8?.assetPath;
case 9:
basePath = _data.assets.comboNumber9?.assetPath;
default:
basePath = null;
}
if (basePath == null) return fallback?.buildComboNumSpritePath(digit);
var parts = basePath?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length < 1) return null;
if (parts.length == 1) return parts[0];
return parts[1];
}
public function getComboNumSpriteOffsets(digit:Int):Array<Float>
{
switch (digit)
{
case 0:
var result = _data.assets.comboNumber0?.offsets;
if (result == null) result = fallback?.getComboNumSpriteOffsets(digit) ?? [0, 0];
return result;
case 1:
var result = _data.assets.comboNumber1?.offsets;
if (result == null) result = fallback?.getComboNumSpriteOffsets(digit) ?? [0, 0];
return result;
case 2:
var result = _data.assets.comboNumber2?.offsets;
if (result == null) result = fallback?.getComboNumSpriteOffsets(digit) ?? [0, 0];
return result;
case 3:
var result = _data.assets.comboNumber3?.offsets;
if (result == null) result = fallback?.getComboNumSpriteOffsets(digit) ?? [0, 0];
return result;
case 4:
var result = _data.assets.comboNumber4?.offsets;
if (result == null) result = fallback?.getComboNumSpriteOffsets(digit) ?? [0, 0];
return result;
case 5:
var result = _data.assets.comboNumber5?.offsets;
if (result == null) result = fallback?.getComboNumSpriteOffsets(digit) ?? [0, 0];
return result;
case 6:
var result = _data.assets.comboNumber6?.offsets;
if (result == null) result = fallback?.getComboNumSpriteOffsets(digit) ?? [0, 0];
return result;
case 7:
var result = _data.assets.comboNumber7?.offsets;
if (result == null) result = fallback?.getComboNumSpriteOffsets(digit) ?? [0, 0];
return result;
case 8:
var result = _data.assets.comboNumber8?.offsets;
if (result == null) result = fallback?.getComboNumSpriteOffsets(digit) ?? [0, 0];
return result;
case 9:
var result = _data.assets.comboNumber9?.offsets;
if (result == null) result = fallback?.getComboNumSpriteOffsets(digit) ?? [0, 0];
return result;
default:
return [0, 0];
}
}
2025-02-24 18:45:03 +00:00
public function buildSplashSprite(target:NoteSplash):Void
{
var atlas:Null<FlxAtlasFrames> = buildSplashFrames(false);
if (atlas == null)
{
throw 'Could not load spritesheet for note style: $id';
}
target.frames = atlas;
target.antialiasing = !(_data.assets.noteSplash?.isPixel ?? false);
buildSplashAnimations(target);
target.splashFramerate = getSplashFramerate();
target.splashFramerateVariance = getSplashFramerateVariance();
target.alpha = _data.assets.noteSplash?.alpha ?? 1.0;
target.blend = _data.assets.noteSplash?.data?.blendMode ?? "normal";
2025-02-24 18:45:03 +00:00
var scale = getSplashScale();
target.scale.set(scale, scale);
target.updateHitbox();
}
var splashFrames:Null<FlxAtlasFrames> = null;
function buildSplashFrames(force:Bool = false):Null<FlxAtlasFrames>
{
var splashAssetPath:Null<String> = getSplashAssetPath();
if (splashAssetPath == null)
{
FlxG.log.warn('Note Splash asset path not found: ${id}');
return null;
}
if (!FunkinSprite.isTextureCached(Paths.image(splashAssetPath)))
{
FlxG.log.warn('Note Splash texture not cached: ${splashAssetPath}');
}
@:nullSafety(Off)
{
if (splashFrames?.parent?.isDestroyed ?? true) splashFrames = null;
2025-02-24 18:45:03 +00:00
}
if (splashFrames != null && !force) return splashFrames;
2025-03-03 02:44:41 +00:00
splashFrames = Paths.getSparrowAtlas(splashAssetPath, getAssetLibrary(getSplashAssetPath(true)));
2025-02-24 18:45:03 +00:00
splashFrames.parent.persist = true;
if (splashFrames == null)
{
throw 'Could not load notesplash frames for note style: $id';
}
return splashFrames;
}
public function getSplashAssetPath(raw:Bool = false):Null<String>
2025-02-24 18:45:03 +00:00
{
if (raw)
{
var rawPath:Null<String> = _data?.assets?.noteSplash?.assetPath;
if (rawPath == null) return fallback?.getSplashAssetPath(true);
return rawPath;
}
var parts = getSplashAssetPath(true)?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length == 0) return null;
if (parts.length == 1) return getSplashAssetPath(true);
return parts[1];
}
function buildSplashAnimations(target:NoteSplash):Void
{
2025-02-24 22:11:13 +00:00
final addSplashAnim:NoteDirection->Void = function(dir:NoteDirection) {
var animData:Null<Array<AnimationData>> = fetchSplashAnimationData(dir);
if (animData != null)
2025-02-24 18:45:03 +00:00
{
2025-02-24 22:11:13 +00:00
for (anim in animData)
FlxAnimationUtil.addAtlasAnimation(target, anim);
2025-02-24 18:45:03 +00:00
}
2025-02-24 22:11:13 +00:00
};
2025-02-24 18:45:03 +00:00
2025-02-24 22:11:13 +00:00
addSplashAnim(LEFT);
addSplashAnim(RIGHT);
addSplashAnim(UP);
addSplashAnim(DOWN);
2025-02-24 18:45:03 +00:00
}
public function isSplashAnimated():Bool
{
return _data?.assets?.noteSplash?.animated ?? fallback?.isSplashAnimated() ?? false;
}
public function getSplashScale():Float
{
return _data?.assets?.noteSplash?.scale ?? fallback?.getSplashScale() ?? 1.0;
}
function fetchSplashAnimationData(dir:NoteDirection):Null<Array<AnimationData>>
{
var result:Null<Array<AnimationData>> = switch (dir)
{
2025-02-24 22:11:13 +00:00
case LEFT: _data.assets?.noteSplash?.data?.leftSplashes?.toNamedArray("splashLEFT");
case DOWN: _data.assets?.noteSplash?.data?.downSplashes?.toNamedArray("splashDOWN");
case UP: _data.assets?.noteSplash?.data?.upSplashes?.toNamedArray("splashUP");
case RIGHT: _data.assets?.noteSplash?.data?.rightSplashes?.toNamedArray("splashRIGHT");
2025-02-24 18:45:03 +00:00
};
return result ?? fallback?.fetchSplashAnimationData(dir);
}
public function getSplashOffsets():Array<Float>
{
return _data?.assets?.noteSplash?.offsets ?? fallback?.getSplashOffsets() ?? [0.0, 0.0];
}
public function getSplashFramerate():Int
{
return _data?.assets?.noteSplash?.data?.framerateDefault ?? fallback?.getSplashFramerate() ?? 24;
}
public function getSplashFramerateVariance():Int
{
return _data?.assets?.noteSplash?.data?.framerateVariance ?? fallback?.getSplashFramerateVariance() ?? 2;
}
2025-03-03 02:44:41 +00:00
public function buildHoldCoverSprite(target:NoteHoldCover):Void
{
2025-03-12 01:06:28 +00:00
// NoteHoldCover has "glow" and "sparks". Right now it only implements "glow"
// but "sparks" I believe is meant to be used for the ending of the hold note
var glowAtlas:Null<FlxFramesCollection> = buildHoldCoverFrames(false);
if (glowAtlas == null)
2025-03-03 02:44:41 +00:00
{
throw 'Could not load spritesheet for note style: $id';
}
2025-03-12 01:06:28 +00:00
target.glow.frames = glowAtlas;
2025-03-03 02:44:41 +00:00
target.antialiasing = !(_data.assets.holdNoteCover?.isPixel ?? false);
2025-03-12 01:06:28 +00:00
target.glow.antialiasing = !(_data.assets.holdNoteCover?.isPixel ?? false);
target.scale.set(_data.assets.holdNoteCover?.scale ?? 1.0, _data.assets.holdNoteCover?.scale ?? 1.0);
target.updateHitbox();
target.glow.updateHitbox();
2025-03-03 02:44:41 +00:00
buildHoldCoverAnimations(target);
}
2025-03-12 01:06:28 +00:00
// we use FlxFramesCollection here so we can combine em into one atlas later
var holdCoverFrames:Null<FlxFramesCollection> = null;
2025-03-03 02:44:41 +00:00
2025-03-12 01:06:28 +00:00
function buildHoldCoverFrames(force:Bool = false):Null<FlxFramesCollection>
2025-03-03 02:44:41 +00:00
{
// If the hold cover frames got unloaded, ensure they get reloaded.
if (holdCoverFrames?.parent?.isDestroyed ?? true) holdCoverFrames = null;
// If the hold cover frames are already loaded, just use those.
2025-03-03 02:44:41 +00:00
if (holdCoverFrames != null && !force) return holdCoverFrames;
2025-03-03 02:44:41 +00:00
for (direction in Strumline.DIRECTIONS)
{
2025-03-12 01:06:28 +00:00
// We make a FlxFramesCollection here so we can combine em into one atlas later
var atlas:Null<FlxFramesCollection> = buildHoldCoverFrameForDirection(direction);
if (holdCoverFrames == null) holdCoverFrames = atlas;
else if (atlas != null) holdCoverFrames = FlxAnimationUtil.combineFramesCollections(holdCoverFrames, atlas);
2025-03-03 02:44:41 +00:00
}
return holdCoverFrames;
}
2025-03-12 01:06:28 +00:00
function buildHoldCoverFrameForDirection(direction:NoteDirection):Null<FlxFramesCollection>
2025-03-03 02:44:41 +00:00
{
var directionData = switch (direction)
{
case LEFT: _data.assets?.holdNoteCover?.data?.left;
case DOWN: _data.assets?.holdNoteCover?.data?.down;
case UP: _data.assets?.holdNoteCover?.data?.up;
case RIGHT: _data.assets?.holdNoteCover?.data?.right;
};
if (directionData == null) return null;
2025-03-12 01:06:28 +00:00
var holdCoverAssetPath:Null<String> = getHoldCoverDirectionAssetPath(direction) ?? fallback?.getHoldCoverDirectionAssetPath(direction);
2025-03-03 02:44:41 +00:00
if (holdCoverAssetPath == null)
{
FlxG.log.warn('Hold Note Cover asset path not found: ${id}');
return null;
}
if (!FunkinSprite.isTextureCached(Paths.image(holdCoverAssetPath)))
{
FlxG.log.warn('Hold Note Cover texture not cached: ${holdCoverAssetPath}');
}
var atlas:FlxFramesCollection = Paths.getSparrowAtlas(holdCoverAssetPath, getAssetLibrary(getHoldCoverDirectionAssetPath(direction, true)));
if (atlas == null) return null;
2025-03-12 01:06:28 +00:00
atlas.parent.persist = true;
2025-03-03 02:44:41 +00:00
return atlas;
}
function buildHoldCoverAnimations(target:NoteHoldCover):Void
{
2025-03-12 01:06:28 +00:00
for (direction in Strumline.DIRECTIONS)
{
var animData:Null<Array<AnimationData>> = fetchHoldCoverAnimationData(direction);
if (animData != null)
{
// this entry is the base animation that plays ("holdCover$noteColor" animation), which we want to loop
animData[1].looped = true;
FlxAnimationUtil.addAtlasAnimations(target.glow, animData);
}
}
2025-03-03 02:44:41 +00:00
}
2025-03-12 01:06:28 +00:00
function fetchHoldCoverAnimationData(dir:NoteDirection):Null<Array<AnimationData>>
2025-03-03 02:44:41 +00:00
{
2025-03-12 01:06:28 +00:00
var noteColor:String = dir.colorName.toTitleCase();
var result:Array<Null<AnimationData>> = switch (dir)
{
case LEFT: [
_data.assets?.holdNoteCover?.data?.left?.start?.toNamed('holdCoverStart$noteColor'),
_data.assets?.holdNoteCover?.data?.left?.hold?.toNamed('holdCover$noteColor'),
_data.assets?.holdNoteCover?.data?.left?.end?.toNamed('holdCoverEnd$noteColor'),
];
case DOWN: [
_data.assets?.holdNoteCover?.data?.down?.start?.toNamed('holdCoverStart$noteColor'),
_data.assets?.holdNoteCover?.data?.down?.hold?.toNamed('holdCover$noteColor'),
_data.assets?.holdNoteCover?.data?.down?.end?.toNamed('holdCoverEnd$noteColor'),
];
case UP: [
_data.assets?.holdNoteCover?.data?.up?.start?.toNamed('holdCoverStart$noteColor'),
_data.assets?.holdNoteCover?.data?.up?.hold?.toNamed('holdCover$noteColor'),
_data.assets?.holdNoteCover?.data?.up?.end?.toNamed('holdCoverEnd$noteColor'),
];
case RIGHT: [
_data.assets?.holdNoteCover?.data?.right?.start?.toNamed('holdCoverStart$noteColor'),
_data.assets?.holdNoteCover?.data?.right?.hold?.toNamed('holdCover$noteColor'),
_data.assets?.holdNoteCover?.data?.right?.end?.toNamed('holdCoverEnd$noteColor'),
];
default: [];
};
// New variable so we can change the type.
var filteredResult:Array<AnimationData> = thx.Arrays.filterNull(result);
if (filteredResult.length == 0) return fallback?.fetchHoldCoverAnimationData(dir) ?? [];
return filteredResult;
2025-03-03 02:44:41 +00:00
}
2025-03-12 01:06:28 +00:00
// fallbacks:
// assetPath for a direction -> root assetpath -> fallback assetpath for direction -> fallback root assetpath
// `direction` here is required, however it only is used for fallback purposes
function getHoldCoverRootAssetPath(direction:NoteDirection, raw:Bool = false):Null<String>
2025-03-03 02:44:41 +00:00
{
if (raw)
{
var rawPath:Null<String> = _data?.assets?.holdNoteCover?.assetPath;
2025-03-12 01:06:28 +00:00
// remember, if we need a fallback for our *root* asset path,
// we fallback and look for the *direction* asset path first
if (rawPath == null) return fallback?.getHoldCoverDirectionAssetPath(direction, true);
2025-03-03 02:44:41 +00:00
return rawPath;
}
2025-03-12 01:06:28 +00:00
var parts:Array<String> = getHoldCoverRootAssetPath(direction, true)?.split(Constants.LIBRARY_SEPARATOR) ?? [];
2025-03-03 02:44:41 +00:00
if (parts.length == 0) return null;
2025-03-12 01:06:28 +00:00
if (parts.length == 1) return getHoldCoverRootAssetPath(direction, true);
2025-03-03 02:44:41 +00:00
return parts[1];
}
public function getHoldCoverDirectionAssetPath(direction:NoteDirection, raw:Bool = false):Null<String>
2025-03-03 02:44:41 +00:00
{
if (raw)
{
var rawPath:Null<String> = switch (direction)
{
case LEFT: _data?.assets?.holdNoteCover?.data?.left?.assetPath;
case DOWN: _data?.assets?.holdNoteCover?.data?.down?.assetPath;
case UP: _data?.assets?.holdNoteCover?.data?.up?.assetPath;
case RIGHT: _data?.assets?.holdNoteCover?.data?.right?.assetPath;
};
2025-03-12 01:06:28 +00:00
// we don't want to fallback directly just yet, we want to check our root assetpath first
if (rawPath == null) return getHoldCoverRootAssetPath(direction, true);
2025-03-03 02:44:41 +00:00
return rawPath;
}
2025-03-12 01:06:28 +00:00
2025-03-03 02:44:41 +00:00
var parts:Array<String> = getHoldCoverDirectionAssetPath(direction, true)?.split(Constants.LIBRARY_SEPARATOR) ?? [];
if (parts.length == 0) return null;
if (parts.length == 1) return getHoldCoverDirectionAssetPath(direction, true);
return parts[1];
2025-03-12 01:06:28 +00:00
}
public function getHoldCoverOffsets():Array<Float>
{
return _data?.assets?.holdNoteCover?.offsets ?? fallback?.getHoldCoverOffsets() ?? [0.0, 0.0];
2025-03-03 02:44:41 +00:00
}
public function destroy():Void {}
2025-03-12 01:06:28 +00:00
/**
* Returns a string of the library name for the given asset id
* `default:assets/images/awesome.png` returns `default`
* If you pass a asset path with no library name (no `:` aka LIBRARY_SEPARATOR) it will return null
* @param id The asset id to get the library name from
* @return Null<String> The library name, or null if no library name is present
*/
2025-03-03 02:44:41 +00:00
function getAssetLibrary(?id:String):Null<String>
{
2025-03-12 01:06:28 +00:00
var parts:Array<String> = id?.split(Constants.LIBRARY_SEPARATOR) ?? [];
2025-03-03 02:44:41 +00:00
if (parts.length <= 1) return null;
2025-03-12 01:06:28 +00:00
2025-03-03 02:44:41 +00:00
return parts[0];
}
public function toString():String
{
return 'NoteStyle($id)';
}
static function _fetchData(id:String):NoteStyleData
{
var result = NoteStyleRegistry.instance.parseEntryDataWithMigration(id, NoteStyleRegistry.instance.fetchEntryVersion(id));
if (result == null)
{
throw 'Could not parse note style data for id: $id';
}
else
{
return result;
}
}
}