mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2024-11-14 19:25:16 -05:00
Merge branch 'feature/2-chart-2-editor'
This commit is contained in:
commit
7f9171203a
70 changed files with 3837 additions and 727 deletions
54
docs/style-guide.md
Normal file
54
docs/style-guide.md
Normal file
|
@ -0,0 +1,54 @@
|
|||
# Funkin' Repo Code Style Guide
|
||||
|
||||
This short document is designed to give a run-down on how code should be formatted to maintain a consistent style throughout, making the repo easier to maintain.
|
||||
|
||||
## Notes on IDEs and Extensions
|
||||
|
||||
The Visual Studio Code IDE is highly recommended, as this repo contains various configuration that works with VSCode extensions to automatically style certain things for you.
|
||||
|
||||
VSCode is also the only IDE that has any good tools for Haxe so yeah.
|
||||
|
||||
## Whitespace and Indentation
|
||||
|
||||
The Haxe extension of VSCode will use the repo's `hxformat.json` to tell VSCode how to format the code files of the repo. Enabling VSCode's "Format on Save" feature is recommended.
|
||||
|
||||
## Variable and Function Names
|
||||
|
||||
It is recommended to name variables and functions with descriptive titles, in lowerCamelCase. There is no penalty for giving a variable a longer name as long as it isn't excessive.
|
||||
|
||||
## Code Comments
|
||||
|
||||
The CodeDox extension for VSCode provides extensive support for JavaDoc-style code comments, and these should be used for public functions wherever possible to ensure the usage of each function is clear.
|
||||
|
||||
Example:
|
||||
```
|
||||
/**
|
||||
* Finds the largest deviation from the desired time inside this VoicesGroup.
|
||||
*
|
||||
* @param targetTime The time to check against.
|
||||
* If none is provided, it checks the time of all members against the first member of this VoicesGroup.
|
||||
* @return The largest deviation from the target time found.
|
||||
*/
|
||||
public function checkSyncError(?targetTime:Float):Float
|
||||
```
|
||||
|
||||
## License Headers
|
||||
|
||||
Do not include headers specifying code license on individual files in the repo, since the main `LICENSE.md` file covers all of them.
|
||||
|
||||
## Imports
|
||||
|
||||
Imports should be placed in a single group, in alphabetical order, at the top of the code file. The exception is conditional imports (using compile defines), which should be placed at the end of the list (and sorted alphabetically where possible).
|
||||
|
||||
Example:
|
||||
```
|
||||
import haxe.format.JsonParser;
|
||||
import openfl.Assets;
|
||||
import openfl.geom.Matrix;
|
||||
import openfl.geom.Matrix3D;
|
||||
#if sys
|
||||
import funkin.io.FileUtil;
|
||||
import sys.io.File;
|
||||
#end
|
||||
```
|
||||
|
205
hmm.json
205
hmm.json
|
@ -1,104 +1,105 @@
|
|||
{
|
||||
"dependencies": [{
|
||||
"name": "discord_rpc",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "2d83fa8",
|
||||
"url": "https://github.com/Aidan63/linc_discord-rpc"
|
||||
},
|
||||
{
|
||||
"name": "flixel",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "a629f9a5",
|
||||
"url": "https://github.com/MasterEric/flixel"
|
||||
},
|
||||
{
|
||||
"name": "flixel-addons",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "752c3d7",
|
||||
"url": "https://github.com/MasterEric/flixel-addons"
|
||||
},
|
||||
{
|
||||
"name": "flixel-ui",
|
||||
"type": "haxelib",
|
||||
"version": "2.4.0"
|
||||
},
|
||||
{
|
||||
"name": "flxanimate",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "18b2060",
|
||||
"url": "https://github.com/Dot-Stuff/flxanimate"
|
||||
},
|
||||
{
|
||||
"name": "format",
|
||||
"type": "haxelib",
|
||||
"version": "3.5.0"
|
||||
},
|
||||
{
|
||||
"name": "haxeui-core",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "fc8d656b",
|
||||
"url": "https://github.com/haxeui/haxeui-core/"
|
||||
},
|
||||
{
|
||||
"name": "haxeui-flixel",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "80941a7",
|
||||
"url": "https://github.com/haxeui/haxeui-flixel"
|
||||
},
|
||||
{
|
||||
"name": "hmm",
|
||||
"type": "haxelib",
|
||||
"version": "2.1.0"
|
||||
},
|
||||
{
|
||||
"name": "hscript",
|
||||
"type": "haxelib",
|
||||
"version": "2.5.0"
|
||||
},
|
||||
{
|
||||
"name": "hxcpp",
|
||||
"type": "haxelib",
|
||||
"version": "4.2.1"
|
||||
},
|
||||
{
|
||||
"name": "hxcpp-debug-server",
|
||||
"type": "haxelib",
|
||||
"version": "1.2.4"
|
||||
},
|
||||
{
|
||||
"name": "hxp",
|
||||
"type": "haxelib",
|
||||
"version": "1.2.2"
|
||||
},
|
||||
{
|
||||
"name": "lime",
|
||||
"type": "haxelib",
|
||||
"version": "8.0.0"
|
||||
},
|
||||
{
|
||||
"name": "openfl",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "3fd5763c1",
|
||||
"url": "https://github.com/MasterEric/openfl"
|
||||
},
|
||||
{
|
||||
"name": "polymod",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "547c8ee",
|
||||
"url": "https://github.com/larsiusprime/polymod"
|
||||
},
|
||||
{
|
||||
"name": "thx.semver",
|
||||
"type": "haxelib",
|
||||
"version": "0.2.2"
|
||||
}
|
||||
]
|
||||
"dependencies": [
|
||||
{
|
||||
"name": "discord_rpc",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "2d83fa8",
|
||||
"url": "https://github.com/Aidan63/linc_discord-rpc"
|
||||
},
|
||||
{
|
||||
"name": "flixel",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "a629f9a5",
|
||||
"url": "https://github.com/MasterEric/flixel"
|
||||
},
|
||||
{
|
||||
"name": "flixel-addons",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "752c3d7",
|
||||
"url": "https://github.com/MasterEric/flixel-addons"
|
||||
},
|
||||
{
|
||||
"name": "flixel-ui",
|
||||
"type": "haxelib",
|
||||
"version": "2.4.0"
|
||||
},
|
||||
{
|
||||
"name": "flxanimate",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "18b2060",
|
||||
"url": "https://github.com/Dot-Stuff/flxanimate"
|
||||
},
|
||||
{
|
||||
"name": "format",
|
||||
"type": "haxelib",
|
||||
"version": "3.5.0"
|
||||
},
|
||||
{
|
||||
"name": "haxeui-core",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "e5cf78d",
|
||||
"url": "https://github.com/haxeui/haxeui-core/"
|
||||
},
|
||||
{
|
||||
"name": "haxeui-flixel",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "f03bb6d",
|
||||
"url": "https://github.com/haxeui/haxeui-flixel"
|
||||
},
|
||||
{
|
||||
"name": "hmm",
|
||||
"type": "haxelib",
|
||||
"version": "2.1.0"
|
||||
},
|
||||
{
|
||||
"name": "hscript",
|
||||
"type": "haxelib",
|
||||
"version": "2.5.0"
|
||||
},
|
||||
{
|
||||
"name": "hxcpp",
|
||||
"type": "haxelib",
|
||||
"version": "4.2.1"
|
||||
},
|
||||
{
|
||||
"name": "hxcpp-debug-server",
|
||||
"type": "haxelib",
|
||||
"version": "1.2.4"
|
||||
},
|
||||
{
|
||||
"name": "hxp",
|
||||
"type": "haxelib",
|
||||
"version": null
|
||||
},
|
||||
{
|
||||
"name": "lime",
|
||||
"type": "haxelib",
|
||||
"version": null
|
||||
},
|
||||
{
|
||||
"name": "openfl",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "3fd5763c1",
|
||||
"url": "https://github.com/MasterEric/openfl"
|
||||
},
|
||||
{
|
||||
"name": "polymod",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "547c8ee",
|
||||
"url": "https://github.com/larsiusprime/polymod"
|
||||
},
|
||||
{
|
||||
"name": "thx.semver",
|
||||
"type": "haxelib",
|
||||
"version": "0.2.2"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -78,7 +78,13 @@ class Main extends Sprite
|
|||
*/
|
||||
|
||||
#if !debug
|
||||
initialState = funkin.TitleState;
|
||||
/**
|
||||
* Someone was like "hey let's make a state that only runs code on debug builds"
|
||||
* then put essential initialization code in it.
|
||||
* The easiest fix is to make it run in all builds.
|
||||
* -Eric
|
||||
*/
|
||||
// initialState = funkin.TitleState;
|
||||
#end
|
||||
|
||||
initHaxeUI();
|
||||
|
|
|
@ -5,8 +5,6 @@ import flixel.group.FlxSpriteGroup;
|
|||
import flixel.math.FlxMath;
|
||||
import flixel.util.FlxTimer;
|
||||
|
||||
using StringTools;
|
||||
|
||||
/**
|
||||
* Loosley based on FlxTypeText lolol
|
||||
*/
|
||||
|
|
|
@ -63,13 +63,33 @@ class Conductor
|
|||
}
|
||||
|
||||
/**
|
||||
* Duration of a step in milliseconds. Calculated based on bpm.
|
||||
* Duration of a step (quarter) in milliseconds. Calculated based on bpm.
|
||||
*/
|
||||
public static var stepCrochet(get, null):Float;
|
||||
|
||||
static function get_stepCrochet():Float
|
||||
{
|
||||
return crochet / 4;
|
||||
return crochet / timeSignatureNumerator;
|
||||
}
|
||||
|
||||
public static var timeSignatureNumerator(get, null):Int;
|
||||
|
||||
static function get_timeSignatureNumerator():Int
|
||||
{
|
||||
if (currentTimeChange == null)
|
||||
return 4;
|
||||
|
||||
return currentTimeChange.timeSignatureNum;
|
||||
}
|
||||
|
||||
public static var timeSignatureDenominator(get, null):Int;
|
||||
|
||||
static function get_timeSignatureDenominator():Int
|
||||
{
|
||||
if (currentTimeChange == null)
|
||||
return 4;
|
||||
|
||||
return currentTimeChange.timeSignatureDen;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -96,7 +116,20 @@ class Conductor
|
|||
public static var offset:Float = 0;
|
||||
|
||||
// TODO: Add code to update this.
|
||||
public static var beatsPerMeasure:Int = 4;
|
||||
public static var beatsPerMeasure(get, null):Int;
|
||||
|
||||
static function get_beatsPerMeasure():Int
|
||||
{
|
||||
return timeSignatureNumerator;
|
||||
}
|
||||
|
||||
public static var stepsPerMeasure(get, null):Int;
|
||||
|
||||
static function get_stepsPerMeasure():Int
|
||||
{
|
||||
// Is this always x4?
|
||||
return timeSignatureNumerator * 4;
|
||||
}
|
||||
|
||||
private function new()
|
||||
{
|
||||
|
@ -124,11 +157,17 @@ class Conductor
|
|||
* Forcibly defines the current BPM of the song.
|
||||
* Useful for things like the chart editor that need to manipulate BPM in real time.
|
||||
*
|
||||
* Set to null to reset to the BPM defined by the timeChanges.
|
||||
*
|
||||
* WARNING: Avoid this for things like setting the BPM of the title screen music,
|
||||
* you should have a metadata file for it instead.
|
||||
*/
|
||||
public static function forceBPM(bpm:Float)
|
||||
public static function forceBPM(?bpm:Float = null)
|
||||
{
|
||||
if (bpm != null)
|
||||
trace('[CONDUCTOR] Forcing BPM to ' + bpm);
|
||||
else
|
||||
trace('[CONDUCTOR] Resetting BPM to default');
|
||||
Conductor.bpmOverride = bpm;
|
||||
}
|
||||
|
||||
|
@ -213,10 +252,8 @@ class Conductor
|
|||
}
|
||||
}
|
||||
|
||||
public static function mapTimeChanges(currentChart:SongDifficulty)
|
||||
public static function mapTimeChanges(songTimeChanges:Array<SongTimeChange>)
|
||||
{
|
||||
var songTimeChanges:Array<SongTimeChange> = currentChart.timeChanges;
|
||||
|
||||
timeChanges = [];
|
||||
|
||||
for (currentTimeChange in songTimeChanges)
|
||||
|
|
|
@ -18,8 +18,6 @@ import lime.math.Rectangle;
|
|||
import lime.utils.Assets;
|
||||
import openfl.filters.ShaderFilter;
|
||||
|
||||
using StringTools;
|
||||
|
||||
class CoolUtil
|
||||
{
|
||||
public static var difficultyArray:Array<String> = ['EASY', "NORMAL", "HARD"];
|
||||
|
|
|
@ -4,8 +4,6 @@ import flixel.FlxSprite;
|
|||
import flixel.group.FlxGroup.FlxTypedGroup;
|
||||
import flixel.math.FlxPoint;
|
||||
|
||||
using StringTools;
|
||||
|
||||
class CutsceneCharacter extends FlxTypedGroup<FlxSprite>
|
||||
{
|
||||
public var coolPos:FlxPoint = FlxPoint.get();
|
||||
|
|
|
@ -11,8 +11,6 @@ import flixel.util.FlxColor;
|
|||
import flixel.util.FlxTimer;
|
||||
import funkin.play.PlayState;
|
||||
|
||||
using StringTools;
|
||||
|
||||
class DialogueBox extends FlxSpriteGroup
|
||||
{
|
||||
var box:FlxSprite;
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
package funkin;
|
||||
|
||||
import Sys.sleep;
|
||||
|
||||
using StringTools;
|
||||
|
||||
#if discord_rpc
|
||||
import discord_rpc.DiscordRpc;
|
||||
#end
|
||||
|
|
|
@ -36,8 +36,6 @@ import funkin.shaderslmfao.StrokeShader;
|
|||
import lime.app.Future;
|
||||
import lime.utils.Assets;
|
||||
|
||||
using StringTools;
|
||||
|
||||
class FreeplayState extends MusicBeatSubstate
|
||||
{
|
||||
var songs:Array<SongMetadata> = [];
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package funkin;
|
||||
|
||||
import flixel.system.debug.log.LogStyle;
|
||||
import flixel.addons.transition.FlxTransitionSprite.GraphicTransTileDiamond;
|
||||
import flixel.addons.transition.FlxTransitionableState;
|
||||
import flixel.addons.transition.TransitionData;
|
||||
|
@ -15,10 +16,8 @@ import funkin.play.song.SongData.SongDataParser;
|
|||
import funkin.play.stage.StageData;
|
||||
import funkin.ui.PreferencesMenu;
|
||||
import funkin.util.macro.MacroUtil;
|
||||
import funkin.util.WindowUtil;
|
||||
import openfl.display.BitmapData;
|
||||
|
||||
using StringTools;
|
||||
|
||||
#if colyseus
|
||||
import io.colyseus.Client;
|
||||
import io.colyseus.Room;
|
||||
|
@ -88,8 +87,16 @@ class InitState extends FlxTransitionableState
|
|||
if (FlxG.save.data.mute != null)
|
||||
FlxG.sound.muted = FlxG.save.data.mute;
|
||||
|
||||
// Make errors and warnings less annoying.
|
||||
LogStyle.ERROR.openConsole = false;
|
||||
LogStyle.ERROR.errorSound = null;
|
||||
LogStyle.WARNING.openConsole = false;
|
||||
LogStyle.WARNING.errorSound = null;
|
||||
|
||||
// FlxG.save.close();
|
||||
// FlxG.sound.loadSavedPrefs();
|
||||
WindowUtil.initWindowEvents();
|
||||
|
||||
PreferencesMenu.initPrefs();
|
||||
PlayerSettings.init();
|
||||
Highscore.load();
|
||||
|
@ -125,6 +132,8 @@ class InitState extends FlxTransitionableState
|
|||
ModuleHandler.buildModuleCallbacks();
|
||||
ModuleHandler.loadModuleCache();
|
||||
|
||||
FlxG.debugger.toggleKeys = [F2];
|
||||
|
||||
#if song
|
||||
var song = getSong();
|
||||
|
||||
|
|
|
@ -31,8 +31,10 @@ class LatencyState extends MusicBeatSubstate
|
|||
var offsetsPerBeat:Array<Int> = [];
|
||||
var swagSong:HomemadeMusic;
|
||||
|
||||
#if debug
|
||||
var funnyStatsGraph:CoolStatsGraph;
|
||||
var realStats:CoolStatsGraph;
|
||||
#end
|
||||
|
||||
override function create()
|
||||
{
|
||||
|
@ -42,11 +44,13 @@ class LatencyState extends MusicBeatSubstate
|
|||
FlxG.sound.music = swagSong;
|
||||
FlxG.sound.music.play();
|
||||
|
||||
#if debug
|
||||
funnyStatsGraph = new CoolStatsGraph(0, Std.int(FlxG.height / 2), FlxG.width, Std.int(FlxG.height / 2), FlxColor.PINK, "time");
|
||||
FlxG.addChildBelowMouse(funnyStatsGraph);
|
||||
|
||||
realStats = new CoolStatsGraph(0, Std.int(FlxG.height / 2), FlxG.width, Std.int(FlxG.height / 2), FlxColor.YELLOW, "REAL");
|
||||
FlxG.addChildBelowMouse(realStats);
|
||||
#end
|
||||
|
||||
FlxG.stage.addEventListener(KeyboardEvent.KEY_DOWN, key ->
|
||||
{
|
||||
|
@ -170,8 +174,10 @@ class LatencyState extends MusicBeatSubstate
|
|||
trace(FlxG.sound.music._channel.position);
|
||||
*/
|
||||
|
||||
#if debug
|
||||
funnyStatsGraph.update(FlxG.sound.music.time % 500);
|
||||
realStats.update(swagSong.getTimeWithDiff() % 500);
|
||||
#end
|
||||
|
||||
if (FlxG.keys.justPressed.S)
|
||||
{
|
||||
|
|
|
@ -188,10 +188,13 @@ class LoadingState extends MusicBeatState
|
|||
{
|
||||
Paths.setCurrentLevel('tutorial');
|
||||
}
|
||||
else if (PlayState.storyWeek == 8) {
|
||||
else if (PlayState.storyWeek == 8)
|
||||
{
|
||||
// TODO: Refactor this code.
|
||||
Paths.setCurrentLevel("weekend1");
|
||||
} else {
|
||||
}
|
||||
else
|
||||
{
|
||||
Paths.setCurrentLevel("week" + PlayState.storyWeek);
|
||||
}
|
||||
#if NO_PRELOAD_ALL
|
||||
|
@ -251,7 +254,7 @@ class LoadingState extends MusicBeatState
|
|||
}
|
||||
else
|
||||
{
|
||||
if (StringTools.endsWith(path, ".bundle"))
|
||||
if (path.endsWith(".bundle"))
|
||||
{
|
||||
rootPath = path;
|
||||
path += "/library.json";
|
||||
|
@ -351,5 +354,5 @@ class MultiCallback
|
|||
return fired.copy();
|
||||
|
||||
public function getUnfired()
|
||||
return [for (id in unfired.keys()) id];
|
||||
return unfired.array();
|
||||
}
|
||||
|
|
|
@ -28,9 +28,6 @@ import funkin.util.Constants;
|
|||
import funkin.util.WindowUtil;
|
||||
import lime.app.Application;
|
||||
import openfl.filters.ShaderFilter;
|
||||
|
||||
using StringTools;
|
||||
|
||||
#if discord_rpc
|
||||
import Discord.DiscordClient;
|
||||
#end
|
||||
|
|
|
@ -60,6 +60,7 @@ class MusicBeatState extends FlxUIState
|
|||
{
|
||||
super.update(elapsed);
|
||||
|
||||
// Emergency exit button.
|
||||
if (FlxG.keys.justPressed.F4)
|
||||
FlxG.switchState(new MainMenuState());
|
||||
|
||||
|
@ -67,7 +68,7 @@ class MusicBeatState extends FlxUIState
|
|||
if (FlxG.keys.justPressed.F5)
|
||||
debug_refreshModules();
|
||||
|
||||
// ` / ~
|
||||
// ` / ~ to open the debug menu.
|
||||
if (FlxG.keys.justPressed.GRAVEACCENT)
|
||||
{
|
||||
// TODO: Does this break anything?
|
||||
|
@ -76,7 +77,10 @@ class MusicBeatState extends FlxUIState
|
|||
FlxG.state.openSubState(new DebugMenuSubState());
|
||||
}
|
||||
|
||||
// Display Conductor info in the watch window.
|
||||
FlxG.watch.addQuick("songPos", Conductor.songPosition);
|
||||
FlxG.watch.addQuick("currentStepTime", Conductor.currentStepTime);
|
||||
FlxG.watch.addQuick("bpm", Conductor.bpm);
|
||||
|
||||
dispatchEvent(new UpdateScriptEvent(elapsed));
|
||||
}
|
||||
|
|
|
@ -15,8 +15,6 @@ import io.newgrounds.objects.events.Result.GetCurrentVersionResult;
|
|||
import io.newgrounds.objects.events.Result.GetVersionResult;
|
||||
import lime.app.Application;
|
||||
import openfl.display.Stage;
|
||||
|
||||
using StringTools;
|
||||
#end
|
||||
|
||||
/**
|
||||
|
|
|
@ -10,8 +10,6 @@ import funkin.shaderslmfao.ColorSwap;
|
|||
import funkin.ui.PreferencesMenu;
|
||||
import funkin.util.Constants;
|
||||
|
||||
using StringTools;
|
||||
|
||||
class Note extends FlxSprite
|
||||
{
|
||||
public var data = new NoteData();
|
||||
|
|
|
@ -6,8 +6,6 @@ import funkin.play.PlayState;
|
|||
import haxe.Json;
|
||||
import lime.utils.Assets;
|
||||
|
||||
using StringTools;
|
||||
|
||||
typedef SwagSong =
|
||||
{
|
||||
var song:String;
|
||||
|
|
|
@ -15,9 +15,6 @@ import funkin.play.PlayState;
|
|||
import funkin.play.song.SongData.SongDataParser;
|
||||
import lime.net.curl.CURLCode;
|
||||
import openfl.Assets;
|
||||
|
||||
using StringTools;
|
||||
|
||||
#if discord_rpc
|
||||
import Discord.DiscordClient;
|
||||
#end
|
||||
|
|
|
@ -23,8 +23,6 @@ import openfl.events.NetStatusEvent;
|
|||
import openfl.media.Video;
|
||||
import openfl.net.NetStream;
|
||||
|
||||
using StringTools;
|
||||
|
||||
#if desktop
|
||||
#end
|
||||
class TitleState extends MusicBeatState
|
||||
|
|
|
@ -7,30 +7,38 @@ import flixel.system.FlxSound;
|
|||
// when needed
|
||||
class VoicesGroup extends FlxTypedGroup<FlxSound>
|
||||
{
|
||||
public var time(default, set):Float = 0;
|
||||
public var time(get, set):Float;
|
||||
|
||||
public var volume(default, set):Float = 1;
|
||||
public var volume(get, set):Float;
|
||||
|
||||
public var pitch(default, set):Float = 1;
|
||||
public var pitch(get, set):Float;
|
||||
|
||||
// make it a group that you add to?
|
||||
public function new(song:String, ?files:Array<String> = null)
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
}
|
||||
|
||||
// TODO: Remove this.
|
||||
public static function build(song:String, ?files:Array<String> = null):VoicesGroup
|
||||
{
|
||||
var result = new VoicesGroup();
|
||||
|
||||
if (files == null)
|
||||
{
|
||||
// Add an empty voice.
|
||||
add(new FlxSound());
|
||||
return;
|
||||
result.add(new FlxSound());
|
||||
return result;
|
||||
}
|
||||
|
||||
for (sndFile in files)
|
||||
{
|
||||
var snd:FlxSound = new FlxSound().loadEmbedded(Paths.voices(song, '$sndFile'));
|
||||
FlxG.sound.list.add(snd); // adds it to sound group for proper volumes
|
||||
add(snd); // adds it to main group for other shit
|
||||
result.add(snd); // adds it to main group for other shit
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -83,6 +91,14 @@ class VoicesGroup extends FlxTypedGroup<FlxSound>
|
|||
});
|
||||
}
|
||||
|
||||
function get_time():Float
|
||||
{
|
||||
if (getFirstAlive() != null)
|
||||
return getFirstAlive().time;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
function set_time(time:Float):Float
|
||||
{
|
||||
forEachAlive(function(snd)
|
||||
|
@ -94,6 +110,14 @@ class VoicesGroup extends FlxTypedGroup<FlxSound>
|
|||
return time;
|
||||
}
|
||||
|
||||
function get_volume():Float
|
||||
{
|
||||
if (getFirstAlive() != null)
|
||||
return getFirstAlive().volume;
|
||||
else
|
||||
return 1;
|
||||
}
|
||||
|
||||
// in PlayState, adjust the code so that it only mutes the player1 vocal tracks?
|
||||
function set_volume(volume:Float):Float
|
||||
{
|
||||
|
@ -105,9 +129,20 @@ class VoicesGroup extends FlxTypedGroup<FlxSound>
|
|||
return volume;
|
||||
}
|
||||
|
||||
function get_pitch():Float
|
||||
{
|
||||
#if FLX_PITCH
|
||||
if (getFirstAlive() != null)
|
||||
return getFirstAlive().pitch;
|
||||
else
|
||||
#end
|
||||
return 1;
|
||||
}
|
||||
|
||||
function set_pitch(val:Float):Float
|
||||
{
|
||||
#if HAS_PITCH
|
||||
#if FLX_PITCH
|
||||
trace('Setting audio pitch to ' + val);
|
||||
forEachAlive(function(snd)
|
||||
{
|
||||
snd.pitch = val;
|
||||
|
|
|
@ -17,8 +17,6 @@ import io.newgrounds.objects.events.Result.GetCurrentVersionResult;
|
|||
import io.newgrounds.objects.events.Result.GetVersionResult;
|
||||
#end
|
||||
|
||||
using StringTools;
|
||||
|
||||
/**
|
||||
* Contains any script functions which should be BLOCKED from use by modded scripts.
|
||||
*/
|
||||
|
|
|
@ -17,8 +17,6 @@ import io.newgrounds.objects.events.Result.GetCurrentVersionResult;
|
|||
import io.newgrounds.objects.events.Result.GetVersionResult;
|
||||
#end
|
||||
|
||||
using StringTools;
|
||||
|
||||
/**
|
||||
* Contains any script functions which should be ALLOWD for use by modded scripts.
|
||||
*/
|
||||
|
|
208
source/funkin/audio/FlxAudioGroup.hx
Normal file
208
source/funkin/audio/FlxAudioGroup.hx
Normal file
|
@ -0,0 +1,208 @@
|
|||
package funkin.audio;
|
||||
|
||||
import flixel.group.FlxGroup.FlxTypedGroup;
|
||||
import flixel.system.FlxSound;
|
||||
|
||||
/**
|
||||
* A group of FlxSounds which can be controlled as a whole.
|
||||
*
|
||||
* Add sounds to the group using `add()`, and then control them
|
||||
* as a whole using the properties and methods of this class.
|
||||
*
|
||||
* It is assumed that all the sounds will play at the same time,
|
||||
* and have the same duration.
|
||||
*/
|
||||
class FlxAudioGroup extends FlxTypedGroup<FlxSound>
|
||||
{
|
||||
/**
|
||||
* The position in time of the sounds in the group.
|
||||
* Measured in milliseconds.
|
||||
*/
|
||||
public var time(get, set):Float;
|
||||
|
||||
function get_time():Float
|
||||
{
|
||||
if (getFirstAlive() != null)
|
||||
return getFirstAlive().time;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
function set_time(time:Float):Float
|
||||
{
|
||||
forEachAlive(function(sound:FlxSound)
|
||||
{
|
||||
// account for different offsets per sound?
|
||||
sound.time = time;
|
||||
});
|
||||
|
||||
return time;
|
||||
}
|
||||
|
||||
/**
|
||||
* The volume of the sounds in the group.
|
||||
*/
|
||||
public var volume(get, set):Float;
|
||||
|
||||
function get_volume():Float
|
||||
{
|
||||
if (getFirstAlive() != null)
|
||||
return getFirstAlive().volume;
|
||||
else
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
function set_volume(volume:Float):Float
|
||||
{
|
||||
forEachAlive(function(sound:FlxSound)
|
||||
{
|
||||
sound.volume = volume;
|
||||
});
|
||||
|
||||
return volume;
|
||||
}
|
||||
|
||||
/**
|
||||
* The pitch of the sounds in the group, as a multiplier of 1.0x.
|
||||
* `2.0` would play the audio twice as fast with a higher pitch,
|
||||
* and `0.5` would play the audio at half speed with a lower pitch.
|
||||
*/
|
||||
public var pitch(get, set):Float;
|
||||
|
||||
function get_pitch():Float
|
||||
{
|
||||
#if FLX_PITCH
|
||||
if (getFirstAlive() != null)
|
||||
return getFirstAlive().pitch;
|
||||
else
|
||||
#end
|
||||
return 1;
|
||||
}
|
||||
|
||||
function set_pitch(val:Float):Float
|
||||
{
|
||||
#if FLX_PITCH
|
||||
trace('Setting audio pitch to ' + val);
|
||||
forEachAlive(function(sound:FlxSound)
|
||||
{
|
||||
sound.pitch = val;
|
||||
});
|
||||
#end
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether members of the group should be destroyed when they finish playing.
|
||||
*/
|
||||
public var autoDestroyMembers(default, set):Bool = false;
|
||||
|
||||
function set_autoDestroyMembers(value:Bool):Bool
|
||||
{
|
||||
autoDestroyMembers = value;
|
||||
forEachAlive(function(sound:FlxSound)
|
||||
{
|
||||
sound.autoDestroy = value;
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a sound to the group.
|
||||
*/
|
||||
public override function add(sound:FlxSound):FlxSound
|
||||
{
|
||||
var result:FlxSound = super.add(sound);
|
||||
|
||||
if (result == null)
|
||||
return null;
|
||||
|
||||
// Apply parameters to the new sound.
|
||||
result.autoDestroy = this.autoDestroyMembers;
|
||||
result.pitch = this.pitch;
|
||||
result.volume = this.volume;
|
||||
|
||||
// We have to play, then pause the sound to set the time,
|
||||
// else the sound will restart immediately when played.
|
||||
result.play(true, 0.0);
|
||||
result.pause();
|
||||
result.time = this.time;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause all the sounds in the group.
|
||||
*/
|
||||
public function pause()
|
||||
{
|
||||
forEachAlive(function(sound:FlxSound)
|
||||
{
|
||||
sound.pause();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Play all the sounds in the group.
|
||||
*/
|
||||
public function play(forceRestart:Bool = false, startTime:Float = 0.0, ?endTime:Float)
|
||||
{
|
||||
forEachAlive(function(sound:FlxSound)
|
||||
{
|
||||
sound.play(forceRestart, startTime, endTime);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume all the sounds in the group.
|
||||
*/
|
||||
public function resume()
|
||||
{
|
||||
forEachAlive(function(sound:FlxSound)
|
||||
{
|
||||
sound.resume();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all the sounds in the group.
|
||||
*/
|
||||
public function stop()
|
||||
{
|
||||
forEachAlive(function(sound:FlxSound)
|
||||
{
|
||||
sound.stop();
|
||||
});
|
||||
}
|
||||
|
||||
public override function clear():Void {
|
||||
this.stop();
|
||||
|
||||
super.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the deviation of the sounds in the group from the target time.
|
||||
*
|
||||
* @param targetTime The time to compare the sounds to.
|
||||
* If null, the current time of the first sound in the group is used.
|
||||
* @return The largest deviation of the sounds in the group from the target time.
|
||||
*/
|
||||
public function calcDeviation(?targetTime:Float):Float
|
||||
{
|
||||
var deviation:Float = 0;
|
||||
|
||||
forEachAlive(function(sound:FlxSound)
|
||||
{
|
||||
if (targetTime == null)
|
||||
targetTime = sound.time;
|
||||
else
|
||||
{
|
||||
var diff:Float = sound.time - targetTime;
|
||||
if (Math.abs(diff) > Math.abs(deviation))
|
||||
deviation = diff;
|
||||
}
|
||||
});
|
||||
|
||||
return deviation;
|
||||
}
|
||||
}
|
119
source/funkin/audio/VocalGroup.hx
Normal file
119
source/funkin/audio/VocalGroup.hx
Normal file
|
@ -0,0 +1,119 @@
|
|||
package funkin.audio;
|
||||
|
||||
import flixel.system.FlxSound;
|
||||
|
||||
/**
|
||||
* An audio group that allows for specific control of vocal tracks.
|
||||
*/
|
||||
class VocalGroup extends FlxAudioGroup
|
||||
{
|
||||
/**
|
||||
* The player's vocal track.
|
||||
*/
|
||||
var playerVocals:FlxSound;
|
||||
|
||||
/**
|
||||
* The opponent's vocal track.
|
||||
*/
|
||||
var opponentVocals:FlxSound;
|
||||
|
||||
/**
|
||||
* The volume of the player's vocal track.
|
||||
* Nore that this value is multiplied by the overall volume of the group.
|
||||
*/
|
||||
public var playerVolume(default, set):Float;
|
||||
|
||||
function set_playerVolume(value:Float):Float
|
||||
{
|
||||
playerVolume = value;
|
||||
if (playerVocals != null)
|
||||
{
|
||||
// Make sure volume is capped at 1.0.
|
||||
playerVocals.volume = Math.min(playerVolume * this.volume, 1.0);
|
||||
}
|
||||
return playerVolume;
|
||||
}
|
||||
|
||||
/**
|
||||
* The volume of the opponent's vocal track.
|
||||
* Nore that this value is multiplied by the overall volume of the group.
|
||||
*/
|
||||
public var opponentVolume(default, set):Float;
|
||||
|
||||
function set_opponentVolume(value:Float):Float
|
||||
{
|
||||
opponentVolume = value;
|
||||
if (opponentVocals != null)
|
||||
{
|
||||
// Make sure volume is capped at 1.0.
|
||||
opponentVocals.volume = opponentVolume * this.volume;
|
||||
}
|
||||
return opponentVolume;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the player's vocal track.
|
||||
* Stops and removes the existing player track if one exists.
|
||||
*/
|
||||
public function setPlayerVocals(sound:FlxSound):FlxSound
|
||||
{
|
||||
if (playerVocals != null)
|
||||
{
|
||||
playerVocals.stop();
|
||||
remove(playerVocals);
|
||||
playerVocals = null;
|
||||
}
|
||||
|
||||
playerVocals = add(sound);
|
||||
playerVocals.volume = this.playerVolume * this.volume;
|
||||
|
||||
return playerVocals;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the opponent's vocal track.
|
||||
* Stops and removes the existing player track if one exists.
|
||||
*/
|
||||
public function setOpponentVocals(sound:FlxSound):FlxSound
|
||||
{
|
||||
if (opponentVocals != null)
|
||||
{
|
||||
opponentVocals.stop();
|
||||
remove(opponentVocals);
|
||||
opponentVocals = null;
|
||||
}
|
||||
|
||||
opponentVocals = add(sound);
|
||||
opponentVocals.volume = this.opponentVolume * this.volume;
|
||||
|
||||
return opponentVocals;
|
||||
}
|
||||
|
||||
/**
|
||||
* In this extension of FlxAudioGroup, there is a separate overall volume
|
||||
* which affects all the members of the group.
|
||||
*/
|
||||
var _volume = 1.0;
|
||||
|
||||
override function get_volume():Float
|
||||
{
|
||||
return _volume;
|
||||
}
|
||||
|
||||
override function set_volume(value:Float):Float
|
||||
{
|
||||
_volume = super.set_volume(value);
|
||||
|
||||
if (playerVocals != null)
|
||||
{
|
||||
playerVocals.volume = playerVolume * _volume;
|
||||
}
|
||||
|
||||
if (opponentVocals != null)
|
||||
{
|
||||
opponentVocals.volume = opponentVolume * _volume;
|
||||
}
|
||||
|
||||
return _volume;
|
||||
}
|
||||
}
|
|
@ -34,7 +34,6 @@ import openfl.events.IOErrorEvent;
|
|||
import openfl.net.FileReference;
|
||||
|
||||
using Lambda;
|
||||
using StringTools;
|
||||
using flixel.util.FlxSpriteUtil; // add in "compiler save" that saves the JSON directly to the debug json using File.write() stuff on windows / sys
|
||||
|
||||
class ChartingState extends MusicBeatState
|
||||
|
@ -445,7 +444,7 @@ class ChartingState extends MusicBeatState
|
|||
add(playheadTest);
|
||||
|
||||
// WONT WORK FOR TUTORIAL OR TEST SONG!!! REDO LATER
|
||||
vocals = new VoicesGroup(daSong, _song.voiceList);
|
||||
vocals = VoicesGroup.build(daSong, _song.voiceList);
|
||||
// vocals = new FlxSound().loadEmbedded(Paths.voices(daSong));
|
||||
// FlxG.sound.list.add(vocals);
|
||||
|
||||
|
|
|
@ -3,8 +3,6 @@ package funkin.freeplayStuff;
|
|||
import flixel.FlxSprite;
|
||||
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
|
||||
|
||||
using StringTools;
|
||||
|
||||
class FreeplayScore extends FlxTypedSpriteGroup<ScoreNum>
|
||||
{
|
||||
public var scoreShit(default, set):Int = 0;
|
||||
|
|
|
@ -3,4 +3,11 @@
|
|||
import funkin.Paths;
|
||||
import flixel.FlxG; // This one in particular causes a compile error if you're using macros.
|
||||
|
||||
// These are great.
|
||||
using Lambda;
|
||||
using StringTools;
|
||||
|
||||
using funkin.util.tools.IteratorTools;
|
||||
using funkin.util.tools.StringTools;
|
||||
|
||||
#end
|
||||
|
|
282
source/funkin/input/Cursor.hx
Normal file
282
source/funkin/input/Cursor.hx
Normal file
|
@ -0,0 +1,282 @@
|
|||
package funkin.input;
|
||||
|
||||
import openfl.utils.Assets;
|
||||
import lime.app.Future;
|
||||
import openfl.display.BitmapData;
|
||||
|
||||
class Cursor
|
||||
{
|
||||
public static var cursorMode(default, set):CursorMode;
|
||||
|
||||
static final CURSOR_DEFAULT_PARAMS:CursorParams = {
|
||||
graphic: "assets/images/cursor/cursor-default.png",
|
||||
scale: 1.0,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
};
|
||||
static var assetCursorDefault:BitmapData = null;
|
||||
|
||||
static final CURSOR_CROSS_PARAMS:CursorParams = {
|
||||
graphic: "assets/images/cursor/cursor-cross.png",
|
||||
scale: 1.0,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
};
|
||||
static var assetCursorCross:BitmapData = null;
|
||||
|
||||
static final CURSOR_ERASER_PARAMS:CursorParams = {
|
||||
graphic: "assets/images/cursor/cursor-eraser.png",
|
||||
scale: 1.0,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
};
|
||||
static var assetCursorEraser:BitmapData = null;
|
||||
|
||||
static final CURSOR_GRABBING_PARAMS:CursorParams = {
|
||||
graphic: "assets/images/cursor/cursor-grabbing.png",
|
||||
scale: 1.0,
|
||||
offsetX: 32,
|
||||
offsetY: 0,
|
||||
};
|
||||
static var assetCursorGrabbing:BitmapData = null;
|
||||
|
||||
static final CURSOR_HOURGLASS_PARAMS:CursorParams = {
|
||||
graphic: "assets/images/cursor/cursor-hourglass.png",
|
||||
scale: 1.0,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
};
|
||||
static var assetCursorHourglass:BitmapData = null;
|
||||
|
||||
static final CURSOR_POINTER_PARAMS:CursorParams = {
|
||||
graphic: "assets/images/cursor/cursor-pointer.png",
|
||||
scale: 1.0,
|
||||
offsetX: 8,
|
||||
offsetY: 0,
|
||||
};
|
||||
static var assetCursorPointer:BitmapData = null;
|
||||
|
||||
static final CURSOR_TEXT_PARAMS:CursorParams = {
|
||||
graphic: "assets/images/cursor/cursor-text.png",
|
||||
scale: 1.0,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
};
|
||||
static var assetCursorText:BitmapData = null;
|
||||
|
||||
static final CURSOR_ZOOM_IN_PARAMS:CursorParams = {
|
||||
graphic: "assets/images/cursor/cursor-zoom-in.png",
|
||||
scale: 1.0,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
};
|
||||
static var assetCursorZoomIn:BitmapData = null;
|
||||
|
||||
static final CURSOR_ZOOM_OUT_PARAMS:CursorParams = {
|
||||
graphic: "assets/images/cursor/cursor-zoom-out.png",
|
||||
scale: 1.0,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
};
|
||||
static var assetCursorZoomOut:BitmapData = null;
|
||||
|
||||
static function set_cursorMode(value:CursorMode):CursorMode
|
||||
{
|
||||
if (cursorMode != value)
|
||||
{
|
||||
cursorMode = value;
|
||||
setCursorGraphic(cursorMode);
|
||||
}
|
||||
return cursorMode;
|
||||
}
|
||||
|
||||
public static inline function show():Void
|
||||
{
|
||||
FlxG.mouse.visible = true;
|
||||
}
|
||||
|
||||
public static inline function hide():Void
|
||||
{
|
||||
FlxG.mouse.visible = false;
|
||||
}
|
||||
|
||||
static function setCursorGraphic(?value:CursorMode = null):Void
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
FlxG.mouse.unload();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (value)
|
||||
{
|
||||
case Default:
|
||||
if (assetCursorDefault == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_DEFAULT_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorDefault = bitmapData;
|
||||
applyCursorParams(assetCursorDefault, CURSOR_DEFAULT_PARAMS);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorDefault, CURSOR_DEFAULT_PARAMS);
|
||||
}
|
||||
|
||||
case Cross:
|
||||
if (assetCursorCross == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_CROSS_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorCross = bitmapData;
|
||||
applyCursorParams(assetCursorCross, CURSOR_CROSS_PARAMS);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorCross, CURSOR_CROSS_PARAMS);
|
||||
}
|
||||
|
||||
case Eraser:
|
||||
if (assetCursorEraser == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_ERASER_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorEraser = bitmapData;
|
||||
applyCursorParams(assetCursorEraser, CURSOR_ERASER_PARAMS);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorEraser, CURSOR_ERASER_PARAMS);
|
||||
}
|
||||
|
||||
case Grabbing:
|
||||
if (assetCursorGrabbing == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_GRABBING_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorGrabbing = bitmapData;
|
||||
applyCursorParams(assetCursorGrabbing, CURSOR_GRABBING_PARAMS);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorGrabbing, CURSOR_GRABBING_PARAMS);
|
||||
}
|
||||
|
||||
case Hourglass:
|
||||
if (assetCursorHourglass == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_HOURGLASS_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorHourglass = bitmapData;
|
||||
applyCursorParams(assetCursorHourglass, CURSOR_HOURGLASS_PARAMS);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorHourglass, CURSOR_HOURGLASS_PARAMS);
|
||||
}
|
||||
|
||||
case Pointer:
|
||||
if (assetCursorPointer == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_POINTER_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorPointer = bitmapData;
|
||||
applyCursorParams(assetCursorPointer, CURSOR_POINTER_PARAMS);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorPointer, CURSOR_POINTER_PARAMS);
|
||||
}
|
||||
|
||||
case Text:
|
||||
if (assetCursorText == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_TEXT_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorText = bitmapData;
|
||||
applyCursorParams(assetCursorText, CURSOR_TEXT_PARAMS);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorText, CURSOR_TEXT_PARAMS);
|
||||
}
|
||||
|
||||
case ZoomIn:
|
||||
if (assetCursorZoomIn == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_ZOOM_IN_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorZoomIn = bitmapData;
|
||||
applyCursorParams(assetCursorZoomIn, CURSOR_ZOOM_IN_PARAMS);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorZoomIn, CURSOR_ZOOM_IN_PARAMS);
|
||||
}
|
||||
|
||||
case ZoomOut:
|
||||
if (assetCursorZoomOut == null)
|
||||
{
|
||||
var future:Future<BitmapData> = Assets.loadBitmapData(CURSOR_ZOOM_OUT_PARAMS.graphic);
|
||||
future.onComplete(function(bitmapData:BitmapData)
|
||||
{
|
||||
assetCursorZoomOut = bitmapData;
|
||||
applyCursorParams(assetCursorZoomOut, CURSOR_ZOOM_OUT_PARAMS);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
applyCursorParams(assetCursorZoomOut, CURSOR_ZOOM_OUT_PARAMS);
|
||||
}
|
||||
|
||||
default:
|
||||
setCursorGraphic(null);
|
||||
}
|
||||
}
|
||||
|
||||
static inline function applyCursorParams(graphic:BitmapData, params:CursorParams):Void
|
||||
{
|
||||
FlxG.mouse.load(graphic, params.scale, params.offsetX, params.offsetY);
|
||||
}
|
||||
}
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/cursor
|
||||
enum CursorMode
|
||||
{
|
||||
Default;
|
||||
Cross;
|
||||
Eraser;
|
||||
Grabbing;
|
||||
Hourglass;
|
||||
Pointer;
|
||||
Text;
|
||||
ZoomIn;
|
||||
ZoomOut;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static data describing how a cursor should be rendered.
|
||||
*/
|
||||
typedef CursorParams =
|
||||
{
|
||||
graphic:String,
|
||||
scale:Float,
|
||||
offsetX:Int,
|
||||
offsetY:Int,
|
||||
}
|
|
@ -7,6 +7,7 @@ import funkin.play.stage.StageData;
|
|||
import polymod.Polymod;
|
||||
import polymod.backends.PolymodAssets.PolymodAssetType;
|
||||
import polymod.format.ParseRules.TextFileFormat;
|
||||
import funkin.util.FileUtil;
|
||||
|
||||
class PolymodHandler
|
||||
{
|
||||
|
@ -25,10 +26,7 @@ class PolymodHandler
|
|||
|
||||
public static function createModRoot()
|
||||
{
|
||||
if (!sys.FileSystem.exists(MOD_FOLDER))
|
||||
{
|
||||
sys.FileSystem.createDirectory(MOD_FOLDER);
|
||||
}
|
||||
FileUtil.createDirIfNotExists(MOD_FOLDER);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -183,7 +181,11 @@ class PolymodHandler
|
|||
public static function getAllMods():Array<ModMetadata>
|
||||
{
|
||||
trace('Scanning the mods folder...');
|
||||
var modMetadata = Polymod.scan();
|
||||
var modMetadata = Polymod.scan({
|
||||
modRoot: MOD_FOLDER,
|
||||
apiVersionRule: API_VERSION,
|
||||
errorCallback: PolymodErrorHandler.onPolymodError
|
||||
});
|
||||
trace('Found ${modMetadata.length} mods when scanning.');
|
||||
return modMetadata;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package funkin.modding.base;
|
||||
|
||||
import flixel.addons.display.FlxRuntimeShader;
|
||||
import polymod.hscript.HScriptedClass;
|
||||
|
||||
@:hscriptClass
|
||||
class ScriptedFlxRuntimeShader extends FlxRuntimeShader implements HScriptedClass {}
|
||||
class ScriptedFlxRuntimeShader extends flixel.addons.display.FlxRuntimeShader implements HScriptedClass
|
||||
{
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package funkin.modding.base;
|
||||
|
||||
import flixel.FlxSprite;
|
||||
import polymod.hscript.HScriptedClass;
|
||||
|
||||
@:hscriptClass
|
||||
class ScriptedFlxSprite extends FlxSprite implements HScriptedClass {}
|
||||
class ScriptedFlxSprite extends flixel.FlxSprite implements HScriptedClass
|
||||
{
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package funkin.modding.base;
|
||||
|
||||
import flixel.group.FlxSpriteGroup;
|
||||
import polymod.hscript.HScriptedClass;
|
||||
|
||||
@:hscriptClass
|
||||
class ScriptedFlxSpriteGroup extends FlxSpriteGroup implements HScriptedClass {}
|
||||
class ScriptedFlxSpriteGroup extends flixel.group.FlxSpriteGroup implements HScriptedClass
|
||||
{
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package funkin.modding.base;
|
||||
|
||||
import flixel.FlxState;
|
||||
import polymod.hscript.HScriptedClass;
|
||||
|
||||
@:hscriptClass
|
||||
class ScriptedFlxState extends FlxState implements HScriptedClass {}
|
||||
class ScriptedFlxState extends flixel.FlxState implements HScriptedClass
|
||||
{
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package funkin.modding.base;
|
||||
|
||||
import flixel.FlxSubState;
|
||||
import polymod.hscript.HScriptedClass;
|
||||
|
||||
@:hscriptClass
|
||||
class ScriptedFlxSubState extends FlxSubState implements HScriptedClass {}
|
||||
class ScriptedFlxSubState extends flixel.FlxSubState implements HScriptedClass
|
||||
{
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package funkin.modding.base;
|
||||
|
||||
import flixel.addons.transition.FlxTransitionableState;
|
||||
import polymod.hscript.HScriptedClass;
|
||||
|
||||
@:hscriptClass
|
||||
class ScriptedFlxTransitionableState extends FlxTransitionableState implements HScriptedClass {}
|
||||
class ScriptedFlxTransitionableState extends flixel.addons.transition.FlxTransitionableState implements HScriptedClass
|
||||
{
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package funkin.modding.base;
|
||||
|
||||
import flixel.addons.ui.FlxUIState;
|
||||
import polymod.hscript.HScriptedClass;
|
||||
|
||||
@:hscriptClass
|
||||
class ScriptedFlxUIState extends FlxUIState implements HScriptedClass {}
|
||||
class ScriptedFlxUIState extends flixel.addons.ui.FlxUIState implements HScriptedClass
|
||||
{
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package funkin.modding.base;
|
||||
|
||||
import funkin.MusicBeatState;
|
||||
import polymod.hscript.HScriptedClass;
|
||||
|
||||
@:hscriptClass
|
||||
class ScriptedMusicBeatState extends MusicBeatState implements HScriptedClass {}
|
||||
class ScriptedMusicBeatState extends funkin.MusicBeatState implements HScriptedClass
|
||||
{
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package funkin.modding.base;
|
||||
|
||||
import funkin.MusicBeatSubstate;
|
||||
import polymod.hscript.HScriptedClass;
|
||||
|
||||
@:hscriptClass
|
||||
class ScriptedMusicBeatSubstate extends MusicBeatSubstate implements HScriptedClass {}
|
||||
class ScriptedMusicBeatSubstate extends funkin.MusicBeatSubstate implements HScriptedClass
|
||||
{
|
||||
}
|
||||
|
|
1
source/funkin/modding/base/import.hx
Normal file
1
source/funkin/modding/base/import.hx
Normal file
|
@ -0,0 +1 @@
|
|||
import polymod.hscript.HScriptedClass;
|
|
@ -6,8 +6,6 @@ import funkin.modding.events.ScriptEventDispatcher;
|
|||
import funkin.modding.module.Module;
|
||||
import funkin.modding.module.ScriptedModule;
|
||||
|
||||
using funkin.util.IteratorTools;
|
||||
|
||||
/**
|
||||
* Utility functions for loading and manipulating active modules.
|
||||
*/
|
||||
|
|
|
@ -10,8 +10,6 @@ import funkin.modding.events.ScriptEvent;
|
|||
import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
|
||||
import flixel.util.FlxTimer;
|
||||
|
||||
using StringTools;
|
||||
|
||||
class Countdown
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -11,8 +11,6 @@ import funkin.play.PlayState;
|
|||
import funkin.play.character.BaseCharacter;
|
||||
import funkin.ui.PreferencesMenu;
|
||||
|
||||
using StringTools;
|
||||
|
||||
/**
|
||||
* A substate which renders over the PlayState when the player dies.
|
||||
* Displays the player death animation, plays the music, and handles restarting the song.
|
||||
|
|
|
@ -196,17 +196,11 @@ class HealthIcon extends FlxSprite
|
|||
// Make the health icons bump (the update function causes them to lerp back down).
|
||||
if (this.width > this.height)
|
||||
{
|
||||
var targetSize = Std.int(CoolUtil.coolLerp(this.width + HEALTH_ICON_SIZE * 0.2, HEALTH_ICON_SIZE, 0.15));
|
||||
targetSize = Std.int(Math.min(targetSize, HEALTH_ICON_SIZE * 1.2));
|
||||
|
||||
setGraphicSize(targetSize, 0);
|
||||
setGraphicSize(Std.int(this.width + (HEALTH_ICON_SIZE * this.size.x * 0.2)), 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
var targetSize = Std.int(CoolUtil.coolLerp(this.height + HEALTH_ICON_SIZE * 0.2, HEALTH_ICON_SIZE, 0.15));
|
||||
targetSize = Std.int(Math.min(targetSize, HEALTH_ICON_SIZE * 1.2));
|
||||
|
||||
setGraphicSize(0, targetSize);
|
||||
setGraphicSize(0, Std.int(this.height + (HEALTH_ICON_SIZE * this.size.y * 0.2)));
|
||||
}
|
||||
this.updateHitbox();
|
||||
}
|
||||
|
|
|
@ -43,9 +43,6 @@ import funkin.ui.stageBuildShit.StageOffsetSubstate;
|
|||
import funkin.util.Constants;
|
||||
import funkin.util.SortUtil;
|
||||
import lime.ui.Haptic;
|
||||
|
||||
using StringTools;
|
||||
|
||||
#if discord_rpc
|
||||
import Discord.DiscordClient;
|
||||
#end
|
||||
|
@ -356,7 +353,7 @@ class PlayState extends MusicBeatState
|
|||
|
||||
if (currentSong_NEW != null)
|
||||
{
|
||||
Conductor.mapTimeChanges(currentChart);
|
||||
Conductor.mapTimeChanges(currentChart.timeChanges);
|
||||
// Conductor.bpm = currentChart.getStartingBPM();
|
||||
|
||||
// TODO: Support for dialog.
|
||||
|
@ -1032,9 +1029,9 @@ class PlayState extends MusicBeatState
|
|||
currentSong.song = currentSong.song;
|
||||
|
||||
if (currentSong.needsVoices)
|
||||
vocals = new VoicesGroup(currentSong.song, currentSong.voiceList);
|
||||
vocals = VoicesGroup.build(currentSong.song, currentSong.voiceList);
|
||||
else
|
||||
vocals = new VoicesGroup(currentSong.song, null);
|
||||
vocals = VoicesGroup.build(currentSong.song, null);
|
||||
|
||||
vocals.members[0].onComplete = function()
|
||||
{
|
||||
|
|
|
@ -6,8 +6,6 @@ import funkin.noteStuff.NoteBasic.NoteDir;
|
|||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.play.stage.Bopper;
|
||||
|
||||
using StringTools;
|
||||
|
||||
/**
|
||||
* A Character is a stage prop which bops to the music as well as controlled by the strumlines.
|
||||
*
|
||||
|
@ -96,8 +94,9 @@ class BaseCharacter extends Bopper
|
|||
if (animOffsets == value)
|
||||
return value;
|
||||
|
||||
var xDiff = animOffsets[0] - value[0];
|
||||
var yDiff = animOffsets[1] - value[1];
|
||||
// Make sure animOffets are halved when scale is 0.5.
|
||||
var xDiff = (animOffsets[0] * this.scale.x) - value[0];
|
||||
var yDiff = (animOffsets[1] * this.scale.y) - value[1];
|
||||
|
||||
// Call the super function so that camera focus point is not affected.
|
||||
super.set_x(this.x + xDiff);
|
||||
|
|
|
@ -16,8 +16,6 @@ import funkin.util.assets.DataAssets;
|
|||
import haxe.Json;
|
||||
import openfl.utils.Assets;
|
||||
|
||||
using StringTools;
|
||||
|
||||
class CharacterDataParser
|
||||
{
|
||||
/**
|
||||
|
@ -225,7 +223,7 @@ class CharacterDataParser
|
|||
|
||||
public static function listCharacterIds():Array<String>
|
||||
{
|
||||
return [for (x in characterCache.keys()) x];
|
||||
return characterCache.keys().array();
|
||||
}
|
||||
|
||||
static function clearCharacterCache():Void
|
||||
|
@ -258,7 +256,7 @@ class CharacterDataParser
|
|||
static function loadCharacterFile(charPath:String):String
|
||||
{
|
||||
var charFilePath:String = Paths.json('characters/${charPath}');
|
||||
var rawJson = StringTools.trim(Assets.getText(charFilePath));
|
||||
var rawJson = Assets.getText(charFilePath).trim();
|
||||
|
||||
while (!StringTools.endsWith(rawJson, "}"))
|
||||
{
|
||||
|
|
|
@ -45,6 +45,11 @@ class Song // implements IPlayStateScriptedClass
|
|||
populateFromMetadata();
|
||||
}
|
||||
|
||||
public function getRawMetadata():Array<SongMetadata>
|
||||
{
|
||||
return _metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate the song data from the provided metadata,
|
||||
* including data from individual difficulties. Does not load chart data.
|
||||
|
@ -122,8 +127,11 @@ class Song // implements IPlayStateScriptedClass
|
|||
/**
|
||||
* Retrieve the metadata for a specific difficulty, including the chart if it is loaded.
|
||||
*/
|
||||
public inline function getDifficulty(diffId:String):SongDifficulty
|
||||
public inline function getDifficulty(?diffId:String):SongDifficulty
|
||||
{
|
||||
if (diffId == null)
|
||||
diffId = difficulties.keys().array()[0];
|
||||
|
||||
return difficulties.get(diffId);
|
||||
}
|
||||
|
||||
|
@ -214,7 +222,7 @@ class SongDifficulty
|
|||
|
||||
public function getPlayableChars():Array<String>
|
||||
{
|
||||
return [for (i in chars.keys()) i];
|
||||
return chars.keys().array();
|
||||
}
|
||||
|
||||
public function getEvents():Array<SongEvent>
|
||||
|
@ -246,7 +254,7 @@ class SongDifficulty
|
|||
|
||||
public function buildVocals(charId:String = "bf"):VoicesGroup
|
||||
{
|
||||
var result:VoicesGroup = new VoicesGroup(this.song.songId, this.buildVoiceList());
|
||||
var result:VoicesGroup = VoicesGroup.build(this.song.songId, this.buildVoiceList());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,8 +8,6 @@ import haxe.Json;
|
|||
import openfl.utils.Assets;
|
||||
import thx.semver.Version;
|
||||
|
||||
using StringTools;
|
||||
|
||||
/**
|
||||
* Contains utilities for loading and parsing stage data.
|
||||
*/
|
||||
|
@ -96,12 +94,12 @@ class SongDataParser
|
|||
if (songCache.exists(songId))
|
||||
{
|
||||
var song:Song = songCache.get(songId);
|
||||
trace('[STAGEDATA] Successfully fetch song: ${songId}');
|
||||
trace('[SONGDATA] Successfully fetch song: ${songId}');
|
||||
return song;
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('[STAGEDATA] Failed to fetch song, not found in cache: ${songId}');
|
||||
trace('[SONGDATA] Failed to fetch song, not found in cache: ${songId}');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -116,7 +114,7 @@ class SongDataParser
|
|||
|
||||
public static function listSongIds():Array<String>
|
||||
{
|
||||
return [for (x in songCache.keys()) x];
|
||||
return songCache.keys().array();
|
||||
}
|
||||
|
||||
public static function parseSongMetadata(songId:String):Array<SongMetadata>
|
||||
|
@ -261,9 +259,25 @@ abstract SongMetadata(RawSongMetadata)
|
|||
noteSkin: 'Normal'
|
||||
},
|
||||
generatedBy: SongValidator.DEFAULT_GENERATEDBY,
|
||||
|
||||
// Variation ID.
|
||||
variation: variation
|
||||
};
|
||||
}
|
||||
|
||||
public function clone(?newVariation:String = null):SongMetadata
|
||||
{
|
||||
var result = new SongMetadata(this.songName, this.artist, newVariation == null ? this.variation : newVariation);
|
||||
result.version = this.version;
|
||||
result.timeFormat = this.timeFormat;
|
||||
result.divisions = this.divisions;
|
||||
result.timeChanges = this.timeChanges;
|
||||
result.loop = this.loop;
|
||||
result.playData = this.playData;
|
||||
result.generatedBy = this.generatedBy;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
typedef SongPlayData =
|
||||
|
|
|
@ -135,7 +135,7 @@ class SongDataUtils
|
|||
|
||||
trace('Read ' + notesString.length + ' characters from clipboard.');
|
||||
|
||||
var notes:Array<SongNoteData> = SerializerUtil.fromJSON(notesString);
|
||||
var notes:Array<SongNoteData> = notesString.parseJSON();
|
||||
|
||||
if (notes == null)
|
||||
{
|
||||
|
|
|
@ -25,7 +25,7 @@ class SongSerializer
|
|||
if (fileData == null)
|
||||
return null;
|
||||
|
||||
var songChartData:SongChartData = SerializerUtil.fromJSON(fileData);
|
||||
var songChartData:SongChartData = fileData.parseJSON();
|
||||
|
||||
return songChartData;
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ class SongSerializer
|
|||
if (fileData == null)
|
||||
return null;
|
||||
|
||||
var songMetadata:SongMetadata = SerializerUtil.fromJSON(fileData);
|
||||
var songMetadata:SongMetadata = fileData.parseJSON();
|
||||
|
||||
return songMetadata;
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ class SongSerializer
|
|||
if (data == null)
|
||||
return;
|
||||
|
||||
var songChartData:SongChartData = SerializerUtil.fromJSON(data);
|
||||
var songChartData:SongChartData = data.parseJSON();
|
||||
|
||||
if (songChartData != null)
|
||||
callback(songChartData);
|
||||
|
@ -79,7 +79,7 @@ class SongSerializer
|
|||
if (data == null)
|
||||
return;
|
||||
|
||||
var songMetadata:SongMetadata = SerializerUtil.fromJSON(data);
|
||||
var songMetadata:SongMetadata = data.parseJSON();
|
||||
|
||||
if (songMetadata != null)
|
||||
callback(songMetadata);
|
||||
|
|
|
@ -6,6 +6,9 @@ import flixel.util.FlxTimer;
|
|||
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
|
||||
typedef AnimationFrameCallback = String->Int->Int->Void;
|
||||
typedef AnimationFinishedCallback = String->Void;
|
||||
|
||||
/**
|
||||
* A Bopper is a stage prop which plays a dance animation.
|
||||
* Y'know, a thingie that bops. A bopper.
|
||||
|
@ -68,8 +71,8 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
|
|||
|
||||
this.x += xDiff;
|
||||
this.y += yDiff;
|
||||
|
||||
return animOffsets = value;
|
||||
|
||||
}
|
||||
|
||||
private var animOffsets(default, set):Array<Float> = [0, 0];
|
||||
|
@ -113,6 +116,7 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
|
|||
*/
|
||||
function onAnimationFinished(name:String)
|
||||
{
|
||||
// TODO: Can we make a system of like, animation priority or something?
|
||||
if (!canPlayOtherAnims)
|
||||
{
|
||||
canPlayOtherAnims = true;
|
||||
|
@ -131,10 +135,10 @@ class Bopper extends FlxSprite implements IPlayStateScriptedClass
|
|||
function onAnimationFrame(name:String = "", frameNumber:Int = -1, frameIndex:Int = -1)
|
||||
{
|
||||
// Do nothing by default.
|
||||
// This can be overridden by, for example, scripted characters.
|
||||
// This can be overridden by, for example, scripted characters,
|
||||
// or by calling `animationFrame.add()`.
|
||||
|
||||
// Try not to do anything expensive here, it runs many times a second.
|
||||
|
||||
// Sometimes this gets called with empty values? IDK why but adding defaults keeps it from crashing.
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,8 +8,6 @@ import funkin.util.assets.DataAssets;
|
|||
import haxe.Json;
|
||||
import openfl.Assets;
|
||||
|
||||
using StringTools;
|
||||
|
||||
/**
|
||||
* Contains utilities for loading and parsing stage data.
|
||||
*/
|
||||
|
@ -143,7 +141,7 @@ class StageDataParser
|
|||
|
||||
public static function listStageIds():Array<String>
|
||||
{
|
||||
return [for (x in stageCache.keys()) x];
|
||||
return stageCache.keys().array();
|
||||
}
|
||||
|
||||
static function loadStageFile(stagePath:String):String
|
||||
|
|
|
@ -6,8 +6,6 @@ import flixel.tweens.FlxTween;
|
|||
import funkin.play.PlayState;
|
||||
import funkin.util.Constants;
|
||||
|
||||
using StringTools;
|
||||
|
||||
class PopUpStuff extends FlxTypedGroup<FlxSprite>
|
||||
{
|
||||
override public function new()
|
||||
|
|
|
@ -33,7 +33,6 @@ import openfl.net.URLLoader;
|
|||
import openfl.net.URLRequest;
|
||||
import openfl.utils.ByteArray;
|
||||
|
||||
using StringTools;
|
||||
using flixel.util.FlxSpriteUtil;
|
||||
|
||||
#if web
|
||||
|
|
|
@ -64,6 +64,7 @@ class AddNotesCommand implements ChartEditorCommand
|
|||
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
|
@ -76,6 +77,7 @@ class AddNotesCommand implements ChartEditorCommand
|
|||
state.currentSelection = [];
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
|
@ -109,6 +111,7 @@ class RemoveNotesCommand implements ChartEditorCommand
|
|||
state.currentSelection = [];
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
|
@ -124,6 +127,7 @@ class RemoveNotesCommand implements ChartEditorCommand
|
|||
state.currentSelection = notes;
|
||||
state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
|
@ -409,6 +413,7 @@ class CutNotesCommand implements ChartEditorCommand
|
|||
// Delete the notes.
|
||||
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
|
||||
state.currentSelection = [];
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
state.sortChartData();
|
||||
|
@ -419,6 +424,7 @@ class CutNotesCommand implements ChartEditorCommand
|
|||
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(notes);
|
||||
state.currentSelection = notes;
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
|
@ -453,6 +459,7 @@ class FlipNotesCommand implements ChartEditorCommand
|
|||
|
||||
state.currentSelection = flippedNotes;
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
state.sortChartData();
|
||||
|
@ -465,6 +472,7 @@ class FlipNotesCommand implements ChartEditorCommand
|
|||
|
||||
state.currentSelection = notes;
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
|
@ -498,6 +506,7 @@ class PasteNotesCommand implements ChartEditorCommand
|
|||
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(addedNotes);
|
||||
state.currentSelection = addedNotes.copy();
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
|
@ -509,6 +518,7 @@ class PasteNotesCommand implements ChartEditorCommand
|
|||
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes);
|
||||
state.currentSelection = [];
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
|
@ -539,6 +549,7 @@ class AddEventsCommand implements ChartEditorCommand
|
|||
// TODO: Allow selecting events.
|
||||
// state.currentSelection = events;
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
|
@ -551,6 +562,7 @@ class AddEventsCommand implements ChartEditorCommand
|
|||
|
||||
state.currentSelection = [];
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
|
@ -581,6 +593,7 @@ class ExtendNoteLengthCommand implements ChartEditorCommand
|
|||
{
|
||||
note.length = newLength;
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
|
@ -591,6 +604,7 @@ class ExtendNoteLengthCommand implements ChartEditorCommand
|
|||
{
|
||||
note.length = oldLength;
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
|
|
|
@ -1,5 +1,497 @@
|
|||
package funkin.ui.debug.charting;
|
||||
|
||||
import flixel.FlxSprite;
|
||||
import flixel.util.FlxTimer;
|
||||
import funkin.input.Cursor;
|
||||
import funkin.play.character.BaseCharacter;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.play.song.SongData.SongDataParser;
|
||||
import funkin.play.song.SongData.SongPlayableChar;
|
||||
import funkin.play.song.SongData.SongTimeChange;
|
||||
import haxe.ui.components.Button;
|
||||
import haxe.ui.components.DropDown;
|
||||
import haxe.ui.components.Image;
|
||||
import haxe.ui.components.Label;
|
||||
import haxe.ui.components.Link;
|
||||
import haxe.ui.components.NumberStepper;
|
||||
import haxe.ui.components.TextField;
|
||||
import haxe.ui.containers.Box;
|
||||
import haxe.ui.containers.dialogs.Dialog;
|
||||
import haxe.ui.containers.dialogs.Dialogs;
|
||||
import haxe.ui.containers.properties.Property;
|
||||
import haxe.ui.containers.properties.PropertyGrid;
|
||||
import haxe.ui.containers.properties.PropertyGroup;
|
||||
import haxe.ui.containers.VBox;
|
||||
import haxe.ui.events.MouseEvent;
|
||||
import haxe.ui.events.UIEvent;
|
||||
|
||||
using Lambda;
|
||||
|
||||
class ChartEditorDialogHandler
|
||||
{
|
||||
static final CHART_EDITOR_DIALOG_ABOUT_LAYOUT = Paths.ui('chart-editor/dialogs/about');
|
||||
static final CHART_EDITOR_DIALOG_WELCOME_LAYOUT = Paths.ui('chart-editor/dialogs/welcome');
|
||||
static final CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT = Paths.ui('chart-editor/dialogs/upload-inst');
|
||||
static final CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT = Paths.ui('chart-editor/dialogs/song-metadata');
|
||||
static final CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT = Paths.ui('chart-editor/dialogs/song-metadata-chargroup');
|
||||
static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT = Paths.ui('chart-editor/dialogs/upload-vocals');
|
||||
static final CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT = Paths.ui('chart-editor/dialogs/upload-vocals-entry');
|
||||
static final CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT = Paths.ui('chart-editor/dialogs/user-guide');
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public static inline function openAboutDialog(state:ChartEditorState):Dialog
|
||||
{
|
||||
return openDialog(state, CHART_EDITOR_DIALOG_ABOUT_LAYOUT, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and opens a dialog letting the user create a new chart, open a recent chart, or load from a template.
|
||||
*/
|
||||
public static function openWelcomeDialog(state:ChartEditorState, closable:Bool = true):Dialog
|
||||
{
|
||||
var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_WELCOME_LAYOUT, true, closable);
|
||||
|
||||
// TODO: Add callbacks to the dialog buttons
|
||||
|
||||
// Switch the graphic for frames.
|
||||
var bfSpritePlaceholder:Image = dialog.findComponent('bfSprite', Image);
|
||||
|
||||
// TODO: Replace this bullshit with a custom HaxeUI component that loads the sprite from the game's assets.
|
||||
|
||||
if (bfSpritePlaceholder != null)
|
||||
{
|
||||
var bfSprite:FlxSprite = new FlxSprite(0, 0);
|
||||
|
||||
bfSprite.visible = false;
|
||||
|
||||
var frames = Paths.getSparrowAtlas(bfSpritePlaceholder.resource);
|
||||
bfSprite.frames = frames;
|
||||
|
||||
bfSprite.animation.addByPrefix('idle', 'Boyfriend DJ0', 24, true);
|
||||
bfSprite.animation.play('idle');
|
||||
|
||||
bfSpritePlaceholder.rootComponent.add(bfSprite);
|
||||
bfSpritePlaceholder.visible = false;
|
||||
|
||||
new FlxTimer().start(0.10, (_timer:FlxTimer) ->
|
||||
{
|
||||
bfSprite.x = bfSpritePlaceholder.screenLeft;
|
||||
bfSprite.y = bfSpritePlaceholder.screenTop;
|
||||
bfSprite.setGraphicSize(Std.int(bfSpritePlaceholder.width), Std.int(bfSpritePlaceholder.height));
|
||||
bfSprite.visible = true;
|
||||
});
|
||||
}
|
||||
|
||||
// Add handlers to the "Create From Song" section.
|
||||
var linkCreateBasic:Link = dialog.findComponent('splashCreateFromSongBasic', Link);
|
||||
linkCreateBasic.onClick = (_event) ->
|
||||
{
|
||||
dialog.hideDialog(DialogButton.CANCEL);
|
||||
|
||||
// Create song wizard
|
||||
var uploadInstDialog = openUploadInstDialog(state, false);
|
||||
uploadInstDialog.onDialogClosed = (_event) ->
|
||||
{
|
||||
state.isHaxeUIDialogOpen = false;
|
||||
if (_event.button == DialogButton.APPLY)
|
||||
{
|
||||
var songMetadataDialog = openSongMetadataDialog(state);
|
||||
songMetadataDialog.onDialogClosed = (_event) ->
|
||||
{
|
||||
state.isHaxeUIDialogOpen = false;
|
||||
if (_event.button == DialogButton.APPLY)
|
||||
{
|
||||
var uploadVocalsDialog = openUploadVocalsDialog(state);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: Get the list of songs and insert them as links into the "Create From Song" section.
|
||||
|
||||
/*
|
||||
var linkTemplateDadBattle:Link = dialog.findComponent('splashTemplateDadBattle', Link);
|
||||
linkTemplateDadBattle.onClick = (_event) ->
|
||||
{
|
||||
dialog.hideDialog(DialogButton.CANCEL);
|
||||
|
||||
// Load song from template
|
||||
state.loadSongAsTemplate('dadbattle');
|
||||
}
|
||||
var linkTemplateBopeebo:Link = dialog.findComponent('splashTemplateBopeebo', Link);
|
||||
linkTemplateBopeebo.onClick = (_event) ->
|
||||
{
|
||||
dialog.hideDialog(DialogButton.CANCEL);
|
||||
|
||||
// Load song from template
|
||||
state.loadSongAsTemplate('bopeebo');
|
||||
}
|
||||
*/
|
||||
|
||||
var splashTemplateContainer:VBox = dialog.findComponent('splashTemplateContainer', VBox);
|
||||
|
||||
var songList:Array<String> = SongDataParser.listSongIds();
|
||||
|
||||
for (targetSongId in songList) {
|
||||
var songData = SongDataParser.fetchSong(targetSongId);
|
||||
|
||||
if (songData == null)
|
||||
continue;
|
||||
|
||||
var songName = songData.getDifficulty().songName;
|
||||
|
||||
var linkTemplateSong:Link = new Link();
|
||||
linkTemplateSong.text = songName;
|
||||
linkTemplateSong.onClick = (_event) ->
|
||||
{
|
||||
dialog.hideDialog(DialogButton.CANCEL);
|
||||
|
||||
// Load song from template
|
||||
state.loadSongAsTemplate(targetSongId);
|
||||
}
|
||||
|
||||
splashTemplateContainer.addComponent(linkTemplateSong);
|
||||
}
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public static function openUploadInstDialog(state:ChartEditorState, ?closable:Bool = true):Dialog
|
||||
{
|
||||
var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_INST_LAYOUT, true, closable);
|
||||
|
||||
var instrumentalBox:Box = dialog.findComponent('instrumentalBox', Box);
|
||||
|
||||
instrumentalBox.onMouseOver = (_event) ->
|
||||
{
|
||||
instrumentalBox.swapClass('upload-bg', 'upload-bg-hover');
|
||||
Cursor.cursorMode = Pointer;
|
||||
}
|
||||
|
||||
instrumentalBox.onMouseOut = (_event) ->
|
||||
{
|
||||
instrumentalBox.swapClass('upload-bg-hover', 'upload-bg');
|
||||
Cursor.cursorMode = Default;
|
||||
}
|
||||
|
||||
var onDropFile:String->Void;
|
||||
|
||||
instrumentalBox.onClick = (_event) ->
|
||||
{
|
||||
Dialogs.openBinaryFile("Open Instrumental", [{label: "Audio File (.ogg)", extension: "ogg"}], function(selectedFile)
|
||||
{
|
||||
if (selectedFile != null)
|
||||
{
|
||||
trace('Selected file: ' + selectedFile);
|
||||
state.loadInstrumentalFromBytes(selectedFile.bytes);
|
||||
dialog.hideDialog(DialogButton.APPLY);
|
||||
removeDropHandler(onDropFile);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onDropFile = (path:String) ->
|
||||
{
|
||||
trace('Dropped file: ' + path);
|
||||
state.loadInstrumentalFromPath(path);
|
||||
dialog.hideDialog(DialogButton.APPLY);
|
||||
removeDropHandler(onDropFile);
|
||||
};
|
||||
|
||||
addDropHandler(onDropFile);
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
static function addDropHandler(handler:String->Void)
|
||||
{
|
||||
#if desktop
|
||||
FlxG.stage.window.onDropFile.add(handler);
|
||||
#else
|
||||
trace('addDropHandler not implemented for this platform');
|
||||
#end
|
||||
}
|
||||
|
||||
static function removeDropHandler(handler:String->Void)
|
||||
{
|
||||
#if desktop
|
||||
FlxG.stage.window.onDropFile.remove(handler);
|
||||
#end
|
||||
}
|
||||
|
||||
public static function openSongMetadataDialog(state:ChartEditorState):Dialog
|
||||
{
|
||||
var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_SONG_METADATA_LAYOUT, true, false);
|
||||
|
||||
var dialogSongName:TextField = dialog.findComponent('dialogSongName', TextField);
|
||||
dialogSongName.onChange = (event:UIEvent) ->
|
||||
{
|
||||
var valid = event.target.text != null && event.target.text != "";
|
||||
|
||||
if (valid)
|
||||
{
|
||||
dialogSongName.removeClass('invalid-value');
|
||||
state.currentSongMetadata.songName = event.target.text;
|
||||
}
|
||||
else
|
||||
{
|
||||
state.currentSongMetadata.songName = null;
|
||||
}
|
||||
};
|
||||
state.currentSongMetadata.songName = null;
|
||||
|
||||
var dialogSongArtist:TextField = dialog.findComponent('dialogSongArtist', TextField);
|
||||
dialogSongArtist.onChange = (event:UIEvent) ->
|
||||
{
|
||||
var valid = event.target.text != null && event.target.text != "";
|
||||
|
||||
if (valid)
|
||||
{
|
||||
dialogSongArtist.removeClass('invalid-value');
|
||||
state.currentSongMetadata.artist = event.target.text;
|
||||
}
|
||||
else
|
||||
{
|
||||
state.currentSongMetadata.artist = null;
|
||||
}
|
||||
};
|
||||
state.currentSongMetadata.artist = null;
|
||||
|
||||
var dialogStage:DropDown = dialog.findComponent('dialogStage', DropDown);
|
||||
dialogStage.onChange = (event:UIEvent) ->
|
||||
{
|
||||
var valid = event.data != null && event.data.id != null;
|
||||
|
||||
if (event.data.id == null)
|
||||
return;
|
||||
state.currentSongMetadata.playData.stage = event.data.id;
|
||||
};
|
||||
state.currentSongMetadata.playData.stage = null;
|
||||
|
||||
var dialogNoteSkin:DropDown = dialog.findComponent('dialogNoteSkin', DropDown);
|
||||
dialogNoteSkin.onChange = (event:UIEvent) ->
|
||||
{
|
||||
if (event.data.id == null)
|
||||
return;
|
||||
state.currentSongMetadata.playData.noteSkin = event.data.id;
|
||||
};
|
||||
state.currentSongMetadata.playData.noteSkin = null;
|
||||
|
||||
var dialogBPM:NumberStepper = dialog.findComponent('dialogBPM', NumberStepper);
|
||||
dialogBPM.onChange = (event:UIEvent) ->
|
||||
{
|
||||
if (event.value == null || event.value <= 0)
|
||||
return;
|
||||
|
||||
var timeChanges = state.currentSongMetadata.timeChanges;
|
||||
if (timeChanges == null || timeChanges.length == 0)
|
||||
{
|
||||
timeChanges = [new SongTimeChange(-1, 0, event.value, 4, 4, [4, 4, 4, 4])];
|
||||
}
|
||||
else
|
||||
{
|
||||
timeChanges[0].bpm = event.value;
|
||||
}
|
||||
|
||||
Conductor.forceBPM(event.value);
|
||||
|
||||
state.currentSongMetadata.timeChanges = timeChanges;
|
||||
};
|
||||
|
||||
var dialogCharGrid:PropertyGrid = dialog.findComponent('dialogCharGrid', PropertyGrid);
|
||||
var dialogCharAdd:Button = dialog.findComponent('dialogCharAdd', Button);
|
||||
dialogCharAdd.onClick = (_event) ->
|
||||
{
|
||||
var charGroup:PropertyGroup;
|
||||
charGroup = buildCharGroup(state, null, () ->
|
||||
{
|
||||
dialogCharGrid.removeComponent(charGroup);
|
||||
});
|
||||
dialogCharGrid.addComponent(charGroup);
|
||||
};
|
||||
|
||||
// Empty the character list.
|
||||
state.currentSongMetadata.playData.playableChars = {};
|
||||
// Add at least one character group with no Remove button.
|
||||
dialogCharGrid.addComponent(buildCharGroup(state, 'bf', null));
|
||||
|
||||
var dialogContinue:Button = dialog.findComponent('dialogContinue', Button);
|
||||
dialogContinue.onClick = (_event) ->
|
||||
{
|
||||
dialog.hideDialog(DialogButton.APPLY);
|
||||
};
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
static function buildCharGroup(state:ChartEditorState, ?key:String = null, removeFunc:Void->Void):PropertyGroup
|
||||
{
|
||||
var groupKey = key;
|
||||
|
||||
var getCharData = () ->
|
||||
{
|
||||
if (groupKey == null)
|
||||
groupKey = 'newChar${state.currentSongMetadata.playData.playableChars.keys().count()}';
|
||||
|
||||
var result = state.currentSongMetadata.playData.playableChars.get(groupKey);
|
||||
if (result == null)
|
||||
{
|
||||
result = new SongPlayableChar('', 'dad');
|
||||
state.currentSongMetadata.playData.playableChars.set(groupKey, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
var moveCharGroup = (target:String) ->
|
||||
{
|
||||
var charData = getCharData();
|
||||
state.currentSongMetadata.playData.playableChars.remove(groupKey);
|
||||
state.currentSongMetadata.playData.playableChars.set(target, charData);
|
||||
groupKey = target;
|
||||
}
|
||||
|
||||
var removeGroup = () ->
|
||||
{
|
||||
state.currentSongMetadata.playData.playableChars.remove(groupKey);
|
||||
removeFunc();
|
||||
}
|
||||
|
||||
var charData = getCharData();
|
||||
|
||||
var charGroup:PropertyGroup = cast state.buildComponent(CHART_EDITOR_DIALOG_SONG_METADATA_CHARGROUP_LAYOUT);
|
||||
|
||||
var charGroupPlayer:DropDown = charGroup.findComponent('charGroupPlayer', DropDown);
|
||||
charGroupPlayer.onChange = (event:UIEvent) ->
|
||||
{
|
||||
charGroup.text = event.data.text;
|
||||
moveCharGroup(event.data.id);
|
||||
};
|
||||
|
||||
if (key == null)
|
||||
{
|
||||
// Find the next available player character.
|
||||
trace(charGroupPlayer.dataSource.data);
|
||||
}
|
||||
|
||||
var charGroupOpponent:DropDown = charGroup.findComponent('charGroupOpponent', DropDown);
|
||||
charGroupOpponent.onChange = (event:UIEvent) ->
|
||||
{
|
||||
charData.opponent = event.data.id;
|
||||
};
|
||||
charGroupOpponent.value = getCharData().opponent;
|
||||
|
||||
var charGroupGirlfriend:DropDown = charGroup.findComponent('charGroupGirlfriend', DropDown);
|
||||
charGroupGirlfriend.onChange = (event:UIEvent) ->
|
||||
{
|
||||
charData.girlfriend = event.data.id;
|
||||
};
|
||||
charGroupGirlfriend.value = getCharData().girlfriend;
|
||||
|
||||
var charGroupRemove:Button = charGroup.findComponent('charGroupRemove', Button);
|
||||
charGroupRemove.onClick = (_event:MouseEvent) ->
|
||||
{
|
||||
removeGroup();
|
||||
};
|
||||
|
||||
if (removeFunc == null)
|
||||
charGroupRemove.hidden = true;
|
||||
|
||||
return charGroup;
|
||||
}
|
||||
|
||||
public static function openUploadVocalsDialog(state:ChartEditorState, ?closable:Bool = true):Dialog
|
||||
{
|
||||
var charIdsForVocals = [];
|
||||
|
||||
for (charKey in state.currentSongMetadata.playData.playableChars.keys())
|
||||
{
|
||||
var charData = state.currentSongMetadata.playData.playableChars.get(charKey);
|
||||
charIdsForVocals.push(charKey);
|
||||
if (charData.opponent != null)
|
||||
charIdsForVocals.push(charData.opponent);
|
||||
}
|
||||
|
||||
var dialog:Dialog = openDialog(state, CHART_EDITOR_DIALOG_UPLOAD_VOCALS_LAYOUT, true, closable);
|
||||
|
||||
var dialogContainer = dialog.findComponent('vocalContainer');
|
||||
|
||||
var onDropFile:String->Void;
|
||||
|
||||
for (charKey in charIdsForVocals)
|
||||
{
|
||||
trace('Adding vocal upload for character ${charKey}');
|
||||
var charMetadata:BaseCharacter = CharacterDataParser.fetchCharacter(charKey);
|
||||
var charName:String = charMetadata.characterName;
|
||||
|
||||
var vocalsEntry = state.buildComponent(CHART_EDITOR_DIALOG_UPLOAD_VOCALS_ENTRY_LAYOUT);
|
||||
|
||||
var vocalsEntryLabel:Label = vocalsEntry.findComponent('vocalsEntryLabel', Label);
|
||||
vocalsEntryLabel.text = 'Click to browse for a vocal track for $charName.';
|
||||
|
||||
vocalsEntry.onClick = (_event) ->
|
||||
{
|
||||
Dialogs.openBinaryFile('Open $charName Vocals', [{label: "Audio File (.ogg)", extension: "ogg"}], function(selectedFile)
|
||||
{
|
||||
if (selectedFile != null)
|
||||
{
|
||||
trace('Selected file: ' + selectedFile.name + "~" + selectedFile.fullPath);
|
||||
vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${selectedFile.name}';
|
||||
state.loadVocalsFromBytes(selectedFile.bytes);
|
||||
removeDropHandler(onDropFile);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
dialogContainer.addComponent(vocalsEntry);
|
||||
}
|
||||
|
||||
var dialogContinue:Button = dialog.findComponent('dialogContinue', Button);
|
||||
dialogContinue.onClick = (_event) ->
|
||||
{
|
||||
dialog.hideDialog(DialogButton.APPLY);
|
||||
};
|
||||
|
||||
// TODO: Redo the logic for file drop handler to be more robust.
|
||||
// We need to distinguish which component the mouse is over when the file is dropped.
|
||||
|
||||
onDropFile = (path:String) ->
|
||||
{
|
||||
trace('Dropped file: ' + path);
|
||||
};
|
||||
addDropHandler(onDropFile);
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and opens a dialog displaying the user guide, providing guidance and help on how to use the chart editor.
|
||||
*/
|
||||
public static inline function openUserGuideDialog(state:ChartEditorState):Dialog
|
||||
{
|
||||
return openDialog(state, CHART_EDITOR_DIALOG_USER_GUIDE_LAYOUT, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and opens a dialog from a given layout path.
|
||||
* @param modal Makes the background uninteractable while the dialog is open.
|
||||
* @param closable Hides the close button on the dialog, preventing it from being closed unless the user interacts with the dialog.
|
||||
*/
|
||||
static function openDialog(state:ChartEditorState, key:String, modal:Bool = true, closable:Bool = true):Dialog
|
||||
{
|
||||
var dialog:Dialog = cast state.buildComponent(key);
|
||||
dialog.destroyOnClose = true;
|
||||
dialog.closable = closable;
|
||||
dialog.showDialog(modal);
|
||||
|
||||
state.isHaxeUIDialogOpen = true;
|
||||
dialog.onDialogClosed = (_event) ->
|
||||
{
|
||||
state.isHaxeUIDialogOpen = false;
|
||||
};
|
||||
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package funkin.ui.debug.charting;
|
||||
|
||||
import flixel.FlxObject;
|
||||
import flixel.FlxBasic;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.graphics.frames.FlxFramesCollection;
|
||||
import flixel.graphics.frames.FlxTileFrames;
|
||||
|
@ -12,17 +14,14 @@ import funkin.play.song.SongData.SongNoteData;
|
|||
*/
|
||||
class ChartEditorNoteSprite extends FlxSprite
|
||||
{
|
||||
public var parentState:ChartEditorState;
|
||||
|
||||
/**
|
||||
* The note data that this sprite represents.
|
||||
* You can set this to null to kill the sprite and flag it for recycling.
|
||||
*/
|
||||
public var noteData(default, set):SongNoteData;
|
||||
|
||||
/**
|
||||
* The note skin that this sprite displays.
|
||||
*/
|
||||
public var noteSkin(default, set):String = 'Normal';
|
||||
|
||||
/**
|
||||
* This note is the previous sprite in a sustain chain.
|
||||
*/
|
||||
|
@ -33,10 +32,12 @@ class ChartEditorNoteSprite extends FlxSprite
|
|||
*/
|
||||
public var childNoteSprite(default, set):ChartEditorNoteSprite = null;
|
||||
|
||||
public function new()
|
||||
public function new(parent:ChartEditorState)
|
||||
{
|
||||
super();
|
||||
|
||||
this.parentState = parent;
|
||||
|
||||
if (noteFrameCollection == null)
|
||||
{
|
||||
initFrameCollection();
|
||||
|
@ -131,26 +132,12 @@ class ChartEditorNoteSprite extends FlxSprite
|
|||
playNoteAnimation();
|
||||
|
||||
// Update the position to match the note data.
|
||||
setNotePosition();
|
||||
updateNotePosition();
|
||||
|
||||
return this.noteData;
|
||||
}
|
||||
|
||||
function set_noteSkin(value:String):String
|
||||
{
|
||||
// Don't update if the skin hasn't changed.
|
||||
if (value == this.noteSkin)
|
||||
return this.noteSkin;
|
||||
|
||||
this.noteSkin = value;
|
||||
|
||||
// Make sure to update the graphic to match the note skin.
|
||||
playNoteAnimation();
|
||||
|
||||
return this.noteSkin;
|
||||
}
|
||||
|
||||
function setNotePosition()
|
||||
public function updateNotePosition(?origin:FlxObject)
|
||||
{
|
||||
var cursorColumn:Int = this.noteData.data;
|
||||
|
||||
|
@ -179,7 +166,13 @@ class ChartEditorNoteSprite extends FlxSprite
|
|||
|
||||
// Notes far in the song will start far down, but the group they belong to will have a high negative offset.
|
||||
// TODO: stepTime doesn't account for fluctuating BPMs.
|
||||
this.y = this.noteData.stepTime * ChartEditorState.GRID_SIZE;
|
||||
if (this.noteData.stepTime >= 0)
|
||||
this.y = this.noteData.stepTime * ChartEditorState.GRID_SIZE;
|
||||
|
||||
if (origin != null) {
|
||||
this.x += origin.x;
|
||||
this.y += origin.y;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -214,7 +207,6 @@ class ChartEditorNoteSprite extends FlxSprite
|
|||
if (this.parentNoteSprite != null)
|
||||
{
|
||||
this.noteData = this.parentNoteSprite.noteData;
|
||||
this.noteSkin = this.parentNoteSprite.noteSkin;
|
||||
}
|
||||
|
||||
return this.parentNoteSprite;
|
||||
|
@ -227,13 +219,12 @@ class ChartEditorNoteSprite extends FlxSprite
|
|||
if (this.parentNoteSprite != null)
|
||||
{
|
||||
this.noteData = this.parentNoteSprite.noteData;
|
||||
this.noteSkin = this.parentNoteSprite.noteSkin;
|
||||
}
|
||||
|
||||
return this.childNoteSprite;
|
||||
}
|
||||
|
||||
function playNoteAnimation()
|
||||
public function playNoteAnimation()
|
||||
{
|
||||
// Decide whether to display a note or a sustain.
|
||||
var baseAnimationName:String = 'tap';
|
||||
|
@ -241,7 +232,7 @@ class ChartEditorNoteSprite extends FlxSprite
|
|||
baseAnimationName = (this.childNoteSprite != null) ? 'hold' : 'holdEnd';
|
||||
|
||||
// Play the appropriate animation for the type, direction, and skin.
|
||||
var animationName = '${baseAnimationName}${this.noteData.getDirectionName()}${this.noteSkin}';
|
||||
var animationName = '${baseAnimationName}${this.noteData.getDirectionName()}${this.parentState.currentSongNoteSkin}';
|
||||
|
||||
this.animation.play(animationName);
|
||||
|
||||
|
@ -266,7 +257,7 @@ class ChartEditorNoteSprite extends FlxSprite
|
|||
this.updateHitbox();
|
||||
|
||||
// TODO: Make this an attribute of the note skin.
|
||||
this.antialiasing = (noteSkin != 'Pixel');
|
||||
this.antialiasing = (this.parentState.currentSongNoteSkin != 'Pixel');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,6 @@
|
|||
package funkin.ui.debug.charting;
|
||||
|
||||
import flixel.FlxSprite;
|
||||
import flixel.addons.display.FlxGridOverlay;
|
||||
import flixel.addons.display.FlxSliceSprite;
|
||||
import flixel.math.FlxRect;
|
||||
|
@ -32,18 +33,24 @@ class ChartEditorThemeHandler
|
|||
static final GRID_COLOR_1_DARK:FlxColor = 0xFF181919;
|
||||
|
||||
// Color 2 of the grid pattern. Alternates with Color 1.
|
||||
static final GRID_COLOR_2_LIGHT:FlxColor = 0xFFD9D5D5;
|
||||
static final GRID_COLOR_2_DARK:FlxColor = 0xFF262A2A;
|
||||
static final GRID_COLOR_2_LIGHT:FlxColor = 0xFFF8F8F8;
|
||||
static final GRID_COLOR_2_DARK:FlxColor = 0xFF202020;
|
||||
|
||||
// Color 3 of the grid pattern. Borders the other colors.
|
||||
static final GRID_COLOR_3_LIGHT:FlxColor = 0xFFD9D5D5;
|
||||
static final GRID_COLOR_3_DARK:FlxColor = 0xFF262A2A;
|
||||
|
||||
// Vertical divider between characters.
|
||||
static final GRID_STRUMLINE_DIVIDER_COLOR_LIGHT:FlxColor = 0xFF000000;
|
||||
static final GRID_STRUMLINE_DIVIDER_COLOR_LIGHT:FlxColor = 0xFF111111;
|
||||
static final GRID_STRUMLINE_DIVIDER_COLOR_DARK:FlxColor = 0xFFC4C4C4;
|
||||
static final GRID_STRUMLINE_DIVIDER_WIDTH:Float = 2;
|
||||
// static final GRID_STRUMLINE_DIVIDER_WIDTH:Float = 2;
|
||||
static final GRID_STRUMLINE_DIVIDER_WIDTH:Float = ChartEditorState.GRID_SELECTION_BORDER_WIDTH;
|
||||
|
||||
// Horizontal divider between measures.
|
||||
static final GRID_MEASURE_DIVIDER_COLOR_LIGHT:FlxColor = 0xFF000000;
|
||||
static final GRID_MEASURE_DIVIDER_COLOR_LIGHT:FlxColor = 0xFF111111;
|
||||
static final GRID_MEASURE_DIVIDER_COLOR_DARK:FlxColor = 0xFFC4C4C4;
|
||||
static final GRID_MEASURE_DIVIDER_WIDTH:Float = 2;
|
||||
// static final GRID_MEASURE_DIVIDER_WIDTH:Float = 2;
|
||||
static final GRID_MEASURE_DIVIDER_WIDTH:Float = ChartEditorState.GRID_SELECTION_BORDER_WIDTH;
|
||||
|
||||
// Border on the square highlighting selected notes.
|
||||
static final SELECTION_SQUARE_BORDER_COLOR_LIGHT:FlxColor = 0xFF339933;
|
||||
|
@ -55,9 +62,9 @@ class ChartEditorThemeHandler
|
|||
static final SELECTION_SQUARE_FILL_COLOR_LIGHT:FlxColor = 0x4033FF33;
|
||||
static final SELECTION_SQUARE_FILL_COLOR_DARK:FlxColor = 0x4033FF33;
|
||||
|
||||
// TODO: Un-hardcode these to be based on time signature.
|
||||
static final STEPS_PER_BEAT:Int = 4;
|
||||
static final BEATS_PER_MEASURE:Int = 4;
|
||||
static final PLAYHEAD_BLOCK_BORDER_WIDTH:Int = 2;
|
||||
static final PLAYHEAD_BLOCK_BORDER_COLOR:FlxColor = 0xFF9D0011;
|
||||
static final PLAYHEAD_BLOCK_FILL_COLOR:FlxColor = 0xFFBD0231;
|
||||
|
||||
public static function updateTheme(state:ChartEditorState):Void
|
||||
{
|
||||
|
@ -100,11 +107,70 @@ class ChartEditorThemeHandler
|
|||
|
||||
// 2 * (Strumline Size) + 1 grid squares wide, by (4 * quarter notes per measure) grid squares tall.
|
||||
// This gets reused to fill the screen.
|
||||
var gridWidth = ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2 + 1);
|
||||
var gridHeight = ChartEditorState.GRID_SIZE * (STEPS_PER_BEAT * BEATS_PER_MEASURE);
|
||||
var gridWidth:Int = Std.int(ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2 + 1));
|
||||
var gridHeight:Int = Std.int(ChartEditorState.GRID_SIZE * (Conductor.stepsPerMeasure));
|
||||
state.gridBitmap = FlxGridOverlay.createGrid(ChartEditorState.GRID_SIZE, ChartEditorState.GRID_SIZE, gridWidth, gridHeight, true, gridColor1,
|
||||
gridColor2);
|
||||
|
||||
// Selection borders
|
||||
var selectionBorderColor:FlxColor = switch (state.currentTheme)
|
||||
{
|
||||
case Light: GRID_COLOR_3_LIGHT;
|
||||
case Dark: GRID_COLOR_3_DARK;
|
||||
default: GRID_COLOR_3_LIGHT;
|
||||
};
|
||||
|
||||
// Selection border at top.
|
||||
state.gridBitmap.fillRect(new Rectangle(0, -(ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2), state.gridBitmap.width,
|
||||
ChartEditorState.GRID_SELECTION_BORDER_WIDTH),
|
||||
selectionBorderColor);
|
||||
|
||||
// Selection borders in the middle.
|
||||
for (i in 1...(Conductor.stepsPerMeasure))
|
||||
{
|
||||
state.gridBitmap.fillRect(new Rectangle(0, (ChartEditorState.GRID_SIZE * i) - (ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2),
|
||||
state.gridBitmap.width, ChartEditorState.GRID_SELECTION_BORDER_WIDTH),
|
||||
selectionBorderColor);
|
||||
}
|
||||
|
||||
// Selection border at bottom.
|
||||
state.gridBitmap.fillRect(new Rectangle(0, state.gridBitmap.height - (ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2), state.gridBitmap.width,
|
||||
ChartEditorState.GRID_SELECTION_BORDER_WIDTH),
|
||||
selectionBorderColor);
|
||||
|
||||
// Selection border at left.
|
||||
state.gridBitmap.fillRect(new Rectangle(-(ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2), 0, ChartEditorState.GRID_SELECTION_BORDER_WIDTH,
|
||||
state.gridBitmap.height),
|
||||
selectionBorderColor);
|
||||
|
||||
// Selection borders across the middle.
|
||||
for (i in 1...(ChartEditorState.STRUMLINE_SIZE * 2 + 1))
|
||||
{
|
||||
state.gridBitmap.fillRect(new Rectangle((ChartEditorState.GRID_SIZE * i) - (ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2), 0,
|
||||
ChartEditorState.GRID_SELECTION_BORDER_WIDTH, state.gridBitmap.height),
|
||||
selectionBorderColor);
|
||||
}
|
||||
|
||||
// Selection border at right.
|
||||
state.gridBitmap.fillRect(new Rectangle(state.gridBitmap.width - (ChartEditorState.GRID_SELECTION_BORDER_WIDTH / 2), 0,
|
||||
ChartEditorState.GRID_SELECTION_BORDER_WIDTH, state.gridBitmap.height),
|
||||
selectionBorderColor);
|
||||
|
||||
// Draw dividers between the measures.
|
||||
|
||||
var gridMeasureDividerColor:FlxColor = switch (state.currentTheme)
|
||||
{
|
||||
case Light: GRID_MEASURE_DIVIDER_COLOR_LIGHT;
|
||||
case Dark: GRID_MEASURE_DIVIDER_COLOR_DARK;
|
||||
default: GRID_MEASURE_DIVIDER_COLOR_LIGHT;
|
||||
};
|
||||
|
||||
// Divider at top
|
||||
state.gridBitmap.fillRect(new Rectangle(0, 0, state.gridBitmap.width, GRID_MEASURE_DIVIDER_WIDTH / 2), gridMeasureDividerColor);
|
||||
// Divider at bottom
|
||||
var dividerLineBY = state.gridBitmap.height - (GRID_MEASURE_DIVIDER_WIDTH / 2);
|
||||
state.gridBitmap.fillRect(new Rectangle(0, dividerLineBY, state.gridBitmap.width, GRID_MEASURE_DIVIDER_WIDTH / 2), gridMeasureDividerColor);
|
||||
|
||||
// Draw dividers between the strumlines.
|
||||
|
||||
var gridStrumlineDividerColor:FlxColor = switch (state.currentTheme)
|
||||
|
@ -121,20 +187,10 @@ class ChartEditorThemeHandler
|
|||
var dividerLineBX = ChartEditorState.GRID_SIZE * (ChartEditorState.STRUMLINE_SIZE * 2) - (GRID_STRUMLINE_DIVIDER_WIDTH / 2);
|
||||
state.gridBitmap.fillRect(new Rectangle(dividerLineBX, 0, GRID_STRUMLINE_DIVIDER_WIDTH, state.gridBitmap.height), gridStrumlineDividerColor);
|
||||
|
||||
// Draw dividers between the measures.
|
||||
|
||||
var gridMeasureDividerColor:FlxColor = switch (state.currentTheme)
|
||||
{
|
||||
case Light: GRID_MEASURE_DIVIDER_COLOR_LIGHT;
|
||||
case Dark: GRID_MEASURE_DIVIDER_COLOR_DARK;
|
||||
default: GRID_MEASURE_DIVIDER_COLOR_LIGHT;
|
||||
};
|
||||
|
||||
// Divider at top
|
||||
state.gridBitmap.fillRect(new Rectangle(0, 0, state.gridBitmap.width, GRID_MEASURE_DIVIDER_WIDTH / 2), gridMeasureDividerColor);
|
||||
// Divider at bottom
|
||||
var dividerLineBY = state.gridBitmap.height - (GRID_MEASURE_DIVIDER_WIDTH / 2);
|
||||
state.gridBitmap.fillRect(new Rectangle(0, dividerLineBY, GRID_MEASURE_DIVIDER_WIDTH / 2, state.gridBitmap.height), gridMeasureDividerColor);
|
||||
if (state.gridTiledSprite != null) {
|
||||
state.gridTiledSprite.loadGraphic(state.gridBitmap);
|
||||
}
|
||||
// Else, gridTiledSprite will be built later.
|
||||
}
|
||||
|
||||
static function updateSelectionSquare(state:ChartEditorState):Void
|
||||
|
@ -169,4 +225,20 @@ class ChartEditorThemeHandler
|
|||
- (2 * SELECTION_SQUARE_BORDER_WIDTH + 8)),
|
||||
32, 32);
|
||||
}
|
||||
|
||||
public static function buildPlayheadBlock():FlxSprite
|
||||
{
|
||||
var playheadBlock:FlxSprite = new FlxSprite();
|
||||
|
||||
var playheadBlockBitmap:BitmapData = new BitmapData(ChartEditorState.PLAYHEAD_SCROLL_AREA_WIDTH, ChartEditorState.PLAYHEAD_HEIGHT * 2, true);
|
||||
|
||||
playheadBlockBitmap.fillRect(new Rectangle(0, 0, ChartEditorState.PLAYHEAD_SCROLL_AREA_WIDTH, ChartEditorState.PLAYHEAD_HEIGHT * 2),
|
||||
PLAYHEAD_BLOCK_BORDER_COLOR);
|
||||
playheadBlockBitmap.fillRect(new Rectangle(PLAYHEAD_BLOCK_BORDER_WIDTH, PLAYHEAD_BLOCK_BORDER_WIDTH,
|
||||
ChartEditorState.PLAYHEAD_SCROLL_AREA_WIDTH - (2 * PLAYHEAD_BLOCK_BORDER_WIDTH),
|
||||
ChartEditorState.PLAYHEAD_HEIGHT * 2 - (2 * PLAYHEAD_BLOCK_BORDER_WIDTH)),
|
||||
PLAYHEAD_BLOCK_FILL_COLOR);
|
||||
|
||||
return playheadBlock.loadGraphic(playheadBlockBitmap);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
package funkin.ui.debug.charting;
|
||||
|
||||
import funkin.play.song.SongData.SongTimeChange;
|
||||
import haxe.ui.components.Slider;
|
||||
import haxe.ui.components.NumberStepper;
|
||||
import haxe.ui.components.NumberStepper;
|
||||
import haxe.ui.components.TextField;
|
||||
import funkin.play.character.BaseCharacter.CharacterType;
|
||||
import funkin.ui.haxeui.components.CharacterPlayer;
|
||||
import funkin.play.song.SongSerializer;
|
||||
import haxe.ui.components.Button;
|
||||
import haxe.ui.components.DropDown;
|
||||
import haxe.ui.containers.Group;
|
||||
import haxe.ui.containers.dialogs.Dialog;
|
||||
|
@ -77,33 +86,58 @@ class ChartEditorToolboxHandler
|
|||
toolbox = buildToolboxNoteDataLayout(state);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT:
|
||||
toolbox = buildToolboxEventDataLayout(state);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_SONGDATA_LAYOUT:
|
||||
toolbox = buildToolboxSongDataLayout(state);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT:
|
||||
toolbox = buildToolboxDifficultyLayout(state);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
|
||||
toolbox = buildToolboxMetadataLayout(state);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT:
|
||||
toolbox = buildToolboxCharactersLayout(state);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
|
||||
toolbox = buildToolboxPlayerPreviewLayout(state);
|
||||
case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:
|
||||
toolbox = buildToolboxOpponentPreviewLayout(state);
|
||||
default:
|
||||
trace('ChartEditorToolboxHandler.initToolbox() - Unknown toolbox ID: $id');
|
||||
toolbox = null;
|
||||
}
|
||||
|
||||
// Make sure we can reuse the toolbox later.
|
||||
toolbox.destroyOnClose = false;
|
||||
state.activeToolboxes.set(id, toolbox);
|
||||
|
||||
return toolbox;
|
||||
}
|
||||
|
||||
public static function getToolbox(state:ChartEditorState, id:String):Dialog
|
||||
{
|
||||
var toolbox:Dialog = state.activeToolboxes.get(id);
|
||||
|
||||
// Initialize the toolbox without showing it.
|
||||
if (toolbox == null)
|
||||
toolbox = initToolbox(state, id);
|
||||
|
||||
return toolbox;
|
||||
}
|
||||
|
||||
static function buildToolboxToolsLayout(state:ChartEditorState):Dialog
|
||||
{
|
||||
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_TOOLS_LAYOUT);
|
||||
|
||||
if (toolbox == null) return null;
|
||||
|
||||
// Starting position.
|
||||
toolbox.x = 50;
|
||||
toolbox.y = 50;
|
||||
|
||||
toolbox.onDialogClosed = (event:DialogEvent) ->
|
||||
{
|
||||
state.setUISelected('menubarItemToggleToolboxTools', false);
|
||||
state.setUICheckboxSelected('menubarItemToggleToolboxTools', false);
|
||||
}
|
||||
|
||||
var toolsGroup:Group = toolbox.findComponent("toolboxToolsGroup", Group);
|
||||
|
||||
if (toolsGroup == null) return null;
|
||||
|
||||
toolsGroup.onChange = (event:UIEvent) ->
|
||||
{
|
||||
switch (event.target.id)
|
||||
|
@ -124,13 +158,15 @@ class ChartEditorToolboxHandler
|
|||
{
|
||||
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_NOTEDATA_LAYOUT);
|
||||
|
||||
if (toolbox == null) return null;
|
||||
|
||||
// Starting position.
|
||||
toolbox.x = 75;
|
||||
toolbox.y = 100;
|
||||
|
||||
toolbox.onDialogClosed = (event:DialogEvent) ->
|
||||
{
|
||||
state.setUISelected('menubarItemToggleToolboxNotes', false);
|
||||
state.setUICheckboxSelected('menubarItemToggleToolboxNotes', false);
|
||||
}
|
||||
|
||||
var toolboxNotesNoteKind:DropDown = toolbox.findComponent("toolboxNotesNoteKind", DropDown);
|
||||
|
@ -147,38 +183,251 @@ class ChartEditorToolboxHandler
|
|||
{
|
||||
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENTDATA_LAYOUT);
|
||||
|
||||
if (toolbox == null) return null;
|
||||
|
||||
// Starting position.
|
||||
toolbox.x = 100;
|
||||
toolbox.y = 150;
|
||||
|
||||
toolbox.onDialogClosed = (event:DialogEvent) ->
|
||||
{
|
||||
state.setUISelected('menubarItemToggleToolboxEvents', false);
|
||||
state.setUICheckboxSelected('menubarItemToggleToolboxEvents', false);
|
||||
}
|
||||
|
||||
return toolbox;
|
||||
}
|
||||
|
||||
static function buildToolboxSongDataLayout(state:ChartEditorState):Dialog
|
||||
static function buildToolboxDifficultyLayout(state:ChartEditorState):Dialog
|
||||
{
|
||||
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_SONGDATA_LAYOUT);
|
||||
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_DIFFICULTY_LAYOUT);
|
||||
|
||||
if (toolbox == null) return null;
|
||||
|
||||
// Starting position.
|
||||
toolbox.x = 950;
|
||||
toolbox.y = 50;
|
||||
toolbox.x = 125;
|
||||
toolbox.y = 200;
|
||||
|
||||
toolbox.onDialogClosed = (event:DialogEvent) ->
|
||||
{
|
||||
state.setUISelected('menubarItemToggleToolboxSong', false);
|
||||
state.setUICheckboxSelected('menubarItemToggleToolboxDifficulty', false);
|
||||
}
|
||||
|
||||
var difficultyToolboxSaveMetadata:Button = toolbox.findComponent("difficultyToolboxSaveMetadata", Button);
|
||||
var difficultyToolboxSaveChart:Button = toolbox.findComponent("difficultyToolboxSaveChart", Button);
|
||||
var difficultyToolboxSaveAll:Button = toolbox.findComponent("difficultyToolboxSaveAll", Button);
|
||||
var difficultyToolboxLoadMetadata:Button = toolbox.findComponent("difficultyToolboxLoadMetadata", Button);
|
||||
var difficultyToolboxLoadChart:Button = toolbox.findComponent("difficultyToolboxLoadChart", Button);
|
||||
|
||||
difficultyToolboxSaveMetadata.onClick = (event:UIEvent) ->
|
||||
{
|
||||
SongSerializer.exportSongMetadata(state.currentSongMetadata);
|
||||
};
|
||||
|
||||
difficultyToolboxSaveChart.onClick = (event:UIEvent) ->
|
||||
{
|
||||
SongSerializer.exportSongChartData(state.currentSongChartData);
|
||||
};
|
||||
|
||||
difficultyToolboxSaveAll.onClick = (event:UIEvent) ->
|
||||
{
|
||||
state.exportAllSongData();
|
||||
};
|
||||
|
||||
difficultyToolboxLoadMetadata.onClick = (event:UIEvent) ->
|
||||
{
|
||||
// Replace metadata for current variation.
|
||||
SongSerializer.importSongMetadataAsync(function(songMetadata)
|
||||
{
|
||||
state.currentSongMetadata = songMetadata;
|
||||
});
|
||||
};
|
||||
|
||||
difficultyToolboxLoadChart.onClick = (event:UIEvent) ->
|
||||
{
|
||||
// Replace chart data for current variation.
|
||||
SongSerializer.importSongChartDataAsync(function(songChartData)
|
||||
{
|
||||
state.currentSongChartData = songChartData;
|
||||
state.noteDisplayDirty = true;
|
||||
});
|
||||
};
|
||||
|
||||
state.difficultySelectDirty = true;
|
||||
|
||||
return toolbox;
|
||||
}
|
||||
|
||||
static function buildToolboxMetadataLayout(state:ChartEditorState):Dialog
|
||||
{
|
||||
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
|
||||
|
||||
if (toolbox == null) return null;
|
||||
|
||||
// Starting position.
|
||||
toolbox.x = 150;
|
||||
toolbox.y = 250;
|
||||
|
||||
toolbox.onDialogClosed = (event:DialogEvent) ->
|
||||
{
|
||||
state.setUICheckboxSelected('menubarItemToggleToolboxMetadata', false);
|
||||
}
|
||||
|
||||
var inputSongName:TextField = toolbox.findComponent('inputSongName', TextField);
|
||||
inputSongName.onChange = (event:UIEvent) ->
|
||||
{
|
||||
var valid = event.target.text != null && event.target.text != "";
|
||||
|
||||
if (valid)
|
||||
{
|
||||
inputSongName.removeClass('invalid-value');
|
||||
state.currentSongMetadata.songName = event.target.text;
|
||||
}
|
||||
else
|
||||
{
|
||||
state.currentSongMetadata.songName = null;
|
||||
}
|
||||
};
|
||||
|
||||
var inputSongArtist:TextField = toolbox.findComponent('inputSongArtist', TextField);
|
||||
inputSongArtist.onChange = (event:UIEvent) ->
|
||||
{
|
||||
var valid = event.target.text != null && event.target.text != "";
|
||||
|
||||
if (valid)
|
||||
{
|
||||
inputSongArtist.removeClass('invalid-value');
|
||||
state.currentSongMetadata.artist = event.target.text;
|
||||
}
|
||||
else
|
||||
{
|
||||
state.currentSongMetadata.artist = null;
|
||||
}
|
||||
};
|
||||
|
||||
var inputStage:DropDown = toolbox.findComponent('inputStage', DropDown);
|
||||
inputStage.onChange = (event:UIEvent) ->
|
||||
{
|
||||
var valid = event.data != null && event.data.id != null;
|
||||
|
||||
if (valid) {
|
||||
state.currentSongMetadata.playData.stage = event.data.id;
|
||||
}
|
||||
};
|
||||
|
||||
var inputNoteSkin:DropDown = toolbox.findComponent('inputNoteSkin', DropDown);
|
||||
inputNoteSkin.onChange = (event:UIEvent) ->
|
||||
{
|
||||
if (event.data.id == null)
|
||||
return;
|
||||
state.currentSongMetadata.playData.noteSkin = event.data.id;
|
||||
};
|
||||
|
||||
var inputBPM:NumberStepper = toolbox.findComponent('inputBPM', NumberStepper);
|
||||
inputBPM.onChange = (event:UIEvent) ->
|
||||
{
|
||||
if (event.value == null || event.value <= 0)
|
||||
return;
|
||||
|
||||
var timeChanges = state.currentSongMetadata.timeChanges;
|
||||
if (timeChanges == null || timeChanges.length == 0)
|
||||
{
|
||||
timeChanges = [new SongTimeChange(-1, 0, event.value, 4, 4, [4, 4, 4, 4])];
|
||||
}
|
||||
else
|
||||
{
|
||||
timeChanges[0].bpm = event.value;
|
||||
}
|
||||
|
||||
Conductor.forceBPM(event.value);
|
||||
|
||||
state.currentSongMetadata.timeChanges = timeChanges;
|
||||
};
|
||||
|
||||
var inputScrollSpeed:Slider = toolbox.findComponent('inputScrollSpeed', Slider);
|
||||
inputScrollSpeed.onChange = (event:UIEvent) ->
|
||||
{
|
||||
var valid = event.target.value != null && event.target.value > 0;
|
||||
|
||||
if (valid)
|
||||
{
|
||||
inputScrollSpeed.removeClass('invalid-value');
|
||||
state.currentSongChartData.scrollSpeed = event.target.value;
|
||||
}
|
||||
else
|
||||
{
|
||||
state.currentSongChartData.scrollSpeed = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return toolbox;
|
||||
}
|
||||
|
||||
static function buildToolboxCharactersLayout(state:ChartEditorState):Dialog
|
||||
{
|
||||
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT);
|
||||
|
||||
if (toolbox == null) return null;
|
||||
|
||||
// Starting position.
|
||||
toolbox.x = 175;
|
||||
toolbox.y = 300;
|
||||
|
||||
toolbox.onDialogClosed = (event:DialogEvent) ->
|
||||
{
|
||||
state.setUICheckboxSelected('menubarItemToggleToolboxCharacters', false);
|
||||
}
|
||||
|
||||
return toolbox;
|
||||
}
|
||||
|
||||
static function buildDialog(state:ChartEditorState, id:String):Dialog
|
||||
static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):Dialog
|
||||
{
|
||||
var dialog:Dialog = cast state.buildComponent(id);
|
||||
dialog.destroyOnClose = false;
|
||||
return dialog;
|
||||
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);
|
||||
|
||||
if (toolbox == null) return null;
|
||||
|
||||
// Starting position.
|
||||
toolbox.x = 200;
|
||||
toolbox.y = 350;
|
||||
|
||||
toolbox.onDialogClosed = (event:DialogEvent) ->
|
||||
{
|
||||
state.setUICheckboxSelected('menubarItemToggleToolboxPlayerPreview', false);
|
||||
}
|
||||
|
||||
var charPlayer:CharacterPlayer = toolbox.findComponent('charPlayer');
|
||||
// TODO: We need to implement character swapping in ChartEditorState.
|
||||
charPlayer.loadCharacter('bf');
|
||||
//charPlayer.setScale(0.5);
|
||||
charPlayer.setCharacterType(CharacterType.BF);
|
||||
charPlayer.flip = true;
|
||||
|
||||
return toolbox;
|
||||
}
|
||||
|
||||
static function buildToolboxOpponentPreviewLayout(state:ChartEditorState):Dialog
|
||||
{
|
||||
var toolbox:Dialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT);
|
||||
|
||||
if (toolbox == null) return null;
|
||||
|
||||
// Starting position.
|
||||
toolbox.x = 200;
|
||||
toolbox.y = 350;
|
||||
|
||||
toolbox.onDialogClosed = (event:DialogEvent) ->
|
||||
{
|
||||
state.setUICheckboxSelected('menubarItemToggleToolboxOpponentPreview', false);
|
||||
}
|
||||
|
||||
var charPlayer:CharacterPlayer = toolbox.findComponent('charPlayer');
|
||||
// TODO: We need to implement character swapping in ChartEditorState.
|
||||
charPlayer.loadCharacter('dad');
|
||||
// charPlayer.setScale(0.5);
|
||||
charPlayer.setCharacterType(CharacterType.DAD);
|
||||
charPlayer.flip = false;
|
||||
|
||||
return toolbox;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
package funkin.ui.haxeui;
|
||||
|
||||
import haxe.ui.containers.menus.MenuCheckBox;
|
||||
import haxe.ui.components.CheckBox;
|
||||
import haxe.ui.events.DragEvent;
|
||||
import haxe.ui.events.MouseEvent;
|
||||
import haxe.ui.events.UIEvent;
|
||||
import haxe.ui.RuntimeComponentBuilder;
|
||||
import haxe.ui.core.Component;
|
||||
import haxe.ui.core.Screen;
|
||||
|
@ -93,6 +98,83 @@ class HaxeUIState extends MusicBeatState
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an onClick listener to a HaxeUI menu bar item.
|
||||
*/
|
||||
function addUIClickListener(key:String, callback:MouseEvent->Void)
|
||||
{
|
||||
var target:Component = findComponent(key);
|
||||
if (target == null)
|
||||
{
|
||||
// Gracefully handle the case where the item can't be located.
|
||||
trace('WARN: Could not locate menu item: $key');
|
||||
}
|
||||
else
|
||||
{
|
||||
target.onClick = callback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an onChange listener to a HaxeUI input component such as a slider or text field.
|
||||
*/
|
||||
function addUIChangeListener(key:String, callback:UIEvent->Void)
|
||||
{
|
||||
var target:Component = findComponent(key);
|
||||
if (target == null)
|
||||
{
|
||||
// Gracefully handle the case where the item can't be located.
|
||||
trace('WARN: Could not locate menu item: $key');
|
||||
}
|
||||
else
|
||||
{
|
||||
target.onChange = callback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of a HaxeUI component.
|
||||
* Usually modifies the text of a label or value of a text field.
|
||||
*/
|
||||
function setUIValue<T>(key:String, value:T):T
|
||||
{
|
||||
var target:Component = findComponent(key);
|
||||
if (target == null)
|
||||
{
|
||||
// Gracefully handle the case where the item can't be located.
|
||||
trace('WARN: Could not locate menu item: $key');
|
||||
return value;
|
||||
}
|
||||
else
|
||||
{
|
||||
return target.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of a HaxeUI checkbox,
|
||||
* since that's on 'selected' instead of 'value'.
|
||||
*/
|
||||
public function setUICheckboxSelected<T>(key:String, value:Bool):Bool
|
||||
{
|
||||
var targetA:CheckBox = findComponent(key, CheckBox);
|
||||
|
||||
if (targetA != null)
|
||||
{
|
||||
return targetA.selected = value;
|
||||
}
|
||||
|
||||
var targetB:MenuCheckBox = findComponent(key, MenuCheckBox);
|
||||
if (targetB != null)
|
||||
{
|
||||
return targetB.selected = value;
|
||||
}
|
||||
|
||||
// Gracefully handle the case where the item can't be located.
|
||||
trace('WARN: Could not locate check box: $key');
|
||||
return value;
|
||||
}
|
||||
|
||||
public function findComponent<T:Component>(criteria:String = null, type:Class<T> = null, recursive:Null<Bool> = null, searchType:String = "id"):Null<T>
|
||||
{
|
||||
if (component == null)
|
||||
|
|
276
source/funkin/ui/haxeui/components/CharacterPlayer.hx
Normal file
276
source/funkin/ui/haxeui/components/CharacterPlayer.hx
Normal file
|
@ -0,0 +1,276 @@
|
|||
package funkin.ui.haxeui.components;
|
||||
|
||||
import flixel.FlxSprite;
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import flixel.graphics.frames.FlxFramesCollection;
|
||||
import flixel.math.FlxRect;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
|
||||
import funkin.play.character.BaseCharacter;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import haxe.ui.containers.Box;
|
||||
import haxe.ui.core.Component;
|
||||
import haxe.ui.core.IDataComponent;
|
||||
import haxe.ui.data.DataSource;
|
||||
import haxe.ui.events.AnimationEvent;
|
||||
import haxe.ui.events.UIEvent;
|
||||
import haxe.ui.geom.Size;
|
||||
import haxe.ui.layouts.DefaultLayout;
|
||||
import haxe.ui.styles.Style;
|
||||
import openfl.Assets;
|
||||
|
||||
private typedef AnimationInfo =
|
||||
{
|
||||
var name:String;
|
||||
var prefix:String;
|
||||
var frameRate:Null<Int>; // default 30
|
||||
var looped:Null<Bool>; // default true
|
||||
var flipX:Null<Bool>; // default false
|
||||
var flipY:Null<Bool>; // default false
|
||||
}
|
||||
|
||||
@:composite(Layout)
|
||||
class CharacterPlayer extends Box
|
||||
{
|
||||
private var character:BaseCharacter;
|
||||
|
||||
public function new(?defaultToBf:Bool = true)
|
||||
{
|
||||
super();
|
||||
this._overrideSkipTransformChildren = false;
|
||||
|
||||
if (defaultToBf)
|
||||
{
|
||||
loadCharacter('bf');
|
||||
}
|
||||
}
|
||||
|
||||
private var _charId:String;
|
||||
|
||||
public var charId(get, set):String;
|
||||
|
||||
private function get_charId():String
|
||||
{
|
||||
return _charId;
|
||||
}
|
||||
|
||||
private function set_charId(value:String):String
|
||||
{
|
||||
_charId = value;
|
||||
loadCharacter(_charId);
|
||||
return value;
|
||||
}
|
||||
|
||||
private var _redispatchLoaded:Bool = false; // possible haxeui bug: if listener is added after event is dispatched, event is "lost"... needs thinking about, is it smart to "collect and redispatch"? Not sure
|
||||
private var _redispatchStart:Bool = false; // possible haxeui bug: if listener is added after event is dispatched, event is "lost"... needs thinking about, is it smart to "collect and redispatch"? Not sure
|
||||
|
||||
public override function onReady()
|
||||
{
|
||||
super.onReady();
|
||||
|
||||
invalidateComponentLayout();
|
||||
|
||||
if (_redispatchLoaded)
|
||||
{
|
||||
_redispatchLoaded = false;
|
||||
dispatch(new AnimationEvent(AnimationEvent.LOADED));
|
||||
}
|
||||
|
||||
if (_redispatchStart)
|
||||
{
|
||||
_redispatchStart = false;
|
||||
dispatch(new AnimationEvent(AnimationEvent.START));
|
||||
}
|
||||
|
||||
parentComponent._overrideSkipTransformChildren = false;
|
||||
}
|
||||
|
||||
public function loadCharacter(id:String)
|
||||
{
|
||||
if (id == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (character != null)
|
||||
{
|
||||
remove(character);
|
||||
character.destroy();
|
||||
character = null;
|
||||
}
|
||||
|
||||
var newCharacter:BaseCharacter = CharacterDataParser.fetchCharacter(id);
|
||||
|
||||
if (newCharacter == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
character = newCharacter;
|
||||
if (_characterType != null)
|
||||
{
|
||||
character.characterType = _characterType;
|
||||
}
|
||||
if (flip)
|
||||
{
|
||||
character.flipX = !character.flipX;
|
||||
}
|
||||
|
||||
character.scale.x *= _scale;
|
||||
character.scale.y *= _scale;
|
||||
|
||||
character.animation.callback = function(name:String = "", frameNumber:Int = -1, frameIndex:Int = -1)
|
||||
{
|
||||
@:privateAccess
|
||||
character.onAnimationFrame(name, frameNumber, frameIndex);
|
||||
dispatch(new AnimationEvent(AnimationEvent.FRAME));
|
||||
};
|
||||
character.animation.finishCallback = function(name:String = "")
|
||||
{
|
||||
@:privateAccess
|
||||
character.onAnimationFinished(name);
|
||||
dispatch(new AnimationEvent(AnimationEvent.END));
|
||||
};
|
||||
add(character);
|
||||
|
||||
invalidateComponentLayout();
|
||||
|
||||
if (hasEvent(AnimationEvent.LOADED))
|
||||
{
|
||||
dispatch(new AnimationEvent(AnimationEvent.LOADED));
|
||||
}
|
||||
else
|
||||
{
|
||||
_redispatchLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private override function repositionChildren()
|
||||
{
|
||||
super.repositionChildren();
|
||||
|
||||
@:privateAccess
|
||||
var animOffsets = character.animOffsets;
|
||||
|
||||
character.x = this.screenX + ((this.width / 2) - (character.frameWidth / 2));
|
||||
character.x -= animOffsets[0];
|
||||
character.y = this.screenY + ((this.height / 2) - (character.frameHeight / 2));
|
||||
character.y -= animOffsets[1];
|
||||
}
|
||||
|
||||
private var _characterType:CharacterType;
|
||||
public function setCharacterType(value:CharacterType)
|
||||
{
|
||||
_characterType = value;
|
||||
if (character != null)
|
||||
{
|
||||
character.characterType = value;
|
||||
}
|
||||
}
|
||||
|
||||
public var flip(default, set):Bool;
|
||||
|
||||
private function set_flip(value:Bool):Bool
|
||||
{
|
||||
if (value == flip)
|
||||
return value;
|
||||
|
||||
if (character != null)
|
||||
{
|
||||
character.flipX = !character.flipX;
|
||||
}
|
||||
|
||||
return flip = value;
|
||||
}
|
||||
|
||||
var _scale:Float = 1.0;
|
||||
public function setScale(value)
|
||||
{
|
||||
_scale = value;
|
||||
if (character != null)
|
||||
{
|
||||
character.scale.x *= _scale;
|
||||
character.scale.y *= _scale;
|
||||
}
|
||||
}
|
||||
|
||||
public function onUpdate(event:UpdateScriptEvent)
|
||||
{
|
||||
if (character != null)
|
||||
character.onUpdate(event);
|
||||
}
|
||||
|
||||
public function onBeatHit(event:SongTimeScriptEvent):Void
|
||||
{
|
||||
if (character != null)
|
||||
character.onBeatHit(event);
|
||||
|
||||
this.repositionChildren();
|
||||
}
|
||||
|
||||
public function onStepHit(event:SongTimeScriptEvent):Void
|
||||
{
|
||||
if (character != null)
|
||||
character.onStepHit(event);
|
||||
}
|
||||
|
||||
public function onNoteHit(event:NoteScriptEvent):Void
|
||||
{
|
||||
if (character != null)
|
||||
character.onNoteHit(event);
|
||||
|
||||
this.repositionChildren();
|
||||
}
|
||||
|
||||
public function onNoteMiss(event:NoteScriptEvent):Void
|
||||
{
|
||||
if (character != null)
|
||||
character.onNoteMiss(event);
|
||||
|
||||
this.repositionChildren();
|
||||
}
|
||||
|
||||
public function onNoteGhostMiss(event:GhostMissNoteScriptEvent):Void
|
||||
{
|
||||
if (character != null)
|
||||
character.onNoteGhostMiss(event);
|
||||
|
||||
this.repositionChildren();
|
||||
}
|
||||
}
|
||||
|
||||
@:access(funkin.ui.haxeui.components.CharacterPlayer)
|
||||
private class Layout extends DefaultLayout
|
||||
{
|
||||
public override function repositionChildren()
|
||||
{
|
||||
var player = cast(_component, CharacterPlayer);
|
||||
var sprite:BaseCharacter = player.character;
|
||||
if (sprite == null)
|
||||
{
|
||||
return super.repositionChildren();
|
||||
}
|
||||
|
||||
@:privateAccess
|
||||
var animOffsets = sprite.animOffsets;
|
||||
|
||||
sprite.x = _component.screenLeft + ((_component.width / 2) - (sprite.frameWidth / 2));
|
||||
sprite.x += animOffsets[0];
|
||||
sprite.y = _component.screenTop + ((_component.height / 2) - (sprite.frameHeight / 2));
|
||||
sprite.y += animOffsets[1];
|
||||
}
|
||||
|
||||
public override function calcAutoSize(exclusions:Array<Component> = null):Size
|
||||
{
|
||||
var player = cast(_component, CharacterPlayer);
|
||||
var sprite = player.character;
|
||||
if (sprite == null)
|
||||
{
|
||||
return super.calcAutoSize(exclusions);
|
||||
}
|
||||
var size = new Size();
|
||||
size.width = sprite.frameWidth + paddingLeft + paddingRight;
|
||||
size.height = sprite.frameHeight + paddingTop + paddingBottom;
|
||||
return size;
|
||||
}
|
||||
}
|
11
source/funkin/util/DateUtil.hx
Normal file
11
source/funkin/util/DateUtil.hx
Normal file
|
@ -0,0 +1,11 @@
|
|||
package funkin.util;
|
||||
|
||||
class DateUtil
|
||||
{
|
||||
public static function generateTimestamp():String
|
||||
{
|
||||
var date = Date.now();
|
||||
return
|
||||
'${date.getFullYear()}-${Std.string(date.getMonth() + 1).lpad('0', 2)}-${Std.string(date.getDate()).lpad('0', 2)}-${Std.string(date.getHours()).lpad('0', 2)}-${Std.string(date.getMinutes()).lpad('0', 2)}-${Std.string(date.getSeconds()).lpad('0', 2)}';
|
||||
}
|
||||
}
|
524
source/funkin/util/FileUtil.hx
Normal file
524
source/funkin/util/FileUtil.hx
Normal file
|
@ -0,0 +1,524 @@
|
|||
package funkin.util;
|
||||
|
||||
import cpp.abi.Abi;
|
||||
import haxe.zip.Entry;
|
||||
import lime.utils.Bytes;
|
||||
import lime.ui.FileDialog;
|
||||
import openfl.net.FileFilter;
|
||||
import haxe.io.Path;
|
||||
import lime.utils.Resource;
|
||||
|
||||
/**
|
||||
* Utilities for reading and writing files on various platforms.
|
||||
*/
|
||||
class FileUtil
|
||||
{
|
||||
/**
|
||||
* Browses for a single file, then calls `onSelect(path)` when a path chosen.
|
||||
* Note that on HTML5 this will immediately fail, you should call `openFile(onOpen:Resource->Void)` instead.
|
||||
*
|
||||
* @param typeFilter Filters what kinds of files can be selected.
|
||||
* @return Whether the file dialog was opened successfully.
|
||||
*/
|
||||
public static function browseForFile(?typeFilter:Array<FileFilter>, ?onSelect:String->Void, ?onCancel:Void->Void, ?defaultPath:String,
|
||||
?dialogTitle:String):Bool
|
||||
{
|
||||
#if desktop
|
||||
var filter = convertTypeFilter(typeFilter);
|
||||
|
||||
var fileDialog = new FileDialog();
|
||||
if (onSelect != null)
|
||||
fileDialog.onSelect.add(onSelect);
|
||||
if (onCancel != null)
|
||||
fileDialog.onCancel.add(onCancel);
|
||||
|
||||
fileDialog.browse(OPEN, filter, defaultPath, dialogTitle);
|
||||
return true;
|
||||
#elseif html5
|
||||
onCancel();
|
||||
return false;
|
||||
#else
|
||||
onCancel();
|
||||
return false;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Browses for a directory, then calls `onSelect(path)` when a path chosen.
|
||||
* Note that on HTML5 this will immediately fail.
|
||||
*
|
||||
* @param typeFilter TODO What does this do?
|
||||
* @return Whether the file dialog was opened successfully.
|
||||
*/
|
||||
public static function browseForDirectory(?typeFilter:Array<FileFilter>, ?onSelect:String->Void, ?onCancel:Void->Void, ?defaultPath:String,
|
||||
?dialogTitle:String):Bool
|
||||
{
|
||||
#if desktop
|
||||
var filter = convertTypeFilter(typeFilter);
|
||||
|
||||
var fileDialog = new FileDialog();
|
||||
if (onSelect != null)
|
||||
fileDialog.onSelect.add(onSelect);
|
||||
if (onCancel != null)
|
||||
fileDialog.onCancel.add(onCancel);
|
||||
|
||||
fileDialog.browse(OPEN_DIRECTORY, filter, defaultPath, dialogTitle);
|
||||
return true;
|
||||
#elseif html5
|
||||
onCancel();
|
||||
return false;
|
||||
#else
|
||||
onCancel();
|
||||
return false;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Browses for multiple file, then calls `onSelect(paths)` when a path chosen.
|
||||
* Note that on HTML5 this will immediately fail.
|
||||
*
|
||||
* @return Whether the file dialog was opened successfully.
|
||||
*/
|
||||
public static function browseForMultipleFiles(?typeFilter:Array<FileFilter>, ?onSelect:Array<String>->Void, ?onCancel:Void->Void, ?defaultPath:String,
|
||||
?dialogTitle:String):Bool
|
||||
{
|
||||
#if desktop
|
||||
var filter = convertTypeFilter(typeFilter);
|
||||
|
||||
var fileDialog = new FileDialog();
|
||||
if (onSelect != null)
|
||||
fileDialog.onSelectMultiple.add(onSelect);
|
||||
if (onCancel != null)
|
||||
fileDialog.onCancel.add(onCancel);
|
||||
|
||||
fileDialog.browse(OPEN_MULTIPLE, filter, defaultPath, dialogTitle);
|
||||
return true;
|
||||
#elseif html5
|
||||
onCancel();
|
||||
return false;
|
||||
#else
|
||||
onCancel();
|
||||
return false;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Browses for a file location to save to, then calls `onSelect(path)` when a path chosen.
|
||||
* Note that on HTML5 you can't do much with this, you should call `saveFile(resource:haxe.io.Bytes)` instead.
|
||||
*
|
||||
* @param typeFilter TODO What does this do?
|
||||
* @return Whether the file dialog was opened successfully.
|
||||
*/
|
||||
public static function browseForSaveFile(?typeFilter:Array<FileFilter>, ?onSelect:String->Void, ?onCancel:Void->Void, ?defaultPath:String,
|
||||
?dialogTitle:String):Bool
|
||||
{
|
||||
#if desktop
|
||||
var filter = convertTypeFilter(typeFilter);
|
||||
|
||||
var fileDialog = new FileDialog();
|
||||
if (onSelect != null)
|
||||
fileDialog.onSelect.add(onSelect);
|
||||
if (onCancel != null)
|
||||
fileDialog.onCancel.add(onCancel);
|
||||
|
||||
fileDialog.browse(SAVE, filter, defaultPath, dialogTitle);
|
||||
return true;
|
||||
#elseif html5
|
||||
onCancel();
|
||||
return false;
|
||||
#else
|
||||
onCancel();
|
||||
return false;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Browses for a single file location, then reads it and passes it to `onOpen(resource:haxe.io.Bytes)`.
|
||||
* Works great on desktop and HTML5.
|
||||
*
|
||||
* @param typeFilter TODO What does this do?
|
||||
* @return Whether the file dialog was opened successfully.
|
||||
*/
|
||||
public static function openFile(?typeFilter:Array<FileFilter>, ?onOpen:Bytes->Void, ?onCancel:Void->Void, ?defaultPath:String, ?dialogTitle:String):Bool
|
||||
{
|
||||
#if desktop
|
||||
var filter = convertTypeFilter(typeFilter);
|
||||
|
||||
var fileDialog = new FileDialog();
|
||||
if (onOpen != null)
|
||||
fileDialog.onOpen.add(onOpen);
|
||||
if (onCancel != null)
|
||||
fileDialog.onCancel.add(onCancel);
|
||||
|
||||
fileDialog.open(filter, defaultPath, dialogTitle);
|
||||
return true;
|
||||
#elseif html5
|
||||
var filter = convertTypeFilter(typeFilter);
|
||||
|
||||
var onFileLoaded = function(event)
|
||||
{
|
||||
var loadedFileRef:FileReference = event.target;
|
||||
trace('Loaded file: ' + loadedFileRef.name);
|
||||
onOpen(loadedFileRef.data);
|
||||
}
|
||||
|
||||
var onFileSelected = function(event)
|
||||
{
|
||||
var selectedFileRef:FileReference = event.target;
|
||||
trace('Selected file: ' + selectedFileRef.name);
|
||||
selectedFileRef.addEventListener(Event.COMPLETE, onFileLoaded);
|
||||
selectedFileRef.load();
|
||||
}
|
||||
|
||||
var fileRef = new FileReference();
|
||||
file.addEventListener(Event.SELECT, onFileSelected);
|
||||
file.open(filter, defaultPath, dialogTitle);
|
||||
#else
|
||||
onCancel();
|
||||
return false;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Browses for a single file location, then writes the provided `haxe.io.Bytes` data and calls `onSave(path)` when done.
|
||||
* Works great on desktop and HTML5.
|
||||
*
|
||||
* @param typeFilter TODO What does this do?
|
||||
* @return Whether the file dialog was opened successfully.
|
||||
*/
|
||||
public static function saveFile(data:Bytes, ?onSave:String->Void, ?onCancel:Void->Void, ?defaultFileName:String, ?dialogTitle:String):Bool
|
||||
{
|
||||
#if desktop
|
||||
var filter = defaultFileName != null ? Path.extension(defaultFileName) : null;
|
||||
|
||||
var fileDialog = new FileDialog();
|
||||
if (onSave != null)
|
||||
fileDialog.onSelect.add(onSave);
|
||||
if (onCancel != null)
|
||||
fileDialog.onCancel.add(onCancel);
|
||||
|
||||
fileDialog.save(data, filter, defaultFileName, dialogTitle);
|
||||
return true;
|
||||
#elseif html5
|
||||
var filter = defaultFileName != null ? Path.extension(defaultFileName) : null;
|
||||
|
||||
var fileDialog = new FileDialog();
|
||||
if (onSave != null)
|
||||
fileDialog.onSave.add(onSave);
|
||||
if (onCancel != null)
|
||||
fileDialog.onCancel.add(onCancel);
|
||||
|
||||
fileDialog.save(data, filter, defaultFileName, dialogTitle);
|
||||
#else
|
||||
onCancel();
|
||||
return false;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompts the user to save multiple files.
|
||||
* On desktop, this will prompt the user for a directory, then write all of the files to there.
|
||||
* On HTML5, this will zip the files up and prompt the user to save that.
|
||||
*
|
||||
* @param typeFilter TODO What does this do?
|
||||
* @return Whether the file dialog was opened successfully.
|
||||
*/
|
||||
public static function saveMultipleFiles(resources:Array<Entry>, ?onSaveAll:Array<String>->Void, ?onCancel:Void->Void, ?defaultPath:String,
|
||||
?force:Bool = false):Bool
|
||||
{
|
||||
#if desktop
|
||||
// Prompt the user for a directory, then write all of the files to there.
|
||||
var onSelectDir = function(targetPath:String)
|
||||
{
|
||||
var paths:Array<String> = [];
|
||||
for (resource in resources)
|
||||
{
|
||||
var filePath = haxe.io.Path.join([targetPath, resource.fileName]);
|
||||
try
|
||||
{
|
||||
if (resource.data == null)
|
||||
{
|
||||
trace('WARNING: File $filePath has no data or content. Skipping.');
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
writeBytesToPath(filePath, resource.data, force ? Force : Skip);
|
||||
}
|
||||
}
|
||||
catch (e:Dynamic)
|
||||
{
|
||||
trace('Failed to write file (probably already exists): $filePath' + filePath);
|
||||
continue;
|
||||
}
|
||||
paths.push(filePath);
|
||||
}
|
||||
onSaveAll(paths);
|
||||
}
|
||||
|
||||
browseForDirectory(null, onSelectDir, onCancel, defaultPath, "Choose directory to save all files to...");
|
||||
|
||||
return true;
|
||||
#elseif html5
|
||||
saveFilesAsZIP(resources, onSaveAll, onCancel, defaultPath, force);
|
||||
|
||||
return true;
|
||||
#else
|
||||
onCancel();
|
||||
return false;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an array of file entries and prompts the user to save them as a ZIP file.
|
||||
*/
|
||||
public static function saveFilesAsZIP(resources:Array<Entry>, ?onSave:Array<String>->Void, ?onCancel:Void->Void, ?defaultPath:String,
|
||||
?force:Bool = false):Bool
|
||||
{
|
||||
// Create a ZIP file.
|
||||
var zipBytes = createZIPFromEntries(resources);
|
||||
|
||||
var onSave = function(path:String)
|
||||
{
|
||||
onSave([path]);
|
||||
};
|
||||
|
||||
// Prompt the user to save the ZIP file.
|
||||
saveFile(zipBytes, onSave, onCancel, defaultPath, "Save files as ZIP...");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes an array of file entries and forcibly writes a ZIP to the given path.
|
||||
* Only works on desktop, because HTML5 doesn't allow you to write files to arbitrary paths.
|
||||
* Use `saveFilesAsZIP` instead.
|
||||
* @param force Whether to force overwrite an existing file.
|
||||
*/
|
||||
public static function saveFilesAsZIPToPath(resources:Array<Entry>, path:String, ?force:Bool = false):Bool
|
||||
{
|
||||
#if desktop
|
||||
// Create a ZIP file.
|
||||
var zipBytes = createZIPFromEntries(resources);
|
||||
|
||||
// Write the ZIP.
|
||||
writeBytesToPath(path, zipBytes, force ? Force : Skip);
|
||||
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Write string file contents directly to a given path.
|
||||
* Only works on desktop.
|
||||
*
|
||||
* @param mode Whether to Force, Skip, or Ask to overwrite an existing file.
|
||||
*/
|
||||
public static function writeStringToPath(path:String, data:String, mode:FileWriteMode = Skip)
|
||||
{
|
||||
#if sys
|
||||
createDirIfNotExists(Path.directory(path));
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case Force:
|
||||
sys.io.File.saveContent(path, data);
|
||||
case Skip:
|
||||
if (!sys.FileSystem.exists(path))
|
||||
{
|
||||
sys.io.File.saveContent(path, data);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw 'File already exists: $path';
|
||||
}
|
||||
case Ask:
|
||||
if (sys.FileSystem.exists(path))
|
||||
{
|
||||
// TODO: We don't have the technology to use native popups yet.
|
||||
}
|
||||
else
|
||||
{
|
||||
sys.io.File.saveContent(path, data);
|
||||
}
|
||||
}
|
||||
#else
|
||||
throw 'Direct file writing by path not supported on this platform.';
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Write byte file contents directly to a given path.
|
||||
* Only works on desktop.
|
||||
*
|
||||
* @param mode Whether to Force, Skip, or Ask to overwrite an existing file.
|
||||
*/
|
||||
public static function writeBytesToPath(path:String, data:Bytes, mode:FileWriteMode = Skip)
|
||||
{
|
||||
#if sys
|
||||
createDirIfNotExists(Path.directory(path));
|
||||
|
||||
switch (mode)
|
||||
{
|
||||
case Force:
|
||||
sys.io.File.saveBytes(path, data);
|
||||
case Skip:
|
||||
if (!sys.FileSystem.exists(path))
|
||||
{
|
||||
sys.io.File.saveBytes(path, data);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw 'File already exists: $path';
|
||||
}
|
||||
case Ask:
|
||||
if (sys.FileSystem.exists(path))
|
||||
{
|
||||
// TODO: We don't have the technology to use native popups yet.
|
||||
}
|
||||
else
|
||||
{
|
||||
sys.io.File.saveBytes(path, data);
|
||||
}
|
||||
}
|
||||
#else
|
||||
throw 'Direct file writing by path not supported on this platform.';
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Write string file contents directly to the end of a file at the given path.
|
||||
* Only works on desktop.
|
||||
*/
|
||||
public static function appendStringToPath(path:String, data:String)
|
||||
{
|
||||
sys.io.File.append(path, false).writeString(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a directory if it doesn't already exist.
|
||||
* Only works on desktop.
|
||||
*/
|
||||
public static function createDirIfNotExists(dir:String)
|
||||
{
|
||||
#if sys
|
||||
if (!sys.FileSystem.exists(dir))
|
||||
{
|
||||
sys.FileSystem.createDirectory(dir);
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
||||
static var tempDir:String = null;
|
||||
static final TEMP_ENV_VARS:Array<String> = ['TEMP', 'TMPDIR', 'TEMPDIR', 'TMP'];
|
||||
|
||||
/**
|
||||
* Get the path to a temporary directory we can use for writing files.
|
||||
* Only works on desktop.
|
||||
*/
|
||||
public static function getTempDir():String
|
||||
{
|
||||
if (tempDir != null)
|
||||
return tempDir;
|
||||
|
||||
#if sys
|
||||
#if windows
|
||||
var path:String = null;
|
||||
|
||||
for (envName in TEMP_ENV_VARS)
|
||||
{
|
||||
path = Sys.getEnv(envName);
|
||||
|
||||
if (path == "")
|
||||
path = null;
|
||||
if (path != null)
|
||||
break;
|
||||
}
|
||||
|
||||
return tempDir = Path.join([path, 'funkin/']);
|
||||
#else
|
||||
return tempDir = '/tmp/funkin/';
|
||||
#end
|
||||
#else
|
||||
return null;
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Bytes object containing a ZIP file, containing the provided entries.
|
||||
*
|
||||
* @param entries The entries to add to the ZIP file.
|
||||
* @return The ZIP file as a Bytes object.
|
||||
*/
|
||||
public static function createZIPFromEntries(entries:Array<Entry>):Bytes
|
||||
{
|
||||
var o = new haxe.io.BytesOutput();
|
||||
|
||||
var zipWriter = new haxe.zip.Writer(o);
|
||||
zipWriter.write(entries.list());
|
||||
|
||||
return o.getBytes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ZIP file entry from a file name and its string contents.
|
||||
*
|
||||
* @param name The name of the file. You can use slashes to create subdirectories.
|
||||
* @param content The string contents of the file.
|
||||
* @return The resulting entry.
|
||||
*/
|
||||
public static function makeZIPEntry(name:String, content:String):Entry
|
||||
{
|
||||
var data = haxe.io.Bytes.ofString(content, UTF8);
|
||||
|
||||
return {
|
||||
fileName: name,
|
||||
fileSize: data.length,
|
||||
|
||||
data: data,
|
||||
dataSize: data.length,
|
||||
|
||||
compressed: false,
|
||||
|
||||
fileTime: Date.now(),
|
||||
crc32: null,
|
||||
extraFields: null,
|
||||
};
|
||||
}
|
||||
|
||||
static function convertTypeFilter(typeFilter:Array<FileFilter>):String
|
||||
{
|
||||
var filter = null;
|
||||
if (typeFilter != null)
|
||||
{
|
||||
var filters = [];
|
||||
for (type in typeFilter)
|
||||
{
|
||||
filters.push(StringTools.replace(StringTools.replace(type.extension, "*.", ""), ";", ","));
|
||||
}
|
||||
filter = filters.join(";");
|
||||
}
|
||||
|
||||
return filter;
|
||||
}
|
||||
}
|
||||
|
||||
enum FileWriteMode
|
||||
{
|
||||
/**
|
||||
* Forcibly overwrite the file if it already exists.
|
||||
*/
|
||||
Force;
|
||||
|
||||
/**
|
||||
* Ask the user if they want to overwrite the file if it already exists.
|
||||
*/
|
||||
Ask;
|
||||
|
||||
/**
|
||||
* Skip the file if it already exists.
|
||||
*/
|
||||
Skip;
|
||||
}
|
2
source/funkin/util/SaveDataUtil.hx
Normal file
2
source/funkin/util/SaveDataUtil.hx
Normal file
|
@ -0,0 +1,2 @@
|
|||
package funkin.util;
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package funkin.util;
|
||||
|
||||
import flixel.util.FlxSignal.FlxTypedSignal;
|
||||
|
||||
class WindowUtil
|
||||
{
|
||||
public static function openURL(targetUrl:String)
|
||||
|
@ -12,7 +14,23 @@ class WindowUtil
|
|||
FlxG.openURL(targetUrl);
|
||||
#end
|
||||
#else
|
||||
trace('Cannot open')
|
||||
trace('Cannot open');
|
||||
#end
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatched when the game window is closed.
|
||||
*/
|
||||
public static final windowExit:FlxTypedSignal<Int->Void> = new FlxTypedSignal<Int->Void>();
|
||||
|
||||
public static function initWindowEvents()
|
||||
{
|
||||
// onUpdate is called every frame just before rendering.
|
||||
|
||||
// onExit is called when the game window is closed.
|
||||
openfl.Lib.current.stage.application.onExit.add(function(exitCode:Int)
|
||||
{
|
||||
windowExit.dispatch(exitCode);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package funkin.util.assets;
|
||||
|
||||
using StringTools;
|
||||
|
||||
class DataAssets
|
||||
{
|
||||
static function buildDataPath(path:String):String
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package funkin.util;
|
||||
package funkin.util.tools;
|
||||
|
||||
/**
|
||||
* A static extension which provides utility functions for Iterators.
|
59
source/funkin/util/tools/StringTools.hx
Normal file
59
source/funkin/util/tools/StringTools.hx
Normal file
|
@ -0,0 +1,59 @@
|
|||
package funkin.util.tools;
|
||||
|
||||
/**
|
||||
* A static extension which provides utility functions for Strings.
|
||||
*/
|
||||
class StringTools
|
||||
{
|
||||
/**
|
||||
* Converts a string to title case. For example, "hello world" becomes "Hello World".
|
||||
*
|
||||
* @param value The string to convert.
|
||||
* @return The converted string.
|
||||
*/
|
||||
public static function toTitleCase(value:String):String
|
||||
{
|
||||
var words:Array<String> = value.split(" ");
|
||||
var result:String = "";
|
||||
for (i in 0...words.length)
|
||||
{
|
||||
var word:String = words[i];
|
||||
result += word.charAt(0).toUpperCase() + word.substr(1).toLowerCase();
|
||||
if (i < words.length - 1)
|
||||
{
|
||||
result += " ";
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string to lower kebab case. For example, "Hello World" becomes "hello-world".
|
||||
*
|
||||
* @param value The string to convert.
|
||||
* @return The converted string.
|
||||
*/
|
||||
public static function toLowerKebabCase(value:String):String {
|
||||
return value.toLowerCase().replace(' ', "-");
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string to upper kebab case, aka screaming kebab case. For example, "Hello World" becomes "HELLO-WORLD".
|
||||
*
|
||||
* @param value The string to convert.
|
||||
* @return The converted string.
|
||||
*/
|
||||
public static function toUpperKebabCase(value:String):String {
|
||||
return value.toUpperCase().replace(' ', "-");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the string data as JSON and returns the resulting object.
|
||||
*
|
||||
* @return The parsed object.
|
||||
*/
|
||||
public static function parseJSON(value:String):Dynamic
|
||||
{
|
||||
return SerializerUtil.fromJSON(value);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue