mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2024-12-23 21:56:46 +00:00
Merge branch 'rewrite/master' into burgerballs/song-resync-tweaks
This commit is contained in:
commit
f1af005cf3
6
.github/workflows/build-game.yml
vendored
6
.github/workflows/build-game.yml
vendored
|
@ -45,7 +45,11 @@ jobs:
|
|||
uses: ./.github/actions/setup-haxe
|
||||
with:
|
||||
gh-token: ${{ steps.app_token.outputs.token }}
|
||||
|
||||
- name: Setup HXCPP dev commit
|
||||
run: |
|
||||
cd .haxelib/hxcpp/git/tools/hxcpp
|
||||
haxe compile.hxml
|
||||
cd ../../../../..
|
||||
- name: Build game
|
||||
if: ${{ matrix.target == 'windows' }}
|
||||
run: |
|
||||
|
|
28
hmm.json
28
hmm.json
|
@ -11,14 +11,14 @@
|
|||
"name": "flixel",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "a7d8e3bad89a0a3506a4714121f73d8e34522c49",
|
||||
"ref": "10c2a203c43a78ff1ff26b8368fd736576829d8d",
|
||||
"url": "https://github.com/FunkinCrew/flixel"
|
||||
},
|
||||
{
|
||||
"name": "flixel-addons",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "a523c3b56622f0640933944171efed46929e360e",
|
||||
"ref": "9c6fb47968e894eb36bf10e94725cd7640c49281",
|
||||
"url": "https://github.com/FunkinCrew/flixel-addons"
|
||||
},
|
||||
{
|
||||
|
@ -30,7 +30,7 @@
|
|||
"name": "flixel-ui",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "719b4f10d94186ed55f6fef1b6618d32abec8c15",
|
||||
"ref": "d0afed7293c71ffdb1184751317fc709b44c9056",
|
||||
"url": "https://github.com/HaxeFlixel/flixel-ui"
|
||||
},
|
||||
{
|
||||
|
@ -99,8 +99,10 @@
|
|||
},
|
||||
{
|
||||
"name": "hxcpp",
|
||||
"type": "haxelib",
|
||||
"version": "4.3.2"
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"url": "https://github.com/HaxeFoundation/hxcpp",
|
||||
"ref": "01cfee282a9a783e10c5a7774a3baaf547e6b0a7"
|
||||
},
|
||||
{
|
||||
"name": "hxcpp-debug-server",
|
||||
|
@ -121,6 +123,20 @@
|
|||
"ref": "a8c26f18463c98da32f744c214fe02273e1823fa",
|
||||
"url": "https://github.com/FunkinCrew/json2object"
|
||||
},
|
||||
{
|
||||
"name": "jsonpatch",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "f9b83215acd586dc28754b4ae7f69d4c06c3b4d3",
|
||||
"url": "https://github.com/EliteMasterEric/jsonpatch"
|
||||
},
|
||||
{
|
||||
"name": "jsonpath",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "7a24193717b36393458c15c0435bb7c4470ecdda",
|
||||
"url": "https://github.com/EliteMasterEric/jsonpath"
|
||||
},
|
||||
{
|
||||
"name": "lime",
|
||||
"type": "git",
|
||||
|
@ -167,7 +183,7 @@
|
|||
"name": "polymod",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "bfbe30d81601b3543d80dce580108ad6b7e182c7",
|
||||
"ref": "98945c6c7f5ecde01a32c4623d3515bf012a023a",
|
||||
"url": "https://github.com/larsiusprime/polymod"
|
||||
},
|
||||
{
|
||||
|
|
|
@ -27,6 +27,7 @@ import funkin.data.dialogue.speaker.SpeakerRegistry;
|
|||
import funkin.data.freeplay.album.AlbumRegistry;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.play.notes.notekind.NoteKindManager;
|
||||
import funkin.modding.module.ModuleHandler;
|
||||
import funkin.ui.title.TitleState;
|
||||
import funkin.util.CLIUtil;
|
||||
|
@ -176,6 +177,8 @@ class InitState extends FlxState
|
|||
// Move it to use a BaseRegistry.
|
||||
CharacterDataParser.loadCharacterCache();
|
||||
|
||||
NoteKindManager.loadScripts();
|
||||
|
||||
ModuleHandler.buildModuleCallbacks();
|
||||
ModuleHandler.loadModuleCache();
|
||||
ModuleHandler.callOnCreate();
|
||||
|
|
|
@ -46,7 +46,7 @@ class SongEventRegistry
|
|||
|
||||
if (event != null)
|
||||
{
|
||||
trace(' Loaded built-in song event: (${event.id})');
|
||||
trace(' Loaded built-in song event: ${event.id}');
|
||||
eventCache.set(event.id, event);
|
||||
}
|
||||
else
|
||||
|
@ -59,9 +59,9 @@ class SongEventRegistry
|
|||
static function registerScriptedEvents()
|
||||
{
|
||||
var scriptedEventClassNames:Array<String> = ScriptedSongEvent.listScriptClasses();
|
||||
trace('Instantiating ${scriptedEventClassNames.length} scripted song events...');
|
||||
if (scriptedEventClassNames == null || scriptedEventClassNames.length == 0) return;
|
||||
|
||||
trace('Instantiating ${scriptedEventClassNames.length} scripted song events...');
|
||||
for (eventCls in scriptedEventClassNames)
|
||||
{
|
||||
var event:SongEvent = ScriptedSongEvent.init(eventCls, "UKNOWN");
|
||||
|
|
|
@ -109,6 +109,14 @@ typedef NoteStyleAssetData<T> =
|
|||
@:optional
|
||||
var isPixel:Bool;
|
||||
|
||||
/**
|
||||
* If true, animations will be played on the graphic.
|
||||
* @default `false` to save performance.
|
||||
*/
|
||||
@:default(false)
|
||||
@:optional
|
||||
var animated:Bool;
|
||||
|
||||
/**
|
||||
* The structure of this data depends on the asset.
|
||||
*/
|
||||
|
|
|
@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [2.2.4]
|
||||
### Added
|
||||
- Added `playData.characters.opponentVocals` to specify which vocal track(s) to play for the opponent.
|
||||
- If the value isn't present, it will use the `playData.characters.opponent`, but if it is present, it will be used (even if it's empty, in which case no vocals will be used for the opponent)
|
||||
- Added `playData.characters.playerVocals` to specify which vocal track(s) to play for the player.
|
||||
- If the value isn't present, it will use the `playData.characters.player`, but if it is present, it will be used (even if it's empty, in which case no vocals will be used for the player)
|
||||
|
||||
## [2.2.3]
|
||||
### Added
|
||||
- Added `charter` field to denote authorship of a chart.
|
||||
|
|
|
@ -529,12 +529,26 @@ class SongCharacterData implements ICloneable<SongCharacterData>
|
|||
@:default([])
|
||||
public var altInstrumentals:Array<String> = [];
|
||||
|
||||
public function new(player:String = '', girlfriend:String = '', opponent:String = '', instrumental:String = '')
|
||||
@:optional
|
||||
public var opponentVocals:Null<Array<String>> = null;
|
||||
|
||||
@:optional
|
||||
public var playerVocals:Null<Array<String>> = null;
|
||||
|
||||
public function new(player:String = '', girlfriend:String = '', opponent:String = '', instrumental:String = '', ?altInstrumentals:Array<String>,
|
||||
?opponentVocals:Array<String>, ?playerVocals:Array<String>)
|
||||
{
|
||||
this.player = player;
|
||||
this.girlfriend = girlfriend;
|
||||
this.opponent = opponent;
|
||||
this.instrumental = instrumental;
|
||||
|
||||
this.altInstrumentals = altInstrumentals;
|
||||
this.opponentVocals = opponentVocals;
|
||||
this.playerVocals = playerVocals;
|
||||
|
||||
if (opponentVocals == null) this.opponentVocals = [opponent];
|
||||
if (playerVocals == null) this.playerVocals = [player];
|
||||
}
|
||||
|
||||
public function clone():SongCharacterData
|
||||
|
@ -722,18 +736,6 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
|
|||
{
|
||||
return new SongEventDataRaw(this.time, this.eventKind, this.value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap SongEventData in an abstract so we can overload operators.
|
||||
*/
|
||||
@:forward(time, eventKind, value, activated, getStepTime, clone)
|
||||
abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
|
||||
{
|
||||
public function new(time:Float, eventKind:String, value:Dynamic = null)
|
||||
{
|
||||
this = new SongEventDataRaw(time, eventKind, value);
|
||||
}
|
||||
|
||||
public function valueAsStruct(?defaultKey:String = "key"):Dynamic
|
||||
{
|
||||
|
@ -757,27 +759,27 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
|
|||
}
|
||||
}
|
||||
|
||||
public inline function getHandler():Null<SongEvent>
|
||||
public function getHandler():Null<SongEvent>
|
||||
{
|
||||
return SongEventRegistry.getEvent(this.eventKind);
|
||||
}
|
||||
|
||||
public inline function getSchema():Null<SongEventSchema>
|
||||
public function getSchema():Null<SongEventSchema>
|
||||
{
|
||||
return SongEventRegistry.getEventSchema(this.eventKind);
|
||||
}
|
||||
|
||||
public inline function getDynamic(key:String):Null<Dynamic>
|
||||
public function getDynamic(key:String):Null<Dynamic>
|
||||
{
|
||||
return this.value == null ? null : Reflect.field(this.value, key);
|
||||
}
|
||||
|
||||
public inline function getBool(key:String):Null<Bool>
|
||||
public function getBool(key:String):Null<Bool>
|
||||
{
|
||||
return this.value == null ? null : cast Reflect.field(this.value, key);
|
||||
}
|
||||
|
||||
public inline function getInt(key:String):Null<Int>
|
||||
public function getInt(key:String):Null<Int>
|
||||
{
|
||||
if (this.value == null) return null;
|
||||
var result = Reflect.field(this.value, key);
|
||||
|
@ -787,7 +789,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
|
|||
return cast result;
|
||||
}
|
||||
|
||||
public inline function getFloat(key:String):Null<Float>
|
||||
public function getFloat(key:String):Null<Float>
|
||||
{
|
||||
if (this.value == null) return null;
|
||||
var result = Reflect.field(this.value, key);
|
||||
|
@ -797,17 +799,17 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
|
|||
return cast result;
|
||||
}
|
||||
|
||||
public inline function getString(key:String):String
|
||||
public function getString(key:String):String
|
||||
{
|
||||
return this.value == null ? null : cast Reflect.field(this.value, key);
|
||||
}
|
||||
|
||||
public inline function getArray(key:String):Array<Dynamic>
|
||||
public function getArray(key:String):Array<Dynamic>
|
||||
{
|
||||
return this.value == null ? null : cast Reflect.field(this.value, key);
|
||||
}
|
||||
|
||||
public inline function getBoolArray(key:String):Array<Bool>
|
||||
public function getBoolArray(key:String):Array<Bool>
|
||||
{
|
||||
return this.value == null ? null : cast Reflect.field(this.value, key);
|
||||
}
|
||||
|
@ -839,6 +841,19 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
|
|||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap SongEventData in an abstract so we can overload operators.
|
||||
*/
|
||||
@:forward(time, eventKind, value, activated, getStepTime, clone, getHandler, getSchema, getDynamic, getBool, getInt, getFloat, getString, getArray,
|
||||
getBoolArray, buildTooltip, valueAsStruct)
|
||||
abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
|
||||
{
|
||||
public function new(time:Float, eventKind:String, value:Dynamic = null)
|
||||
{
|
||||
this = new SongEventDataRaw(time, eventKind, value);
|
||||
}
|
||||
|
||||
public function clone():SongEventData
|
||||
{
|
||||
|
@ -951,12 +966,18 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
|
|||
return this.kind = value;
|
||||
}
|
||||
|
||||
public function new(time:Float, data:Int, length:Float = 0, kind:String = '')
|
||||
@:alias("p")
|
||||
@:default([])
|
||||
@:optional
|
||||
public var params:Array<NoteParamData>;
|
||||
|
||||
public function new(time:Float, data:Int, length:Float = 0, kind:String = '', ?params:Array<NoteParamData>)
|
||||
{
|
||||
this.time = time;
|
||||
this.data = data;
|
||||
this.length = length;
|
||||
this.kind = kind;
|
||||
this.params = params ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1051,9 +1072,19 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
|
|||
_stepLength = null;
|
||||
}
|
||||
|
||||
public function cloneParams():Array<NoteParamData>
|
||||
{
|
||||
var params:Array<NoteParamData> = [];
|
||||
for (param in this.params)
|
||||
{
|
||||
params.push(param.clone());
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
public function clone():SongNoteDataRaw
|
||||
{
|
||||
return new SongNoteDataRaw(this.time, this.data, this.length, this.kind);
|
||||
return new SongNoteDataRaw(this.time, this.data, this.length, this.kind, cloneParams());
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
|
@ -1069,9 +1100,9 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
|
|||
@:forward
|
||||
abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
|
||||
{
|
||||
public function new(time:Float, data:Int, length:Float = 0, kind:String = '')
|
||||
public function new(time:Float, data:Int, length:Float = 0, kind:String = '', ?params:Array<NoteParamData>)
|
||||
{
|
||||
this = new SongNoteDataRaw(time, data, length, kind);
|
||||
this = new SongNoteDataRaw(time, data, length, kind, params);
|
||||
}
|
||||
|
||||
public static function buildDirectionName(data:Int, strumlineSize:Int = 4):String
|
||||
|
@ -1115,7 +1146,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
|
|||
if (other.kind == '' || this.kind == null) return false;
|
||||
}
|
||||
|
||||
return this.time == other.time && this.data == other.data && this.length == other.length;
|
||||
return this.time == other.time && this.data == other.data && this.length == other.length && this.params == other.params;
|
||||
}
|
||||
|
||||
@:op(A != B)
|
||||
|
@ -1134,7 +1165,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
|
|||
if (other.kind == '') return true;
|
||||
}
|
||||
|
||||
return this.time != other.time || this.data != other.data || this.length != other.length;
|
||||
return this.time != other.time || this.data != other.data || this.length != other.length || this.params != other.params;
|
||||
}
|
||||
|
||||
@:op(A > B)
|
||||
|
@ -1171,7 +1202,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
|
|||
|
||||
public function clone():SongNoteData
|
||||
{
|
||||
return new SongNoteData(this.time, this.data, this.length, this.kind);
|
||||
return new SongNoteData(this.time, this.data, this.length, this.kind, this.params);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1183,3 +1214,30 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
|
|||
+ (this.kind != '' ? ' [kind: ${this.kind}])' : ')');
|
||||
}
|
||||
}
|
||||
|
||||
class NoteParamData implements ICloneable<NoteParamData>
|
||||
{
|
||||
@:alias("n")
|
||||
public var name:String;
|
||||
|
||||
@:alias("v")
|
||||
@:jcustomparse(funkin.data.DataParse.dynamicValue)
|
||||
@:jcustomwrite(funkin.data.DataWrite.dynamicValue)
|
||||
public var value:Dynamic;
|
||||
|
||||
public function new(name:String, value:Dynamic)
|
||||
{
|
||||
this.name = name;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public function clone():NoteParamData
|
||||
{
|
||||
return new NoteParamData(this.name, this.value);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
return 'NoteParamData(${this.name}, ${this.value})';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -199,6 +199,8 @@ class FNFLegacyImporter
|
|||
{
|
||||
// Handle the dumb logic for mustHitSection.
|
||||
var noteData = note.data;
|
||||
if (noteData < 0) continue; // Exclude Psych event notes.
|
||||
if (noteData > (STRUMLINE_SIZE * 2)) noteData = noteData % (2 * STRUMLINE_SIZE); // Handle other engine event notes.
|
||||
|
||||
// Flip notes if mustHitSection is FALSE (not true lol).
|
||||
if (!mustHitSection)
|
||||
|
|
|
@ -7,6 +7,7 @@ import funkin.data.dialogue.speaker.SpeakerRegistry;
|
|||
import funkin.data.event.SongEventRegistry;
|
||||
import funkin.data.story.level.LevelRegistry;
|
||||
import funkin.data.notestyle.NoteStyleRegistry;
|
||||
import funkin.play.notes.notekind.NoteKindManager;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.data.freeplay.player.PlayerRegistry;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
|
@ -233,6 +234,8 @@ class PolymodHandler
|
|||
// NOTE: Scripted classes are automatically aliased to their parent class.
|
||||
Polymod.addImportAlias('flixel.math.FlxPoint', flixel.math.FlxPoint.FlxBasePoint);
|
||||
|
||||
Polymod.addImportAlias('funkin.data.event.SongEventSchema', funkin.data.event.SongEventSchema.SongEventSchemaRaw);
|
||||
|
||||
// Add blacklisting for prohibited classes and packages.
|
||||
|
||||
// `Sys`
|
||||
|
@ -251,6 +254,10 @@ class PolymodHandler
|
|||
// Lib.load() can load malicious DLLs
|
||||
Polymod.blacklistImport('cpp.Lib');
|
||||
|
||||
// `Unserializer`
|
||||
// Unserializerr.DEFAULT_RESOLVER.resolveClass() can access blacklisted packages
|
||||
Polymod.blacklistImport('Unserializer');
|
||||
|
||||
// `polymod.*`
|
||||
// You can probably unblacklist a module
|
||||
for (cls in ClassMacro.listClassesInPackage('polymod'))
|
||||
|
@ -383,6 +390,7 @@ class PolymodHandler
|
|||
StageRegistry.instance.loadEntries();
|
||||
|
||||
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
|
||||
NoteKindManager.loadScripts();
|
||||
ModuleHandler.loadModuleCache();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ import funkin.play.notes.NoteSprite;
|
|||
import funkin.play.notes.notestyle.NoteStyle;
|
||||
import funkin.play.notes.Strumline;
|
||||
import funkin.play.notes.SustainTrail;
|
||||
import funkin.play.notes.notekind.NoteKindManager;
|
||||
import funkin.play.scoring.Scoring;
|
||||
import funkin.play.song.Song;
|
||||
import funkin.play.stage.Stage;
|
||||
|
@ -503,7 +504,7 @@ class PlayState extends MusicBeatSubState
|
|||
public var camGame:FlxCamera;
|
||||
|
||||
/**
|
||||
* The camera which contains, and controls visibility of, a video cutscene.
|
||||
* The camera which contains, and controls visibility of, a video cutscene, dialogue, pause menu and sticker transition.
|
||||
*/
|
||||
public var camCutscene:FlxCamera;
|
||||
|
||||
|
@ -578,7 +579,8 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
// TODO: Refactor or document
|
||||
var generatedMusic:Bool = false;
|
||||
var perfectMode:Bool = false;
|
||||
|
||||
var skipEndingTransition:Bool = false;
|
||||
|
||||
static final BACKGROUND_COLOR:FlxColor = FlxColor.BLACK;
|
||||
|
||||
|
@ -975,7 +977,7 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
FlxTransitionableState.skipNextTransIn = true;
|
||||
FlxTransitionableState.skipNextTransOut = true;
|
||||
pauseSubState.camera = camHUD;
|
||||
pauseSubState.camera = camCutscene;
|
||||
openSubState(pauseSubState);
|
||||
// boyfriendPos.put(); // TODO: Why is this here?
|
||||
}
|
||||
|
@ -1165,6 +1167,9 @@ class PlayState extends MusicBeatSubState
|
|||
// super.dispatchEvent(event) dispatches event to module scripts.
|
||||
super.dispatchEvent(event);
|
||||
|
||||
// Dispatch event to note kind scripts
|
||||
NoteKindManager.callEvent(event);
|
||||
|
||||
// Dispatch event to stage script.
|
||||
ScriptEventDispatcher.callEvent(currentStage, event);
|
||||
|
||||
|
@ -1176,8 +1181,6 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
// Dispatch event to conversation script.
|
||||
ScriptEventDispatcher.callEvent(currentConversation, event);
|
||||
|
||||
// TODO: Dispatch event to note scripts
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1348,64 +1351,13 @@ class PlayState extends MusicBeatSubState
|
|||
}
|
||||
|
||||
/**
|
||||
* Removes any references to the current stage, then clears the stage cache,
|
||||
* then reloads all the stages.
|
||||
*
|
||||
* This is useful for when you want to edit a stage without reloading the whole game.
|
||||
* Reloading works on both the JSON and the HXC, if applicable.
|
||||
*
|
||||
* Call this by pressing F5 on a debug build.
|
||||
*/
|
||||
override function debug_refreshModules():Void
|
||||
override function reloadAssets():Void
|
||||
{
|
||||
// Prevent further gameplay updates, which will try to reference dead objects.
|
||||
criticalFailure = true;
|
||||
|
||||
// Remove the current stage. If the stage gets deleted while it's still in use,
|
||||
// it'll probably crash the game or something.
|
||||
if (this.currentStage != null)
|
||||
{
|
||||
remove(currentStage);
|
||||
var event:ScriptEvent = new ScriptEvent(DESTROY, false);
|
||||
ScriptEventDispatcher.callEvent(currentStage, event);
|
||||
currentStage = null;
|
||||
}
|
||||
|
||||
if (!overrideMusic)
|
||||
{
|
||||
// Stop the instrumental.
|
||||
if (FlxG.sound.music != null)
|
||||
{
|
||||
FlxG.sound.music.destroy();
|
||||
FlxG.sound.music = null;
|
||||
}
|
||||
|
||||
// Stop the vocals.
|
||||
if (vocals != null && vocals.exists)
|
||||
{
|
||||
vocals.destroy();
|
||||
vocals = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Stop the instrumental.
|
||||
if (FlxG.sound.music != null)
|
||||
{
|
||||
FlxG.sound.music.stop();
|
||||
}
|
||||
|
||||
// Stop the vocals.
|
||||
if (vocals != null && vocals.exists)
|
||||
{
|
||||
vocals.stop();
|
||||
}
|
||||
}
|
||||
|
||||
super.debug_refreshModules();
|
||||
|
||||
var event:ScriptEvent = new ScriptEvent(CREATE, false);
|
||||
ScriptEventDispatcher.callEvent(currentSong, event);
|
||||
funkin.modding.PolymodHandler.forceReloadAssets();
|
||||
lastParams.targetSong = SongRegistry.instance.fetchEntry(currentSong.id);
|
||||
LoadingState.loadPlayState(lastParams);
|
||||
}
|
||||
|
||||
override function stepHit():Bool
|
||||
|
@ -1501,9 +1453,6 @@ class PlayState extends MusicBeatSubState
|
|||
if (playerStrumline != null) playerStrumline.onBeatHit();
|
||||
if (opponentStrumline != null) opponentStrumline.onBeatHit();
|
||||
|
||||
// Make the characters dance on the beat
|
||||
danceOnBeat();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1514,26 +1463,6 @@ class PlayState extends MusicBeatSubState
|
|||
super.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles characters dancing to the beat of the current song.
|
||||
*
|
||||
* TODO: Move some of this logic into `Bopper.hx`, or individual character scripts.
|
||||
*/
|
||||
function danceOnBeat():Void
|
||||
{
|
||||
if (currentStage == null) return;
|
||||
|
||||
// TODO: Add HEY! song events to Tutorial.
|
||||
if (Conductor.instance.currentBeat % 16 == 15
|
||||
&& currentStage.getDad().characterId == 'gf'
|
||||
&& Conductor.instance.currentBeat > 16
|
||||
&& Conductor.instance.currentBeat < 48)
|
||||
{
|
||||
currentStage.getBoyfriend().playAnimation('hey', true);
|
||||
currentStage.getDad().playAnimation('cheer', true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the game and HUD cameras.
|
||||
*/
|
||||
|
@ -1934,7 +1863,6 @@ class PlayState extends MusicBeatSubState
|
|||
if (!result) return;
|
||||
|
||||
isInCutscene = false;
|
||||
camCutscene.visible = false;
|
||||
|
||||
// TODO: Maybe tween in the camera after any cutscenes.
|
||||
camHUD.visible = true;
|
||||
|
@ -2000,7 +1928,9 @@ class PlayState extends MusicBeatSubState
|
|||
return;
|
||||
}
|
||||
|
||||
FlxG.sound.music.onComplete = endSong.bind(false);
|
||||
FlxG.sound.music.onComplete = function() {
|
||||
endSong(skipEndingTransition);
|
||||
};
|
||||
// A negative instrumental offset means the song skips the first few milliseconds of the track.
|
||||
// This just gets added into the startTimestamp behavior so we don't need to do anything extra.
|
||||
FlxG.sound.music.play(true, startTimestamp - Conductor.instance.instrumentalOffset);
|
||||
|
@ -2612,12 +2542,6 @@ class PlayState extends MusicBeatSubState
|
|||
*/
|
||||
function debugKeyShit():Void
|
||||
{
|
||||
#if !debug
|
||||
perfectMode = false;
|
||||
#else
|
||||
if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible;
|
||||
#end
|
||||
|
||||
#if CHART_EDITOR_SUPPORTED
|
||||
// Open the stage editor overlaying the current state.
|
||||
if (controls.DEBUG_STAGE)
|
||||
|
@ -2649,6 +2573,9 @@ class PlayState extends MusicBeatSubState
|
|||
#end
|
||||
|
||||
#if (debug || FORCE_DEBUG_VERSION)
|
||||
// H: Hide the HUD.
|
||||
if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible;
|
||||
|
||||
// 1: End the song immediately.
|
||||
if (FlxG.keys.justPressed.ONE) endSong(true);
|
||||
|
||||
|
|
|
@ -521,6 +521,9 @@ class BaseCharacter extends Bopper
|
|||
{
|
||||
super.onNoteHit(event);
|
||||
|
||||
// If another script cancelled the event, don't do anything.
|
||||
if (event.eventCanceled) return;
|
||||
|
||||
if (event.note.noteData.getMustHitNote() && characterType == BF)
|
||||
{
|
||||
// If the note is from the same strumline, play the sing animation.
|
||||
|
@ -553,6 +556,9 @@ class BaseCharacter extends Bopper
|
|||
{
|
||||
super.onNoteMiss(event);
|
||||
|
||||
// If another script cancelled the event, don't do anything.
|
||||
if (event.eventCanceled) return;
|
||||
|
||||
if (event.note.noteData.getMustHitNote() && characterType == BF)
|
||||
{
|
||||
// If the note is from the same strumline, play the sing animation.
|
||||
|
|
|
@ -81,7 +81,6 @@ class VideoCutscene
|
|||
// Trigger the cutscene. Don't play the song in the background.
|
||||
PlayState.instance.isInCutscene = true;
|
||||
PlayState.instance.camHUD.visible = false;
|
||||
PlayState.instance.camCutscene.visible = true;
|
||||
|
||||
// Display a black screen to hide the game while the video is playing.
|
||||
blackScreen = new FlxSprite(-200, -200).makeGraphic(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
|
||||
|
@ -305,7 +304,6 @@ class VideoCutscene
|
|||
vid = null;
|
||||
#end
|
||||
|
||||
PlayState.instance.camCutscene.visible = true;
|
||||
PlayState.instance.camHUD.visible = true;
|
||||
|
||||
FlxTween.tween(blackScreen, {alpha: 0}, transitionTime,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package funkin.play.notes;
|
||||
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.NoteParamData;
|
||||
import funkin.play.notes.notestyle.NoteStyle;
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import flixel.FlxSprite;
|
||||
|
@ -65,6 +66,22 @@ class NoteSprite extends FunkinSprite
|
|||
return this.noteData.kind = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* An array of custom parameters for this note
|
||||
*/
|
||||
public var params(get, set):Array<NoteParamData>;
|
||||
|
||||
function get_params():Array<NoteParamData>
|
||||
{
|
||||
return this.noteData?.params ?? [];
|
||||
}
|
||||
|
||||
function set_params(value:Array<NoteParamData>):Array<NoteParamData>
|
||||
{
|
||||
if (this.noteData == null) return value;
|
||||
return this.noteData.params = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The data of the note (i.e. the direction.)
|
||||
*/
|
||||
|
@ -74,7 +91,7 @@ class NoteSprite extends FunkinSprite
|
|||
{
|
||||
if (frames == null) return value;
|
||||
|
||||
animation.play(DIRECTION_COLORS[value] + 'Scroll');
|
||||
playNoteAnimation(value);
|
||||
|
||||
this.direction = value;
|
||||
return this.direction;
|
||||
|
@ -135,19 +152,37 @@ class NoteSprite extends FunkinSprite
|
|||
this.hsvShader = new HSVShader();
|
||||
|
||||
setupNoteGraphic(noteStyle);
|
||||
|
||||
// Disables the update() function for performance.
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
function setupNoteGraphic(noteStyle:NoteStyle):Void
|
||||
/**
|
||||
* Creates frames and animations
|
||||
* @param noteStyle The `NoteStyle` instance
|
||||
*/
|
||||
public function setupNoteGraphic(noteStyle:NoteStyle):Void
|
||||
{
|
||||
noteStyle.buildNoteSprite(this);
|
||||
|
||||
setGraphicSize(Strumline.STRUMLINE_SIZE);
|
||||
updateHitbox();
|
||||
|
||||
this.shader = hsvShader;
|
||||
|
||||
// `false` disables the update() function for performance.
|
||||
this.active = noteStyle.isNoteAnimated();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the value of the param with the given name
|
||||
* @param name Name of the param
|
||||
* @return Null<Dynamic>
|
||||
*/
|
||||
public function getParam(name:String):Null<Dynamic>
|
||||
{
|
||||
for (param in params)
|
||||
{
|
||||
if (param.name == name)
|
||||
{
|
||||
return param.value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
#if FLX_DEBUG
|
||||
|
@ -173,6 +208,11 @@ class NoteSprite extends FunkinSprite
|
|||
}
|
||||
#end
|
||||
|
||||
function playNoteAnimation(value:Int):Void
|
||||
{
|
||||
animation.play(DIRECTION_COLORS[value] + 'Scroll');
|
||||
}
|
||||
|
||||
public function desaturate():Void
|
||||
{
|
||||
this.hsvShader.saturation = 0.2;
|
||||
|
|
|
@ -16,6 +16,7 @@ import funkin.data.song.SongData.SongNoteData;
|
|||
import funkin.ui.options.PreferencesMenu;
|
||||
import funkin.util.SortUtil;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.play.notes.notekind.NoteKindManager;
|
||||
|
||||
/**
|
||||
* A group of sprites which handles the receptor, the note splashes, and the notes (with sustains) for a given player.
|
||||
|
@ -708,11 +709,15 @@ class Strumline extends FlxSpriteGroup
|
|||
|
||||
if (noteSprite != null)
|
||||
{
|
||||
var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind, this.noteStyle.id) ?? this.noteStyle;
|
||||
noteSprite.setupNoteGraphic(noteKindStyle);
|
||||
|
||||
noteSprite.direction = note.getDirection();
|
||||
noteSprite.noteData = note;
|
||||
|
||||
noteSprite.x = this.x;
|
||||
noteSprite.x += getXPos(DIRECTIONS[note.getDirection() % KEY_COUNT]);
|
||||
noteSprite.x -= (noteSprite.width - Strumline.STRUMLINE_SIZE) / 2; // Center it
|
||||
noteSprite.x -= NUDGE;
|
||||
// noteSprite.x += INITIAL_OFFSET;
|
||||
noteSprite.y = -9999;
|
||||
|
@ -727,6 +732,9 @@ class Strumline extends FlxSpriteGroup
|
|||
|
||||
if (holdNoteSprite != null)
|
||||
{
|
||||
var noteKindStyle:NoteStyle = NoteKindManager.getNoteStyle(note.kind, this.noteStyle.id) ?? this.noteStyle;
|
||||
holdNoteSprite.setupHoldNoteGraphic(noteKindStyle);
|
||||
|
||||
holdNoteSprite.parentStrumline = this;
|
||||
holdNoteSprite.noteData = note;
|
||||
holdNoteSprite.strumTime = note.time;
|
||||
|
|
|
@ -99,7 +99,27 @@ class SustainTrail extends FlxSprite
|
|||
*/
|
||||
public function new(noteDirection:NoteDirection, sustainLength:Float, noteStyle:NoteStyle)
|
||||
{
|
||||
super(0, 0, noteStyle.getHoldNoteAssetPath());
|
||||
super(0, 0);
|
||||
|
||||
// BASIC SETUP
|
||||
this.sustainLength = sustainLength;
|
||||
this.fullSustainLength = sustainLength;
|
||||
this.noteDirection = noteDirection;
|
||||
|
||||
setupHoldNoteGraphic(noteStyle);
|
||||
|
||||
indices = new DrawData<Int>(12, true, TRIANGLE_VERTEX_INDICES);
|
||||
|
||||
this.active = true; // This NEEDS to be true for the note to be drawn!
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates hold note graphic and applies correct zooming
|
||||
* @param noteStyle The note style
|
||||
*/
|
||||
public function setupHoldNoteGraphic(noteStyle:NoteStyle):Void
|
||||
{
|
||||
loadGraphic(noteStyle.getHoldNoteAssetPath());
|
||||
|
||||
antialiasing = true;
|
||||
|
||||
|
@ -109,13 +129,14 @@ class SustainTrail extends FlxSprite
|
|||
endOffset = bottomClip = 1;
|
||||
antialiasing = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
endOffset = 0.5;
|
||||
bottomClip = 0.9;
|
||||
}
|
||||
|
||||
zoom = 1.0;
|
||||
zoom *= noteStyle.fetchHoldNoteScale();
|
||||
|
||||
// BASIC SETUP
|
||||
this.sustainLength = sustainLength;
|
||||
this.fullSustainLength = sustainLength;
|
||||
this.noteDirection = noteDirection;
|
||||
|
||||
zoom *= 0.7;
|
||||
|
||||
// CALCULATE SIZE
|
||||
|
@ -131,9 +152,6 @@ class SustainTrail extends FlxSprite
|
|||
updateColorTransform();
|
||||
|
||||
updateClipping();
|
||||
indices = new DrawData<Int>(12, true, TRIANGLE_VERTEX_INDICES);
|
||||
|
||||
this.active = true; // This NEEDS to be true for the note to be drawn!
|
||||
}
|
||||
|
||||
function getBaseScrollSpeed()
|
||||
|
@ -195,6 +213,11 @@ class SustainTrail extends FlxSprite
|
|||
*/
|
||||
public function updateClipping(songTime:Float = 0):Void
|
||||
{
|
||||
if (graphic == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), parentStrumline?.scrollSpeed ?? 1.0), 0, graphicHeight);
|
||||
if (clipHeight <= 0.1)
|
||||
{
|
||||
|
|
119
source/funkin/play/notes/notekind/NoteKind.hx
Normal file
119
source/funkin/play/notes/notekind/NoteKind.hx
Normal file
|
@ -0,0 +1,119 @@
|
|||
package funkin.play.notes.notekind;
|
||||
|
||||
import funkin.modding.IScriptedClass.INoteScriptedClass;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import flixel.math.FlxMath;
|
||||
|
||||
/**
|
||||
* Class for note scripts
|
||||
*/
|
||||
class NoteKind implements INoteScriptedClass
|
||||
{
|
||||
/**
|
||||
* The name of the note kind
|
||||
*/
|
||||
public var noteKind:String;
|
||||
|
||||
/**
|
||||
* Description used in chart editor
|
||||
*/
|
||||
public var description:String;
|
||||
|
||||
/**
|
||||
* Custom note style
|
||||
*/
|
||||
public var noteStyleId:Null<String>;
|
||||
|
||||
/**
|
||||
* Custom parameters for the chart editor
|
||||
*/
|
||||
public var params:Array<NoteKindParam>;
|
||||
|
||||
public function new(noteKind:String, description:String = "", ?noteStyleId:String, ?params:Array<NoteKindParam>)
|
||||
{
|
||||
this.noteKind = noteKind;
|
||||
this.description = description;
|
||||
this.noteStyleId = noteStyleId;
|
||||
this.params = params ?? [];
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
return noteKind;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve all notes of this kind
|
||||
* @return Array<NoteSprite>
|
||||
*/
|
||||
function getNotes():Array<NoteSprite>
|
||||
{
|
||||
var allNotes:Array<NoteSprite> = PlayState.instance.playerStrumline.notes.members.concat(PlayState.instance.opponentStrumline.notes.members);
|
||||
return allNotes.filter(function(note:NoteSprite) {
|
||||
return note != null && note.noteData.kind == this.noteKind;
|
||||
});
|
||||
}
|
||||
|
||||
public function onScriptEvent(event:ScriptEvent):Void {}
|
||||
|
||||
public function onCreate(event:ScriptEvent):Void {}
|
||||
|
||||
public function onDestroy(event:ScriptEvent):Void {}
|
||||
|
||||
public function onUpdate(event:UpdateScriptEvent):Void {}
|
||||
|
||||
public function onNoteIncoming(event:NoteScriptEvent):Void {}
|
||||
|
||||
public function onNoteHit(event:HitNoteScriptEvent):Void {}
|
||||
|
||||
public function onNoteMiss(event:NoteScriptEvent):Void {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract for setting the type of the `NoteKindParam`
|
||||
* This was supposed to be an enum but polymod kept being annoying
|
||||
*/
|
||||
abstract NoteKindParamType(String) from String to String
|
||||
{
|
||||
public static final STRING:String = 'String';
|
||||
|
||||
public static final INT:String = 'Int';
|
||||
|
||||
public static final FLOAT:String = 'Float';
|
||||
}
|
||||
|
||||
typedef NoteKindParamData =
|
||||
{
|
||||
/**
|
||||
* If `min` is null, there is no minimum
|
||||
*/
|
||||
?min:Null<Float>,
|
||||
|
||||
/**
|
||||
* If `max` is null, there is no maximum
|
||||
*/
|
||||
?max:Null<Float>,
|
||||
|
||||
/**
|
||||
* If `step` is null, it will use 1.0
|
||||
*/
|
||||
?step:Null<Float>,
|
||||
|
||||
/**
|
||||
* If `precision` is null, there will be 0 decimal places
|
||||
*/
|
||||
?precision:Null<Int>,
|
||||
|
||||
?defaultValue:Dynamic
|
||||
}
|
||||
|
||||
/**
|
||||
* Typedef for creating custom parameters in the chart editor
|
||||
*/
|
||||
typedef NoteKindParam =
|
||||
{
|
||||
name:String,
|
||||
description:String,
|
||||
type:NoteKindParamType,
|
||||
?data:NoteKindParamData
|
||||
}
|
121
source/funkin/play/notes/notekind/NoteKindManager.hx
Normal file
121
source/funkin/play/notes/notekind/NoteKindManager.hx
Normal file
|
@ -0,0 +1,121 @@
|
|||
package funkin.play.notes.notekind;
|
||||
|
||||
import funkin.modding.events.ScriptEventDispatcher;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
|
||||
import funkin.data.notestyle.NoteStyleRegistry;
|
||||
import funkin.play.notes.notestyle.NoteStyle;
|
||||
import funkin.play.notes.notekind.ScriptedNoteKind;
|
||||
import funkin.play.notes.notekind.NoteKind.NoteKindParam;
|
||||
|
||||
class NoteKindManager
|
||||
{
|
||||
static var noteKinds:Map<String, NoteKind> = [];
|
||||
|
||||
public static function loadScripts():Void
|
||||
{
|
||||
var scriptedClassName:Array<String> = ScriptedNoteKind.listScriptClasses();
|
||||
if (scriptedClassName.length > 0)
|
||||
{
|
||||
trace('Instantiating ${scriptedClassName.length} scripted note kind(s)...');
|
||||
for (scriptedClass in scriptedClassName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var script:NoteKind = ScriptedNoteKind.init(scriptedClass, "unknown");
|
||||
trace(' Initialized scripted note kind: ${script.noteKind}');
|
||||
noteKinds.set(script.noteKind, script);
|
||||
ChartEditorDropdowns.NOTE_KINDS.set(script.noteKind, script.description);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
trace(' FAILED to instantiate scripted note kind: ${scriptedClass}');
|
||||
trace(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the given event for note kind scripts
|
||||
* @param event The event
|
||||
*/
|
||||
public static function callEvent(event:ScriptEvent):Void
|
||||
{
|
||||
// if it is a note script event,
|
||||
// then only call the event for the specific note kind script
|
||||
if (Std.isOfType(event, NoteScriptEvent))
|
||||
{
|
||||
var noteEvent:NoteScriptEvent = cast(event, NoteScriptEvent);
|
||||
|
||||
var noteKind:NoteKind = noteKinds.get(noteEvent.note.kind);
|
||||
|
||||
if (noteKind != null)
|
||||
{
|
||||
ScriptEventDispatcher.callEvent(noteKind, event);
|
||||
}
|
||||
}
|
||||
else // call the event for all note kind scripts
|
||||
{
|
||||
for (noteKind in noteKinds.iterator())
|
||||
{
|
||||
ScriptEventDispatcher.callEvent(noteKind, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the note style from the given note kind
|
||||
* @param noteKind note kind name
|
||||
* @param suffix Used for song note styles
|
||||
* @return NoteStyle
|
||||
*/
|
||||
public static function getNoteStyle(noteKind:String, ?suffix:String):Null<NoteStyle>
|
||||
{
|
||||
var noteStyleId:Null<String> = getNoteStyleId(noteKind, suffix);
|
||||
|
||||
if (noteStyleId == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return NoteStyleRegistry.instance.fetchEntry(noteStyleId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the note style id from the given note kind
|
||||
* @param noteKind Note kind name
|
||||
* @param suffix Used for song note styles
|
||||
* @return Null<String>
|
||||
*/
|
||||
public static function getNoteStyleId(noteKind:String, ?suffix:String):Null<String>
|
||||
{
|
||||
if (suffix == '')
|
||||
{
|
||||
suffix = null;
|
||||
}
|
||||
|
||||
var noteStyleId:Null<String> = noteKinds.get(noteKind)?.noteStyleId;
|
||||
if (noteStyleId != null && suffix != null)
|
||||
{
|
||||
noteStyleId = NoteStyleRegistry.instance.hasEntry('$noteStyleId-$suffix') ? '$noteStyleId-$suffix' : noteStyleId;
|
||||
}
|
||||
|
||||
return noteStyleId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrive custom params of the given note kind
|
||||
* @param noteKind Name of the note kind
|
||||
* @return Array<NoteKindParam>
|
||||
*/
|
||||
public static function getParams(noteKind:Null<String>):Array<NoteKindParam>
|
||||
{
|
||||
if (noteKind == null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return noteKinds.get(noteKind)?.params ?? [];
|
||||
}
|
||||
}
|
9
source/funkin/play/notes/notekind/ScriptedNoteKind.hx
Normal file
9
source/funkin/play/notes/notekind/ScriptedNoteKind.hx
Normal file
|
@ -0,0 +1,9 @@
|
|||
package funkin.play.notes.notekind;
|
||||
|
||||
/**
|
||||
* A script that can be tied to a NoteKind.
|
||||
* Create a scripted class that extends NoteKind,
|
||||
* then call `super('noteKind')` in the constructor to use this.
|
||||
*/
|
||||
@:hscriptClass
|
||||
class ScriptedNoteKind extends NoteKind implements polymod.hscript.HScriptedClass {}
|
|
@ -89,12 +89,14 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
|
|||
|
||||
target.frames = atlas;
|
||||
|
||||
target.scale.x = _data.assets.note.scale;
|
||||
target.scale.y = _data.assets.note.scale;
|
||||
target.antialiasing = !_data.assets.note.isPixel;
|
||||
|
||||
// Apply the animations.
|
||||
buildNoteAnimations(target);
|
||||
|
||||
// Set the scale.
|
||||
target.setGraphicSize(Strumline.STRUMLINE_SIZE * getNoteScale());
|
||||
target.updateHitbox();
|
||||
}
|
||||
|
||||
var noteFrames:FlxAtlasFrames = null;
|
||||
|
@ -156,6 +158,16 @@ class NoteStyle implements IRegistryEntry<NoteStyleData>
|
|||
target.animation.addByPrefix('redScroll', rightData.prefix, rightData.frameRate, rightData.looped, rightData.flipX, rightData.flipY);
|
||||
}
|
||||
|
||||
public function isNoteAnimated():Bool
|
||||
{
|
||||
return _data.assets.note.animated;
|
||||
}
|
||||
|
||||
public function getNoteScale():Float
|
||||
{
|
||||
return _data.assets.note.scale;
|
||||
}
|
||||
|
||||
function fetchNoteAnimationData(dir:NoteDirection):AnimationData
|
||||
{
|
||||
var result:Null<AnimationData> = switch (dir)
|
||||
|
|
|
@ -277,7 +277,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
|||
// If there are no difficulties in the metadata, there's a problem.
|
||||
if (metadata.playData.difficulties.length == 0)
|
||||
{
|
||||
throw 'Song $id has no difficulties listed in metadata!';
|
||||
trace('[WARN] Song $id has no difficulties listed in metadata!');
|
||||
}
|
||||
|
||||
// There may be more difficulties in the chart file than in the metadata,
|
||||
|
@ -494,6 +494,24 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
|||
return diffFiltered;
|
||||
}
|
||||
|
||||
public function listSuffixedDifficulties(variationIds:Array<String>, ?showLocked:Bool, ?showHidden:Bool):Array<String>
|
||||
{
|
||||
var result = [];
|
||||
|
||||
for (variation in variationIds)
|
||||
{
|
||||
var difficulties = listDifficulties(variation, null, showLocked, showHidden);
|
||||
for (difficulty in difficulties)
|
||||
{
|
||||
var suffixedDifficulty = (variation != Constants.DEFAULT_VARIATION
|
||||
&& variation != 'erect') ? '$difficulty-${variation}' : difficulty;
|
||||
result.push(suffixedDifficulty);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public function hasDifficulty(diffId:String, ?variationId:String, ?variationIds:Array<String>):Bool
|
||||
{
|
||||
if (variationIds == null) variationIds = [];
|
||||
|
@ -706,10 +724,11 @@ class SongDifficulty
|
|||
* Cache the vocals for a given character.
|
||||
* @param id The character we are about to play.
|
||||
*/
|
||||
public inline function cacheVocals():Void
|
||||
public function cacheVocals():Void
|
||||
{
|
||||
for (voice in buildVoiceList())
|
||||
{
|
||||
trace('Caching vocal track: $voice');
|
||||
FlxG.sound.cache(voice);
|
||||
}
|
||||
}
|
||||
|
@ -721,6 +740,20 @@ class SongDifficulty
|
|||
* @param id The character we are about to play.
|
||||
*/
|
||||
public function buildVoiceList():Array<String>
|
||||
{
|
||||
var result:Array<String> = [];
|
||||
result = result.concat(buildPlayerVoiceList());
|
||||
result = result.concat(buildOpponentVoiceList());
|
||||
if (result.length == 0)
|
||||
{
|
||||
var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
|
||||
// Try to use `Voices.ogg` if no other voices are found.
|
||||
if (Assets.exists(Paths.voices(this.song.id, ''))) result.push(Paths.voices(this.song.id, '$suffix'));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public function buildPlayerVoiceList():Array<String>
|
||||
{
|
||||
var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
|
||||
|
||||
|
@ -728,62 +761,88 @@ class SongDifficulty
|
|||
// For example, if `Voices-bf-car-erect.ogg` does not exist, check for `Voices-bf-erect.ogg`.
|
||||
// Then, check for `Voices-bf-car.ogg`, then `Voices-bf.ogg`.
|
||||
|
||||
var playerId:String = characters.player;
|
||||
var voicePlayer:String = Paths.voices(this.song.id, '-$playerId$suffix');
|
||||
while (voicePlayer != null && !Assets.exists(voicePlayer))
|
||||
if (characters.playerVocals == null)
|
||||
{
|
||||
// Remove the last suffix.
|
||||
// For example, bf-car becomes bf.
|
||||
playerId = playerId.split('-').slice(0, -1).join('-');
|
||||
// Try again.
|
||||
voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
|
||||
}
|
||||
if (voicePlayer == null)
|
||||
{
|
||||
// Try again without $suffix.
|
||||
playerId = characters.player;
|
||||
voicePlayer = Paths.voices(this.song.id, '-${playerId}');
|
||||
while (voicePlayer != null && !Assets.exists(voicePlayer))
|
||||
var playerId:String = characters.player;
|
||||
var playerVoice:String = Paths.voices(this.song.id, '-${playerId}$suffix');
|
||||
|
||||
while (playerVoice != null && !Assets.exists(playerVoice))
|
||||
{
|
||||
// Remove the last suffix.
|
||||
// For example, bf-car becomes bf.
|
||||
playerId = playerId.split('-').slice(0, -1).join('-');
|
||||
// Try again.
|
||||
voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
|
||||
playerVoice = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
|
||||
}
|
||||
if (playerVoice == null)
|
||||
{
|
||||
// Try again without $suffix.
|
||||
playerId = characters.player;
|
||||
playerVoice = Paths.voices(this.song.id, '-${playerId}');
|
||||
while (playerVoice != null && !Assets.exists(playerVoice))
|
||||
{
|
||||
// Remove the last suffix.
|
||||
playerId = playerId.split('-').slice(0, -1).join('-');
|
||||
// Try again.
|
||||
playerVoice = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var opponentId:String = characters.opponent;
|
||||
var voiceOpponent:String = Paths.voices(this.song.id, '-${opponentId}$suffix');
|
||||
while (voiceOpponent != null && !Assets.exists(voiceOpponent))
|
||||
{
|
||||
// Remove the last suffix.
|
||||
opponentId = opponentId.split('-').slice(0, -1).join('-');
|
||||
// Try again.
|
||||
voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
|
||||
return playerVoice != null ? [playerVoice] : [];
|
||||
}
|
||||
if (voiceOpponent == null)
|
||||
else
|
||||
{
|
||||
// Try again without $suffix.
|
||||
opponentId = characters.opponent;
|
||||
voiceOpponent = Paths.voices(this.song.id, '-${opponentId}');
|
||||
while (voiceOpponent != null && !Assets.exists(voiceOpponent))
|
||||
// The metadata explicitly defines the list of voices.
|
||||
var playerIds:Array<String> = characters?.playerVocals ?? [characters.player];
|
||||
var playerVoices:Array<String> = playerIds.map((id) -> Paths.voices(this.song.id, '-$id$suffix'));
|
||||
|
||||
return playerVoices;
|
||||
}
|
||||
}
|
||||
|
||||
public function buildOpponentVoiceList():Array<String>
|
||||
{
|
||||
var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
|
||||
|
||||
// Automatically resolve voices by removing suffixes.
|
||||
// For example, if `Voices-bf-car-erect.ogg` does not exist, check for `Voices-bf-erect.ogg`.
|
||||
// Then, check for `Voices-bf-car.ogg`, then `Voices-bf.ogg`.
|
||||
|
||||
if (characters.opponentVocals == null)
|
||||
{
|
||||
var opponentId:String = characters.opponent;
|
||||
var opponentVoice:String = Paths.voices(this.song.id, '-${opponentId}$suffix');
|
||||
while (opponentVoice != null && !Assets.exists(opponentVoice))
|
||||
{
|
||||
// Remove the last suffix.
|
||||
opponentId = opponentId.split('-').slice(0, -1).join('-');
|
||||
// Try again.
|
||||
voiceOpponent = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
|
||||
opponentVoice = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
|
||||
}
|
||||
if (opponentVoice == null)
|
||||
{
|
||||
// Try again without $suffix.
|
||||
opponentId = characters.opponent;
|
||||
opponentVoice = Paths.voices(this.song.id, '-${opponentId}');
|
||||
while (opponentVoice != null && !Assets.exists(opponentVoice))
|
||||
{
|
||||
// Remove the last suffix.
|
||||
opponentId = opponentId.split('-').slice(0, -1).join('-');
|
||||
// Try again.
|
||||
opponentVoice = opponentId == '' ? null : Paths.voices(this.song.id, '-${opponentId}$suffix');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result:Array<String> = [];
|
||||
if (voicePlayer != null) result.push(voicePlayer);
|
||||
if (voiceOpponent != null) result.push(voiceOpponent);
|
||||
if (voicePlayer == null && voiceOpponent == null)
|
||||
{
|
||||
// Try to use `Voices.ogg` if no other voices are found.
|
||||
if (Assets.exists(Paths.voices(this.song.id, ''))) result.push(Paths.voices(this.song.id, '$suffix'));
|
||||
return opponentVoice != null ? [opponentVoice] : [];
|
||||
}
|
||||
else
|
||||
{
|
||||
// The metadata explicitly defines the list of voices.
|
||||
var opponentIds:Array<String> = characters?.opponentVocals ?? [characters.opponent];
|
||||
var opponentVoices:Array<String> = opponentIds.map((id) -> Paths.voices(this.song.id, '-$id$suffix'));
|
||||
|
||||
return opponentVoices;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -795,26 +854,19 @@ class SongDifficulty
|
|||
{
|
||||
var result:VoicesGroup = new VoicesGroup();
|
||||
|
||||
var voiceList:Array<String> = buildVoiceList();
|
||||
|
||||
if (voiceList.length == 0)
|
||||
{
|
||||
trace('Could not find any voices for song ${this.song.id}');
|
||||
return result;
|
||||
}
|
||||
var playerVoiceList:Array<String> = this.buildPlayerVoiceList();
|
||||
var opponentVoiceList:Array<String> = this.buildOpponentVoiceList();
|
||||
|
||||
// Add player vocals.
|
||||
if (voiceList[0] != null) result.addPlayerVoice(FunkinSound.load(voiceList[0]));
|
||||
// Add opponent vocals.
|
||||
if (voiceList[1] != null) result.addOpponentVoice(FunkinSound.load(voiceList[1]));
|
||||
|
||||
// Add additional vocals.
|
||||
if (voiceList.length > 2)
|
||||
for (playerVoice in playerVoiceList)
|
||||
{
|
||||
for (i in 2...voiceList.length)
|
||||
{
|
||||
result.add(FunkinSound.load(Assets.getSound(voiceList[i])));
|
||||
}
|
||||
result.addPlayerVoice(FunkinSound.load(playerVoice));
|
||||
}
|
||||
|
||||
// Add opponent vocals.
|
||||
for (opponentVoice in opponentVoiceList)
|
||||
{
|
||||
result.addOpponentVoice(FunkinSound.load(opponentVoice));
|
||||
}
|
||||
|
||||
result.playerVoicesOffset = offsets.getVocalOffset(characters.player);
|
||||
|
|
|
@ -45,8 +45,8 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
|
|||
public var idleSuffix(default, set):String = '';
|
||||
|
||||
/**
|
||||
* If this bopper is rendered with pixel art,
|
||||
* disable anti-aliasing and render at 6x scale.
|
||||
* If this bopper is rendered with pixel art, disable anti-aliasing.
|
||||
* @default `false`
|
||||
*/
|
||||
public var isPixel(default, set):Bool = false;
|
||||
|
||||
|
|
|
@ -152,6 +152,32 @@ class AtlasText extends FlxTypedSpriteGroup<AtlasChar>
|
|||
}
|
||||
}
|
||||
|
||||
public function getWidth():Int
|
||||
{
|
||||
var width = 0;
|
||||
for (char in this.text.split(""))
|
||||
{
|
||||
switch (char)
|
||||
{
|
||||
case " ":
|
||||
{
|
||||
width += 40;
|
||||
}
|
||||
case "\n":
|
||||
{}
|
||||
case char:
|
||||
{
|
||||
var sprite = new AtlasChar(atlas, char);
|
||||
sprite.revive();
|
||||
sprite.char = char;
|
||||
sprite.alpha = 1;
|
||||
width += Std.int(sprite.width);
|
||||
}
|
||||
}
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
override function toString()
|
||||
{
|
||||
return "InputItem, " + FlxStringUtil.getDebugString([
|
||||
|
|
|
@ -78,9 +78,6 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
|
|||
{
|
||||
// Emergency exit button.
|
||||
if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
|
||||
|
||||
// This can now be used in EVERY STATE YAY!
|
||||
if (FlxG.keys.justPressed.F5) debug_refreshModules();
|
||||
}
|
||||
|
||||
override function update(elapsed:Float)
|
||||
|
@ -114,12 +111,10 @@ class MusicBeatState extends FlxTransitionableState implements IEventHandler
|
|||
ModuleHandler.callEvent(event);
|
||||
}
|
||||
|
||||
function debug_refreshModules()
|
||||
function reloadAssets()
|
||||
{
|
||||
PolymodHandler.forceReloadAssets();
|
||||
|
||||
this.destroy();
|
||||
|
||||
// Create a new instance of the current state, so old data is cleared.
|
||||
FlxG.resetState();
|
||||
}
|
||||
|
|
|
@ -72,9 +72,6 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
|
|||
// Emergency exit button.
|
||||
if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
|
||||
|
||||
// This can now be used in EVERY STATE YAY!
|
||||
if (FlxG.keys.justPressed.F5) debug_refreshModules();
|
||||
|
||||
// Display Conductor info in the watch window.
|
||||
FlxG.watch.addQuick("musicTime", FlxG.sound.music?.time ?? 0.0);
|
||||
Conductor.watchQuick(conductorInUse);
|
||||
|
@ -82,7 +79,7 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
|
|||
dispatchEvent(new UpdateScriptEvent(elapsed));
|
||||
}
|
||||
|
||||
function debug_refreshModules()
|
||||
function reloadAssets()
|
||||
{
|
||||
PolymodHandler.forceReloadAssets();
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ import funkin.data.song.SongData.SongEventData;
|
|||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongOffsets;
|
||||
import funkin.data.song.SongData.NoteParamData;
|
||||
import funkin.data.song.SongDataUtils;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.data.stage.StageData;
|
||||
|
@ -45,6 +46,7 @@ import funkin.input.TurboActionHandler;
|
|||
import funkin.input.TurboButtonHandler;
|
||||
import funkin.input.TurboKeyHandler;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.play.notes.notekind.NoteKindManager;
|
||||
import funkin.play.character.BaseCharacter.CharacterType;
|
||||
import funkin.play.character.CharacterData;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
|
@ -282,6 +284,21 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
*/
|
||||
public static final WELCOME_MUSIC_FADE_IN_DURATION:Float = 10.0;
|
||||
|
||||
/**
|
||||
* A map of the keys for every live input style.
|
||||
*/
|
||||
public static final LIVE_INPUT_KEYS:Map<ChartEditorLiveInputStyle, Array<FlxKey>> = [
|
||||
NumberKeys => [
|
||||
FIVE, SIX, SEVEN, EIGHT,
|
||||
ONE, TWO, THREE, FOUR
|
||||
],
|
||||
WASDKeys => [
|
||||
LEFT, DOWN, UP, RIGHT,
|
||||
A, S, W, D
|
||||
],
|
||||
None => []
|
||||
];
|
||||
|
||||
/**
|
||||
* INSTANCE DATA
|
||||
*/
|
||||
|
@ -538,6 +555,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
*/
|
||||
var noteKindToPlace:Null<String> = null;
|
||||
|
||||
/**
|
||||
* The note params to use for notes being placed in the chart. Defaults to `[]`.
|
||||
*/
|
||||
var noteParamsToPlace:Array<NoteParamData> = [];
|
||||
|
||||
/**
|
||||
* The event type to use for events being placed in the chart. Defaults to `''`.
|
||||
*/
|
||||
|
@ -1401,7 +1423,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
|
||||
function get_currentSongNoteStyle():String
|
||||
{
|
||||
if (currentSongMetadata.playData.noteStyle == null)
|
||||
if (currentSongMetadata.playData.noteStyle == null
|
||||
|| currentSongMetadata.playData.noteStyle == ''
|
||||
|| currentSongMetadata.playData.noteStyle == 'item')
|
||||
{
|
||||
// Initialize to the default value if not set.
|
||||
currentSongMetadata.playData.noteStyle = Constants.DEFAULT_NOTE_STYLE;
|
||||
|
@ -2436,7 +2460,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
|
||||
gridGhostNote = new ChartEditorNoteSprite(this);
|
||||
gridGhostNote.alpha = 0.6;
|
||||
gridGhostNote.noteData = new SongNoteData(0, 0, 0, "");
|
||||
gridGhostNote.noteData = new SongNoteData(0, 0, 0, "", []);
|
||||
gridGhostNote.visible = false;
|
||||
add(gridGhostNote);
|
||||
gridGhostNote.zIndex = 11;
|
||||
|
@ -3584,6 +3608,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
|
||||
// The note sprite handles animation playback and positioning.
|
||||
noteSprite.noteData = noteData;
|
||||
noteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
|
||||
noteSprite.overrideStepTime = null;
|
||||
noteSprite.overrideData = null;
|
||||
|
||||
|
@ -3607,6 +3632,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
|
||||
holdNoteSprite.setHeightDirectly(noteLengthPixels);
|
||||
|
||||
holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteSprite.noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
|
||||
|
||||
holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
|
||||
|
||||
trace(holdNoteSprite.x + ', ' + holdNoteSprite.y + ', ' + holdNoteSprite.width + ', ' + holdNoteSprite.height);
|
||||
|
@ -3669,9 +3696,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
|
||||
holdNoteSprite.noteData = noteData;
|
||||
holdNoteSprite.noteDirection = noteData.getDirection();
|
||||
|
||||
holdNoteSprite.setHeightDirectly(noteLengthPixels);
|
||||
|
||||
holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
|
||||
|
||||
holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
|
||||
|
||||
displayedHoldNoteData.push(noteData);
|
||||
|
@ -4569,7 +4597,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
gridGhostHoldNote.noteData = currentPlaceNoteData;
|
||||
gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection();
|
||||
gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true);
|
||||
|
||||
gridGhostHoldNote.noteStyle = NoteKindManager.getNoteStyleId(currentPlaceNoteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
|
||||
gridGhostHoldNote.updateHoldNotePosition(renderedHoldNotes);
|
||||
}
|
||||
else
|
||||
|
@ -4726,7 +4754,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
else
|
||||
{
|
||||
// Create a note and place it in the chart.
|
||||
var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace);
|
||||
var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace,
|
||||
ChartEditorState.cloneNoteParams(noteParamsToPlace));
|
||||
|
||||
performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
|
||||
|
||||
|
@ -4885,12 +4914,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
|
||||
if (gridGhostNote == null) throw "ERROR: Tried to handle cursor, but gridGhostNote is null! Check ChartEditorState.buildGrid()";
|
||||
|
||||
var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, noteKindToPlace);
|
||||
var noteData:SongNoteData = gridGhostNote.noteData != null ? gridGhostNote.noteData : new SongNoteData(cursorMs, cursorColumn, 0, noteKindToPlace,
|
||||
ChartEditorState.cloneNoteParams(noteParamsToPlace));
|
||||
|
||||
if (cursorColumn != noteData.data || noteKindToPlace != noteData.kind)
|
||||
if (cursorColumn != noteData.data || noteKindToPlace != noteData.kind || noteParamsToPlace != noteData.params)
|
||||
{
|
||||
noteData.kind = noteKindToPlace;
|
||||
noteData.params = noteParamsToPlace;
|
||||
noteData.data = cursorColumn;
|
||||
gridGhostNote.noteStyle = NoteKindManager.getNoteStyleId(noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
|
||||
gridGhostNote.playNoteAnimation();
|
||||
}
|
||||
noteData.time = cursorSnappedMs;
|
||||
|
@ -5129,46 +5161,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
function handlePlayhead():Void
|
||||
{
|
||||
// Place notes at the playhead with the keyboard.
|
||||
switch (currentLiveInputStyle)
|
||||
for (note => key in LIVE_INPUT_KEYS[currentLiveInputStyle])
|
||||
{
|
||||
case ChartEditorLiveInputStyle.WASDKeys:
|
||||
if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4);
|
||||
if (FlxG.keys.justReleased.A) finishPlaceNoteAtPlayhead(4);
|
||||
if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5);
|
||||
if (FlxG.keys.justReleased.S) finishPlaceNoteAtPlayhead(5);
|
||||
if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6);
|
||||
if (FlxG.keys.justReleased.W) finishPlaceNoteAtPlayhead(6);
|
||||
if (FlxG.keys.justPressed.D) placeNoteAtPlayhead(7);
|
||||
if (FlxG.keys.justReleased.D) finishPlaceNoteAtPlayhead(7);
|
||||
|
||||
if (FlxG.keys.justPressed.LEFT) placeNoteAtPlayhead(0);
|
||||
if (FlxG.keys.justReleased.LEFT) finishPlaceNoteAtPlayhead(0);
|
||||
if (FlxG.keys.justPressed.DOWN) placeNoteAtPlayhead(1);
|
||||
if (FlxG.keys.justReleased.DOWN) finishPlaceNoteAtPlayhead(1);
|
||||
if (FlxG.keys.justPressed.UP) placeNoteAtPlayhead(2);
|
||||
if (FlxG.keys.justReleased.UP) finishPlaceNoteAtPlayhead(2);
|
||||
if (FlxG.keys.justPressed.RIGHT) placeNoteAtPlayhead(3);
|
||||
if (FlxG.keys.justReleased.RIGHT) finishPlaceNoteAtPlayhead(3);
|
||||
case ChartEditorLiveInputStyle.NumberKeys:
|
||||
// Flipped because Dad is on the left but represents data 0-3.
|
||||
if (FlxG.keys.justPressed.ONE) placeNoteAtPlayhead(4);
|
||||
if (FlxG.keys.justReleased.ONE) finishPlaceNoteAtPlayhead(4);
|
||||
if (FlxG.keys.justPressed.TWO) placeNoteAtPlayhead(5);
|
||||
if (FlxG.keys.justReleased.TWO) finishPlaceNoteAtPlayhead(5);
|
||||
if (FlxG.keys.justPressed.THREE) placeNoteAtPlayhead(6);
|
||||
if (FlxG.keys.justReleased.THREE) finishPlaceNoteAtPlayhead(6);
|
||||
if (FlxG.keys.justPressed.FOUR) placeNoteAtPlayhead(7);
|
||||
if (FlxG.keys.justReleased.FOUR) finishPlaceNoteAtPlayhead(7);
|
||||
|
||||
if (FlxG.keys.justPressed.FIVE) placeNoteAtPlayhead(0);
|
||||
if (FlxG.keys.justReleased.FIVE) finishPlaceNoteAtPlayhead(0);
|
||||
if (FlxG.keys.justPressed.SIX) placeNoteAtPlayhead(1);
|
||||
if (FlxG.keys.justPressed.SEVEN) placeNoteAtPlayhead(2);
|
||||
if (FlxG.keys.justReleased.SEVEN) finishPlaceNoteAtPlayhead(2);
|
||||
if (FlxG.keys.justPressed.EIGHT) placeNoteAtPlayhead(3);
|
||||
if (FlxG.keys.justReleased.EIGHT) finishPlaceNoteAtPlayhead(3);
|
||||
case ChartEditorLiveInputStyle.None:
|
||||
// Do nothing.
|
||||
if (FlxG.keys.checkStatus(key, JUST_PRESSED)) placeNoteAtPlayhead(note)
|
||||
else if (FlxG.keys.checkStatus(key, JUST_RELEASED)) finishPlaceNoteAtPlayhead(note);
|
||||
}
|
||||
|
||||
// Place events at playhead.
|
||||
|
@ -5196,7 +5192,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
if (notesAtPos.length == 0 && !removeNoteInstead)
|
||||
{
|
||||
trace('Placing note. ${column}');
|
||||
var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace);
|
||||
var newNoteData:SongNoteData = new SongNoteData(playheadPosSnappedMs, column, 0, noteKindToPlace, ChartEditorState.cloneNoteParams(noteParamsToPlace));
|
||||
performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
|
||||
currentLiveInputPlaceNoteData[column] = newNoteData;
|
||||
}
|
||||
|
@ -5282,6 +5278,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
ghostHold.visible = true;
|
||||
ghostHold.alpha = 0.6;
|
||||
ghostHold.setHeightDirectly(0);
|
||||
ghostHold.noteStyle = NoteKindManager.getNoteStyleId(ghostHold.noteData.kind, currentSongNoteStyle) ?? currentSongNoteStyle;
|
||||
ghostHold.updateHoldNotePosition(renderedHoldNotes);
|
||||
}
|
||||
|
||||
|
@ -5648,6 +5645,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
FlxG.watch.addQuick('musicTime', audioInstTrack?.time ?? 0.0);
|
||||
|
||||
FlxG.watch.addQuick('noteKindToPlace', noteKindToPlace);
|
||||
FlxG.watch.addQuick('noteParamsToPlace', noteParamsToPlace);
|
||||
FlxG.watch.addQuick('eventKindToPlace', eventKindToPlace);
|
||||
|
||||
FlxG.watch.addQuick('scrollPosInPixels', scrollPositionInPixels);
|
||||
|
@ -6511,6 +6509,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
public static function cloneNoteParams(paramsToClone:Array<NoteParamData>):Array<NoteParamData>
|
||||
{
|
||||
var params:Array<NoteParamData> = [];
|
||||
for (param in paramsToClone)
|
||||
{
|
||||
params.push(param.clone());
|
||||
}
|
||||
return params;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,6 +2,7 @@ package funkin.ui.debug.charting.components;
|
|||
|
||||
import funkin.play.notes.Strumline;
|
||||
import funkin.data.notestyle.NoteStyleRegistry;
|
||||
import funkin.play.notes.notestyle.NoteStyle;
|
||||
import flixel.FlxObject;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.graphics.frames.FlxFramesCollection;
|
||||
|
@ -15,6 +16,7 @@ import flixel.math.FlxMath;
|
|||
* A sprite that can be used to display the trail of a hold note in a chart.
|
||||
* Designed to be used and reused efficiently. Has no gameplay functionality.
|
||||
*/
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
@:nullSafety
|
||||
class ChartEditorHoldNoteSprite extends SustainTrail
|
||||
{
|
||||
|
@ -23,6 +25,22 @@ class ChartEditorHoldNoteSprite extends SustainTrail
|
|||
*/
|
||||
public var parentState:ChartEditorState;
|
||||
|
||||
@:isVar
|
||||
public var noteStyle(get, set):Null<String>;
|
||||
|
||||
function get_noteStyle():Null<String>
|
||||
{
|
||||
return this.noteStyle ?? this.parentState.currentSongNoteStyle;
|
||||
}
|
||||
|
||||
@:nullSafety(Off)
|
||||
function set_noteStyle(value:Null<String>):Null<String>
|
||||
{
|
||||
this.noteStyle = value;
|
||||
this.updateHoldNoteGraphic();
|
||||
return value;
|
||||
}
|
||||
|
||||
public function new(parent:ChartEditorState)
|
||||
{
|
||||
var noteStyle = NoteStyleRegistry.instance.fetchDefault();
|
||||
|
@ -30,14 +48,50 @@ class ChartEditorHoldNoteSprite extends SustainTrail
|
|||
super(0, 100, noteStyle);
|
||||
|
||||
this.parentState = parent;
|
||||
}
|
||||
|
||||
@:nullSafety(Off)
|
||||
function updateHoldNoteGraphic():Void
|
||||
{
|
||||
var bruhStyle:Null<NoteStyle> = NoteStyleRegistry.instance.fetchEntry(noteStyle);
|
||||
if (bruhStyle == null) bruhStyle = NoteStyleRegistry.instance.fetchDefault();
|
||||
setupHoldNoteGraphic(bruhStyle);
|
||||
}
|
||||
|
||||
override function setupHoldNoteGraphic(noteStyle:NoteStyle):Void
|
||||
{
|
||||
loadGraphic(noteStyle.getHoldNoteAssetPath());
|
||||
|
||||
antialiasing = true;
|
||||
|
||||
this.isPixel = noteStyle.isHoldNotePixel();
|
||||
if (isPixel)
|
||||
{
|
||||
endOffset = bottomClip = 1;
|
||||
antialiasing = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
endOffset = 0.5;
|
||||
bottomClip = 0.9;
|
||||
}
|
||||
|
||||
zoom = 1.0;
|
||||
zoom *= noteStyle.fetchHoldNoteScale();
|
||||
zoom *= 0.7;
|
||||
zoom *= ChartEditorState.GRID_SIZE / Strumline.STRUMLINE_SIZE;
|
||||
|
||||
graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2
|
||||
graphicHeight = sustainLength * 0.45; // sustainHeight
|
||||
|
||||
flipY = false;
|
||||
|
||||
alpha = 1.0;
|
||||
|
||||
updateColorTransform();
|
||||
|
||||
updateClipping();
|
||||
|
||||
setup();
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,11 @@ import flixel.graphics.frames.FlxAtlasFrames;
|
|||
import flixel.graphics.frames.FlxFrame;
|
||||
import flixel.graphics.frames.FlxTileFrames;
|
||||
import flixel.math.FlxPoint;
|
||||
import funkin.data.animation.AnimationData;
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.notestyle.NoteStyleRegistry;
|
||||
import funkin.play.notes.notestyle.NoteStyle;
|
||||
import funkin.play.notes.NoteDirection;
|
||||
|
||||
/**
|
||||
* A sprite that can be used to display a note in a chart.
|
||||
|
@ -36,7 +40,8 @@ class ChartEditorNoteSprite extends FlxSprite
|
|||
/**
|
||||
* The name of the note style currently in use.
|
||||
*/
|
||||
public var noteStyle(get, never):String;
|
||||
@:isVar
|
||||
public var noteStyle(get, set):Null<String>;
|
||||
|
||||
public var overrideStepTime(default, set):Null<Float> = null;
|
||||
|
||||
|
@ -66,72 +71,80 @@ class ChartEditorNoteSprite extends FlxSprite
|
|||
|
||||
this.parentState = parent;
|
||||
|
||||
var entries:Array<String> = NoteStyleRegistry.instance.listEntryIds();
|
||||
|
||||
if (noteFrameCollection == null)
|
||||
{
|
||||
initFrameCollection();
|
||||
buildEmptyFrameCollection();
|
||||
|
||||
for (entry in entries)
|
||||
{
|
||||
addNoteStyleFrames(fetchNoteStyle(entry));
|
||||
}
|
||||
}
|
||||
|
||||
if (noteFrameCollection == null) throw 'ERROR: Could not initialize note sprite animations.';
|
||||
|
||||
this.frames = noteFrameCollection;
|
||||
|
||||
// Initialize all the animations, not just the one we're going to use immediately,
|
||||
// so that later we can reuse the sprite without having to initialize more animations during scrolling.
|
||||
this.animation.addByPrefix('tapLeftFunkin', 'purple instance');
|
||||
this.animation.addByPrefix('tapDownFunkin', 'blue instance');
|
||||
this.animation.addByPrefix('tapUpFunkin', 'green instance');
|
||||
this.animation.addByPrefix('tapRightFunkin', 'red instance');
|
||||
|
||||
this.animation.addByPrefix('holdLeftFunkin', 'LeftHoldPiece');
|
||||
this.animation.addByPrefix('holdDownFunkin', 'DownHoldPiece');
|
||||
this.animation.addByPrefix('holdUpFunkin', 'UpHoldPiece');
|
||||
this.animation.addByPrefix('holdRightFunkin', 'RightHoldPiece');
|
||||
|
||||
this.animation.addByPrefix('holdEndLeftFunkin', 'LeftHoldEnd');
|
||||
this.animation.addByPrefix('holdEndDownFunkin', 'DownHoldEnd');
|
||||
this.animation.addByPrefix('holdEndUpFunkin', 'UpHoldEnd');
|
||||
this.animation.addByPrefix('holdEndRightFunkin', 'RightHoldEnd');
|
||||
|
||||
this.animation.addByPrefix('tapLeftPixel', 'pixel4');
|
||||
this.animation.addByPrefix('tapDownPixel', 'pixel5');
|
||||
this.animation.addByPrefix('tapUpPixel', 'pixel6');
|
||||
this.animation.addByPrefix('tapRightPixel', 'pixel7');
|
||||
for (entry in entries)
|
||||
{
|
||||
addNoteStyleAnimations(fetchNoteStyle(entry));
|
||||
}
|
||||
}
|
||||
|
||||
static var noteFrameCollection:Null<FlxFramesCollection> = null;
|
||||
|
||||
/**
|
||||
* We load all the note frames once, then reuse them.
|
||||
*/
|
||||
static function initFrameCollection():Void
|
||||
function fetchNoteStyle(noteStyleId:String):NoteStyle
|
||||
{
|
||||
buildEmptyFrameCollection();
|
||||
if (noteFrameCollection == null) return;
|
||||
var result = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
|
||||
if (result != null) return result;
|
||||
return NoteStyleRegistry.instance.fetchDefault();
|
||||
}
|
||||
|
||||
// TODO: Automatically iterate over the list of note skins.
|
||||
@:access(funkin.play.notes.notestyle.NoteStyle)
|
||||
@:nullSafety(Off)
|
||||
static function addNoteStyleFrames(noteStyle:NoteStyle):Void
|
||||
{
|
||||
var prefix:String = noteStyle.id.toTitleCase();
|
||||
|
||||
// Normal notes
|
||||
var frameCollectionNormal:FlxAtlasFrames = Paths.getSparrowAtlas('NOTE_assets');
|
||||
|
||||
for (frame in frameCollectionNormal.frames)
|
||||
var frameCollection:FlxAtlasFrames = Paths.getSparrowAtlas(noteStyle.getNoteAssetPath(), noteStyle.getNoteAssetLibrary());
|
||||
if (frameCollection == null)
|
||||
{
|
||||
noteFrameCollection.pushFrame(frame);
|
||||
trace('Could not retrieve frame collection for ${noteStyle}: ${Paths.image(noteStyle.getNoteAssetPath(), noteStyle.getNoteAssetLibrary())}');
|
||||
FlxG.log.error('Could not retrieve frame collection for ${noteStyle}: ${Paths.image(noteStyle.getNoteAssetPath(), noteStyle.getNoteAssetLibrary())}');
|
||||
return;
|
||||
}
|
||||
|
||||
// Pixel notes
|
||||
var graphicPixel = FlxG.bitmap.add(Paths.image('weeb/pixelUI/arrows-pixels', 'week6'), false, null);
|
||||
if (graphicPixel == null) trace('ERROR: Could not load graphic: ' + Paths.image('weeb/pixelUI/arrows-pixels', 'week6'));
|
||||
var frameCollectionPixel = FlxTileFrames.fromGraphic(graphicPixel, new FlxPoint(17, 17));
|
||||
for (i in 0...frameCollectionPixel.frames.length)
|
||||
for (frame in frameCollection.frames)
|
||||
{
|
||||
var frame:Null<FlxFrame> = frameCollectionPixel.frames[i];
|
||||
if (frame == null) continue;
|
||||
|
||||
frame.name = 'pixel' + i;
|
||||
noteFrameCollection.pushFrame(frame);
|
||||
// cloning the frame because else
|
||||
// we will fuck up the frame data used in game
|
||||
var clonedFrame:FlxFrame = frame.copyTo();
|
||||
clonedFrame.name = '$prefix${clonedFrame.name}';
|
||||
noteFrameCollection.pushFrame(clonedFrame);
|
||||
}
|
||||
}
|
||||
|
||||
@:access(funkin.play.notes.notestyle.NoteStyle)
|
||||
@:nullSafety(Off)
|
||||
function addNoteStyleAnimations(noteStyle:NoteStyle):Void
|
||||
{
|
||||
var prefix:String = noteStyle.id.toTitleCase();
|
||||
var suffix:String = noteStyle.id.toTitleCase();
|
||||
|
||||
var leftData:AnimationData = noteStyle.fetchNoteAnimationData(NoteDirection.LEFT);
|
||||
this.animation.addByPrefix('tapLeft$suffix', '$prefix${leftData.prefix}', leftData.frameRate, leftData.looped, leftData.flipX, leftData.flipY);
|
||||
|
||||
var downData:AnimationData = noteStyle.fetchNoteAnimationData(NoteDirection.DOWN);
|
||||
this.animation.addByPrefix('tapDown$suffix', '$prefix${downData.prefix}', downData.frameRate, downData.looped, downData.flipX, downData.flipY);
|
||||
|
||||
var upData:AnimationData = noteStyle.fetchNoteAnimationData(NoteDirection.UP);
|
||||
this.animation.addByPrefix('tapUp$suffix', '$prefix${upData.prefix}', upData.frameRate, upData.looped, upData.flipX, upData.flipY);
|
||||
|
||||
var rightData:AnimationData = noteStyle.fetchNoteAnimationData(NoteDirection.RIGHT);
|
||||
this.animation.addByPrefix('tapRight$suffix', '$prefix${rightData.prefix}', rightData.frameRate, rightData.looped, rightData.flipX, rightData.flipY);
|
||||
}
|
||||
|
||||
@:nullSafety(Off)
|
||||
static function buildEmptyFrameCollection():Void
|
||||
{
|
||||
|
@ -185,12 +198,24 @@ class ChartEditorNoteSprite extends FlxSprite
|
|||
}
|
||||
}
|
||||
|
||||
function get_noteStyle():String
|
||||
function get_noteStyle():Null<String>
|
||||
{
|
||||
// Fall back to Funkin' if it's not a valid note style.
|
||||
return if (NOTE_STYLES.contains(this.parentState.currentSongNoteStyle)) this.parentState.currentSongNoteStyle else 'funkin';
|
||||
if (this.noteStyle == null)
|
||||
{
|
||||
var result = this.parentState.currentSongNoteStyle;
|
||||
return result;
|
||||
}
|
||||
return this.noteStyle;
|
||||
}
|
||||
|
||||
function set_noteStyle(value:Null<String>):Null<String>
|
||||
{
|
||||
this.noteStyle = value;
|
||||
this.playNoteAnimation();
|
||||
return value;
|
||||
}
|
||||
|
||||
@:nullSafety(Off)
|
||||
public function playNoteAnimation():Void
|
||||
{
|
||||
if (this.noteData == null) return;
|
||||
|
@ -200,6 +225,7 @@ class ChartEditorNoteSprite extends FlxSprite
|
|||
|
||||
// Play the appropriate animation for the type, direction, and skin.
|
||||
var dirName:String = overrideData != null ? SongNoteData.buildDirectionName(overrideData) : this.noteData.getDirectionName();
|
||||
var noteStyleSuffix:String = this.noteStyle?.toTitleCase() ?? Constants.DEFAULT_NOTE_STYLE.toTitleCase();
|
||||
var animationName:String = '${baseAnimationName}${dirName}${this.noteStyle.toTitleCase()}';
|
||||
|
||||
this.animation.play(animationName);
|
||||
|
@ -209,12 +235,12 @@ class ChartEditorNoteSprite extends FlxSprite
|
|||
switch (baseAnimationName)
|
||||
{
|
||||
case 'tap':
|
||||
this.setGraphicSize(0, ChartEditorState.GRID_SIZE);
|
||||
this.setGraphicSize(ChartEditorState.GRID_SIZE, 0);
|
||||
this.updateHitbox();
|
||||
}
|
||||
this.updateHitbox();
|
||||
|
||||
// TODO: Make this an attribute of the note skin.
|
||||
this.antialiasing = (this.parentState.currentSongNoteStyle != 'Pixel');
|
||||
var bruhStyle:NoteStyle = fetchNoteStyle(this.noteStyle);
|
||||
this.antialiasing = !bruhStyle._data?.assets?.note?.isPixel ?? true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,8 +2,16 @@ package funkin.ui.debug.charting.toolboxes;
|
|||
|
||||
import haxe.ui.components.DropDown;
|
||||
import haxe.ui.components.TextField;
|
||||
import haxe.ui.components.Label;
|
||||
import haxe.ui.components.NumberStepper;
|
||||
import haxe.ui.containers.Grid;
|
||||
import haxe.ui.core.Component;
|
||||
import haxe.ui.events.UIEvent;
|
||||
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
|
||||
import funkin.play.notes.notekind.NoteKindManager;
|
||||
import funkin.play.notes.notekind.NoteKind.NoteKindParam;
|
||||
import funkin.play.notes.notekind.NoteKind.NoteKindParamType;
|
||||
import funkin.data.song.SongData.NoteParamData;
|
||||
|
||||
/**
|
||||
* The toolbox which allows modifying information like Note Kind.
|
||||
|
@ -12,8 +20,22 @@ import funkin.ui.debug.charting.util.ChartEditorDropdowns;
|
|||
@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/toolboxes/note-data.xml"))
|
||||
class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
|
||||
{
|
||||
// 100 is the height used in note-data.xml
|
||||
static final DIALOG_HEIGHT:Int = 100;
|
||||
|
||||
// toolboxNotesGrid.height + 45
|
||||
// this is what i found out by printing this.height and grid.height
|
||||
// and then seeing that this.height is 100 and grid.height is 55
|
||||
static final HEIGHT_OFFSET:Int = 45;
|
||||
|
||||
// minimizing creates a gray bar the bottom, which would obscure the components,
|
||||
// which is why we use an extra offset of 20
|
||||
static final MINIMIZE_FIX:Int = 20;
|
||||
|
||||
var toolboxNotesGrid:Grid;
|
||||
var toolboxNotesNoteKind:DropDown;
|
||||
var toolboxNotesCustomKind:TextField;
|
||||
var toolboxNotesParams:Array<ToolboxNoteKindParam> = [];
|
||||
|
||||
var _initializing:Bool = true;
|
||||
|
||||
|
@ -54,12 +76,35 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
|
|||
toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
|
||||
}
|
||||
|
||||
createNoteKindParams(noteKind);
|
||||
|
||||
if (!_initializing && chartEditorState.currentNoteSelection.length > 0)
|
||||
{
|
||||
// Edit the note data of any selected notes.
|
||||
for (note in chartEditorState.currentNoteSelection)
|
||||
{
|
||||
// Edit the note data of any selected notes.
|
||||
note.kind = chartEditorState.noteKindToPlace;
|
||||
note.params = ChartEditorState.cloneNoteParams(chartEditorState.noteParamsToPlace);
|
||||
|
||||
// update note sprites
|
||||
for (noteSprite in chartEditorState.renderedNotes.members)
|
||||
{
|
||||
if (noteSprite.noteData == note)
|
||||
{
|
||||
noteSprite.noteStyle = NoteKindManager.getNoteStyleId(note.kind) ?? chartEditorState.currentSongNoteStyle;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// update hold note sprites
|
||||
for (holdNoteSprite in chartEditorState.renderedHoldNotes.members)
|
||||
{
|
||||
if (holdNoteSprite.noteData == note)
|
||||
{
|
||||
holdNoteSprite.noteStyle = NoteKindManager.getNoteStyleId(note.kind) ?? chartEditorState.currentSongNoteStyle;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
chartEditorState.saveDataDirty = true;
|
||||
chartEditorState.noteDisplayDirty = true;
|
||||
|
@ -94,6 +139,8 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
|
|||
|
||||
toolboxNotesNoteKind.value = ChartEditorDropdowns.lookupNoteKind(chartEditorState.noteKindToPlace);
|
||||
toolboxNotesCustomKind.value = chartEditorState.noteKindToPlace;
|
||||
|
||||
createNoteKindParams(chartEditorState.noteKindToPlace);
|
||||
}
|
||||
|
||||
function showCustom():Void
|
||||
|
@ -108,8 +155,149 @@ class ChartEditorNoteDataToolbox extends ChartEditorBaseToolbox
|
|||
toolboxNotesCustomKind.hidden = true;
|
||||
}
|
||||
|
||||
function createNoteKindParams(noteKind:Null<String>):Void
|
||||
{
|
||||
clearNoteKindParams();
|
||||
|
||||
var setParamsToPlace:Bool = false;
|
||||
if (!_initializing)
|
||||
{
|
||||
for (note in chartEditorState.currentNoteSelection)
|
||||
{
|
||||
if (note.kind == chartEditorState.noteKindToPlace)
|
||||
{
|
||||
chartEditorState.noteParamsToPlace = ChartEditorState.cloneNoteParams(note.params);
|
||||
setParamsToPlace = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var noteKindParams:Array<NoteKindParam> = NoteKindManager.getParams(noteKind);
|
||||
|
||||
for (i in 0...noteKindParams.length)
|
||||
{
|
||||
var param:NoteKindParam = noteKindParams[i];
|
||||
|
||||
var paramLabel:Label = new Label();
|
||||
paramLabel.value = param.description;
|
||||
paramLabel.verticalAlign = "center";
|
||||
paramLabel.horizontalAlign = "right";
|
||||
|
||||
var paramComponent:Component = null;
|
||||
|
||||
switch (param.type)
|
||||
{
|
||||
case NoteKindParamType.INT | NoteKindParamType.FLOAT:
|
||||
var paramStepper:NumberStepper = new NumberStepper();
|
||||
paramStepper.value = (setParamsToPlace ? chartEditorState.noteParamsToPlace[i].value : param.data?.defaultValue) ?? 0.0;
|
||||
paramStepper.percentWidth = 100;
|
||||
paramStepper.step = param.data?.step ?? 1;
|
||||
|
||||
// this check should be unnecessary but for some reason
|
||||
// even when these are null it will set it to 0
|
||||
if (param.data?.min != null)
|
||||
{
|
||||
paramStepper.min = param.data.min;
|
||||
}
|
||||
if (param.data?.max != null)
|
||||
{
|
||||
paramStepper.max = param.data.max;
|
||||
}
|
||||
if (param.data?.precision != null)
|
||||
{
|
||||
paramStepper.precision = param.data.precision;
|
||||
}
|
||||
paramComponent = paramStepper;
|
||||
|
||||
case NoteKindParamType.STRING:
|
||||
var paramTextField:TextField = new TextField();
|
||||
paramTextField.value = (setParamsToPlace ? chartEditorState.noteParamsToPlace[i].value : param.data?.defaultValue) ?? '';
|
||||
paramTextField.percentWidth = 100;
|
||||
paramComponent = paramTextField;
|
||||
}
|
||||
|
||||
if (paramComponent == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
paramComponent.onChange = function(event:UIEvent) {
|
||||
chartEditorState.noteParamsToPlace[i].value = paramComponent.value;
|
||||
|
||||
for (note in chartEditorState.currentNoteSelection)
|
||||
{
|
||||
if (note.params.length != noteKindParams.length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (note.params[i].name == param.name)
|
||||
{
|
||||
note.params[i].value = paramComponent.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addNoteKindParam(paramLabel, paramComponent);
|
||||
}
|
||||
|
||||
if (!setParamsToPlace)
|
||||
{
|
||||
var noteParamData:Array<NoteParamData> = [];
|
||||
for (i in 0...noteKindParams.length)
|
||||
{
|
||||
noteParamData.push(new NoteParamData(noteKindParams[i].name, toolboxNotesParams[i].component.value));
|
||||
}
|
||||
chartEditorState.noteParamsToPlace = noteParamData;
|
||||
}
|
||||
}
|
||||
|
||||
function addNoteKindParam(label:Label, component:Component):Void
|
||||
{
|
||||
toolboxNotesParams.push({label: label, component: component});
|
||||
toolboxNotesGrid.addComponent(label);
|
||||
toolboxNotesGrid.addComponent(component);
|
||||
|
||||
this.height = Math.max(DIALOG_HEIGHT, DIALOG_HEIGHT - 30 + toolboxNotesParams.length * 30);
|
||||
}
|
||||
|
||||
function clearNoteKindParams():Void
|
||||
{
|
||||
for (param in toolboxNotesParams)
|
||||
{
|
||||
toolboxNotesGrid.removeComponent(param.component);
|
||||
toolboxNotesGrid.removeComponent(param.label);
|
||||
}
|
||||
toolboxNotesParams = [];
|
||||
this.height = DIALOG_HEIGHT;
|
||||
}
|
||||
|
||||
override function update(elapsed:Float):Void
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
// current dialog is minimized, dont change the height
|
||||
if (this.minimized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var heightToSet:Int = Std.int(Math.max(DIALOG_HEIGHT, (toolboxNotesGrid?.height ?? 50) + HEIGHT_OFFSET)) + MINIMIZE_FIX;
|
||||
if (this.height != heightToSet)
|
||||
{
|
||||
this.height = heightToSet;
|
||||
}
|
||||
}
|
||||
|
||||
public static function build(chartEditorState:ChartEditorState):ChartEditorNoteDataToolbox
|
||||
{
|
||||
return new ChartEditorNoteDataToolbox(chartEditorState);
|
||||
}
|
||||
}
|
||||
|
||||
typedef ToolboxNoteKindParam =
|
||||
{
|
||||
var label:Label;
|
||||
var component:Component;
|
||||
}
|
||||
|
|
|
@ -135,6 +135,14 @@ class ChartEditorDropdowns
|
|||
var noteStyle:Null<NoteStyle> = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
|
||||
if (noteStyle == null) continue;
|
||||
|
||||
// check if the note style has all necessary assets (strums, notes, holdNotes)
|
||||
if (noteStyle._data?.assets?.noteStrumline == null
|
||||
|| noteStyle._data?.assets?.note == null
|
||||
|| noteStyle._data?.assets?.holdNote == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var value = {id: noteStyleId, text: noteStyle.getName()};
|
||||
if (startingStyleId == noteStyleId) returnValue = value;
|
||||
|
||||
|
@ -146,7 +154,7 @@ class ChartEditorDropdowns
|
|||
return returnValue;
|
||||
}
|
||||
|
||||
static final NOTE_KINDS:Map<String, String> = [
|
||||
public static final NOTE_KINDS:Map<String, String> = [
|
||||
// Base
|
||||
"" => "Default",
|
||||
"~CUSTOM~" => "Custom",
|
||||
|
@ -187,11 +195,11 @@ class ChartEditorDropdowns
|
|||
{
|
||||
dropDown.dataSource.clear();
|
||||
|
||||
var returnValue:DropDownEntry = lookupNoteKind('~CUSTOM');
|
||||
var returnValue:DropDownEntry = lookupNoteKind('');
|
||||
|
||||
for (noteKindId in NOTE_KINDS.keys())
|
||||
{
|
||||
var noteKind:String = NOTE_KINDS.get(noteKindId) ?? 'Default';
|
||||
var noteKind:String = NOTE_KINDS.get(noteKindId) ?? 'Unknown';
|
||||
|
||||
var value:DropDownEntry = {id: noteKindId, text: noteKind};
|
||||
if (startingKindId == noteKindId) returnValue = value;
|
||||
|
@ -208,7 +216,7 @@ class ChartEditorDropdowns
|
|||
{
|
||||
if (noteKindId == null) return lookupNoteKind('');
|
||||
if (!NOTE_KINDS.exists(noteKindId)) return {id: '~CUSTOM~', text: 'Custom'};
|
||||
return {id: noteKindId ?? '', text: NOTE_KINDS.get(noteKindId) ?? 'Default'};
|
||||
return {id: noteKindId ?? '', text: NOTE_KINDS.get(noteKindId) ?? 'Unknown'};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -339,7 +339,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
// Only display songs which actually have available difficulties for the current character.
|
||||
var displayedVariations = song.getVariationsByCharacter(currentCharacter);
|
||||
trace('Displayed Variations (${songId}): $displayedVariations');
|
||||
var availableDifficultiesForSong:Array<String> = song.listDifficulties(displayedVariations, false);
|
||||
var availableDifficultiesForSong:Array<String> = song.listSuffixedDifficulties(displayedVariations, false, false);
|
||||
trace('Available Difficulties: $availableDifficultiesForSong');
|
||||
if (availableDifficultiesForSong.length == 0) continue;
|
||||
|
||||
|
@ -1120,7 +1120,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
|
||||
// NOW we can interact with the menu
|
||||
busy = false;
|
||||
grpCapsules.members[curSelected].sparkle.alpha = 0.7;
|
||||
capsule.sparkle.alpha = 0.7;
|
||||
playCurSongPreview(capsule);
|
||||
}, null);
|
||||
|
||||
|
@ -1674,6 +1674,9 @@ class FreeplayState extends MusicBeatSubState
|
|||
songCapsule.init(null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset the song preview in case we changed variations (normal->erect etc)
|
||||
playCurSongPreview();
|
||||
}
|
||||
|
||||
// Set the album graphic and play the animation if relevant.
|
||||
|
@ -1912,8 +1915,10 @@ class FreeplayState extends MusicBeatSubState
|
|||
}
|
||||
}
|
||||
|
||||
public function playCurSongPreview(daSongCapsule:SongMenuItem):Void
|
||||
public function playCurSongPreview(?daSongCapsule:SongMenuItem):Void
|
||||
{
|
||||
if (daSongCapsule == null) daSongCapsule = grpCapsules.members[curSelected];
|
||||
|
||||
if (curSelected == 0)
|
||||
{
|
||||
FunkinSound.playMusic('freeplayRandom',
|
||||
|
@ -2145,7 +2150,7 @@ class FreeplaySongData
|
|||
|
||||
function updateValues(variations:Array<String>):Void
|
||||
{
|
||||
this.songDifficulties = song.listDifficulties(null, variations, false, false);
|
||||
this.songDifficulties = song.listSuffixedDifficulties(variations, false, false);
|
||||
if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY;
|
||||
|
||||
var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, null, variations);
|
||||
|
@ -2207,15 +2212,26 @@ class DifficultySprite extends FlxSprite
|
|||
|
||||
difficultyId = diffId;
|
||||
|
||||
if (Assets.exists(Paths.file('images/freeplay/freeplay${diffId}.xml')))
|
||||
var assetDiffId:String = diffId;
|
||||
while (!Assets.exists(Paths.image('freeplay/freeplay${assetDiffId}')))
|
||||
{
|
||||
this.frames = Paths.getSparrowAtlas('freeplay/freeplay${diffId}');
|
||||
// Remove the last suffix of the difficulty id until we find an asset or there are no more suffixes.
|
||||
var assetDiffIdParts:Array<String> = assetDiffId.split('-');
|
||||
assetDiffIdParts.pop();
|
||||
if (assetDiffIdParts.length == 0) break;
|
||||
assetDiffId = assetDiffIdParts.join('-');
|
||||
}
|
||||
|
||||
// Check for an XML to use an animation instead of an image.
|
||||
if (Assets.exists(Paths.file('images/freeplay/freeplay${assetDiffId}.xml')))
|
||||
{
|
||||
this.frames = Paths.getSparrowAtlas('freeplay/freeplay${assetDiffId}');
|
||||
this.animation.addByPrefix('idle', 'idle0', 24, true);
|
||||
if (Preferences.flashingLights) this.animation.play('idle');
|
||||
}
|
||||
else
|
||||
{
|
||||
this.loadGraphic(Paths.image('freeplay/freeplay' + diffId));
|
||||
this.loadGraphic(Paths.image('freeplay/freeplay' + assetDiffId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -162,7 +162,7 @@ class SongMenuItem extends FlxSpriteGroup
|
|||
|
||||
sparkle = new FlxSprite(ranking.x, ranking.y);
|
||||
sparkle.frames = Paths.getSparrowAtlas('freeplay/sparkle');
|
||||
sparkle.animation.addByPrefix('sparkle', 'sparkle', 24, false);
|
||||
sparkle.animation.addByPrefix('sparkle', 'sparkle Export0', 24, false);
|
||||
sparkle.animation.play('sparkle', true);
|
||||
sparkle.scale.set(0.8, 0.8);
|
||||
sparkle.blend = BlendMode.ADD;
|
||||
|
@ -523,7 +523,6 @@ class SongMenuItem extends FlxSpriteGroup
|
|||
checkWeek(songData?.songId);
|
||||
}
|
||||
|
||||
|
||||
var frameInTicker:Float = 0;
|
||||
var frameInTypeBeat:Int = 0;
|
||||
|
||||
|
|
10
source/funkin/ui/options/MenuItemEnums.hx
Normal file
10
source/funkin/ui/options/MenuItemEnums.hx
Normal file
|
@ -0,0 +1,10 @@
|
|||
package funkin.ui.options;
|
||||
|
||||
// Add enums for use with `EnumPreferenceItem` here!
|
||||
/* Example:
|
||||
class MyOptionEnum
|
||||
{
|
||||
public static inline var YuhUh = "true"; // "true" is the value's ID
|
||||
public static inline var NuhUh = "false";
|
||||
}
|
||||
*/
|
|
@ -8,6 +8,11 @@ import funkin.ui.AtlasText.AtlasFont;
|
|||
import funkin.ui.options.OptionsState.Page;
|
||||
import funkin.graphics.FunkinCamera;
|
||||
import funkin.ui.TextMenuList.TextMenuItem;
|
||||
import funkin.audio.FunkinSound;
|
||||
import funkin.ui.options.MenuItemEnums;
|
||||
import funkin.ui.options.items.CheckboxPreferenceItem;
|
||||
import funkin.ui.options.items.NumberPreferenceItem;
|
||||
import funkin.ui.options.items.EnumPreferenceItem;
|
||||
|
||||
class PreferencesMenu extends Page
|
||||
{
|
||||
|
@ -69,11 +74,51 @@ class PreferencesMenu extends Page
|
|||
}, Preferences.autoPause);
|
||||
}
|
||||
|
||||
override function update(elapsed:Float):Void
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
// Indent the selected item.
|
||||
items.forEach(function(daItem:TextMenuItem) {
|
||||
var thyOffset:Int = 0;
|
||||
|
||||
// Initializing thy text width (if thou text present)
|
||||
var thyTextWidth:Int = 0;
|
||||
if (Std.isOfType(daItem, EnumPreferenceItem)) thyTextWidth = cast(daItem, EnumPreferenceItem).lefthandText.getWidth();
|
||||
else if (Std.isOfType(daItem, NumberPreferenceItem)) thyTextWidth = cast(daItem, NumberPreferenceItem).lefthandText.getWidth();
|
||||
|
||||
if (thyTextWidth != 0)
|
||||
{
|
||||
// Magic number because of the weird offset thats being added by default
|
||||
thyOffset += thyTextWidth - 75;
|
||||
}
|
||||
|
||||
if (items.selectedItem == daItem)
|
||||
{
|
||||
thyOffset += 150;
|
||||
}
|
||||
else
|
||||
{
|
||||
thyOffset += 120;
|
||||
}
|
||||
|
||||
daItem.x = thyOffset;
|
||||
});
|
||||
}
|
||||
|
||||
// - Preference item creation methods -
|
||||
// Should be moved into a separate PreferenceItems class but you can't access PreferencesMenu.items and PreferencesMenu.preferenceItems from outside.
|
||||
|
||||
/**
|
||||
* Creates a pref item that works with booleans
|
||||
* @param onChange Gets called every time the player changes the value; use this to apply the value
|
||||
* @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
|
||||
*/
|
||||
function createPrefItemCheckbox(prefName:String, prefDesc:String, onChange:Bool->Void, defaultValue:Bool):Void
|
||||
{
|
||||
var checkbox:CheckboxPreferenceItem = new CheckboxPreferenceItem(0, 120 * (items.length - 1 + 1), defaultValue);
|
||||
|
||||
items.createItem(120, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() {
|
||||
items.createItem(0, (120 * items.length) + 30, prefName, AtlasFont.BOLD, function() {
|
||||
var value = !checkbox.currentValue;
|
||||
onChange(value);
|
||||
checkbox.currentValue = value;
|
||||
|
@ -82,62 +127,54 @@ class PreferencesMenu extends Page
|
|||
preferenceItems.add(checkbox);
|
||||
}
|
||||
|
||||
override function update(elapsed:Float)
|
||||
/**
|
||||
* Creates a pref item that works with general numbers
|
||||
* @param onChange Gets called every time the player changes the value; use this to apply the value
|
||||
* @param valueFormatter Will get called every time the game needs to display the float value; use this to change how the displayed value looks
|
||||
* @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
|
||||
* @param min Minimum value (example: 0)
|
||||
* @param max Maximum value (example: 10)
|
||||
* @param step The value to increment/decrement by (default = 0.1)
|
||||
* @param precision Rounds decimals up to a `precision` amount of digits (ex: 4 -> 0.1234, 2 -> 0.12)
|
||||
*/
|
||||
function createPrefItemNumber(prefName:String, prefDesc:String, onChange:Float->Void, ?valueFormatter:Float->String, defaultValue:Int, min:Int, max:Int,
|
||||
step:Float = 0.1, precision:Int):Void
|
||||
{
|
||||
super.update(elapsed);
|
||||
var item = new NumberPreferenceItem(0, (120 * items.length) + 30, prefName, defaultValue, min, max, step, precision, onChange, valueFormatter);
|
||||
items.addItem(prefName, item);
|
||||
preferenceItems.add(item.lefthandText);
|
||||
}
|
||||
|
||||
// Indent the selected item.
|
||||
// TODO: Only do this on menu change?
|
||||
items.forEach(function(daItem:TextMenuItem) {
|
||||
if (items.selectedItem == daItem) daItem.x = 150;
|
||||
else
|
||||
daItem.x = 120;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class CheckboxPreferenceItem extends FlxSprite
|
||||
{
|
||||
public var currentValue(default, set):Bool;
|
||||
|
||||
public function new(x:Float, y:Float, defaultValue:Bool = false)
|
||||
{
|
||||
super(x, y);
|
||||
|
||||
frames = Paths.getSparrowAtlas('checkboxThingie');
|
||||
animation.addByPrefix('static', 'Check Box unselected', 24, false);
|
||||
animation.addByPrefix('checked', 'Check Box selecting animation', 24, false);
|
||||
|
||||
setGraphicSize(Std.int(width * 0.7));
|
||||
updateHitbox();
|
||||
|
||||
this.currentValue = defaultValue;
|
||||
}
|
||||
|
||||
override function update(elapsed:Float)
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
switch (animation.curAnim.name)
|
||||
{
|
||||
case 'static':
|
||||
offset.set();
|
||||
case 'checked':
|
||||
offset.set(17, 70);
|
||||
}
|
||||
}
|
||||
|
||||
function set_currentValue(value:Bool):Bool
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
animation.play('checked', true);
|
||||
}
|
||||
else
|
||||
{
|
||||
animation.play('static');
|
||||
}
|
||||
|
||||
return currentValue = value;
|
||||
/**
|
||||
* Creates a pref item that works with number percentages
|
||||
* @param onChange Gets called every time the player changes the value; use this to apply the value
|
||||
* @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
|
||||
* @param min Minimum value (default = 0)
|
||||
* @param max Maximum value (default = 100)
|
||||
*/
|
||||
function createPrefItemPercentage(prefName:String, prefDesc:String, onChange:Int->Void, defaultValue:Int, min:Int = 0, max:Int = 100):Void
|
||||
{
|
||||
var newCallback = function(value:Float) {
|
||||
onChange(Std.int(value));
|
||||
};
|
||||
var formatter = function(value:Float) {
|
||||
return '${value}%';
|
||||
};
|
||||
var item = new NumberPreferenceItem(0, (120 * items.length) + 30, prefName, defaultValue, min, max, 10, 0, newCallback, formatter);
|
||||
items.addItem(prefName, item);
|
||||
preferenceItems.add(item.lefthandText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a pref item that works with enums
|
||||
* @param values Maps enum values to display strings _(ex: `NoteHitSoundType.PingPong => "Ping pong"`)_
|
||||
* @param onChange Gets called every time the player changes the value; use this to apply the value
|
||||
* @param defaultValue The value that is loaded in when the pref item is created (usually your Preferences.settingVariable)
|
||||
*/
|
||||
function createPrefItemEnum(prefName:String, prefDesc:String, values:Map<String, String>, onChange:String->Void, defaultValue:String):Void
|
||||
{
|
||||
var item = new EnumPreferenceItem(0, (120 * items.length) + 30, prefName, values, defaultValue, onChange);
|
||||
items.addItem(prefName, item);
|
||||
preferenceItems.add(item.lefthandText);
|
||||
}
|
||||
}
|
||||
|
|
49
source/funkin/ui/options/items/CheckboxPreferenceItem.hx
Normal file
49
source/funkin/ui/options/items/CheckboxPreferenceItem.hx
Normal file
|
@ -0,0 +1,49 @@
|
|||
package funkin.ui.options.items;
|
||||
|
||||
import flixel.FlxSprite.FlxSprite;
|
||||
|
||||
class CheckboxPreferenceItem extends FlxSprite
|
||||
{
|
||||
public var currentValue(default, set):Bool;
|
||||
|
||||
public function new(x:Float, y:Float, defaultValue:Bool = false)
|
||||
{
|
||||
super(x, y);
|
||||
|
||||
frames = Paths.getSparrowAtlas('checkboxThingie');
|
||||
animation.addByPrefix('static', 'Check Box unselected', 24, false);
|
||||
animation.addByPrefix('checked', 'Check Box selecting animation', 24, false);
|
||||
|
||||
setGraphicSize(Std.int(width * 0.7));
|
||||
updateHitbox();
|
||||
|
||||
this.currentValue = defaultValue;
|
||||
}
|
||||
|
||||
override function update(elapsed:Float)
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
switch (animation.curAnim.name)
|
||||
{
|
||||
case 'static':
|
||||
offset.set();
|
||||
case 'checked':
|
||||
offset.set(17, 70);
|
||||
}
|
||||
}
|
||||
|
||||
function set_currentValue(value:Bool):Bool
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
animation.play('checked', true);
|
||||
}
|
||||
else
|
||||
{
|
||||
animation.play('static');
|
||||
}
|
||||
|
||||
return currentValue = value;
|
||||
}
|
||||
}
|
84
source/funkin/ui/options/items/EnumPreferenceItem.hx
Normal file
84
source/funkin/ui/options/items/EnumPreferenceItem.hx
Normal file
|
@ -0,0 +1,84 @@
|
|||
package funkin.ui.options.items;
|
||||
|
||||
import funkin.ui.TextMenuList;
|
||||
import funkin.ui.AtlasText;
|
||||
import funkin.input.Controls;
|
||||
import funkin.ui.options.MenuItemEnums;
|
||||
import haxe.EnumTools;
|
||||
|
||||
/**
|
||||
* Preference item that allows the player to pick a value from an enum (list of values)
|
||||
*/
|
||||
class EnumPreferenceItem extends TextMenuItem
|
||||
{
|
||||
function controls():Controls
|
||||
{
|
||||
return PlayerSettings.player1.controls;
|
||||
}
|
||||
|
||||
public var lefthandText:AtlasText;
|
||||
|
||||
public var currentValue:String;
|
||||
public var onChangeCallback:Null<String->Void>;
|
||||
public var map:Map<String, String>;
|
||||
public var keys:Array<String> = [];
|
||||
|
||||
var index = 0;
|
||||
|
||||
public function new(x:Float, y:Float, name:String, map:Map<String, String>, defaultValue:String, ?callback:String->Void)
|
||||
{
|
||||
super(x, y, name, function() {
|
||||
callback(this.currentValue);
|
||||
});
|
||||
|
||||
updateHitbox();
|
||||
|
||||
this.map = map;
|
||||
this.currentValue = defaultValue;
|
||||
this.onChangeCallback = callback;
|
||||
|
||||
var i:Int = 0;
|
||||
for (key in map.keys())
|
||||
{
|
||||
this.keys.push(key);
|
||||
if (this.currentValue == key) index = i;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
lefthandText = new AtlasText(15, y, formatted(defaultValue), AtlasFont.DEFAULT);
|
||||
}
|
||||
|
||||
override function update(elapsed:Float):Void
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
// var fancyTextFancyColor:Color;
|
||||
if (selected)
|
||||
{
|
||||
var shouldDecrease:Bool = controls().UI_LEFT_P;
|
||||
var shouldIncrease:Bool = controls().UI_RIGHT_P;
|
||||
|
||||
if (shouldDecrease) index -= 1;
|
||||
if (shouldIncrease) index += 1;
|
||||
|
||||
if (index > keys.length - 1) index = 0;
|
||||
if (index < 0) index = keys.length - 1;
|
||||
|
||||
currentValue = keys[index];
|
||||
if (onChangeCallback != null && (shouldIncrease || shouldDecrease))
|
||||
{
|
||||
onChangeCallback(currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
lefthandText.text = formatted(currentValue);
|
||||
}
|
||||
|
||||
function formatted(value:String):String
|
||||
{
|
||||
// FIXME: Can't add arrows around the text because the font doesn't support < >
|
||||
// var leftArrow:String = selected ? '<' : '';
|
||||
// var rightArrow:String = selected ? '>' : '';
|
||||
return '${map.get(value) ?? value}';
|
||||
}
|
||||
}
|
136
source/funkin/ui/options/items/NumberPreferenceItem.hx
Normal file
136
source/funkin/ui/options/items/NumberPreferenceItem.hx
Normal file
|
@ -0,0 +1,136 @@
|
|||
package funkin.ui.options.items;
|
||||
|
||||
import funkin.ui.TextMenuList;
|
||||
import funkin.ui.AtlasText;
|
||||
import funkin.input.Controls;
|
||||
|
||||
/**
|
||||
* Preference item that allows the player to pick a value between min and max
|
||||
*/
|
||||
class NumberPreferenceItem extends TextMenuItem
|
||||
{
|
||||
function controls():Controls
|
||||
{
|
||||
return PlayerSettings.player1.controls;
|
||||
}
|
||||
|
||||
// Widgets
|
||||
public var lefthandText:AtlasText;
|
||||
|
||||
// Constants
|
||||
static final HOLD_DELAY:Float = 0.3; // seconds
|
||||
static final CHANGE_RATE:Float = 0.08; // seconds
|
||||
|
||||
// Constructor-initialized variables
|
||||
public var currentValue:Float;
|
||||
public var min:Float;
|
||||
public var max:Float;
|
||||
public var step:Float;
|
||||
public var precision:Int;
|
||||
public var onChangeCallback:Null<Float->Void>;
|
||||
public var valueFormatter:Null<Float->String>;
|
||||
|
||||
// Variables
|
||||
var holdDelayTimer:Float = HOLD_DELAY; // seconds
|
||||
var changeRateTimer:Float = 0.0; // seconds
|
||||
|
||||
/**
|
||||
* @param min Minimum value (example: 0)
|
||||
* @param max Maximum value (example: 100)
|
||||
* @param step The value to increment/decrement by (example: 10)
|
||||
* @param callback Will get called every time the user changes the setting; use this to apply/save the setting.
|
||||
* @param valueFormatter Will get called every time the game needs to display the float value; use this to change how the displayed string looks
|
||||
*/
|
||||
public function new(x:Float, y:Float, name:String, defaultValue:Float, min:Float, max:Float, step:Float, precision:Int, ?callback:Float->Void,
|
||||
?valueFormatter:Float->String):Void
|
||||
{
|
||||
super(x, y, name, function() {
|
||||
callback(this.currentValue);
|
||||
});
|
||||
lefthandText = new AtlasText(15, y, formatted(defaultValue), AtlasFont.DEFAULT);
|
||||
|
||||
updateHitbox();
|
||||
|
||||
this.currentValue = defaultValue;
|
||||
this.min = min;
|
||||
this.max = max;
|
||||
this.step = step;
|
||||
this.precision = precision;
|
||||
this.onChangeCallback = callback;
|
||||
this.valueFormatter = valueFormatter;
|
||||
}
|
||||
|
||||
override function update(elapsed:Float):Void
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
// var fancyTextFancyColor:Color;
|
||||
if (selected)
|
||||
{
|
||||
holdDelayTimer -= elapsed;
|
||||
if (holdDelayTimer <= 0.0)
|
||||
{
|
||||
changeRateTimer -= elapsed;
|
||||
}
|
||||
|
||||
var jpLeft:Bool = controls().UI_LEFT_P;
|
||||
var jpRight:Bool = controls().UI_RIGHT_P;
|
||||
|
||||
if (jpLeft || jpRight)
|
||||
{
|
||||
holdDelayTimer = HOLD_DELAY;
|
||||
changeRateTimer = 0.0;
|
||||
}
|
||||
|
||||
var shouldDecrease:Bool = jpLeft;
|
||||
var shouldIncrease:Bool = jpRight;
|
||||
|
||||
if (controls().UI_LEFT && holdDelayTimer <= 0.0 && changeRateTimer <= 0.0)
|
||||
{
|
||||
shouldDecrease = true;
|
||||
changeRateTimer = CHANGE_RATE;
|
||||
}
|
||||
else if (controls().UI_RIGHT && holdDelayTimer <= 0.0 && changeRateTimer <= 0.0)
|
||||
{
|
||||
shouldIncrease = true;
|
||||
changeRateTimer = CHANGE_RATE;
|
||||
}
|
||||
|
||||
// Actually increasing/decreasing the value
|
||||
if (shouldDecrease)
|
||||
{
|
||||
var isBelowMin:Bool = currentValue - step < min;
|
||||
currentValue = (currentValue - step).clamp(min, max);
|
||||
if (onChangeCallback != null && !isBelowMin) onChangeCallback(currentValue);
|
||||
}
|
||||
else if (shouldIncrease)
|
||||
{
|
||||
var isAboveMax:Bool = currentValue + step > max;
|
||||
currentValue = (currentValue + step).clamp(min, max);
|
||||
if (onChangeCallback != null && !isAboveMax) onChangeCallback(currentValue);
|
||||
}
|
||||
}
|
||||
|
||||
lefthandText.text = formatted(currentValue);
|
||||
}
|
||||
|
||||
/** Turns the float into a string */
|
||||
function formatted(value:Float):String
|
||||
{
|
||||
var float:Float = toFixed(value);
|
||||
if (valueFormatter != null)
|
||||
{
|
||||
return valueFormatter(float);
|
||||
}
|
||||
else
|
||||
{
|
||||
return '${float}';
|
||||
}
|
||||
}
|
||||
|
||||
function toFixed(value:Float):Float
|
||||
{
|
||||
var multiplier:Float = Math.pow(10, precision);
|
||||
return Math.floor(value * multiplier) / multiplier;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
package funkin.util.plugins;
|
||||
|
||||
import flixel.FlxG;
|
||||
import flixel.FlxBasic;
|
||||
import funkin.ui.MusicBeatState;
|
||||
import funkin.ui.MusicBeatSubState;
|
||||
|
||||
/**
|
||||
* A plugin which adds functionality to press `F5` to reload all game assets, then reload the current state.
|
||||
|
@ -28,10 +31,15 @@ class ReloadAssetsDebugPlugin extends FlxBasic
|
|||
if (FlxG.keys.justPressed.F5)
|
||||
#end
|
||||
{
|
||||
funkin.modding.PolymodHandler.forceReloadAssets();
|
||||
var state:Dynamic = FlxG.state;
|
||||
if (state is MusicBeatState || state is MusicBeatSubState) state.reloadAssets();
|
||||
else
|
||||
{
|
||||
funkin.modding.PolymodHandler.forceReloadAssets();
|
||||
|
||||
// Create a new instance of the current state, so old data is cleared.
|
||||
FlxG.resetState();
|
||||
// Create a new instance of the current state, so old data is cleared.
|
||||
FlxG.resetState();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue