mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2025-08-31 02:45:13 +00:00
Compare commits
136 commits
211ae319d0
...
7392371a39
Author | SHA1 | Date | |
---|---|---|---|
|
7392371a39 | ||
|
2a38359443 | ||
|
11bb30cd67 | ||
|
b772bcfb2f | ||
|
203e8eb372 | ||
|
e097553f7d | ||
|
a15e3810fe | ||
|
039e1b07c9 | ||
|
250c661576 | ||
|
ec464969b4 | ||
|
0cab4ae169 | ||
|
41984e78af | ||
|
467a54e212 | ||
|
b5bfb27185 | ||
|
229803dc05 | ||
|
c6c5f0c028 | ||
|
7508e4e1a6 | ||
|
03e8ef80c2 | ||
|
91cb1da992 | ||
|
dff8bc623d | ||
|
159e3b8f29 | ||
|
719d115616 | ||
|
6674c1583b | ||
|
1937a4a04e | ||
|
5fb445b99b | ||
|
911c70311b | ||
|
fe335be607 | ||
|
33e8eed060 | ||
|
f35bc4e944 | ||
|
4b82b001c6 | ||
|
fad7528372 | ||
|
85cce30ec8 | ||
|
b52a7d08ff | ||
|
137ffb91c9 | ||
|
f531cf3141 | ||
|
8179beca84 | ||
|
909669f0dc | ||
|
81aca6a045 | ||
|
2ed1ebb8bc | ||
|
508540e117 | ||
|
dca47d382c | ||
|
d12d01c402 | ||
|
d10f2a4b82 | ||
|
5700eba599 | ||
|
85de2a9246 | ||
|
d9ffe55d06 | ||
|
e28fc1478d | ||
|
a772fd2a20 | ||
|
0d32f15658 | ||
|
bd3cd1fd3b | ||
|
9e4365e43f | ||
|
cacd044467 | ||
|
589c01972d | ||
|
460536beba | ||
|
98c9cee395 | ||
|
f17aa92f60 | ||
|
a36a3f0576 | ||
|
a4ee5f9048 | ||
|
1c68fbac61 | ||
|
f1346feffb | ||
|
b1019b822f | ||
|
fbb07501d2 | ||
|
8018cf6ff0 | ||
|
2d14304461 | ||
|
e31ef3665a | ||
|
100a97ba97 | ||
|
1ccd12cfcd | ||
|
a3755d113d | ||
|
451ee3b008 | ||
|
8625f5807a | ||
|
79425bfd62 | ||
|
564b72f90b | ||
|
c1ad4426f2 | ||
|
f597a87d10 | ||
|
15f07e8e26 | ||
|
cb60bb9b68 | ||
|
e563c2c59d | ||
|
65a2673e38 | ||
|
8fcc841e79 | ||
|
9626f354ef | ||
|
57ee401f8e | ||
|
a4ef2514c1 | ||
|
dcc989782e | ||
|
6b712e71e6 | ||
|
bbcc5ab81e | ||
|
1c59256871 | ||
|
3bb5d7c12f | ||
|
cb9e335d79 | ||
|
c59fb53142 | ||
|
afb90615e5 | ||
|
5333cdf0bc | ||
|
36cef9f915 | ||
|
cc8aff94a8 | ||
|
9420842f5e | ||
|
63da7fcab1 | ||
|
456596a34b | ||
|
e640ba1274 | ||
|
42226e32f2 | ||
|
08d1eff7af | ||
|
3d65a18c00 | ||
|
568e57c32f | ||
|
c0f9281ee4 | ||
|
621b4b8ccc | ||
|
940ece9c07 | ||
|
c9405ec609 | ||
|
62058ee971 | ||
|
9a01083d9f | ||
|
64f25395c5 | ||
|
bdc044ce04 | ||
|
9cffb3fd6b | ||
|
50a8e47293 | ||
|
923078bd57 | ||
|
7c136ff38f | ||
|
01b8f69553 | ||
|
fd53ff89a4 | ||
|
657564322d | ||
|
605b5ba5c2 | ||
|
5a4d8344d2 | ||
|
1794a9e57e | ||
|
39c2e7136d | ||
|
d1d96f58e2 | ||
|
3b667044e1 | ||
|
726cda48c9 | ||
|
e736229024 | ||
|
1fecb6ea3e | ||
|
4294e9a6ec | ||
|
a73be72ba0 | ||
|
7947ecaa00 | ||
|
5801411be0 | ||
|
f16e42368b | ||
|
a9bd80d5ec | ||
|
908d6ca834 | ||
|
7be65fc2bd | ||
|
62b2815d04 | ||
|
caf56d496c | ||
|
2a815e91db |
10
.vscode/settings.json
vendored
10
.vscode/settings.json
vendored
|
@ -150,6 +150,16 @@
|
|||
"target": "windows",
|
||||
"args": ["-debug", "-DRESULTS"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Straight to Stage Editor)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DSTAGING", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Straight to Stage Builder)",
|
||||
"target": "windows",
|
||||
"args": ["-debug", "-DSTAGEBUILD", "-DFEATURE_DEBUG_FUNCTIONS"]
|
||||
},
|
||||
{
|
||||
"label": "Windows / Debug (Straight to Animation Editor)",
|
||||
"target": "windows",
|
||||
|
|
14
alsoft.txt
Normal file
14
alsoft.txt
Normal file
|
@ -0,0 +1,14 @@
|
|||
[general]
|
||||
sample-type=float32
|
||||
period_size=441
|
||||
stereo-mode=speakers
|
||||
stereo-encoding=panpot
|
||||
hrtf=false
|
||||
cf_level=0
|
||||
resampler=fast_bsinc24
|
||||
front-stablizer=false
|
||||
output-limiter=false
|
||||
[decoder]
|
||||
hq-mode=false
|
||||
distance-comp=false
|
||||
nfc=false
|
2
art
2
art
|
@ -1 +1 @@
|
|||
Subproject commit 8402339e2e63ae99c441941a46d54bf3f0c0d5fa
|
||||
Subproject commit 78dc310219370144719b4eeef9b3b511c5a44532
|
2
assets
2
assets
|
@ -1 +1 @@
|
|||
Subproject commit c108a7ff0d11bf328e7b232160b8f68c71e21bca
|
||||
Subproject commit 88778a44853255b082ada57e9cb77d8fb4956494
|
4
hmm.json
4
hmm.json
|
@ -77,7 +77,7 @@
|
|||
"name": "hscript",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "27c86f9a761c1d16d4433c4cf252eccb7b2e18de",
|
||||
"ref": "d60bb2947fa609fdc875ccfae89666a6984eeaf2",
|
||||
"url": "https://github.com/FunkinCrew/hscript"
|
||||
},
|
||||
{
|
||||
|
@ -192,7 +192,7 @@
|
|||
"name": "polymod",
|
||||
"type": "git",
|
||||
"dir": null,
|
||||
"ref": "0fbdf27fe124549730accd540cec8a183f8652c0",
|
||||
"ref": "3e030c81de99ca84acde681431f806d8103bcf6e",
|
||||
"url": "https://github.com/larsiusprime/polymod"
|
||||
},
|
||||
{
|
||||
|
|
1896
project.hxp
1896
project.hxp
File diff suppressed because it is too large
Load diff
|
@ -6,6 +6,7 @@ import flixel.math.FlxMath;
|
|||
import funkin.data.song.SongData.SongTimeChange;
|
||||
import funkin.data.song.SongDataUtils;
|
||||
import funkin.save.Save;
|
||||
import funkin.util.TimerUtil.SongSequence;
|
||||
import haxe.Timer;
|
||||
import flixel.sound.FlxSound;
|
||||
|
||||
|
@ -92,6 +93,12 @@ class Conductor
|
|||
*/
|
||||
public var songPosition(default, null):Float = 0;
|
||||
|
||||
/**
|
||||
* The offset between frame time and music time.
|
||||
* Used in `getTimeWithDelta()` to get a more accurate music time when on higher framerates.
|
||||
*/
|
||||
var songPositionDelta(default, null):Float = 0;
|
||||
|
||||
var prevTimestamp:Float = 0;
|
||||
var prevTime:Float = 0;
|
||||
|
||||
|
@ -422,7 +429,8 @@ class Conductor
|
|||
// If the song is playing, limit the song position to the length of the song or beginning of the song.
|
||||
if (FlxG.sound.music != null && FlxG.sound.music.playing)
|
||||
{
|
||||
this.songPosition = Math.min(currentLength, Math.max(0, songPos));
|
||||
this.songPosition = FlxMath.bound(Math.min(this.combinedOffset, 0), songPos, currentLength);
|
||||
this.songPositionDelta += FlxG.elapsed * 1000 * FlxG.sound.music.pitch;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -488,10 +496,23 @@ class Conductor
|
|||
// which it doesn't do every frame!
|
||||
if (prevTime != this.songPosition)
|
||||
{
|
||||
this.songPositionDelta = 0;
|
||||
|
||||
// Update the timestamp for use in-between frames
|
||||
prevTime = this.songPosition;
|
||||
prevTimestamp = Std.int(Timer.stamp() * 1000);
|
||||
}
|
||||
|
||||
if (this == Conductor.instance) @:privateAccess SongSequence.update.dispatch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a more accurate music time for higher framerates.
|
||||
* @return Float
|
||||
*/
|
||||
public function getTimeWithDelta():Float
|
||||
{
|
||||
return this.songPosition + this.songPositionDelta;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -3,6 +3,7 @@ package funkin;
|
|||
/**
|
||||
* A core class which handles tracking score and combo for the current song.
|
||||
*/
|
||||
@:nullSafety
|
||||
class Highscore
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -27,6 +27,7 @@ import funkin.play.character.CharacterData.CharacterDataParser;
|
|||
import funkin.play.notes.notekind.NoteKindManager;
|
||||
import funkin.play.PlayStatePlaylist;
|
||||
import funkin.ui.debug.charting.ChartEditorState;
|
||||
import funkin.ui.debug.stageeditor.StageEditorState;
|
||||
import funkin.ui.title.TitleState;
|
||||
import funkin.ui.transition.LoadingState;
|
||||
import funkin.util.CLIUtil;
|
||||
|
@ -51,6 +52,7 @@ import funkin.api.newgrounds.NewgroundsClient;
|
|||
*
|
||||
* It should not contain any sprites or rendering.
|
||||
*/
|
||||
@:nullSafety
|
||||
class InitState extends FlxState
|
||||
{
|
||||
/**
|
||||
|
@ -241,6 +243,9 @@ class InitState extends FlxState
|
|||
#elseif CHARTING
|
||||
// -DCHARTING
|
||||
FlxG.switchState(() -> new funkin.ui.debug.charting.ChartEditorState());
|
||||
#elseif STAGING
|
||||
// -DSTAGING
|
||||
FlxG.switchState(() -> new funkin.ui.debug.stageeditor.StageEditorState());
|
||||
#elseif STAGEBUILD
|
||||
// -DSTAGEBUILD
|
||||
FlxG.switchState(() -> new funkin.ui.debug.stage.StageBuilderState());
|
||||
|
@ -303,6 +308,13 @@ class InitState extends FlxState
|
|||
fnfcTargetPath: params.chart.chartPath,
|
||||
}));
|
||||
}
|
||||
else if (params.stage.shouldLoadStage)
|
||||
{
|
||||
FlxG.switchState(() -> new StageEditorState(
|
||||
{
|
||||
fnfsTargetPath: params.stage.stagePath,
|
||||
}));
|
||||
}
|
||||
else
|
||||
{
|
||||
FlxG.sound.cache(Paths.music('freakyMenu/freakyMenu'));
|
||||
|
@ -317,7 +329,7 @@ class InitState extends FlxState
|
|||
*/
|
||||
function startSong(songId:String, difficultyId:String = 'normal'):Void
|
||||
{
|
||||
var songData:funkin.play.song.Song = funkin.data.song.SongRegistry.instance.fetchEntry(songId);
|
||||
var songData:Null<funkin.play.song.Song> = funkin.data.song.SongRegistry.instance.fetchEntry(songId);
|
||||
|
||||
if (songData == null)
|
||||
{
|
||||
|
@ -354,6 +366,7 @@ class InitState extends FlxState
|
|||
PlayStatePlaylist.campaignId = 'weekend1';
|
||||
}
|
||||
|
||||
@:nullSafety(Off) // Cannot unify?
|
||||
LoadingState.loadPlayState(
|
||||
{
|
||||
targetSong: songData,
|
||||
|
@ -368,7 +381,7 @@ class InitState extends FlxState
|
|||
*/
|
||||
function startLevel(levelId:String, difficultyId:String = 'normal'):Void
|
||||
{
|
||||
var currentLevel:funkin.ui.story.Level = funkin.data.story.level.LevelRegistry.instance.fetchEntry(levelId);
|
||||
var currentLevel:Null<funkin.ui.story.Level> = funkin.data.story.level.LevelRegistry.instance.fetchEntry(levelId);
|
||||
|
||||
if (currentLevel == null)
|
||||
{
|
||||
|
@ -384,10 +397,19 @@ class InitState extends FlxState
|
|||
PlayStatePlaylist.isStoryMode = true;
|
||||
PlayStatePlaylist.campaignScore = 0;
|
||||
|
||||
var targetSongId:String = PlayStatePlaylist.playlistSongIds.shift();
|
||||
var targetSongId:Null<String> = PlayStatePlaylist.playlistSongIds.shift();
|
||||
|
||||
var targetSong:funkin.play.song.Song = SongRegistry.instance.fetchEntry(targetSongId);
|
||||
var targetSong:Null<funkin.play.song.Song> = null;
|
||||
|
||||
if (targetSongId != null) targetSong = SongRegistry.instance.fetchEntry(targetSongId);
|
||||
|
||||
if (targetSongId == null)
|
||||
{
|
||||
startGameNormally();
|
||||
return;
|
||||
}
|
||||
|
||||
@:nullSafety(Off)
|
||||
LoadingState.loadPlayState(
|
||||
{
|
||||
targetSong: targetSong,
|
||||
|
@ -395,6 +417,7 @@ class InitState extends FlxState
|
|||
});
|
||||
}
|
||||
|
||||
@:nullSafety(Off) // Meh, remove when flixel.system.debug.log.LogStyle is null safe
|
||||
function setupFlixelDebug():Void
|
||||
{
|
||||
//
|
||||
|
@ -474,17 +497,17 @@ class InitState extends FlxState
|
|||
#end
|
||||
}
|
||||
|
||||
function defineSong():String
|
||||
function defineSong():Null<String>
|
||||
{
|
||||
return MacroUtil.getDefine('SONG');
|
||||
}
|
||||
|
||||
function defineLevel():String
|
||||
function defineLevel():Null<String>
|
||||
{
|
||||
return MacroUtil.getDefine('LEVEL');
|
||||
}
|
||||
|
||||
function defineDifficulty():String
|
||||
function defineDifficulty():Null<String>
|
||||
{
|
||||
return MacroUtil.getDefine('DIFFICULTY');
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import openfl.utils.AssetType;
|
|||
/**
|
||||
* A core class which handles determining asset paths.
|
||||
*/
|
||||
@:nullSafety
|
||||
class Paths
|
||||
{
|
||||
static var currentLevel:Null<String> = null;
|
||||
|
@ -136,7 +137,7 @@ class Paths
|
|||
* @param withExtension if it should return with the audio file extension `.mp3` or `.ogg`.
|
||||
* @return String
|
||||
*/
|
||||
public static function inst(song:String, ?suffix:String = '', ?withExtension:Bool = true):String
|
||||
public static function inst(song:String, ?suffix:String = '', withExtension:Bool = true):String
|
||||
{
|
||||
var ext:String = withExtension ? '.${Constants.EXT_SOUND}' : '';
|
||||
return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix$ext';
|
||||
|
|
|
@ -9,12 +9,17 @@ import flixel.util.FlxSignal.FlxTypedSignal;
|
|||
/**
|
||||
* A core class which represents the current player(s) and their controls and other configuration.
|
||||
*/
|
||||
@:nullSafety
|
||||
class PlayerSettings
|
||||
{
|
||||
// TODO: Finish implementation of second player.
|
||||
public static var numPlayers(default, null) = 0;
|
||||
public static var numAvatars(default, null) = 0;
|
||||
// TODO: Making both of these null makes a lot of errors with the controls.
|
||||
// That'd explain why unplugging input devices can cause the game to crash?
|
||||
@:nullSafety(Off)
|
||||
public static var player1(default, null):PlayerSettings;
|
||||
@:nullSafety(Off)
|
||||
public static var player2(default, null):PlayerSettings;
|
||||
|
||||
public static var onAvatarAdd(default, null) = new FlxTypedSignal<PlayerSettings->Void>();
|
||||
|
@ -70,6 +75,7 @@ class PlayerSettings
|
|||
/**
|
||||
* Forcibly destroy the PlayerSettings singletons for each player.
|
||||
*/
|
||||
@:nullSafety(Off)
|
||||
public static function reset():Void
|
||||
{
|
||||
player1 = null;
|
||||
|
|
|
@ -6,6 +6,7 @@ import funkin.util.WindowUtil;
|
|||
/**
|
||||
* A core class which provides a store of user-configurable, globally relevant values.
|
||||
*/
|
||||
@:nullSafety
|
||||
class Preferences
|
||||
{
|
||||
/**
|
||||
|
@ -45,7 +46,7 @@ class Preferences
|
|||
|
||||
static function get_naughtyness():Bool
|
||||
{
|
||||
return Save?.instance?.options?.naughtyness;
|
||||
return Save?.instance?.options?.naughtyness ?? true;
|
||||
}
|
||||
|
||||
static function set_naughtyness(value:Bool):Bool
|
||||
|
@ -64,7 +65,7 @@ class Preferences
|
|||
|
||||
static function get_downscroll():Bool
|
||||
{
|
||||
return Save?.instance?.options?.downscroll;
|
||||
return Save?.instance?.options?.downscroll ?? false;
|
||||
}
|
||||
|
||||
static function set_downscroll(value:Bool):Bool
|
||||
|
@ -102,7 +103,7 @@ class Preferences
|
|||
|
||||
static function get_zoomCamera():Bool
|
||||
{
|
||||
return Save?.instance?.options?.zoomCamera;
|
||||
return Save?.instance?.options?.zoomCamera ?? true;
|
||||
}
|
||||
|
||||
static function set_zoomCamera(value:Bool):Bool
|
||||
|
@ -121,7 +122,7 @@ class Preferences
|
|||
|
||||
static function get_debugDisplay():Bool
|
||||
{
|
||||
return Save?.instance?.options?.debugDisplay;
|
||||
return Save?.instance?.options?.debugDisplay ?? false;
|
||||
}
|
||||
|
||||
static function set_debugDisplay(value:Bool):Bool
|
||||
|
@ -228,7 +229,7 @@ class Preferences
|
|||
|
||||
static function get_unlockedFramerate():Bool
|
||||
{
|
||||
return Save?.instance?.options?.unlockedFramerate;
|
||||
return Save?.instance?.options?.unlockedFramerate ?? false;
|
||||
}
|
||||
|
||||
static function set_unlockedFramerate(value:Bool):Bool
|
||||
|
@ -343,44 +344,6 @@ class Preferences
|
|||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The game will save any screenshots taken to this format.
|
||||
* @default `PNG`
|
||||
*/
|
||||
public static var saveFormat(get, set):Any;
|
||||
|
||||
static function get_saveFormat():Any
|
||||
{
|
||||
return Save?.instance?.options?.screenshot?.saveFormat ?? 'PNG';
|
||||
}
|
||||
|
||||
static function set_saveFormat(value):Any
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.screenshot.saveFormat = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The game will save JPEG screenshots with this quality percentage.
|
||||
* @default `80`
|
||||
*/
|
||||
public static var jpegQuality(get, set):Int;
|
||||
|
||||
static function get_jpegQuality():Int
|
||||
{
|
||||
return Save?.instance?.options?.screenshot?.jpegQuality ?? 80;
|
||||
}
|
||||
|
||||
static function set_jpegQuality(value:Int):Int
|
||||
{
|
||||
var save:Save = Save.instance;
|
||||
save.options.screenshot.jpegQuality = value;
|
||||
save.flush();
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the user's preferences from the save data and apply them.
|
||||
*/
|
||||
|
|
|
@ -8,6 +8,7 @@ import hxdiscord_rpc.Types.DiscordRichPresence;
|
|||
import hxdiscord_rpc.Types.DiscordUser;
|
||||
import sys.thread.Thread;
|
||||
|
||||
@:nullSafety
|
||||
class DiscordClient
|
||||
{
|
||||
static final CLIENT_ID:String = "816168432860790794";
|
||||
|
@ -40,12 +41,12 @@ class DiscordClient
|
|||
trace('[DISCORD] Initializing connection...');
|
||||
|
||||
// Discord.initialize(CLIENT_ID, handlers, true, null);
|
||||
Discord.Initialize(CLIENT_ID, cpp.RawPointer.addressOf(handlers), 1, null);
|
||||
Discord.Initialize(CLIENT_ID, cpp.RawPointer.addressOf(handlers), 1, "");
|
||||
|
||||
createDaemon();
|
||||
}
|
||||
|
||||
var daemon:Thread = null;
|
||||
var daemon:Null<Thread> = null;
|
||||
|
||||
function createDaemon():Void
|
||||
{
|
||||
|
@ -56,8 +57,6 @@ class DiscordClient
|
|||
{
|
||||
while (true)
|
||||
{
|
||||
trace('[DISCORD] Performing client update...');
|
||||
|
||||
#if DISCORD_DISABLE_IO_THREAD
|
||||
Discord.updateConnection();
|
||||
#end
|
||||
|
@ -76,8 +75,6 @@ class DiscordClient
|
|||
|
||||
public function setPresence(params:DiscordClientPresenceParams):Void
|
||||
{
|
||||
trace('[DISCORD] Updating presence... (${params})');
|
||||
|
||||
Discord.updatePresence(buildPresence(params));
|
||||
}
|
||||
|
||||
|
@ -92,17 +89,15 @@ class DiscordClient
|
|||
presence.largeImageText = "Friday Night Funkin'";
|
||||
|
||||
// State should be generally what the person is doing, like "In the Menus" or "Pico (Pico Mix) [Freeplay Hard]"
|
||||
presence.state = cast(params.state, Null<String>);
|
||||
presence.state = cast(params.state, Null<String>) ?? "";
|
||||
// Details should be what the person is specifically doing, including stuff like timestamps (maybe something like "03:24 elapsed").
|
||||
presence.details = cast(params.details, Null<String>);
|
||||
presence.details = cast(params.details, Null<String>) ?? "";
|
||||
|
||||
// The large image displaying what the user is doing.
|
||||
// This should probably be album art.
|
||||
// IMPORTANT NOTE: This can be an asset key uploaded to Discord's developer panel OR any URL you like.
|
||||
presence.largeImageKey = cast(params.largeImageKey, Null<String>) ?? "album-volume1";
|
||||
|
||||
trace('[DISCORD] largeImageKey: ${presence.largeImageKey}');
|
||||
|
||||
// TODO: Make this use the song's album art.
|
||||
// presence.largeImageKey = "icon";
|
||||
// presence.largeImageKey = "https://f4.bcbits.com/img/a0746694746_16.jpg";
|
||||
|
@ -110,7 +105,7 @@ class DiscordClient
|
|||
// The small inset image for what the user is doing.
|
||||
// This can be the opponent's health icon?
|
||||
// NOTE: Like largeImageKey, this can be a URL, or an asset key.
|
||||
presence.smallImageKey = cast(params.smallImageKey, Null<String>);
|
||||
presence.smallImageKey = cast(params.smallImageKey, Null<String>) ?? "";
|
||||
|
||||
// NOTE: In previous versions, this showed as "Elapsed", but now shows as playtime and doesn't look good
|
||||
// presence.startTimestamp = time - 10;
|
||||
|
@ -136,9 +131,9 @@ class DiscordClient
|
|||
|
||||
final username:String = request[0].username;
|
||||
final globalName:String = request[0].username;
|
||||
final discriminator:Int = Std.parseInt(request[0].discriminator);
|
||||
final discriminator:Null<Int> = Std.parseInt(request[0].discriminator);
|
||||
|
||||
if (discriminator != 0)
|
||||
if (discriminator != null && discriminator != 0)
|
||||
{
|
||||
trace('[DISCORD] User: ${username}#${discriminator} (${globalName})');
|
||||
}
|
||||
|
@ -204,4 +199,17 @@ typedef DiscordClientPresenceParams =
|
|||
*/
|
||||
var ?smallImageKey:String;
|
||||
}
|
||||
|
||||
class DiscordClientSandboxed
|
||||
{
|
||||
public static function setPresence(params:DiscordClientPresenceParams)
|
||||
{
|
||||
return DiscordClient.instance.setPresence(params);
|
||||
}
|
||||
|
||||
public static function shutdown()
|
||||
{
|
||||
DiscordClient.instance.shutdown();
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
|
|
@ -2,10 +2,14 @@ package funkin.api.newgrounds;
|
|||
|
||||
#if FEATURE_NEWGROUNDS
|
||||
import io.newgrounds.Call.CallError;
|
||||
import io.newgrounds.components.ScoreBoardComponent;
|
||||
import io.newgrounds.objects.Score;
|
||||
import io.newgrounds.objects.ScoreBoard as LeaderboardData;
|
||||
import io.newgrounds.objects.User;
|
||||
import io.newgrounds.objects.events.Outcome;
|
||||
import io.newgrounds.utils.ScoreBoardList;
|
||||
|
||||
@:nullSafety
|
||||
class Leaderboards
|
||||
{
|
||||
public static function listLeaderboardData():Map<Leaderboard, LeaderboardData>
|
||||
|
@ -16,21 +20,8 @@ class Leaderboards
|
|||
trace('[NEWGROUNDS] Not logged in, cannot fetch medal data!');
|
||||
return [];
|
||||
}
|
||||
else
|
||||
{
|
||||
var result:Map<Leaderboard, LeaderboardData> = [];
|
||||
|
||||
for (leaderboardId in leaderboardList.keys())
|
||||
{
|
||||
var leaderboardData = leaderboardList.get(leaderboardId);
|
||||
if (leaderboardData == null) continue;
|
||||
|
||||
// A little hacky, but it works.
|
||||
result.set(cast leaderboardId, leaderboardData);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
return @:privateAccess leaderboardList._map?.copy() ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -66,6 +57,41 @@ class Leaderboards
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request to receive scores from Newgrounds.
|
||||
* @param leaderboard The leaderboard to fetch scores from.
|
||||
* @param params Additional parameters for fetching the score.
|
||||
*/
|
||||
public static function requestScores(leaderboard:Leaderboard, ?params:RequestScoresParams)
|
||||
{
|
||||
// Silently reject retrieving scores from unknown leaderboards.
|
||||
if (leaderboard == Leaderboard.Unknown) return;
|
||||
|
||||
var leaderboardList = NewgroundsClient.instance.leaderboards;
|
||||
if (leaderboardList == null) return;
|
||||
|
||||
var leaderboardData:Null<LeaderboardData> = leaderboardList.get(leaderboard.getId());
|
||||
if (leaderboardData == null) return;
|
||||
|
||||
var user:Null<User> = null;
|
||||
if ((params?.useCurrentUser ?? false) && NewgroundsClient.instance.isLoggedIn()) user = NewgroundsClient.instance.user;
|
||||
|
||||
leaderboardData.requestScores(params?.limit ?? 10, params?.skip ?? 0, params?.period ?? ALL, params?.social ?? false, params?.tag, user,
|
||||
function(outcome:Outcome<CallError>):Void {
|
||||
switch (outcome)
|
||||
{
|
||||
case SUCCESS:
|
||||
trace('[NEWGROUNDS] Fetched scores!');
|
||||
if (params != null && params.onComplete != null) params.onComplete(leaderboardData.scores);
|
||||
|
||||
case FAIL(error):
|
||||
trace('[NEWGROUNDS] Failed to fetch scores!');
|
||||
trace(error);
|
||||
if (params != null && params.onFail != null) params.onFail();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a score for a Story Level to Newgrounds.
|
||||
*/
|
||||
|
@ -84,9 +110,77 @@ class Leaderboards
|
|||
Leaderboards.submitScore(Leaderboard.getLeaderboardBySong(songId, difficultyId), score, tag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for `Leaderboards` that prevents submitting scores.
|
||||
*/
|
||||
@:nullSafety
|
||||
class LeaderboardsSandboxed
|
||||
{
|
||||
public static function getLeaderboardBySong(songId:String, difficultyId:String)
|
||||
{
|
||||
return Leaderboard.getLeaderboardBySong(songId, difficultyId);
|
||||
}
|
||||
|
||||
public static function getLeaderboardByLevel(levelId:String)
|
||||
{
|
||||
return Leaderboard.getLeaderboardByLevel(levelId);
|
||||
}
|
||||
|
||||
public function requestScores(leaderboard:Leaderboard, params:RequestScoresParams)
|
||||
{
|
||||
Leaderboards.requestScores(leaderboard, params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional parameters for `Leaderboards.requestScores()`
|
||||
*/
|
||||
typedef RequestScoresParams =
|
||||
{
|
||||
/**
|
||||
* How many scores to include in a list.
|
||||
* @default `10`
|
||||
*/
|
||||
var ?limit:Int;
|
||||
|
||||
/**
|
||||
* How many scores to skip before starting the list.
|
||||
* @default `0`
|
||||
*/
|
||||
var ?skip:Int;
|
||||
|
||||
/**
|
||||
* The time-frame to pull the scores from.
|
||||
* @default `Period.ALL`
|
||||
*/
|
||||
var ?period:Period;
|
||||
|
||||
/**
|
||||
* If true, only scores by the user and their friends will be loaded. Ignored if no user is set.
|
||||
* @default `false`
|
||||
*/
|
||||
var ?social:Bool;
|
||||
|
||||
/**
|
||||
* An optional tag to filter the results by.
|
||||
* @default `null`
|
||||
*/
|
||||
var ?tag:String;
|
||||
|
||||
/**
|
||||
* If true, only the scores from the currently logged in user will be loaded.
|
||||
* Additionally, if `social` is set to true, the scores of the user's friend will be loaded.
|
||||
* @default `false`
|
||||
*/
|
||||
var ?useCurrentUser:Bool;
|
||||
|
||||
var ?onComplete:Array<Score>->Void;
|
||||
var ?onFail:Void->Void;
|
||||
}
|
||||
#end
|
||||
|
||||
enum abstract Leaderboard(Int)
|
||||
enum abstract Leaderboard(Int) from Int to Int
|
||||
{
|
||||
/**
|
||||
* Represents an undefined or invalid leaderboard.
|
||||
|
@ -285,7 +379,7 @@ enum abstract Leaderboard(Int)
|
|||
{
|
||||
case "darnell":
|
||||
return DarnellBFMix;
|
||||
case "litup":
|
||||
case "lit-up":
|
||||
return LitUpBFMix;
|
||||
default:
|
||||
return Unknown;
|
||||
|
@ -379,7 +473,7 @@ enum abstract Leaderboard(Int)
|
|||
return Stress;
|
||||
case "darnell":
|
||||
return Darnell;
|
||||
case "litup":
|
||||
case "lit-up":
|
||||
return LitUp;
|
||||
case "2hot":
|
||||
return TwoHot;
|
||||
|
|
|
@ -8,6 +8,7 @@ import openfl.display.BitmapData;
|
|||
import io.newgrounds.utils.MedalList;
|
||||
import haxe.Json;
|
||||
|
||||
@:nullSafety
|
||||
class Medals
|
||||
{
|
||||
public static var medalJSON:Array<MedalJSON> = [];
|
||||
|
@ -21,22 +22,8 @@ class Medals
|
|||
trace('[NEWGROUNDS] Not logged in, cannot fetch medal data!');
|
||||
return [];
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: Why do I have to do this, @:nullSafety is fucked up
|
||||
var result:Map<Medal, MedalData> = [];
|
||||
|
||||
for (medalId in medalList.keys())
|
||||
{
|
||||
var medalData = medalList.get(medalId);
|
||||
if (medalData == null) continue;
|
||||
|
||||
// A little hacky, but it works.
|
||||
result.set(cast medalId, medalData);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
return @:privateAccess medalList._map?.copy() ?? [];
|
||||
}
|
||||
|
||||
public static function award(medal:Medal):Void
|
||||
|
@ -131,32 +118,78 @@ class Medals
|
|||
}
|
||||
}
|
||||
|
||||
public static function awardStoryLevel(id:String):Void
|
||||
public static function fetchMedalData(medal:Medal):Null<FetchedMedalData>
|
||||
{
|
||||
switch (id)
|
||||
var medalList = NewgroundsClient.instance.medals;
|
||||
@:privateAccess
|
||||
if (medalList == null || medalList._map == null) return null;
|
||||
|
||||
var medalData:Null<MedalData> = medalList.get(medal.getId());
|
||||
@:privateAccess
|
||||
if (medalData == null || medalData._data == null)
|
||||
{
|
||||
case 'tutorial':
|
||||
Medals.award(Medal.StoryTutorial);
|
||||
case 'week1':
|
||||
Medals.award(Medal.StoryWeek1);
|
||||
case 'week2':
|
||||
Medals.award(Medal.StoryWeek2);
|
||||
case 'week3':
|
||||
Medals.award(Medal.StoryWeek3);
|
||||
case 'week4':
|
||||
Medals.award(Medal.StoryWeek4);
|
||||
case 'week5':
|
||||
Medals.award(Medal.StoryWeek5);
|
||||
case 'week6':
|
||||
Medals.award(Medal.StoryWeek6);
|
||||
case 'week7':
|
||||
Medals.award(Medal.StoryWeek7);
|
||||
case 'weekend1':
|
||||
Medals.award(Medal.StoryWeekend1);
|
||||
default:
|
||||
trace('[NEWGROUNDS] Story level does not have a medal! (${id}).');
|
||||
trace('[NEWGROUNDS] Could not retrieve data for medal: ${medal}');
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: medalData.id,
|
||||
name: medalData.name,
|
||||
description: medalData.description,
|
||||
icon: medalData.icon,
|
||||
value: medalData.value,
|
||||
difficulty: medalData.difficulty,
|
||||
secret: medalData.secret,
|
||||
unlocked: medalData.unlocked
|
||||
}
|
||||
}
|
||||
|
||||
public static function awardStoryLevel(id:String):Void
|
||||
{
|
||||
var medal:Medal = Medal.getMedalByStoryLevel(id);
|
||||
if (medal == Medal.Unknown)
|
||||
{
|
||||
trace('[NEWGROUNDS] Story level does not have a medal! (${id}).');
|
||||
return;
|
||||
}
|
||||
Medals.award(medal);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for `Medals` that prevents awarding medals.
|
||||
*/
|
||||
class MedalsSandboxed
|
||||
{
|
||||
public static function fetchMedalData(medal:Medal):Null<FetchedMedalData>
|
||||
{
|
||||
return Medals.fetchMedalData(medal);
|
||||
}
|
||||
|
||||
public static function getMedalByStoryLevel(id:String):Medal
|
||||
{
|
||||
return Medal.getMedalByStoryLevel(id);
|
||||
}
|
||||
|
||||
public static function getAllMedals():Array<Medal>
|
||||
{
|
||||
return Medal.getAllMedals();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains data for a Medal, but excludes functions like `sendUnlock()`.
|
||||
*/
|
||||
typedef FetchedMedalData =
|
||||
{
|
||||
var id:Int;
|
||||
var name:String;
|
||||
var description:String;
|
||||
var icon:String;
|
||||
var value:Int;
|
||||
var difficulty:Int;
|
||||
var secret:Bool;
|
||||
var unlocked:Bool;
|
||||
}
|
||||
#end
|
||||
|
||||
|
@ -324,6 +357,8 @@ enum abstract Medal(Int) from Int to Int
|
|||
{
|
||||
switch (levelId)
|
||||
{
|
||||
case "tutorial":
|
||||
return StoryTutorial;
|
||||
case "week1":
|
||||
return StoryWeek1;
|
||||
case "week2":
|
||||
|
@ -344,4 +379,33 @@ enum abstract Medal(Int) from Int to Int
|
|||
return Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all medals aside from the `Unknown` one.
|
||||
*/
|
||||
public static function getAllMedals()
|
||||
{
|
||||
return [
|
||||
StartGame,
|
||||
StoryTutorial,
|
||||
StoryWeek1,
|
||||
StoryWeek2,
|
||||
StoryWeek3,
|
||||
StoryWeek4,
|
||||
StoryWeek5,
|
||||
StoryWeek6,
|
||||
StoryWeek7,
|
||||
StoryWeekend1,
|
||||
CharSelect,
|
||||
FreeplayPicoMix,
|
||||
FreeplayStressPico,
|
||||
LossRating,
|
||||
PerfectRatingHard,
|
||||
GoldPerfectRatingHard,
|
||||
ErectDifficulty,
|
||||
GoldPerfectRatingNightmare,
|
||||
FridayNight,
|
||||
Nice
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -331,4 +331,22 @@ class NewgroundsClient
|
|||
return Save.instance.ngSessionId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for `NewgroundsClient` that prevents submitting cheated data.
|
||||
*/
|
||||
class NewgroundsClientSandboxed
|
||||
{
|
||||
public static var user(get, never):Null<User>;
|
||||
|
||||
static function get_user()
|
||||
{
|
||||
return NewgroundsClient.instance.user;
|
||||
}
|
||||
|
||||
public static function isLoggedIn()
|
||||
{
|
||||
return NewgroundsClient.instance.isLoggedIn();
|
||||
}
|
||||
}
|
||||
#end
|
||||
|
|
32
source/funkin/audio/ALSoftConfig.hx
Normal file
32
source/funkin/audio/ALSoftConfig.hx
Normal file
|
@ -0,0 +1,32 @@
|
|||
package funkin.audio;
|
||||
|
||||
#if desktop
|
||||
import haxe.io.Path;
|
||||
|
||||
import sys.FileSystem;
|
||||
|
||||
/*
|
||||
* A class that simply points the audio backend OpenALSoft to use a custom
|
||||
* configuration when the game starts up.
|
||||
*
|
||||
* The config file overrides a few global OpenALSoft settings to improve audio
|
||||
* quality on desktop targets.
|
||||
*/
|
||||
@:nullSafety
|
||||
class ALSoftConfig
|
||||
{
|
||||
private static function __init__():Void
|
||||
{
|
||||
var configPath:String = Path.directory(Path.withoutExtension(#if hl Sys.getCwd() #else Sys.programPath() #end));
|
||||
#if windows
|
||||
configPath += "/plugins/alsoft.ini";
|
||||
#elseif mac
|
||||
configPath = '${Path.directory(configPath)}/Resources/plugins/alsoft.conf';
|
||||
#else
|
||||
configPath += "/plugins/alsoft.conf";
|
||||
#end
|
||||
|
||||
Sys.putEnv("ALSOFT_CONF", FileSystem.fullPath(configPath));
|
||||
}
|
||||
}
|
||||
#end
|
|
@ -11,6 +11,7 @@ import openfl.utils.AssetType;
|
|||
/**
|
||||
* a FlxSound that just overrides loadEmbedded to allow for "streamed" sounds to load with better performance!
|
||||
*/
|
||||
@:nullSafety
|
||||
class FlxStreamSound extends FlxSound
|
||||
{
|
||||
public function new()
|
||||
|
@ -18,7 +19,7 @@ class FlxStreamSound extends FlxSound
|
|||
super();
|
||||
}
|
||||
|
||||
override public function loadEmbedded(EmbeddedSound:FlxSoundAsset, Looped:Bool = false, AutoDestroy:Bool = false, ?OnComplete:Void->Void):FlxSound
|
||||
override public function loadEmbedded(EmbeddedSound:Null<FlxSoundAsset>, Looped:Bool = false, AutoDestroy:Bool = false, ?OnComplete:Void->Void):FlxSound
|
||||
{
|
||||
if (EmbeddedSound == null) return this;
|
||||
|
||||
|
|
|
@ -551,6 +551,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
|
|||
}
|
||||
FlxTween.cancelTweensOf(this);
|
||||
this._label = 'unknown';
|
||||
this._waveformData = null;
|
||||
}
|
||||
|
||||
@:access(openfl.media.Sound)
|
||||
|
|
|
@ -7,6 +7,7 @@ import flixel.tweens.FlxTween;
|
|||
* A group of FunkinSounds that are all synced together.
|
||||
* Unlike FlxSoundGroup, you can also control their time and pitch.
|
||||
*/
|
||||
@:nullSafety
|
||||
class SoundGroup extends FlxTypedGroup<FunkinSound>
|
||||
{
|
||||
public var time(get, set):Float;
|
||||
|
@ -36,6 +37,7 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
|
|||
return result;
|
||||
}
|
||||
|
||||
@:nullSafety(Off)
|
||||
for (sndFile in files)
|
||||
{
|
||||
var snd:FunkinSound = FunkinSound.load(Paths.voices(song, '$sndFile'));
|
||||
|
@ -70,7 +72,7 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
|
|||
/**
|
||||
* Add a sound to the group.
|
||||
*/
|
||||
public override function add(sound:FunkinSound):FunkinSound
|
||||
public override function add(sound:FunkinSound):Null<FunkinSound>
|
||||
{
|
||||
var result:FunkinSound = super.add(sound);
|
||||
|
||||
|
@ -134,6 +136,7 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
|
|||
/**
|
||||
* Fade in all the sounds in the group.
|
||||
*/
|
||||
@:nullSafety(Off)
|
||||
public function fadeIn(duration:Float, ?from:Float = 0.0, ?to:Float = 1.0, ?onComplete:FlxTween->Void):Void
|
||||
{
|
||||
forEachAlive(function(sound:FunkinSound) {
|
||||
|
@ -144,6 +147,7 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
|
|||
/**
|
||||
* Fade out all the sounds in the group.
|
||||
*/
|
||||
@:nullSafety(Off)
|
||||
public function fadeOut(duration:Float, ?to:Float = 0.0, ?onComplete:FlxTween->Void):Void
|
||||
{
|
||||
forEachAlive(function(sound:FunkinSound) {
|
||||
|
@ -238,7 +242,7 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
|
|||
|
||||
function get_muted():Bool
|
||||
{
|
||||
if (getFirstAlive() != null) return getFirstAlive().muted;
|
||||
if (getFirstAlive() != null) return getFirstAlive()?.muted ?? false;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -3,10 +3,11 @@ package funkin.audio;
|
|||
import flixel.group.FlxGroup.FlxTypedGroup;
|
||||
import funkin.audio.waveform.WaveformData;
|
||||
|
||||
@:nullSafety
|
||||
class VoicesGroup extends SoundGroup
|
||||
{
|
||||
var playerVoices:FlxTypedGroup<FunkinSound>;
|
||||
var opponentVoices:FlxTypedGroup<FunkinSound>;
|
||||
var playerVoices:Null<FlxTypedGroup<FunkinSound>>;
|
||||
var opponentVoices:Null<FlxTypedGroup<FunkinSound>>;
|
||||
|
||||
/**
|
||||
* Control the volume of only the sounds in the player group.
|
||||
|
@ -41,12 +42,12 @@ class VoicesGroup extends SoundGroup
|
|||
public function addPlayerVoice(sound:FunkinSound):Void
|
||||
{
|
||||
super.add(sound);
|
||||
playerVoices.add(sound);
|
||||
playerVoices?.add(sound);
|
||||
}
|
||||
|
||||
function set_playerVolume(volume:Float):Float
|
||||
{
|
||||
playerVoices.forEachAlive(function(voice:FunkinSound) {
|
||||
playerVoices?.forEachAlive(function(voice:FunkinSound) {
|
||||
voice.volume = volume;
|
||||
});
|
||||
return playerVolume = volume;
|
||||
|
@ -59,10 +60,10 @@ class VoicesGroup extends SoundGroup
|
|||
snd.time = time;
|
||||
});
|
||||
|
||||
playerVoices.forEachAlive(function(voice:FunkinSound) {
|
||||
playerVoices?.forEachAlive(function(voice:FunkinSound) {
|
||||
voice.time -= playerVoicesOffset;
|
||||
});
|
||||
opponentVoices.forEachAlive(function(voice:FunkinSound) {
|
||||
opponentVoices?.forEachAlive(function(voice:FunkinSound) {
|
||||
voice.time -= opponentVoicesOffset;
|
||||
});
|
||||
|
||||
|
@ -71,7 +72,7 @@ class VoicesGroup extends SoundGroup
|
|||
|
||||
function set_playerVoicesOffset(offset:Float):Float
|
||||
{
|
||||
playerVoices.forEachAlive(function(voice:FunkinSound) {
|
||||
playerVoices?.forEachAlive(function(voice:FunkinSound) {
|
||||
voice.time += playerVoicesOffset;
|
||||
voice.time -= offset;
|
||||
});
|
||||
|
@ -80,7 +81,7 @@ class VoicesGroup extends SoundGroup
|
|||
|
||||
function set_opponentVoicesOffset(offset:Float):Float
|
||||
{
|
||||
opponentVoices.forEachAlive(function(voice:FunkinSound) {
|
||||
opponentVoices?.forEachAlive(function(voice:FunkinSound) {
|
||||
voice.time += opponentVoicesOffset;
|
||||
voice.time -= offset;
|
||||
});
|
||||
|
@ -93,12 +94,12 @@ class VoicesGroup extends SoundGroup
|
|||
public function addOpponentVoice(sound:FunkinSound):Void
|
||||
{
|
||||
super.add(sound);
|
||||
opponentVoices.add(sound);
|
||||
opponentVoices?.add(sound);
|
||||
}
|
||||
|
||||
function set_opponentVolume(volume:Float):Float
|
||||
{
|
||||
opponentVoices.forEachAlive(function(voice:FunkinSound) {
|
||||
opponentVoices?.forEachAlive(function(voice:FunkinSound) {
|
||||
voice.volume = volume;
|
||||
});
|
||||
return opponentVolume = volume;
|
||||
|
@ -106,26 +107,26 @@ class VoicesGroup extends SoundGroup
|
|||
|
||||
public function getPlayerVoice(index:Int = 0):Null<FunkinSound>
|
||||
{
|
||||
return playerVoices.members[index];
|
||||
return playerVoices?.members[index];
|
||||
}
|
||||
|
||||
public function getOpponentVoice(index:Int = 0):Null<FunkinSound>
|
||||
{
|
||||
return opponentVoices.members[index];
|
||||
return opponentVoices?.members[index];
|
||||
}
|
||||
|
||||
public function getPlayerVoiceWaveform():Null<WaveformData>
|
||||
{
|
||||
if (playerVoices.members.length == 0) return null;
|
||||
if (playerVoices?.members.length == 0) return null;
|
||||
|
||||
return playerVoices.members[0].waveformData;
|
||||
return playerVoices?.members[0].waveformData;
|
||||
}
|
||||
|
||||
public function getOpponentVoiceWaveform():Null<WaveformData>
|
||||
{
|
||||
if (opponentVoices.members.length == 0) return null;
|
||||
if (opponentVoices?.members.length == 0) return null;
|
||||
|
||||
return opponentVoices.members[0].waveformData;
|
||||
return opponentVoices?.members[0].waveformData;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -133,9 +134,9 @@ class VoicesGroup extends SoundGroup
|
|||
*/
|
||||
public function getPlayerVoiceLength():Float
|
||||
{
|
||||
if (playerVoices.members.length == 0) return 0.0;
|
||||
if (playerVoices?.members.length == 0) return 0.0;
|
||||
|
||||
return playerVoices.members[0].length;
|
||||
return playerVoices?.members[0]?.length ?? 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -143,15 +144,15 @@ class VoicesGroup extends SoundGroup
|
|||
*/
|
||||
public function getOpponentVoiceLength():Float
|
||||
{
|
||||
if (opponentVoices.members.length == 0) return 0.0;
|
||||
if (opponentVoices?.members.length == 0) return 0.0;
|
||||
|
||||
return opponentVoices.members[0].length;
|
||||
return opponentVoices?.members[0]?.length ?? 0.0;
|
||||
}
|
||||
|
||||
public override function clear():Void
|
||||
{
|
||||
playerVoices.clear();
|
||||
opponentVoices.clear();
|
||||
playerVoices?.clear();
|
||||
opponentVoices?.clear();
|
||||
super.clear();
|
||||
}
|
||||
|
||||
|
@ -159,13 +160,13 @@ class VoicesGroup extends SoundGroup
|
|||
{
|
||||
if (playerVoices != null)
|
||||
{
|
||||
playerVoices.destroy();
|
||||
playerVoices?.destroy();
|
||||
playerVoices = null;
|
||||
}
|
||||
|
||||
if (opponentVoices != null)
|
||||
{
|
||||
opponentVoices.destroy();
|
||||
opponentVoices?.destroy();
|
||||
opponentVoices = null;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package funkin.audio.visualize;
|
|||
import flixel.FlxSprite;
|
||||
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
|
||||
|
||||
@:nullSafety
|
||||
class ABot extends FlxTypedSpriteGroup<FlxSprite>
|
||||
{
|
||||
public function new()
|
||||
|
|
|
@ -116,7 +116,7 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
|
|||
|
||||
for (i in 0...min(group.members.length, levels.length))
|
||||
{
|
||||
var animFrame:Int = Math.round(levels[i].value * 6);
|
||||
var animFrame:Int = (FlxG.sound.volume == 0 || FlxG.sound.muted) ? 0 : Math.round(levels[i].value * 6);
|
||||
|
||||
// don't display if we're at 0 volume from the level
|
||||
group.members[i].visible = animFrame > 0;
|
||||
|
|
|
@ -3,11 +3,12 @@ package funkin.audio.visualize;
|
|||
import flixel.group.FlxGroup.FlxTypedGroup;
|
||||
import flixel.sound.FlxSound;
|
||||
|
||||
@:nullSafety
|
||||
class PolygonVisGroup extends FlxTypedGroup<PolygonSpectogram>
|
||||
{
|
||||
public var playerVis:PolygonSpectogram;
|
||||
public var opponentVis:PolygonSpectogram;
|
||||
public var instVis:PolygonSpectogram;
|
||||
public var playerVis:Null<PolygonSpectogram>;
|
||||
public var opponentVis:Null<PolygonSpectogram>;
|
||||
public var instVis:Null<PolygonSpectogram>;
|
||||
|
||||
public function new()
|
||||
{
|
||||
|
@ -99,8 +100,14 @@ class PolygonVisGroup extends FlxTypedGroup<PolygonSpectogram>
|
|||
|
||||
public override function destroy():Void
|
||||
{
|
||||
playerVis.destroy();
|
||||
opponentVis.destroy();
|
||||
if (playerVis != null)
|
||||
{
|
||||
playerVis.destroy();
|
||||
}
|
||||
if (opponentVis != null)
|
||||
{
|
||||
opponentVis.destroy();
|
||||
}
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ package funkin.audio.visualize.dsp;
|
|||
Complex number representation.
|
||||
**/
|
||||
@:forward(real, imag) @:notNull @:pure
|
||||
@:nullSafety
|
||||
abstract Complex({
|
||||
final real:Float;
|
||||
final imag:Float;
|
||||
|
|
|
@ -8,6 +8,7 @@ using funkin.audio.visualize.dsp.Signal;
|
|||
/**
|
||||
Fast/Finite Fourier Transforms.
|
||||
**/
|
||||
@:nullSafety
|
||||
class FFT
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -6,6 +6,7 @@ package funkin.audio.visualize.dsp;
|
|||
Usages include 1-indexed sequences or zero-centered buffers with negative indexing.
|
||||
**/
|
||||
@:forward(array, offset)
|
||||
@:nullSafety
|
||||
abstract OffsetArray<T>({
|
||||
final array:Array<T>;
|
||||
final offset:Int;
|
||||
|
|
|
@ -5,12 +5,13 @@ using Lambda;
|
|||
/**
|
||||
Signal processing miscellaneous utilities.
|
||||
**/
|
||||
@:nullSafety
|
||||
class Signal
|
||||
{
|
||||
/**
|
||||
Returns a smoothed version of the input array using a moving average.
|
||||
**/
|
||||
public static function smooth(y:Array<Float>, n:Int):Array<Float>
|
||||
public static function smooth(y:Array<Float>, n:Int):Null<Array<Float>>
|
||||
{
|
||||
if (n <= 0)
|
||||
{
|
||||
|
|
|
@ -2,6 +2,7 @@ package funkin.audio.waveform;
|
|||
|
||||
import funkin.util.TimerUtil;
|
||||
|
||||
@:nullSafety
|
||||
class WaveformDataParser
|
||||
{
|
||||
static final INT16_MAX:Int = 32767;
|
||||
|
@ -10,7 +11,7 @@ class WaveformDataParser
|
|||
static final INT8_MAX:Int = 127;
|
||||
static final INT8_MIN:Int = -128;
|
||||
|
||||
public static function interpretFlxSound(sound:flixel.sound.FlxSound):Null<WaveformData>
|
||||
public static function interpretFlxSound(sound:Null<flixel.sound.FlxSound>):Null<WaveformData>
|
||||
{
|
||||
if (sound == null) return null;
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ typedef EntryConstructorFunction = String->Void;
|
|||
* @param T The type to construct. Must implement `IRegistryEntry`.
|
||||
* @param J The type of the JSON data used when constructing.
|
||||
*/
|
||||
@:nullSafety
|
||||
@:generic
|
||||
@:autoBuild(funkin.util.macro.DataRegistryMacro.buildRegistry())
|
||||
abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructorFunction>), J>
|
||||
|
@ -115,7 +116,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
|||
{
|
||||
try
|
||||
{
|
||||
var entry:T = createEntry(entryId);
|
||||
var entry:Null<T> = createEntry(entryId);
|
||||
if (entry != null)
|
||||
{
|
||||
trace(' Loaded entry data: ${entry}');
|
||||
|
@ -165,7 +166,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
|||
* @param id The ID of the entry.
|
||||
* @return The class name, or `null` if it does not exist.
|
||||
*/
|
||||
public function getScriptedEntryClassName(id:String):String
|
||||
public function getScriptedEntryClassName(id:String):Null<String>
|
||||
{
|
||||
return scriptedEntryIds.get(id);
|
||||
}
|
||||
|
@ -216,7 +217,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
|
|||
public function fetchEntryVersion(id:String):Null<thx.semver.Version>
|
||||
{
|
||||
var entryStr:String = loadEntryFile(id).contents;
|
||||
var entryVersion:thx.semver.Version = VersionUtil.getVersionFromJSON(entryStr);
|
||||
var entryVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(entryStr);
|
||||
return entryVersion;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import json2object.Position;
|
|||
import json2object.Position.Line;
|
||||
import json2object.Error;
|
||||
|
||||
@:nullSafety
|
||||
class DataError
|
||||
{
|
||||
public static function printError(error:Error):Void
|
||||
|
|
|
@ -19,6 +19,7 @@ import thx.semver.VersionRule;
|
|||
*
|
||||
* Functions must be of the signature `(hxjsonast.Json, String) -> T`, where the String is the property name and `T` is the type of the property.
|
||||
*/
|
||||
@:nullSafety
|
||||
class DataParse
|
||||
{
|
||||
/**
|
||||
|
@ -146,7 +147,6 @@ class DataParse
|
|||
throw 'Expected Backdrop property $name to be specify a valid "type", but it was "${backdropType}".';
|
||||
}
|
||||
|
||||
return null;
|
||||
default:
|
||||
throw 'Expected property $name to be an object, but it was ${json.value}.';
|
||||
}
|
||||
|
@ -310,6 +310,7 @@ class DataParse
|
|||
var length:Null<Float> = values[2] == null ? null : Tools.getValue(values[2]);
|
||||
var alt:Null<Bool> = values[3] == null ? null : Tools.getValue(values[3]);
|
||||
|
||||
if (time == null || data == null) throw 'Property $name note is missing time and/or data values.';
|
||||
return new LegacyNote(time, data, length, alt);
|
||||
// return null;
|
||||
default:
|
||||
|
|
|
@ -12,6 +12,7 @@ import haxe.ds.Either;
|
|||
*
|
||||
* NOTE: Result must include quotation marks if the value is a string! json2object will not add them for you!
|
||||
*/
|
||||
@:nullSafety
|
||||
class DataWrite
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package funkin.data.animation;
|
||||
|
||||
@:nullSafety
|
||||
class AnimationDataUtil
|
||||
{
|
||||
public static function toNamed(data:UnnamedAnimationData, ?name:String = ""):AnimationData
|
||||
public static function toNamed(data:UnnamedAnimationData, name:String = ""):AnimationData
|
||||
{
|
||||
return {
|
||||
name: name,
|
||||
|
@ -22,7 +23,7 @@ class AnimationDataUtil
|
|||
* @param name (adds index to name)
|
||||
* @return Array<AnimationData>
|
||||
*/
|
||||
public static function toNamedArray(data:Array<UnnamedAnimationData>, ?name:String = ""):Array<AnimationData>
|
||||
public static function toNamedArray(data:Array<UnnamedAnimationData>, name:String = ""):Array<AnimationData>
|
||||
{
|
||||
return data.mapi(function(animItem, ind) return toNamed(animItem, '$name$ind'));
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import funkin.play.cutscene.dialogue.ScriptedConversation;
|
|||
import funkin.util.tools.ISingleton;
|
||||
import funkin.data.DefaultRegistryImpl;
|
||||
|
||||
@:nullSafety
|
||||
class ConversationRegistry extends BaseRegistry<Conversation, ConversationData> implements ISingleton implements DefaultRegistryImpl
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -6,6 +6,7 @@ import funkin.play.cutscene.dialogue.ScriptedDialogueBox;
|
|||
import funkin.util.tools.ISingleton;
|
||||
import funkin.data.DefaultRegistryImpl;
|
||||
|
||||
@:nullSafety
|
||||
class DialogueBoxRegistry extends BaseRegistry<DialogueBox, DialogueBoxData> implements ISingleton implements DefaultRegistryImpl
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -5,6 +5,7 @@ import funkin.play.cutscene.dialogue.ScriptedSpeaker;
|
|||
import funkin.util.tools.ISingleton;
|
||||
import funkin.data.DefaultRegistryImpl;
|
||||
|
||||
@:nullSafety
|
||||
class SpeakerRegistry extends BaseRegistry<Speaker, SpeakerData> implements ISingleton implements DefaultRegistryImpl
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -8,6 +8,7 @@ import funkin.play.event.ScriptedSongEvent;
|
|||
/**
|
||||
* This class statically handles the parsing of internal and scripted song event handlers.
|
||||
*/
|
||||
@:nullSafety
|
||||
class SongEventRegistry
|
||||
{
|
||||
/**
|
||||
|
@ -87,14 +88,14 @@ class SongEventRegistry
|
|||
return eventCache.values();
|
||||
}
|
||||
|
||||
public static function getEvent(id:String):SongEvent
|
||||
public static function getEvent(id:String):Null<SongEvent>
|
||||
{
|
||||
return eventCache.get(id);
|
||||
}
|
||||
|
||||
public static function getEventSchema(id:String):SongEventSchema
|
||||
public static function getEventSchema(id:String):Null<SongEventSchema>
|
||||
{
|
||||
var event:SongEvent = getEvent(id);
|
||||
var event:Null<SongEvent> = getEvent(id);
|
||||
if (event == null) return null;
|
||||
|
||||
return event.getEventSchema();
|
||||
|
@ -108,7 +109,7 @@ class SongEventRegistry
|
|||
public static function handleEvent(data:SongEventData):Void
|
||||
{
|
||||
var eventKind:String = data.eventKind;
|
||||
var eventHandler:SongEvent = eventCache.get(eventKind);
|
||||
var eventHandler:Null<SongEvent> = eventCache.get(eventKind);
|
||||
|
||||
if (eventHandler != null)
|
||||
{
|
||||
|
|
|
@ -6,6 +6,7 @@ import funkin.ui.freeplay.ScriptedAlbum;
|
|||
import funkin.util.tools.ISingleton;
|
||||
import funkin.data.DefaultRegistryImpl;
|
||||
|
||||
@:nullSafety
|
||||
class AlbumRegistry extends BaseRegistry<Album, AlbumData> implements ISingleton implements DefaultRegistryImpl
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -7,6 +7,7 @@ import funkin.save.Save;
|
|||
import funkin.util.tools.ISingleton;
|
||||
import funkin.data.DefaultRegistryImpl;
|
||||
|
||||
@:nullSafety
|
||||
class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData> implements ISingleton implements DefaultRegistryImpl
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -6,6 +6,7 @@ import funkin.ui.freeplay.ScriptedFreeplayStyle;
|
|||
import funkin.util.tools.ISingleton;
|
||||
import funkin.data.DefaultRegistryImpl;
|
||||
|
||||
@:nullSafety
|
||||
class FreeplayStyleRegistry extends BaseRegistry<FreeplayStyle, FreeplayStyleData> implements ISingleton implements DefaultRegistryImpl
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -6,6 +6,7 @@ import funkin.data.notestyle.NoteStyleData;
|
|||
import funkin.util.tools.ISingleton;
|
||||
import funkin.data.DefaultRegistryImpl;
|
||||
|
||||
@:nullSafety
|
||||
class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData> implements ISingleton implements DefaultRegistryImpl
|
||||
{
|
||||
/**
|
||||
|
@ -24,6 +25,8 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData> implement
|
|||
|
||||
public function fetchDefault():NoteStyle
|
||||
{
|
||||
return fetchEntry(Constants.DEFAULT_NOTE_STYLE);
|
||||
var notestyle:Null<NoteStyle> = fetchEntry(Constants.DEFAULT_NOTE_STYLE);
|
||||
if (notestyle == null) throw 'Default notestyle was null! This should not happen!';
|
||||
return notestyle;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -68,11 +68,12 @@ class SongMetadata implements ICloneable<SongMetadata>
|
|||
@:jignored
|
||||
public var variation:String;
|
||||
|
||||
public function new(songName:String, artist:String, ?variation:String)
|
||||
public function new(songName:String, artist:String, ?charter:String, ?variation:String)
|
||||
{
|
||||
this.version = SongRegistry.SONG_METADATA_VERSION;
|
||||
this.songName = songName;
|
||||
this.artist = artist;
|
||||
this.charter = (charter == null) ? null : charter;
|
||||
this.timeFormat = 'ms';
|
||||
this.divisions = null;
|
||||
this.offsets = new SongOffsets();
|
||||
|
@ -96,7 +97,7 @@ class SongMetadata implements ICloneable<SongMetadata>
|
|||
*/
|
||||
public function clone():SongMetadata
|
||||
{
|
||||
var result:SongMetadata = new SongMetadata(this.songName, this.artist, this.variation);
|
||||
var result:SongMetadata = new SongMetadata(this.songName, this.artist, this.charter, this.variation);
|
||||
result.version = this.version;
|
||||
result.timeFormat = this.timeFormat;
|
||||
result.divisions = this.divisions;
|
||||
|
@ -139,7 +140,7 @@ class SongMetadata implements ICloneable<SongMetadata>
|
|||
*/
|
||||
public function toString():String
|
||||
{
|
||||
return 'SongMetadata(${this.songName} by ${this.artist}, variation ${this.variation})';
|
||||
return 'SongMetadata(${this.songName} by ${this.artist} and ${this.charter}, variation ${this.variation})';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,13 +5,13 @@ import funkin.data.song.SongData.SongEventData;
|
|||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongData.SongTimeChange;
|
||||
import funkin.util.ClipboardUtil;
|
||||
import funkin.util.SerializerUtil;
|
||||
|
||||
using Lambda;
|
||||
|
||||
/**
|
||||
* Utility functions for working with song data, including note data, event data, metadata, etc.
|
||||
*/
|
||||
@:nullSafety
|
||||
class SongDataUtils
|
||||
{
|
||||
/**
|
||||
|
@ -28,7 +28,7 @@ class SongDataUtils
|
|||
var time:Float = note.time + offset;
|
||||
var data:Int = note.data;
|
||||
var length:Float = note.length;
|
||||
var kind:String = note.kind;
|
||||
var kind:Null<String> = note.kind;
|
||||
return new SongNoteData(time, data, length, kind);
|
||||
};
|
||||
|
||||
|
@ -132,7 +132,7 @@ class SongDataUtils
|
|||
* Create an array of notes whose note data is flipped (player becomes opponent and vice versa)
|
||||
* Does not mutate the original array.
|
||||
*/
|
||||
public static function flipNotes(notes:Array<SongNoteData>, ?strumlineSize:Int = 4):Array<SongNoteData>
|
||||
public static function flipNotes(notes:Array<SongNoteData>, strumlineSize:Int = 4):Array<SongNoteData>
|
||||
{
|
||||
return notes.map(function(note:SongNoteData):SongNoteData {
|
||||
var newData = note.data;
|
||||
|
@ -150,7 +150,7 @@ class SongDataUtils
|
|||
*
|
||||
* Offset the provided array of notes such that the first note is at 0 milliseconds.
|
||||
*/
|
||||
public static function buildNoteClipboard(notes:Array<SongNoteData>, ?timeOffset:Int = null):Array<SongNoteData>
|
||||
public static function buildNoteClipboard(notes:Array<SongNoteData>, ?timeOffset:Int):Array<SongNoteData>
|
||||
{
|
||||
if (notes.length == 0) return notes;
|
||||
if (timeOffset == null) timeOffset = Std.int(notes[0].time);
|
||||
|
@ -162,7 +162,7 @@ class SongDataUtils
|
|||
*
|
||||
* Offset the provided array of events such that the first event is at 0 milliseconds.
|
||||
*/
|
||||
public static function buildEventClipboard(events:Array<SongEventData>, ?timeOffset:Int = null):Array<SongEventData>
|
||||
public static function buildEventClipboard(events:Array<SongEventData>, ?timeOffset:Int):Array<SongEventData>
|
||||
{
|
||||
if (events.length == 0) return events;
|
||||
if (timeOffset == null) timeOffset = Std.int(events[0].time);
|
||||
|
|
138
source/funkin/data/song/SongNoteDataUtils.hx
Normal file
138
source/funkin/data/song/SongNoteDataUtils.hx
Normal file
|
@ -0,0 +1,138 @@
|
|||
package funkin.data.song;
|
||||
|
||||
using SongData.SongNoteData;
|
||||
|
||||
/**
|
||||
* Utility class for extra handling of song notes
|
||||
*/
|
||||
@:nullSafety
|
||||
class SongNoteDataUtils
|
||||
{
|
||||
static final CHUNK_INTERVAL_MS:Float = 2500;
|
||||
|
||||
/**
|
||||
* Retrieves all stacked notes. It does this by cycling through "chunks" of notes within a certain interval.
|
||||
*
|
||||
* @param notes Sorted notes by time.
|
||||
* @param threshold The note stack threshold. Refer to `doNotesStack` for more details.
|
||||
* @param includeOverlapped (Optional) If overlapped notes should be included.
|
||||
* @param overlapped (Optional) An array that gets populated with overlapped notes.
|
||||
* Note that it's only guaranteed to work properly if the provided notes are sorted.
|
||||
* @return Stacked notes.
|
||||
*/
|
||||
public static function listStackedNotes(notes:Array<SongNoteData>, threshold:Float, includeOverlapped:Bool = true,
|
||||
?overlapped:Array<SongNoteData>):Array<SongNoteData>
|
||||
{
|
||||
var stackedNotes:Array<SongNoteData> = [];
|
||||
|
||||
var chunkTime:Float = 0;
|
||||
var chunks:Array<Array<SongNoteData>> = [[]];
|
||||
|
||||
for (note in notes)
|
||||
{
|
||||
if (note == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
while (note.time >= chunkTime + CHUNK_INTERVAL_MS)
|
||||
{
|
||||
chunkTime += CHUNK_INTERVAL_MS;
|
||||
chunks.push([]);
|
||||
}
|
||||
|
||||
chunks[chunks.length - 1].push(note);
|
||||
}
|
||||
|
||||
for (chunk in chunks)
|
||||
{
|
||||
for (i in 0...(chunk.length - 1))
|
||||
{
|
||||
for (j in (i + 1)...chunk.length)
|
||||
{
|
||||
var noteI:SongNoteData = chunk[i];
|
||||
var noteJ:SongNoteData = chunk[j];
|
||||
|
||||
if (doNotesStack(noteI, noteJ, threshold))
|
||||
{
|
||||
if (!stackedNotes.fastContains(noteI))
|
||||
{
|
||||
if (includeOverlapped) stackedNotes.push(noteI);
|
||||
|
||||
if (overlapped != null && !overlapped.contains(noteI)) overlapped.push(noteI);
|
||||
}
|
||||
|
||||
if (!stackedNotes.fastContains(noteJ))
|
||||
{
|
||||
stackedNotes.push(noteJ);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stackedNotes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates two arrays of notes but overwrites notes in `lhs` that are overlapped by notes in `rhs`.
|
||||
* Hold notes are only overwritten by longer hold notes.
|
||||
* This operation only modifies the second array and `overwrittenNotes`.
|
||||
*
|
||||
* @param lhs An array of notes
|
||||
* @param rhs An array of notes to concatenate into `lhs`
|
||||
* @param overwrittenNotes An optional array that is modified in-place with the notes in `lhs` that were overwritten.
|
||||
* @param threshold The note stack threshold. Refer to `doNotesStack` for more details.
|
||||
* @return The unsorted resulting array.
|
||||
*/
|
||||
public static function concatOverwrite(lhs:Array<SongNoteData>, rhs:Array<SongNoteData>, ?overwrittenNotes:Array<SongNoteData>,
|
||||
threshold:Float = 0):Array<SongNoteData>
|
||||
{
|
||||
if (lhs == null || rhs == null || rhs.length == 0) return lhs;
|
||||
if (lhs.length == 0) return rhs;
|
||||
|
||||
var result = lhs.copy();
|
||||
for (i in 0...rhs.length)
|
||||
{
|
||||
var noteB:SongNoteData = rhs[i];
|
||||
var hasOverlap:Bool = false;
|
||||
|
||||
for (j in 0...lhs.length)
|
||||
{
|
||||
var noteA:SongNoteData = lhs[j];
|
||||
if (doNotesStack(noteA, noteB, threshold))
|
||||
{
|
||||
// Long hold notes should have priority over shorter hold notes
|
||||
if (noteA.length <= noteB.length)
|
||||
{
|
||||
overwrittenNotes?.push(result[j].clone());
|
||||
result[j] = noteB;
|
||||
}
|
||||
hasOverlap = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasOverlap) result.push(noteB);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param noteA First note.
|
||||
* @param noteB Second note.
|
||||
* @param threshold The note stack threshold, in steps.
|
||||
* @return Returns `true` if both notes are on the same strumline, have the same direction
|
||||
* and their time difference in steps is less than the step-based threshold.
|
||||
* A threshold of 0 will return `true` if notes are nearly perfectly aligned.
|
||||
*/
|
||||
public static function doNotesStack(noteA:SongNoteData, noteB:SongNoteData, threshold:Float = 0):Bool
|
||||
{
|
||||
if (noteA.data != noteB.data) return false;
|
||||
else if (threshold == 0) return Math.ffloor(Math.abs(noteA.time - noteB.time)) < 1;
|
||||
|
||||
final stepDiff:Float = Math.abs(noteA.getStepTime() - noteB.getStepTime());
|
||||
return stepDiff <= threshold + 0.001;
|
||||
}
|
||||
}
|
|
@ -324,7 +324,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata> implements ISingleto
|
|||
}
|
||||
else
|
||||
{
|
||||
throw '[${registryId}] Chart entry ${id}:${variation} does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.';
|
||||
throw '[${registryId}] Chart entry ${id}:${variation} does not support migration to version ${SONG_MUSIC_DATA_VERSION_RULE}.';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -337,7 +337,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata> implements ISingleto
|
|||
}
|
||||
else
|
||||
{
|
||||
throw '[${registryId}] Chart entry "$fileName" does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.';
|
||||
throw '[${registryId}] Chart entry "$fileName" does not support migration to version ${SONG_MUSIC_DATA_VERSION_RULE}.';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,9 +9,10 @@ import funkin.data.song.SongData.SongTimeChange;
|
|||
import funkin.data.song.importer.FNFLegacyData;
|
||||
import funkin.data.song.importer.FNFLegacyData.LegacyNoteSection;
|
||||
|
||||
@:nullSafety
|
||||
class FNFLegacyImporter
|
||||
{
|
||||
public static function parseLegacyDataRaw(input:String, fileName:String = 'raw'):FNFLegacyData
|
||||
public static function parseLegacyDataRaw(input:String, fileName:String = 'raw'):Null<FNFLegacyData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<FNFLegacyData>();
|
||||
parser.ignoreUnknownVariables = true; // Set to true to ignore extra variables that might be included in the JSON.
|
||||
|
@ -38,16 +39,14 @@ class FNFLegacyImporter
|
|||
|
||||
var songMetadata:SongMetadata = new SongMetadata('Import', Constants.DEFAULT_ARTIST, 'default');
|
||||
|
||||
var hadError:Bool = false;
|
||||
|
||||
// Set generatedBy string for debugging.
|
||||
songMetadata.generatedBy = 'Chart Editor Import (FNF Legacy)';
|
||||
|
||||
songMetadata.playData.stage = songData?.song?.stageDefault ?? 'mainStage';
|
||||
songMetadata.songName = songData?.song?.song ?? 'Import';
|
||||
songMetadata.playData.stage = songData.song?.stageDefault ?? 'mainStage';
|
||||
songMetadata.songName = songData.song?.song ?? 'Import';
|
||||
songMetadata.playData.difficulties = [];
|
||||
|
||||
if (songData?.song?.notes != null)
|
||||
if (songData.song?.notes != null)
|
||||
{
|
||||
switch (songData.song.notes)
|
||||
{
|
||||
|
@ -65,7 +64,7 @@ class FNFLegacyImporter
|
|||
|
||||
songMetadata.timeChanges = rebuildTimeChanges(songData);
|
||||
|
||||
songMetadata.playData.characters = new SongCharacterData(songData?.song?.player1 ?? 'bf', 'gf', songData?.song?.player2 ?? 'dad');
|
||||
songMetadata.playData.characters = new SongCharacterData(songData.song?.player1 ?? 'bf', 'gf', songData.song?.player2 ?? 'dad');
|
||||
|
||||
return songMetadata;
|
||||
}
|
||||
|
@ -76,7 +75,7 @@ class FNFLegacyImporter
|
|||
|
||||
var songChartData:SongChartData = new SongChartData([difficulty => 1.0], [], [difficulty => []]);
|
||||
|
||||
if (songData?.song?.notes != null)
|
||||
if (songData.song?.notes != null)
|
||||
{
|
||||
switch (songData.song.notes)
|
||||
{
|
||||
|
@ -84,7 +83,6 @@ class FNFLegacyImporter
|
|||
// One difficulty of notes.
|
||||
songChartData.notes.set(difficulty, migrateNoteSections(notes));
|
||||
case Right(difficulties):
|
||||
var baseDifficulty = null;
|
||||
if (difficulties.easy != null) songChartData.notes.set('easy', migrateNoteSections(difficulties.easy));
|
||||
if (difficulties.normal != null) songChartData.notes.set('normal', migrateNoteSections(difficulties.normal));
|
||||
if (difficulties.hard != null) songChartData.notes.set('hard', migrateNoteSections(difficulties.hard));
|
||||
|
@ -124,8 +122,8 @@ class FNFLegacyImporter
|
|||
noteSections = notes;
|
||||
case Right(difficulties):
|
||||
if (difficulties.normal != null) noteSections = difficulties.normal;
|
||||
if (difficulties.hard != null) noteSections = difficulties.normal;
|
||||
if (difficulties.easy != null) noteSections = difficulties.normal;
|
||||
if (difficulties.hard != null) noteSections = difficulties.hard;
|
||||
if (difficulties.easy != null) noteSections = difficulties.easy;
|
||||
}
|
||||
|
||||
if (noteSections == null || noteSections.length == 0) return result;
|
||||
|
@ -158,7 +156,7 @@ class FNFLegacyImporter
|
|||
{
|
||||
var result:Array<SongTimeChange> = [];
|
||||
|
||||
result.push(new SongTimeChange(0, songData?.song?.bpm ?? Constants.DEFAULT_BPM));
|
||||
result.push(new SongTimeChange(0, songData.song?.bpm ?? Constants.DEFAULT_BPM));
|
||||
|
||||
var noteSections = [];
|
||||
switch (songData.song.notes)
|
||||
|
@ -168,8 +166,8 @@ class FNFLegacyImporter
|
|||
noteSections = notes;
|
||||
case Right(difficulties):
|
||||
if (difficulties.normal != null) noteSections = difficulties.normal;
|
||||
if (difficulties.hard != null) noteSections = difficulties.normal;
|
||||
if (difficulties.easy != null) noteSections = difficulties.normal;
|
||||
if (difficulties.hard != null) noteSections = difficulties.hard;
|
||||
if (difficulties.easy != null) noteSections = difficulties.easy;
|
||||
}
|
||||
|
||||
if (noteSections == null || noteSections.length == 0) return result;
|
||||
|
@ -179,7 +177,7 @@ class FNFLegacyImporter
|
|||
if (noteSection.changeBPM ?? false)
|
||||
{
|
||||
var firstNote:LegacyNote = noteSection.sectionNotes[0];
|
||||
if (firstNote != null) result.push(new SongTimeChange(firstNote.time, noteSection.bpm));
|
||||
if (firstNote != null) result.push(new SongTimeChange(firstNote.time, noteSection.bpm ?? Constants.DEFAULT_BPM));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -205,7 +205,7 @@ typedef StageDataProp =
|
|||
|
||||
/**
|
||||
* The angle of the prop, as a float.
|
||||
* @default 1.0
|
||||
* @default 0.0
|
||||
*/
|
||||
@:optional
|
||||
@:default(0.0)
|
||||
|
@ -284,7 +284,7 @@ typedef StageDataCharacter =
|
|||
|
||||
/**
|
||||
* The angle of the character, as a float.
|
||||
* @default 1.0
|
||||
* @default 0.0
|
||||
*/
|
||||
@:optional
|
||||
@:default(0.0)
|
||||
|
|
|
@ -5,6 +5,7 @@ import funkin.play.stage.ScriptedStage;
|
|||
import funkin.util.tools.ISingleton;
|
||||
import funkin.data.DefaultRegistryImpl;
|
||||
|
||||
@:nullSafety
|
||||
class StageRegistry extends BaseRegistry<Stage, StageData> implements ISingleton implements DefaultRegistryImpl
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -4,6 +4,7 @@ import funkin.data.stickers.StickerData;
|
|||
import funkin.ui.transition.stickers.StickerPack;
|
||||
import funkin.ui.transition.stickers.ScriptedStickerPack;
|
||||
|
||||
@:nullSafety
|
||||
class StickerRegistry extends BaseRegistry<StickerPack, StickerData>
|
||||
{
|
||||
/**
|
||||
|
@ -24,7 +25,9 @@ class StickerRegistry extends BaseRegistry<StickerPack, StickerData>
|
|||
|
||||
public function fetchDefault():StickerPack
|
||||
{
|
||||
return fetchEntry(Constants.DEFAULT_STICKER_PACK);
|
||||
var stickerPack:Null<StickerPack> = fetchEntry(Constants.DEFAULT_STICKER_PACK);
|
||||
if (stickerPack == null) throw 'Default sticker pack was null! This should not happen!';
|
||||
return stickerPack;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,6 +6,7 @@ import funkin.ui.story.ScriptedLevel;
|
|||
import funkin.util.tools.ISingleton;
|
||||
import funkin.data.DefaultRegistryImpl;
|
||||
|
||||
@:nullSafety
|
||||
class LevelRegistry extends BaseRegistry<Level, LevelData> implements ISingleton implements DefaultRegistryImpl
|
||||
{
|
||||
/**
|
||||
|
|
|
@ -32,6 +32,7 @@ import openfl.display._internal.CairoGraphics as GfxRenderer;
|
|||
* A modified `FlxSprite` that supports filters.
|
||||
* The name's pretty much self-explanatory.
|
||||
*/
|
||||
@:nullSafety
|
||||
@:access(openfl.geom.Rectangle)
|
||||
@:access(openfl.filters.BitmapFilter)
|
||||
@:access(flixel.graphics.frames.FlxFrame)
|
||||
|
@ -39,12 +40,12 @@ class FlxFilteredSprite extends FlxSprite
|
|||
{
|
||||
@:noCompletion var _renderer:FlxAnimateFilterRenderer = new FlxAnimateFilterRenderer();
|
||||
|
||||
@:noCompletion var _filterMatrix:FlxMatrix;
|
||||
@:noCompletion var _filterMatrix:FlxMatrix = new FlxMatrix();
|
||||
|
||||
/**
|
||||
* An `Array` of shader filters (aka `BitmapFilter`).
|
||||
*/
|
||||
public var filters(default, set):Array<BitmapFilter>;
|
||||
public var filters(default, set):Null<Array<BitmapFilter>>;
|
||||
|
||||
/**
|
||||
* a flag to update the image with the filters.
|
||||
|
@ -52,11 +53,15 @@ class FlxFilteredSprite extends FlxSprite
|
|||
*/
|
||||
public var filterDirty:Bool = false;
|
||||
|
||||
@:noCompletion var filtered:Bool;
|
||||
@:noCompletion var filtered:Bool = false;
|
||||
|
||||
// These appear to be a little troublesome to null safe.
|
||||
@:nullSafety(Off)
|
||||
@:noCompletion var _blankFrame:FlxFrame;
|
||||
|
||||
@:nullSafety(Off)
|
||||
var _filterBmp1:BitmapData;
|
||||
@:nullSafety(Off)
|
||||
var _filterBmp2:BitmapData;
|
||||
|
||||
override public function update(elapsed:Float)
|
||||
|
@ -162,6 +167,7 @@ class FlxFilteredSprite extends FlxSprite
|
|||
}
|
||||
_flashRect.width += frameWidth;
|
||||
_flashRect.height += frameHeight;
|
||||
@:nullSafety(Off)
|
||||
if (_blankFrame == null) _blankFrame = new FlxFrame(null);
|
||||
|
||||
if (_blankFrame.parent == null || _flashRect.width > _blankFrame.parent.width || _flashRect.height > _blankFrame.parent.height)
|
||||
|
@ -178,6 +184,7 @@ class FlxFilteredSprite extends FlxSprite
|
|||
_filterBmp2 = new BitmapData(_blankFrame.parent.width, _blankFrame.parent.height, 0);
|
||||
}
|
||||
_blankFrame.offset.copyFrom(_frame.offset);
|
||||
@:nullSafety(Off)
|
||||
_blankFrame.parent.bitmap = _renderer.applyFilter(_blankFrame.parent.bitmap, _filterBmp1, _filterBmp2, frame.parent.bitmap, filters, _flashRect,
|
||||
frame.frame.copyToFlash());
|
||||
_blankFrame.frame = FlxRect.get(0, 0, _blankFrame.parent.bitmap.width, _blankFrame.parent.bitmap.height);
|
||||
|
@ -193,7 +200,7 @@ class FlxFilteredSprite extends FlxSprite
|
|||
}
|
||||
|
||||
@:noCompletion
|
||||
function set_filters(value:Array<BitmapFilter>)
|
||||
function set_filters(value:Null<Array<BitmapFilter>>)
|
||||
{
|
||||
if (filters != value) filterDirty = true;
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import openfl.filters.ShaderFilter;
|
|||
* - NOTE: Several other blend modes work without FunkinCamera. Some still do not work.
|
||||
* - NOTE: Framerate-independent camera tweening is fixed in Flixel 6.x. Rest in peace, SwagCamera.
|
||||
*/
|
||||
@:nullSafety
|
||||
@:access(openfl.display.DisplayObject)
|
||||
@:access(openfl.display.BitmapData)
|
||||
@:access(openfl.display3D.Context3D)
|
||||
|
@ -50,11 +51,12 @@ class FunkinCamera extends FlxCamera
|
|||
// Used to identify the camera during debugging.
|
||||
final id:String = 'unknown';
|
||||
|
||||
@:nullSafety(Off)
|
||||
public function new(id:String = 'unknown', x:Int = 0, y:Int = 0, width:Int = 0, height:Int = 0, zoom:Float = 0)
|
||||
{
|
||||
super(x, y, width, height, zoom);
|
||||
this.id = id;
|
||||
bgTexture = pickTexture(width, height);
|
||||
bgTexture = @:nullSafety(Off) pickTexture(width, height);
|
||||
bgBitmap = FixedBitmapData.fromTexture(bgTexture);
|
||||
bgFrame = new FlxFrame(new FlxGraphic('', null));
|
||||
bgFrame.parent.bitmap = bgBitmap;
|
||||
|
@ -74,12 +76,15 @@ class FunkinCamera extends FlxCamera
|
|||
* and the grabbed bitmap will not include any previously rendered sprites
|
||||
* @return the grabbed bitmap data
|
||||
*/
|
||||
public function grabScreen(applyFilters:Bool, isolate:Bool = false):BitmapData
|
||||
public function grabScreen(applyFilters:Bool, isolate:Bool = false):Null<BitmapData>
|
||||
{
|
||||
final texture = pickTexture(width, height);
|
||||
final bitmap = FixedBitmapData.fromTexture(texture);
|
||||
squashTo(bitmap, applyFilters, isolate);
|
||||
grabbed.push(bitmap);
|
||||
if (bitmap != null)
|
||||
{
|
||||
squashTo(bitmap, applyFilters, isolate);
|
||||
grabbed.push(bitmap);
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
|
@ -126,12 +131,14 @@ class FunkinCamera extends FlxCamera
|
|||
if (applyFilters)
|
||||
{
|
||||
bitmap.draw(flashSprite, matrix);
|
||||
@:nullSafety(Off) // TODO: Remove this once openfl.display.Sprite has been null safed.
|
||||
flashSprite.filters = null;
|
||||
filtersApplied = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
final tmp = flashSprite.filters;
|
||||
@:nullSafety(Off)
|
||||
flashSprite.filters = null;
|
||||
bitmap.draw(flashSprite, matrix);
|
||||
flashSprite.filters = tmp;
|
||||
|
@ -197,6 +204,7 @@ class FunkinCamera extends FlxCamera
|
|||
final isolated = grabScreen(false, true);
|
||||
// apply fullscreen blend
|
||||
customBlendShader.blendSwag = blend;
|
||||
@:nullSafety(Off) // I hope this doesn't cause issues
|
||||
customBlendShader.sourceSwag = isolated;
|
||||
customBlendShader.updateViewInfo(FlxG.width, FlxG.height, this);
|
||||
applyFilter(customBlendFilter);
|
||||
|
@ -228,7 +236,7 @@ class FunkinCamera extends FlxCamera
|
|||
bgItemCount = 0;
|
||||
}
|
||||
|
||||
function pickTexture(width:Int, height:Int):TextureBase
|
||||
function pickTexture(width:Int, height:Int):Null<TextureBase>
|
||||
{
|
||||
// zero-sized textures will be problematic
|
||||
width = width < 1 ? 1 : width;
|
||||
|
@ -236,7 +244,9 @@ class FunkinCamera extends FlxCamera
|
|||
if (texturePool.length > 0)
|
||||
{
|
||||
final res = texturePool.pop();
|
||||
BitmapDataUtil.resizeTexture(res, width, height);
|
||||
if (res != null) BitmapDataUtil.resizeTexture(res, width, height);
|
||||
else
|
||||
trace('huh? why is this null? $texturePool');
|
||||
return res;
|
||||
}
|
||||
return Lib.current.stage.context3D.createTexture(width, height, BGRA, true);
|
||||
|
|
|
@ -17,6 +17,7 @@ import flixel.FlxCamera;
|
|||
* - A more efficient method for creating solid color sprites.
|
||||
* - TODO: Better cache handling for textures.
|
||||
*/
|
||||
@:nullSafety
|
||||
class FunkinSprite extends FlxSprite
|
||||
{
|
||||
/**
|
||||
|
@ -158,9 +159,14 @@ class FunkinSprite extends FlxSprite
|
|||
* @param input The OpenFL `TextureBase` to apply
|
||||
* @return This sprite, for chaining
|
||||
*/
|
||||
public function loadTextureBase(input:TextureBase):FunkinSprite
|
||||
public function loadTextureBase(input:TextureBase):Null<FunkinSprite>
|
||||
{
|
||||
var inputBitmap:FixedBitmapData = FixedBitmapData.fromTexture(input);
|
||||
var inputBitmap:Null<FixedBitmapData> = FixedBitmapData.fromTexture(input);
|
||||
if (inputBitmap == null)
|
||||
{
|
||||
FlxG.log.warn('loadTextureBase - input resulted in null bitmap! $input');
|
||||
return null;
|
||||
}
|
||||
|
||||
return loadBitmapData(inputBitmap);
|
||||
}
|
||||
|
@ -219,7 +225,7 @@ class FunkinSprite extends FlxSprite
|
|||
// Move the graphic from the previous cache to the current cache.
|
||||
var graphic = previousCachedTextures.get(key);
|
||||
previousCachedTextures.remove(key);
|
||||
currentCachedTextures.set(key, graphic);
|
||||
if (graphic != null) currentCachedTextures.set(key, graphic);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -271,8 +277,9 @@ class FunkinSprite extends FlxSprite
|
|||
|
||||
static function isGraphicCached(graphic:FlxGraphic):Bool
|
||||
{
|
||||
var result = null;
|
||||
if (graphic == null) return false;
|
||||
var result = FlxG.bitmap.get(graphic.key);
|
||||
result = FlxG.bitmap.get(graphic.key);
|
||||
if (result == null) return false;
|
||||
if (result != graphic)
|
||||
{
|
||||
|
@ -288,8 +295,9 @@ class FunkinSprite extends FlxSprite
|
|||
*/
|
||||
public function isAnimationDynamic(id:String):Bool
|
||||
{
|
||||
var animData = null;
|
||||
if (this.animation == null) return false;
|
||||
var animData = this.animation.getByName(id);
|
||||
animData = this.animation.getByName(id);
|
||||
if (animData == null) return false;
|
||||
return animData.numFrames > 1;
|
||||
}
|
||||
|
@ -428,6 +436,7 @@ class FunkinSprite extends FlxSprite
|
|||
|
||||
public override function destroy():Void
|
||||
{
|
||||
@:nullSafety(Off) // TODO: Remove when flixel.FlxSprite is null safed.
|
||||
frames = null;
|
||||
// Cancel all tweens so they don't continue to run on a destroyed sprite.
|
||||
// This prevents crashes.
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package funkin.modding.base;
|
||||
package funkin.graphics;
|
||||
|
||||
/**
|
||||
* A script that can be tied to a FunkinSprite.
|
||||
* Create a scripted class that extends FunkinSprite to use this.
|
||||
*/
|
||||
@:hscriptClass
|
||||
class ScriptedFunkinSprite extends funkin.graphics.FunkinSprite implements HScriptedClass {}
|
||||
class ScriptedFunkinSprite extends funkin.graphics.FunkinSprite implements polymod.hscript.HScriptedClass {}
|
|
@ -11,6 +11,7 @@ import flxanimate.animate.FlxKeyFrame;
|
|||
/**
|
||||
* A sprite which provides convenience functions for rendering a texture atlas with animations.
|
||||
*/
|
||||
@:nullSafety
|
||||
class FlxAtlasSprite extends FlxAnimate
|
||||
{
|
||||
static final SETTINGS:Settings =
|
||||
|
@ -40,10 +41,11 @@ class FlxAtlasSprite extends FlxAnimate
|
|||
*/
|
||||
public var onAnimationLoop:FlxTypedSignal<String->Void> = new FlxTypedSignal();
|
||||
|
||||
var currentAnimation:String;
|
||||
var currentAnimation:String = '';
|
||||
|
||||
var canPlayOtherAnims:Bool = true;
|
||||
|
||||
@:nullSafety(Off) // null safety HATES new classes atm, it'll be fixed in haxe 4.0.0?
|
||||
public function new(x:Float, y:Float, ?path:String, ?settings:Settings)
|
||||
{
|
||||
if (settings == null) settings = SETTINGS;
|
||||
|
@ -110,7 +112,7 @@ class FlxAtlasSprite extends FlxAnimate
|
|||
|
||||
var _completeAnim:Bool = false;
|
||||
|
||||
var fr:FlxKeyFrame = null;
|
||||
var fr:Null<FlxKeyFrame> = null;
|
||||
|
||||
var looping:Bool = false;
|
||||
|
||||
|
@ -195,8 +197,9 @@ class FlxAtlasSprite extends FlxAnimate
|
|||
|
||||
fr = null;
|
||||
}
|
||||
var frameLabelNames = getFrameLabelNames();
|
||||
// Only call goToFrameLabel if there is a frame label with that name. This prevents annoying warnings!
|
||||
if (getFrameLabelNames().indexOf(id) != -1)
|
||||
if (frameLabelNames != null && frameLabelNames.indexOf(id) != -1)
|
||||
{
|
||||
goToFrameLabel(id);
|
||||
fr = anim.getFrameLabel(id);
|
||||
|
@ -266,7 +269,7 @@ class FlxAtlasSprite extends FlxAnimate
|
|||
this.anim.goToFrameLabel(label);
|
||||
}
|
||||
|
||||
function getFrameLabelNames(?layer:haxe.extern.EitherType<Int, String>):Array<String>
|
||||
function getFrameLabelNames(?layer:haxe.extern.EitherType<Int, String>):Null<Array<String>>
|
||||
{
|
||||
var labels = this.anim.getFrameLabels(layer);
|
||||
var array = [];
|
||||
|
@ -374,6 +377,7 @@ class FlxAtlasSprite extends FlxAnimate
|
|||
var prevFrame:FlxFrame = prevFrames.get(index) ?? frames.getByIndex(index).copyTo();
|
||||
prevFrames.set(index, prevFrame);
|
||||
|
||||
@:nullSafety(Off) // TODO: Remove this once flixel.system.frontEnds.BitmapFrontEnd has been null safed
|
||||
var frame = FlxG.bitmap.add(graphic).imageFrame.frame;
|
||||
frame.copyTo(frames.getByIndex(index));
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import openfl.filters.BitmapFilter;
|
|||
/**
|
||||
* Provides cool stuff for `BitmapData`s that have a hardware texture internally.
|
||||
*/
|
||||
@:nullSafety
|
||||
@:access(openfl.display.BitmapData)
|
||||
@:access(openfl.display3D.textures.TextureBase)
|
||||
@:access(openfl.display3D.Context3D)
|
||||
|
@ -19,7 +20,7 @@ class BitmapDataUtil
|
|||
{
|
||||
static function getCache():{sprite:Sprite, bitmap:Bitmap}
|
||||
{
|
||||
static var cache:{sprite:Sprite, bitmap:Bitmap} = null;
|
||||
static var cache:Null<{sprite:Sprite, bitmap:Bitmap}> = null;
|
||||
if (cache == null)
|
||||
{
|
||||
final sprite = new Sprite();
|
||||
|
@ -56,7 +57,7 @@ class BitmapDataUtil
|
|||
* @param format the format if the internal texture
|
||||
* @return the bitmap
|
||||
*/
|
||||
public static function create(width:Int, height:Int, format:Context3DTextureFormat = BGRA):FixedBitmapData
|
||||
public static function create(width:Int, height:Int, format:Context3DTextureFormat = BGRA):Null<FixedBitmapData>
|
||||
{
|
||||
final texture = Lib.current.stage.context3D.createTexture(width, height, format, true);
|
||||
return FixedBitmapData.fromTexture(texture);
|
||||
|
@ -83,6 +84,7 @@ class BitmapDataUtil
|
|||
* @param width the width
|
||||
* @param height the height
|
||||
*/
|
||||
@:nullSafety(Off) // the final context there is causing an error, idk how to fix it
|
||||
public static function resizeTexture(texture:TextureBase, width:Int, height:Int):Void
|
||||
{
|
||||
if (texture.__width == width && texture.__height == height) return;
|
||||
|
@ -101,6 +103,7 @@ class BitmapDataUtil
|
|||
* @param dst the destination bitmap
|
||||
* @param src the source bitmap
|
||||
*/
|
||||
@:nullSafety(Off) // TODO: Remove this once openfl.display.Sprite has been null safed.
|
||||
public static function copy(dst:BitmapData, src:BitmapData):Void
|
||||
{
|
||||
hardwareCheck(dst);
|
||||
|
|
|
@ -10,6 +10,7 @@ import openfl.display3D.textures.TextureBase;
|
|||
/**
|
||||
* `BitmapData` is kinda broken so I fixed it.
|
||||
*/
|
||||
@:nullSafety
|
||||
@:access(openfl.display3D.textures.TextureBase)
|
||||
@:access(openfl.display.OpenGLRenderer)
|
||||
class FixedBitmapData extends BitmapData
|
||||
|
@ -29,7 +30,7 @@ class FixedBitmapData extends BitmapData
|
|||
* @param texture the texture
|
||||
* @return the bitmap data
|
||||
*/
|
||||
public static function fromTexture(texture:TextureBase):FixedBitmapData
|
||||
public static function fromTexture(texture:Null<TextureBase>):Null<FixedBitmapData>
|
||||
{
|
||||
if (texture == null) return null;
|
||||
final bitmapData:FixedBitmapData = new FixedBitmapData(texture.__width, texture.__height, true, 0);
|
||||
|
|
|
@ -7,6 +7,7 @@ import flixel.util.FlxColor;
|
|||
* Yoinked from AustinEast, thanks hopefully u dont mind me using some of ur good code
|
||||
* instead of my dumbass ugly code bro
|
||||
*/
|
||||
@:nullSafety
|
||||
class MeshRender extends FlxStrip
|
||||
{
|
||||
public var vertex_count(default, null):Int = 0;
|
||||
|
|
|
@ -2,12 +2,13 @@ package funkin.graphics.shaders;
|
|||
|
||||
import flixel.addons.display.FlxRuntimeShader;
|
||||
|
||||
@:nullSafety
|
||||
class AdjustColorShader extends FlxRuntimeShader
|
||||
{
|
||||
public var hue(default, set):Float;
|
||||
public var saturation(default, set):Float;
|
||||
public var brightness(default, set):Float;
|
||||
public var contrast(default, set):Float;
|
||||
public var hue(default, set):Float = 0;
|
||||
public var saturation(default, set):Float = 0;
|
||||
public var brightness(default, set):Float = 0;
|
||||
public var contrast(default, set):Float = 0;
|
||||
|
||||
public function new()
|
||||
{
|
||||
|
|
|
@ -8,12 +8,13 @@ typedef BlendModeShader =
|
|||
var uBlendColor:ShaderParameter<Float>;
|
||||
}
|
||||
|
||||
@:nullSafety
|
||||
class BlendModeEffect
|
||||
{
|
||||
public var shader(default, null):BlendModeShader;
|
||||
|
||||
@:isVar
|
||||
public var color(default, set):FlxColor;
|
||||
public var color(default, set):FlxColor = new FlxColor();
|
||||
|
||||
public function new(shader:BlendModeShader, color:FlxColor):Void
|
||||
{
|
||||
|
|
|
@ -4,10 +4,11 @@ import flixel.addons.display.FlxRuntimeShader;
|
|||
import openfl.display.BitmapData;
|
||||
import openfl.display.ShaderInput;
|
||||
|
||||
@:nullSafety
|
||||
class BlendModesShader extends FlxRuntimeShader
|
||||
{
|
||||
public var camera:ShaderInput<BitmapData>;
|
||||
public var cameraData:BitmapData;
|
||||
public var camera:Null<ShaderInput<BitmapData>>;
|
||||
public var cameraData:Null<BitmapData>;
|
||||
|
||||
public function new()
|
||||
{
|
||||
|
|
|
@ -5,7 +5,7 @@ import flixel.tweens.FlxTween;
|
|||
|
||||
class BlueFade extends FlxShader
|
||||
{
|
||||
public var fadeVal(default, set):Float;
|
||||
public var fadeVal(default, set):Float = 1;
|
||||
|
||||
function set_fadeVal(val:Float):Float
|
||||
{
|
||||
|
|
|
@ -5,9 +5,10 @@ import flixel.addons.display.FlxRuntimeShader;
|
|||
/**
|
||||
* Note... not actually gaussian!
|
||||
*/
|
||||
@:nullSafety
|
||||
class GaussianBlurShader extends FlxRuntimeShader
|
||||
{
|
||||
public var amount:Float;
|
||||
public var amount:Float = 1;
|
||||
|
||||
public function new(amount:Float = 1.0)
|
||||
{
|
||||
|
|
|
@ -2,6 +2,7 @@ package funkin.graphics.shaders;
|
|||
|
||||
import flixel.addons.display.FlxRuntimeShader;
|
||||
|
||||
@:nullSafety
|
||||
class Grayscale extends FlxRuntimeShader
|
||||
{
|
||||
public var amount:Float = 1;
|
||||
|
|
|
@ -2,11 +2,12 @@ package funkin.graphics.shaders;
|
|||
|
||||
import flixel.addons.display.FlxRuntimeShader;
|
||||
|
||||
@:nullSafety
|
||||
class HSVShader extends FlxRuntimeShader
|
||||
{
|
||||
public var hue(default, set):Float;
|
||||
public var saturation(default, set):Float;
|
||||
public var value(default, set):Float;
|
||||
public var hue(default, set):Float = 1;
|
||||
public var saturation(default, set):Float = 1;
|
||||
public var value(default, set):Float = 1;
|
||||
|
||||
public function new(h:Float = 1, s:Float = 1, v:Float = 1)
|
||||
{
|
||||
|
|
|
@ -5,9 +5,10 @@ import flixel.addons.display.FlxRuntimeShader;
|
|||
/**
|
||||
* Create a little dotting effect.
|
||||
*/
|
||||
@:nullSafety
|
||||
class InverseDotsShader extends FlxRuntimeShader
|
||||
{
|
||||
public var amount:Float;
|
||||
public var amount:Float = 0;
|
||||
|
||||
public function new(amount:Float = 1.0)
|
||||
{
|
||||
|
|
|
@ -3,6 +3,7 @@ package funkin.graphics.shaders;
|
|||
import flixel.addons.display.FlxRuntimeShader;
|
||||
import flixel.math.FlxPoint;
|
||||
|
||||
@:nullSafety
|
||||
class MosaicEffect extends FlxRuntimeShader
|
||||
{
|
||||
public var blockSize:FlxPoint = FlxPoint.get(1.0, 1.0);
|
||||
|
|
|
@ -3,6 +3,7 @@ package funkin.graphics.shaders;
|
|||
import flixel.addons.display.FlxRuntimeShader;
|
||||
import openfl.Assets;
|
||||
|
||||
@:nullSafety
|
||||
class PuddleShader extends FlxRuntimeShader
|
||||
{
|
||||
public function new()
|
||||
|
|
|
@ -2,6 +2,7 @@ package funkin.graphics.shaders;
|
|||
|
||||
import flixel.system.FlxAssets.FlxShader;
|
||||
|
||||
@:nullSafety
|
||||
class WaveShader extends FlxShader
|
||||
{
|
||||
@:glFragmentSource('
|
||||
|
|
|
@ -18,16 +18,17 @@ enum WiggleEffectType
|
|||
* 2. Call `sprite.shader = wiggleEffect` on the target sprite.
|
||||
* 3. Call the update() method on the instance every frame.
|
||||
*/
|
||||
@:nullSafety
|
||||
class WiggleEffectRuntime extends FlxRuntimeShader
|
||||
{
|
||||
public static function getEffectTypeId(v:WiggleEffectType):Int
|
||||
public static function getEffectTypeId(v:Null<WiggleEffectType>):Int
|
||||
{
|
||||
return WiggleEffectType.getConstructors().indexOf(Std.string(v));
|
||||
}
|
||||
|
||||
public var effectType(default, set):WiggleEffectType = DREAMY;
|
||||
public var effectType(default, set):Null<WiggleEffectType> = DREAMY;
|
||||
|
||||
function set_effectType(v:WiggleEffectType):WiggleEffectType
|
||||
function set_effectType(v:Null<WiggleEffectType>):Null<WiggleEffectType>
|
||||
{
|
||||
this.setInt('effectType', getEffectTypeId(v));
|
||||
return effectType = v;
|
||||
|
|
|
@ -13,6 +13,7 @@ import openfl.net.NetStream;
|
|||
* This does NOT replace hxvlc, nor does hxvlc replace this.
|
||||
* hxvlc only works on desktop and does not work on HTML5!
|
||||
*/
|
||||
@:nullSafety
|
||||
class FlxVideo extends FunkinSprite
|
||||
{
|
||||
var video:Video;
|
||||
|
@ -22,14 +23,16 @@ class FlxVideo extends FunkinSprite
|
|||
/**
|
||||
* A callback to execute when the video finishes.
|
||||
*/
|
||||
public var finishCallback:Void->Void;
|
||||
public var finishCallback:Null<Void->Void> = null;
|
||||
|
||||
@:nullSafety(Off)
|
||||
public function new(videoPath:String)
|
||||
{
|
||||
super();
|
||||
|
||||
this.videoPath = videoPath;
|
||||
|
||||
@:nullSafety(Off) // Why do I to do this here as well for this to build?
|
||||
makeGraphic(2, 2, FlxColor.TRANSPARENT);
|
||||
|
||||
video = new Video();
|
||||
|
@ -72,7 +75,7 @@ class FlxVideo extends FunkinSprite
|
|||
}
|
||||
|
||||
var videoAvailable:Bool = false;
|
||||
var frameTimer:Float;
|
||||
var frameTimer:Float = 0;
|
||||
|
||||
static final FRAME_RATE:Float = 60;
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ import hxvlc.flixel.FlxVideoSprite;
|
|||
* Not to be confused with FlxVideo, this is a hxvlc based video class
|
||||
* We override it simply to correct/control our volume easier.
|
||||
*/
|
||||
@:nullSafety
|
||||
class FunkinVideoSprite extends FlxVideoSprite
|
||||
{
|
||||
public function new(x:Float = 0, y:Float = 0)
|
||||
|
|
|
@ -1067,9 +1067,9 @@ class Controls extends FlxActionSet
|
|||
case Control.FREEPLAY_CHAR_SELECT:
|
||||
return [X];
|
||||
case Control.FREEPLAY_JUMP_TO_TOP:
|
||||
return [];
|
||||
return [RIGHT_STICK_DIGITAL_UP];
|
||||
case Control.FREEPLAY_JUMP_TO_BOTTOM:
|
||||
return [];
|
||||
return [RIGHT_STICK_DIGITAL_DOWN];
|
||||
case Control.VOLUME_UP:
|
||||
[];
|
||||
case Control.VOLUME_DOWN:
|
||||
|
|
|
@ -10,6 +10,7 @@ import funkin.data.notestyle.NoteStyleRegistry;
|
|||
import funkin.play.notes.notekind.NoteKindManager;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.data.freeplay.player.PlayerRegistry;
|
||||
import funkin.data.freeplay.style.FreeplayStyleRegistry;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
import funkin.data.stickers.StickerRegistry;
|
||||
import funkin.data.freeplay.album.AlbumRegistry;
|
||||
|
@ -255,9 +256,29 @@ class PolymodHandler
|
|||
Polymod.addImportAlias('lime.utils.Assets', funkin.Assets);
|
||||
Polymod.addImportAlias('openfl.utils.Assets', funkin.Assets);
|
||||
|
||||
// Backward compatibility for certain scripted classes outside `funkin.modding.base`.
|
||||
Polymod.addImportAlias('funkin.modding.base.ScriptedFunkinSprite', funkin.graphics.ScriptedFunkinSprite);
|
||||
Polymod.addImportAlias('funkin.modding.base.ScriptedMusicBeatState', funkin.ui.ScriptedMusicBeatState);
|
||||
Polymod.addImportAlias('funkin.modding.base.ScriptedMusicBeatSubState', funkin.ui.ScriptedMusicBeatSubState);
|
||||
|
||||
// `funkin.util.FileUtil` has unrestricted access to the file system.
|
||||
Polymod.addImportAlias('funkin.util.FileUtil', funkin.util.FileUtilSandboxed);
|
||||
|
||||
#if FEATURE_NEWGROUNDS
|
||||
// `funkin.api.newgrounds.Leaderboards` allows for submitting cheated scores.
|
||||
Polymod.addImportAlias('funkin.api.newgrounds.Leaderboards', funkin.api.newgrounds.Leaderboards.LeaderboardsSandboxed);
|
||||
|
||||
// `funkin.api.newgrounds.Medals` allows for unfair granting of medals.
|
||||
Polymod.addImportAlias('funkin.api.newgrounds.Medals', funkin.api.newgrounds.Medals.MedalsSandboxed);
|
||||
|
||||
// `funkin.api.newgrounds.NewgroundsClientSandboxed` allows for submitting cheated data.
|
||||
Polymod.addImportAlias('funkin.api.newgrounds.NewgroundsClient', funkin.api.newgrounds.NewgroundsClient.NewgroundsClientSandboxed);
|
||||
#end
|
||||
|
||||
#if FEATURE_DISCORD_RPC
|
||||
Polymod.addImportAlias('funkin.api.discord.DiscordClient', funkin.api.discord.DiscordClient.DiscordClientSandboxed);
|
||||
#end
|
||||
|
||||
// Add blacklisting for prohibited classes and packages.
|
||||
|
||||
// `Sys`
|
||||
|
@ -276,9 +297,9 @@ class PolymodHandler
|
|||
// Lib.load() can load malicious DLLs
|
||||
Polymod.blacklistImport('cpp.Lib');
|
||||
|
||||
// `Unserializer`
|
||||
// `haxe.Unserializer`
|
||||
// Unserializer.DEFAULT_RESOLVER.resolveClass() can access blacklisted packages
|
||||
Polymod.blacklistImport('Unserializer');
|
||||
Polymod.blacklistImport('haxe.Unserializer');
|
||||
|
||||
// `lime.system.CFFI`
|
||||
// Can load and execute compiled binaries.
|
||||
|
@ -310,6 +331,7 @@ class PolymodHandler
|
|||
{
|
||||
if (cls == null) continue;
|
||||
var className:String = Type.getClassName(cls);
|
||||
if (polymod.hscript._internal.PolymodScriptClass.importOverrides.exists(className)) continue;
|
||||
Polymod.blacklistImport(className);
|
||||
}
|
||||
|
||||
|
@ -322,15 +344,6 @@ class PolymodHandler
|
|||
Polymod.blacklistImport(className);
|
||||
}
|
||||
|
||||
// `funkin.api.newgrounds.*`
|
||||
// Contains functions which allow for cheating medals and leaderboards.
|
||||
for (cls in ClassMacro.listClassesInPackage('funkin.api.newgrounds'))
|
||||
{
|
||||
if (cls == null) continue;
|
||||
var className:String = Type.getClassName(cls);
|
||||
Polymod.blacklistImport(className);
|
||||
}
|
||||
|
||||
// `io.newgrounds.*`
|
||||
// Contains functions which allow for cheating medals and leaderboards.
|
||||
for (cls in ClassMacro.listClassesInPackage('io.newgrounds'))
|
||||
|
@ -348,6 +361,16 @@ class PolymodHandler
|
|||
var className:String = Type.getClassName(cls);
|
||||
Polymod.blacklistImport(className);
|
||||
}
|
||||
|
||||
// `funkin.util.macro.*`
|
||||
// CompiledClassList's get function allows access to sys and Newgrounds classes
|
||||
// None of the classes are suitable for mods anyway
|
||||
for (cls in ClassMacro.listClassesInPackage('funkin.util.macro'))
|
||||
{
|
||||
if (cls == null) continue;
|
||||
var className:String = Type.getClassName(cls);
|
||||
Polymod.blacklistImport(className);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -489,6 +512,7 @@ class PolymodHandler
|
|||
AlbumRegistry.instance.loadEntries();
|
||||
StageRegistry.instance.loadEntries();
|
||||
StickerRegistry.instance.loadEntries();
|
||||
FreeplayStyleRegistry.instance.loadEntries();
|
||||
|
||||
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
|
||||
NoteKindManager.loadScripts();
|
||||
|
|
13
source/funkin/modding/base/Object.hx
Normal file
13
source/funkin/modding/base/Object.hx
Normal file
|
@ -0,0 +1,13 @@
|
|||
package funkin.modding.base;
|
||||
|
||||
/**
|
||||
* An empty base class meant to be extended by scripts.
|
||||
*/
|
||||
|
||||
class Object {
|
||||
public function new() {}
|
||||
|
||||
public function toString():String {
|
||||
return "(Object)";
|
||||
}
|
||||
}
|
8
source/funkin/modding/base/ScriptedFlxBasic.hx
Normal file
8
source/funkin/modding/base/ScriptedFlxBasic.hx
Normal file
|
@ -0,0 +1,8 @@
|
|||
package funkin.modding.base;
|
||||
|
||||
/**
|
||||
* A script that can be tied to an FlxBasic.
|
||||
* Create a scripted class that extends FlxBasic to use this.
|
||||
*/
|
||||
@:hscriptClass
|
||||
class ScriptedFlxBasic extends flixel.FlxBasic implements HScriptedClass {}
|
8
source/funkin/modding/base/ScriptedFlxObject.hx
Normal file
8
source/funkin/modding/base/ScriptedFlxObject.hx
Normal file
|
@ -0,0 +1,8 @@
|
|||
package funkin.modding.base;
|
||||
|
||||
/**
|
||||
* A script that can be tied to an FlxObject.
|
||||
* Create a scripted class that extends FlxObject to use this.
|
||||
*/
|
||||
@:hscriptClass
|
||||
class ScriptedFlxObject extends flixel.FlxObject implements HScriptedClass {}
|
8
source/funkin/modding/base/ScriptedObject.hx
Normal file
8
source/funkin/modding/base/ScriptedObject.hx
Normal file
|
@ -0,0 +1,8 @@
|
|||
package funkin.modding.base;
|
||||
|
||||
/**
|
||||
* A script that can be tied to an Object (empty base class).
|
||||
* Create a scripted class that extends Object to use this.
|
||||
*/
|
||||
@:hscriptClass
|
||||
class ScriptedObject extends funkin.modding.base.Object implements HScriptedClass {}
|
|
@ -255,7 +255,7 @@ class HoldNoteScriptEvent extends NoteScriptEvent
|
|||
*/
|
||||
public var doesNotesplash:Bool = false;
|
||||
|
||||
public function new(type:ScriptEventType, holdNote:SustainTrail, healthChange:Float, score:Int, isComboBreak:Bool, cancelable:Bool = false):Void
|
||||
public function new(type:ScriptEventType, holdNote:SustainTrail, healthChange:Float, score:Int, isComboBreak:Bool, comboCount:Int = 0, cancelable:Bool = false):Void
|
||||
{
|
||||
super(type, null, healthChange, comboCount, true);
|
||||
this.holdNote = holdNote;
|
||||
|
|
|
@ -176,13 +176,13 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
|
|||
|
||||
/**
|
||||
* Called when the game regains focus.
|
||||
* This does not get called if "Auto Pause" is disabled.
|
||||
* This does not get called if "Pause on Unfocus" is disabled.
|
||||
*/
|
||||
public function onFocusGained(event:FocusScriptEvent) {}
|
||||
|
||||
/**
|
||||
* Called when the game loses focus.
|
||||
* This does not get called if "Auto Pause" is disabled.
|
||||
* This does not get called if "Pause on Unfocus" is disabled.
|
||||
*/
|
||||
public function onFocusLost(event:FocusScriptEvent) {}
|
||||
|
||||
|
|
|
@ -239,7 +239,7 @@ class GameOverSubState extends MusicBeatSubState
|
|||
}
|
||||
|
||||
// Smoothly lerp the camera
|
||||
FlxG.camera.zoom = MathUtil.smoothLerp(FlxG.camera.zoom, targetCameraZoom, elapsed, CAMERA_ZOOM_DURATION);
|
||||
FlxG.camera.zoom = MathUtil.smoothLerpPrecision(FlxG.camera.zoom, targetCameraZoom, elapsed, CAMERA_ZOOM_DURATION);
|
||||
|
||||
//
|
||||
// Handle user inputs.
|
||||
|
@ -391,7 +391,6 @@ class GameOverSubState extends MusicBeatSubState
|
|||
if (!isEnding)
|
||||
{
|
||||
isEnding = true;
|
||||
startDeathMusic(1.0, true); // isEnding changes this function's behavior.
|
||||
|
||||
// Stop death quotes immediately.
|
||||
hasPlayedDeathQuote = true;
|
||||
|
@ -401,6 +400,8 @@ class GameOverSubState extends MusicBeatSubState
|
|||
deathQuoteSound = null;
|
||||
}
|
||||
|
||||
startDeathMusic(1.0, true); // isEnding changes this function's behavior.
|
||||
|
||||
if (PlayState.instance.isMinimalMode || boyfriend == null) {}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -68,22 +68,13 @@ class PauseSubState extends MusicBeatSubState
|
|||
];
|
||||
|
||||
/**
|
||||
* Pause menu entries for when the game is paused during a video cutscene.
|
||||
* Pause menu entries for when the game is paused during a cutscene.
|
||||
* `[CUTSCENE]` is replaced with the name of the cutscene type.
|
||||
*/
|
||||
static final PAUSE_MENU_ENTRIES_VIDEO_CUTSCENE:Array<PauseMenuEntry> = [
|
||||
static final PAUSE_MENU_ENTRIES_CUTSCENE:Array<PauseMenuEntry> = [
|
||||
{text: 'Resume', callback: resume},
|
||||
{text: 'Skip Cutscene', callback: skipVideoCutscene},
|
||||
{text: 'Restart Cutscene', callback: restartVideoCutscene},
|
||||
{text: 'Exit to Menu', callback: quitToMenu},
|
||||
];
|
||||
|
||||
/**
|
||||
* Pause menu entries for when the game is paused during a conversation.
|
||||
*/
|
||||
static final PAUSE_MENU_ENTRIES_CONVERSATION:Array<PauseMenuEntry> = [
|
||||
{text: 'Resume', callback: resume},
|
||||
{text: 'Skip Dialogue', callback: skipConversation},
|
||||
{text: 'Restart Dialogue', callback: restartConversation},
|
||||
{text: 'Skip [CUTSCENE]', callback: skipCutscene},
|
||||
{text: 'Restart [CUTSCENE]', callback: restartCutscene},
|
||||
{text: 'Exit to Menu', callback: quitToMenu},
|
||||
];
|
||||
|
||||
|
@ -533,10 +524,15 @@ class PauseSubState extends MusicBeatSubState
|
|||
|
||||
// Add the back button.
|
||||
currentMenuEntries = entries.concat(PAUSE_MENU_ENTRIES_DIFFICULTY.clone());
|
||||
case PauseMode.Conversation:
|
||||
currentMenuEntries = PAUSE_MENU_ENTRIES_CONVERSATION.clone();
|
||||
case PauseMode.Cutscene:
|
||||
currentMenuEntries = PAUSE_MENU_ENTRIES_VIDEO_CUTSCENE.clone();
|
||||
case Cutscene(entryName, pauseName, onResume, onSkip, onRestart):
|
||||
var entries:Array<PauseMenuEntry> = [];
|
||||
|
||||
for (entry in PAUSE_MENU_ENTRIES_CUTSCENE)
|
||||
{
|
||||
entries.push({text: StringTools.replace(entry.text, '[CUTSCENE]', entryName), callback: entry.callback});
|
||||
}
|
||||
|
||||
currentMenuEntries = entries;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -601,10 +597,8 @@ class PauseSubState extends MusicBeatSubState
|
|||
metadataDeaths.text = '${PlayState.instance?.deathCounter} Blue Balls';
|
||||
case Charting:
|
||||
metadataDeaths.text = 'Chart Editor Preview';
|
||||
case Conversation:
|
||||
metadataDeaths.text = 'Dialogue Paused';
|
||||
case Cutscene:
|
||||
metadataDeaths.text = 'Video Paused';
|
||||
case Cutscene(entryName, pauseName, onResume, onSkip, onRestart):
|
||||
metadataDeaths.text = '$pauseName Paused';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -618,8 +612,15 @@ class PauseSubState extends MusicBeatSubState
|
|||
*/
|
||||
static function resume(state:PauseSubState):Void
|
||||
{
|
||||
// Resume a paused video if it exists.
|
||||
VideoCutscene.resumeVideo();
|
||||
switch (state.currentMode)
|
||||
{
|
||||
case Cutscene(entryName, pauseName, onResume, onSkip, onRestart):
|
||||
if (onResume != null)
|
||||
{
|
||||
onResume();
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
state.close();
|
||||
}
|
||||
|
@ -681,46 +682,40 @@ class PauseSubState extends MusicBeatSubState
|
|||
}
|
||||
|
||||
/**
|
||||
* Restart the paused video cutscene, then resume the game.
|
||||
* Restart the paused cutscene, then resume the game.
|
||||
* @param state The current PauseSubState.
|
||||
*/
|
||||
static function restartVideoCutscene(state:PauseSubState):Void
|
||||
static function restartCutscene(state:PauseSubState):Void
|
||||
{
|
||||
VideoCutscene.restartVideo();
|
||||
switch (state.currentMode)
|
||||
{
|
||||
case Cutscene(entryName, pauseName, onResume, onSkip, onRestart):
|
||||
if (onRestart != null)
|
||||
{
|
||||
onRestart(); // VideoCutscene.restartVideo(); on videos
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
state.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip the paused video cutscene, then resume the game.
|
||||
* Skip the paused cutscene, then resume the game.
|
||||
* @param state The current PauseSubState.
|
||||
*/
|
||||
static function skipVideoCutscene(state:PauseSubState):Void
|
||||
static function skipCutscene(state:PauseSubState):Void
|
||||
{
|
||||
VideoCutscene.finishVideo();
|
||||
state.close();
|
||||
}
|
||||
switch (state.currentMode)
|
||||
{
|
||||
case Cutscene(entryName, pauseName, onResume, onSkip, onRestart):
|
||||
if (onSkip != null)
|
||||
{
|
||||
onSkip(); // VideoCutscene.finishVideo(); on videos
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart the paused conversation, then resume the game.
|
||||
* @param state The current PauseSubState.
|
||||
*/
|
||||
static function restartConversation(state:PauseSubState):Void
|
||||
{
|
||||
if (PlayState.instance?.currentConversation == null) return;
|
||||
|
||||
PlayState.instance.currentConversation.resetConversation();
|
||||
state.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip the paused conversation, then resume the game.
|
||||
* @param state The current PauseSubState.
|
||||
*/
|
||||
static function skipConversation(state:PauseSubState):Void
|
||||
{
|
||||
if (PlayState.instance?.currentConversation == null) return;
|
||||
|
||||
PlayState.instance.currentConversation.skipConversation();
|
||||
state.close();
|
||||
}
|
||||
|
||||
|
@ -766,11 +761,15 @@ class PauseSubState extends MusicBeatSubState
|
|||
* Quit the game and return to the chart editor.
|
||||
* @param state The current PauseSubState.
|
||||
*/
|
||||
@:access(funkin.play.PlayState)
|
||||
static function quitToChartEditor(state:PauseSubState):Void
|
||||
{
|
||||
// This should come first because the sounds list gets cleared!
|
||||
PlayState.instance?.forEachPausedSound(s -> s.destroy());
|
||||
state.close();
|
||||
if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position!
|
||||
PlayState.instance.close(); // This only works because PlayState is a substate!
|
||||
FlxG.sound.music?.pause(); // Don't reset song position!
|
||||
PlayState.instance?.vocals?.pause();
|
||||
PlayState.instance?.close(); // This only works because PlayState is a substate!
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -795,14 +794,14 @@ enum PauseMode
|
|||
Difficulty;
|
||||
|
||||
/**
|
||||
* The menu displayed when the player pauses the game during a conversation.
|
||||
* The menu displayed when the player pauses the game during a cutscene.
|
||||
* @param entryName The name to show in entries.
|
||||
* @param pauseName The name to show on the "Cutscene Paused" text.
|
||||
* @param onResume Gets called when `Resume` is selected.
|
||||
* @param onSkip Gets called when `Skip [entryName]` is selected.
|
||||
* @param onRestart Gets called when `Restart [entryName]` is selected.
|
||||
*/
|
||||
Conversation;
|
||||
|
||||
/**
|
||||
* The menu displayed when the player pauses the game during a video cutscene.
|
||||
*/
|
||||
Cutscene;
|
||||
Cutscene(entryName:String, pauseName:String, onResume:Void->Void, onSkip:Void->Void, onRestart:Void->Void);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,6 +7,7 @@ import flixel.FlxObject;
|
|||
import flixel.FlxSubState;
|
||||
import flixel.math.FlxMath;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.sound.FlxSound;
|
||||
import flixel.text.FlxText;
|
||||
import flixel.tweens.FlxTween;
|
||||
import flixel.ui.FlxBar;
|
||||
|
@ -185,6 +186,11 @@ class PlayState extends MusicBeatSubState
|
|||
*/
|
||||
public var needsReset:Bool = false;
|
||||
|
||||
/**
|
||||
* A timer that gets active once resetting happens. Used to vwoosh in notes.
|
||||
*/
|
||||
public var vwooshTimer:FlxTimer = new FlxTimer();
|
||||
|
||||
/**
|
||||
* The current 'Blueball Counter' to display in the pause menu.
|
||||
* Resets when you beat a song or go back to the main menu.
|
||||
|
@ -304,13 +310,13 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
/**
|
||||
* Whether the game is currently in Practice Mode.
|
||||
* If true, player will not lose gain or lose score from notes.
|
||||
* If true, player will not gain or lose score from notes.
|
||||
*/
|
||||
public var isPracticeMode:Bool = false;
|
||||
|
||||
/**
|
||||
* Whether the game is currently in Bot Play Mode.
|
||||
* If true, player will not lose gain or lose score from notes.
|
||||
* If true, player will not gain or lose score from notes.
|
||||
*/
|
||||
public var isBotPlayMode:Bool = false;
|
||||
|
||||
|
@ -432,6 +438,11 @@ class PlayState extends MusicBeatSubState
|
|||
*/
|
||||
var cameraTweensPausedBySubState:List<FlxTween> = new List<FlxTween>();
|
||||
|
||||
/**
|
||||
* Track any sounds we've paused for a Pause substate, so we can unpause them when we return.
|
||||
*/
|
||||
var soundsPausedBySubState:List<FlxSound> = new List<FlxSound>();
|
||||
|
||||
/**
|
||||
* False until `create()` has completed.
|
||||
*/
|
||||
|
@ -807,7 +818,6 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
super.update(elapsed);
|
||||
|
||||
var list = FlxG.sound.list;
|
||||
updateHealthBar();
|
||||
updateScoreText();
|
||||
|
||||
|
@ -837,9 +847,9 @@ class PlayState extends MusicBeatSubState
|
|||
// Reset music properly.
|
||||
if (FlxG.sound.music != null)
|
||||
{
|
||||
FlxG.sound.music.time = startTimestamp - Conductor.instance.combinedOffset;
|
||||
FlxG.sound.music.pitch = playbackRate;
|
||||
FlxG.sound.music.pause();
|
||||
FlxG.sound.music.time = startTimestamp;
|
||||
FlxG.sound.music.pitch = playbackRate;
|
||||
}
|
||||
|
||||
if (!overrideMusic)
|
||||
|
@ -854,7 +864,7 @@ class PlayState extends MusicBeatSubState
|
|||
}
|
||||
}
|
||||
vocals.pause();
|
||||
vocals.time = 0 - Conductor.instance.combinedOffset;
|
||||
vocals.time = startTimestamp - Conductor.instance.instrumentalOffset;
|
||||
|
||||
if (FlxG.sound.music != null) FlxG.sound.music.volume = 1;
|
||||
vocals.volume = 1;
|
||||
|
@ -875,9 +885,6 @@ class PlayState extends MusicBeatSubState
|
|||
// Delete all notes and reset the arrays.
|
||||
regenNoteData();
|
||||
|
||||
// so the song doesn't start too early :D
|
||||
Conductor.instance.update(-5000, false);
|
||||
|
||||
// Reset camera zooming
|
||||
cameraBopIntensity = Constants.DEFAULT_BOP_INTENSITY;
|
||||
hudCameraZoomIntensity = (cameraBopIntensity - 1.0) * 2.0;
|
||||
|
@ -886,10 +893,13 @@ class PlayState extends MusicBeatSubState
|
|||
health = Constants.HEALTH_STARTING;
|
||||
songScore = 0;
|
||||
Highscore.tallies.combo = 0;
|
||||
|
||||
// so the song doesn't start too early :D
|
||||
var vwooshDelay:Float = 0.5;
|
||||
Conductor.instance.update(-vwooshDelay * 1000 + startTimestamp + Conductor.instance.beatLengthMs * -5);
|
||||
|
||||
// timer for vwoosh
|
||||
var vwooshTimer = new FlxTimer();
|
||||
vwooshTimer.start(0.5, function(t:FlxTimer) {
|
||||
Conductor.instance.update(startTimestamp - Conductor.instance.combinedOffset, false);
|
||||
vwooshTimer.start(vwooshDelay, function(_) {
|
||||
if (playerStrumline.notes.length == 0) playerStrumline.updateNotes();
|
||||
if (opponentStrumline.notes.length == 0) opponentStrumline.updateNotes();
|
||||
playerStrumline.vwooshInNotes();
|
||||
|
@ -897,6 +907,9 @@ class PlayState extends MusicBeatSubState
|
|||
Countdown.performCountdown();
|
||||
});
|
||||
|
||||
// Stops any existing countdown.
|
||||
Countdown.stopCountdown();
|
||||
|
||||
// Reset the health icons.
|
||||
currentStage?.getBoyfriend()?.initHealthIcon(false);
|
||||
currentStage?.getDad()?.initHealthIcon(true);
|
||||
|
@ -959,18 +972,16 @@ class PlayState extends MusicBeatSubState
|
|||
// Enable drawing while the substate is open, allowing the game state to be shown behind the pause menu.
|
||||
persistentDraw = true;
|
||||
|
||||
// There is a 1/1000 change to use a special pause menu.
|
||||
// Prevent vwoosh timer from starting countdown in pause menu
|
||||
vwooshTimer.active = false;
|
||||
|
||||
// There is a 1/1000 chance to use a special pause menu.
|
||||
// This prevents the player from resuming, but that's the point.
|
||||
// It's a reference to Gitaroo Man, which doesn't let you pause the game.
|
||||
if (!isSubState && event.gitaroo)
|
||||
{
|
||||
this.remove(currentStage);
|
||||
FlxG.switchState(() -> new GitarooPause(
|
||||
{
|
||||
targetSong: currentSong,
|
||||
targetDifficulty: currentDifficulty,
|
||||
targetVariation: currentVariation,
|
||||
}));
|
||||
FlxG.switchState(() -> new GitarooPause(lastParams));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -1127,6 +1138,8 @@ class PlayState extends MusicBeatSubState
|
|||
playerStrumline.clean();
|
||||
opponentStrumline.clean();
|
||||
|
||||
vwooshTimer.cancel();
|
||||
|
||||
songScore = 0;
|
||||
updateScoreText();
|
||||
|
||||
|
@ -1230,9 +1243,29 @@ class PlayState extends MusicBeatSubState
|
|||
musicPausedBySubState = true;
|
||||
}
|
||||
|
||||
// Pause vocals.
|
||||
// Not tracking that we've done this via a bool because vocal re-syncing involves pausing the vocals anyway.
|
||||
if (vocals != null) vocals.pause();
|
||||
// Pause any sounds that are playing and keep track of them.
|
||||
// Vocals are also paused here but are not included as they are handled separately.
|
||||
if (Std.isOfType(subState, PauseSubState))
|
||||
{
|
||||
FlxG.sound.list.forEachAlive(function(sound:FlxSound) {
|
||||
if (!sound.active || sound == FlxG.sound.music) return;
|
||||
// In case it's a scheduled sound
|
||||
var funkinSound:FunkinSound = cast sound;
|
||||
if (funkinSound != null && !funkinSound.isPlaying) return;
|
||||
if (!sound.playing && sound.time >= 0) return;
|
||||
|
||||
sound.pause();
|
||||
soundsPausedBySubState.add(sound);
|
||||
});
|
||||
|
||||
vocals?.forEach(function(voice:FunkinSound) {
|
||||
soundsPausedBySubState.remove(voice);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
vocals?.pause();
|
||||
}
|
||||
}
|
||||
|
||||
// Pause camera tweening, and keep track of which tweens we pause.
|
||||
|
@ -1281,6 +1314,9 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
if (event.eventCanceled) return;
|
||||
|
||||
// Resume vwooshTimer
|
||||
if (!vwooshTimer.finished) vwooshTimer.active = true;
|
||||
|
||||
// Resume music if we paused it.
|
||||
if (musicPausedBySubState)
|
||||
{
|
||||
|
@ -1288,6 +1324,8 @@ class PlayState extends MusicBeatSubState
|
|||
musicPausedBySubState = false;
|
||||
}
|
||||
|
||||
forEachPausedSound((s) -> needsReset ? s.destroy() : s.resume());
|
||||
|
||||
// Resume camera tweens if we paused any.
|
||||
for (camTween in cameraTweensPausedBySubState)
|
||||
{
|
||||
|
@ -1423,6 +1461,10 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
performCleanup();
|
||||
|
||||
// `performCleanup()` clears the static reference to this state
|
||||
// scripts might still need it, so we set it back to `this`
|
||||
instance = this;
|
||||
|
||||
funkin.modding.PolymodHandler.forceReloadAssets();
|
||||
lastParams.targetSong = SongRegistry.instance.fetchEntry(currentSong.id);
|
||||
LoadingState.loadPlayState(lastParams);
|
||||
|
@ -1510,13 +1552,6 @@ class PlayState extends MusicBeatSubState
|
|||
}
|
||||
// trace('Not bopping camera: ${FlxG.camera.zoom} < ${(1.35 * defaultCameraZoom)} && ${cameraZoomRate} > 0 && ${Conductor.instance.currentBeat} % ${cameraZoomRate} == ${Conductor.instance.currentBeat % cameraZoomRate}}');
|
||||
|
||||
// That combo milestones that got spoiled that one time.
|
||||
// Comes with NEAT visual and audio effects.
|
||||
|
||||
// bruh this var is bonkers i thot it was a function lmfaooo
|
||||
|
||||
// Break up into individual lines to aid debugging.
|
||||
|
||||
if (playerStrumline != null) playerStrumline.onBeatHit();
|
||||
if (opponentStrumline != null) opponentStrumline.onBeatHit();
|
||||
|
||||
|
@ -2056,9 +2091,9 @@ class PlayState extends MusicBeatSubState
|
|||
FlxG.sound.music.onComplete = function() {
|
||||
if (mayPauseGame) endSong(skipEndingTransition);
|
||||
};
|
||||
// A negative instrumental offset means the song skips the first few milliseconds of the track.
|
||||
// This just gets added into the startTimestamp behavior so we don't need to do anything extra.
|
||||
FlxG.sound.music.play(true, Math.max(0, startTimestamp - Conductor.instance.combinedOffset));
|
||||
|
||||
FlxG.sound.music.pause();
|
||||
FlxG.sound.music.time = startTimestamp;
|
||||
FlxG.sound.music.pitch = playbackRate;
|
||||
|
||||
// Prevent the volume from being wrong.
|
||||
|
@ -2067,13 +2102,17 @@ class PlayState extends MusicBeatSubState
|
|||
|
||||
trace('Playing vocals...');
|
||||
add(vocals);
|
||||
vocals.play();
|
||||
vocals.volume = 1.0;
|
||||
|
||||
vocals.time = startTimestamp - Conductor.instance.instrumentalOffset;
|
||||
vocals.pitch = playbackRate;
|
||||
vocals.time = FlxG.sound.music.time;
|
||||
vocals.volume = 1.0;
|
||||
|
||||
// trace('STARTING SONG AT:');
|
||||
// trace('${FlxG.sound.music.time}');
|
||||
// trace('${vocals.time}');
|
||||
resyncVocals();
|
||||
|
||||
FlxG.sound.music.play();
|
||||
vocals.play();
|
||||
|
||||
#if FEATURE_DISCORD_RPC
|
||||
// Updating Discord Rich Presence (with Time Left)
|
||||
|
@ -2102,7 +2141,7 @@ class PlayState extends MusicBeatSubState
|
|||
}
|
||||
|
||||
/**
|
||||
* Resyncronize the vocal tracks if they have become offset from the instrumental.
|
||||
* Resynchronize the vocal tracks if they have become offset from the instrumental.
|
||||
*/
|
||||
function resyncVocals():Void
|
||||
{
|
||||
|
@ -2111,8 +2150,10 @@ class PlayState extends MusicBeatSubState
|
|||
// Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.)
|
||||
if (!(FlxG.sound.music?.playing ?? false)) return;
|
||||
|
||||
var timeToPlayAt:Float = Math.min(FlxG.sound.music.length, Math.max(0, Conductor.instance.songPosition - Conductor.instance.combinedOffset));
|
||||
var timeToPlayAt:Float = Math.min(FlxG.sound.music.length,
|
||||
Math.max(Math.min(Conductor.instance.combinedOffset, 0), Conductor.instance.songPosition) - Conductor.instance.combinedOffset);
|
||||
trace('Resyncing vocals to ${timeToPlayAt}');
|
||||
|
||||
FlxG.sound.music.pause();
|
||||
vocals.pause();
|
||||
|
||||
|
@ -2347,7 +2388,7 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
// Call an event to allow canceling the note miss.
|
||||
// NOTE: This is what handles the character animations!
|
||||
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, Constants.HEALTH_MISS_PENALTY, 0, true);
|
||||
var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, Constants.HEALTH_MISS_PENALTY, Highscore.tallies.combo, true);
|
||||
dispatchEvent(event);
|
||||
|
||||
// Calling event.cancelEvent() skips all the other logic! Neat!
|
||||
|
@ -2414,7 +2455,7 @@ class PlayState extends MusicBeatSubState
|
|||
var healthChange = healthChangeUncapped.clamp(healthChangeMax, 0);
|
||||
var scoreChange = Std.int(Constants.SCORE_HOLD_DROP_PENALTY_PER_SECOND * remainingLengthSec);
|
||||
|
||||
var event:HoldNoteScriptEvent = new HoldNoteScriptEvent(NOTE_HOLD_DROP, holdNote, healthChange, scoreChange, true);
|
||||
var event:HoldNoteScriptEvent = new HoldNoteScriptEvent(NOTE_HOLD_DROP, holdNote, healthChange, scoreChange, true, Highscore.tallies.combo);
|
||||
dispatchEvent(event);
|
||||
|
||||
trace('Penalizing score by ${event.score} and health by ${event.healthChange} for dropping hold note (is combo break: ${event.isComboBreak})!');
|
||||
|
@ -2448,7 +2489,7 @@ class PlayState extends MusicBeatSubState
|
|||
}
|
||||
}
|
||||
|
||||
// Respawns notes that were b
|
||||
// Respawns notes that were between the previous time and the current time when skipping backward, or destroy notes between the previous time and the current time when skipping forward.
|
||||
playerStrumline.handleSkippedNotes();
|
||||
opponentStrumline.handleSkippedNotes();
|
||||
}
|
||||
|
@ -2709,6 +2750,8 @@ class PlayState extends MusicBeatSubState
|
|||
FlxG.switchState(() -> new ChartEditorState(
|
||||
{
|
||||
targetSongId: currentSong.id,
|
||||
targetSongDifficulty: currentDifficulty,
|
||||
targetSongVariation: currentVariation,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -2833,7 +2876,10 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
currentConversation.pauseMusic();
|
||||
|
||||
var pauseSubState:FlxSubState = new PauseSubState({mode: Conversation});
|
||||
var pauseSubState:FlxSubState = new PauseSubState(
|
||||
{
|
||||
mode: Cutscene('Conversation', 'Conversation', null, currentConversation?.skipConversation, currentConversation?.resetConversation)
|
||||
});
|
||||
|
||||
persistentUpdate = false;
|
||||
FlxTransitionableState.skipNextTransIn = true;
|
||||
|
@ -2849,7 +2895,10 @@ class PlayState extends MusicBeatSubState
|
|||
{
|
||||
VideoCutscene.pauseVideo();
|
||||
|
||||
var pauseSubState:FlxSubState = new PauseSubState({mode: Cutscene});
|
||||
var pauseSubState:FlxSubState = new PauseSubState(
|
||||
{
|
||||
mode: Cutscene('Cutscene', 'Video', VideoCutscene.resumeVideo, VideoCutscene.finishVideo.bind(null), VideoCutscene.restartVideo.bind(true))
|
||||
});
|
||||
|
||||
persistentUpdate = false;
|
||||
FlxTransitionableState.skipNextTransIn = true;
|
||||
|
@ -3154,6 +3203,9 @@ class PlayState extends MusicBeatSubState
|
|||
// TODO: Uncache the song.
|
||||
}
|
||||
|
||||
// Prevent vwoosh timer from running outside PlayState (e.g Chart Editor)
|
||||
vwooshTimer.cancel();
|
||||
|
||||
if (overrideMusic)
|
||||
{
|
||||
// Stop the music. Do NOT destroy it, something still references it!
|
||||
|
@ -3175,6 +3227,11 @@ class PlayState extends MusicBeatSubState
|
|||
}
|
||||
}
|
||||
|
||||
forEachPausedSound((s) -> s.destroy());
|
||||
|
||||
FlxTween.globalManager.clear();
|
||||
FlxTimer.globalManager.clear();
|
||||
|
||||
// Remove reference to stage and remove sprites from it to save memory.
|
||||
if (currentStage != null)
|
||||
{
|
||||
|
@ -3429,7 +3486,7 @@ class PlayState extends MusicBeatSubState
|
|||
cancelCameraZoomTween();
|
||||
}
|
||||
|
||||
var prevScrollTargets:Array<Dynamic> = []; // used to snap scroll speed when things go unruely
|
||||
var prevScrollTargets:Array<Dynamic> = []; // used to snap scroll speed when things go unruly
|
||||
|
||||
/**
|
||||
* The magical function that shall tween the scroll speed.
|
||||
|
@ -3483,6 +3540,15 @@ class PlayState extends MusicBeatSubState
|
|||
scrollSpeedTweens = [];
|
||||
}
|
||||
|
||||
function forEachPausedSound(f:FlxSound->Void):Void
|
||||
{
|
||||
for (sound in soundsPausedBySubState)
|
||||
{
|
||||
f(sound);
|
||||
}
|
||||
soundsPausedBySubState.clear();
|
||||
}
|
||||
|
||||
#if FEATURE_DEBUG_FUNCTIONS
|
||||
/**
|
||||
* Jumps forward or backward a number of sections in the song.
|
||||
|
|
|
@ -32,6 +32,7 @@ import funkin.ui.freeplay.FreeplayState;
|
|||
import funkin.ui.MusicBeatSubState;
|
||||
import funkin.ui.story.StoryMenuState;
|
||||
import funkin.modding.base.ScriptedFlxAtlasSprite;
|
||||
import funkin.graphics.ScriptedFunkinSprite;
|
||||
#if FEATURE_NEWGROUNDS
|
||||
import funkin.api.newgrounds.Medals;
|
||||
#end
|
||||
|
@ -258,7 +259,15 @@ class ResultState extends MusicBeatSubState
|
|||
// Add to the scene.
|
||||
add(animation);
|
||||
case 'sparrow':
|
||||
var animation:FunkinSprite = FunkinSprite.createSparrow(offsets[0], offsets[1], animPath);
|
||||
@:nullSafety(Off)
|
||||
var animation:FunkinSprite = null;
|
||||
|
||||
if (animData.scriptClass != null) animation = ScriptedFunkinSprite.init(animData.scriptClass, offsets[0], offsets[1]);
|
||||
else
|
||||
animation = FunkinSprite.createSparrow(offsets[0], offsets[1], animPath);
|
||||
|
||||
if (animation == null) continue;
|
||||
|
||||
animation.animation.addByPrefix('idle', '', 24, false, false, false);
|
||||
|
||||
if (animData.loopFrame != null)
|
||||
|
@ -487,8 +496,7 @@ class ResultState extends MusicBeatSubState
|
|||
bgFlash.visible = true;
|
||||
FlxTween.tween(bgFlash, {alpha: 0}, 5 / 24);
|
||||
// NOTE: Only divide if totalNotes > 0 to prevent divide-by-zero errors.
|
||||
var clearPercentFloat = params.scoreData.tallies.totalNotes == 0 ? 0.0 : (params.scoreData.tallies.sick +
|
||||
params.scoreData.tallies.good
|
||||
var clearPercentFloat = params.scoreData.tallies.totalNotes == 0 ? 0.0 : (params.scoreData.tallies.sick + params.scoreData.tallies.good
|
||||
- params.scoreData.tallies.missed) / params.scoreData.tallies.totalNotes * 100;
|
||||
clearPercentTarget = Math.floor(clearPercentFloat);
|
||||
// Prevent off-by-one errors.
|
||||
|
@ -563,16 +571,6 @@ class ResultState extends MusicBeatSubState
|
|||
// scorePopin.animation.play("score");
|
||||
|
||||
// scorePopin.visible = true;
|
||||
|
||||
if (params.isNewHighscore ?? false)
|
||||
{
|
||||
highscoreNew.visible = true;
|
||||
highscoreNew.animation.play("new");
|
||||
}
|
||||
else
|
||||
{
|
||||
highscoreNew.visible = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import funkin.util.VersionUtil;
|
|||
import haxe.Json;
|
||||
import flixel.graphics.frames.FlxFrame;
|
||||
|
||||
@:nullSafety
|
||||
class CharacterDataParser
|
||||
{
|
||||
/**
|
||||
|
@ -57,7 +58,7 @@ class CharacterDataParser
|
|||
{
|
||||
try
|
||||
{
|
||||
var charData:CharacterData = parseCharacterData(charId);
|
||||
var charData:Null<CharacterData> = parseCharacterData(charId);
|
||||
if (charData != null)
|
||||
{
|
||||
trace(' Loaded character data: ${charId}');
|
||||
|
@ -203,14 +204,14 @@ class CharacterDataParser
|
|||
return null;
|
||||
}
|
||||
|
||||
var charData:CharacterData = characterCache.get(charId);
|
||||
var charScriptClass:String = characterScriptedClass.get(charId);
|
||||
var charData:Null<CharacterData> = characterCache.get(charId);
|
||||
var charScriptClass:Null<String> = characterScriptedClass.get(charId);
|
||||
|
||||
var char:BaseCharacter;
|
||||
var char:Null<BaseCharacter> = null;
|
||||
|
||||
if (charScriptClass != null)
|
||||
{
|
||||
switch (charData.renderType)
|
||||
if (charData != null) switch (charData.renderType)
|
||||
{
|
||||
case CharacterRenderType.AnimateAtlas:
|
||||
char = ScriptedAnimateAtlasCharacter.init(charScriptClass, charId);
|
||||
|
@ -227,7 +228,7 @@ class CharacterDataParser
|
|||
}
|
||||
else
|
||||
{
|
||||
switch (charData.renderType)
|
||||
if (charData != null) switch (charData.renderType)
|
||||
{
|
||||
case CharacterRenderType.AnimateAtlas:
|
||||
char = new AnimateAtlasCharacter(charId);
|
||||
|
@ -283,7 +284,7 @@ class CharacterDataParser
|
|||
/**
|
||||
* Returns the idle frame of a character.
|
||||
*/
|
||||
public static function getCharPixelIconAsset(char:String):FlxFrame
|
||||
public static function getCharPixelIconAsset(char:String):Null<FlxFrame>
|
||||
{
|
||||
var charPath:String = "freeplay/icons/";
|
||||
|
||||
|
@ -323,13 +324,13 @@ class CharacterDataParser
|
|||
}
|
||||
|
||||
var isAnimated = Assets.exists(Paths.file('images/$charPath.xml'));
|
||||
var frame:FlxFrame = null;
|
||||
var frame:Null<FlxFrame> = null;
|
||||
|
||||
if (isAnimated)
|
||||
{
|
||||
var frames = Paths.getSparrowAtlas(charPath);
|
||||
|
||||
var idleFrame:FlxFrame = frames.frames.find(function(frame:FlxFrame):Bool {
|
||||
var idleFrame:Null<FlxFrame> = frames.frames.find(function(frame:FlxFrame):Bool {
|
||||
return frame.name.startsWith('idle');
|
||||
});
|
||||
|
||||
|
@ -380,7 +381,7 @@ class CharacterDataParser
|
|||
{
|
||||
var rawJson:String = loadCharacterFile(charId);
|
||||
|
||||
var charData:CharacterData = migrateCharacterData(rawJson, charId);
|
||||
var charData:Null<CharacterData> = migrateCharacterData(rawJson, charId);
|
||||
|
||||
return validateCharacterData(charId, charData);
|
||||
}
|
||||
|
@ -445,7 +446,7 @@ class CharacterDataParser
|
|||
* @param input
|
||||
* @return The validated character data
|
||||
*/
|
||||
static function validateCharacterData(id:String, input:CharacterData):Null<CharacterData>
|
||||
static function validateCharacterData(id:String, input:Null<CharacterData>):Null<CharacterData>
|
||||
{
|
||||
if (input == null)
|
||||
{
|
||||
|
|
|
@ -207,10 +207,10 @@ class HealthIcon extends FunkinSprite
|
|||
|
||||
if (bopEvery != 0)
|
||||
{
|
||||
lerpIconSize();
|
||||
lerpIconSize(false, elapsed);
|
||||
|
||||
// Lerp the health icon back to its normal angle.
|
||||
this.angle = MathUtil.coolLerp(this.angle, 0, 0.15);
|
||||
this.angle = MathUtil.smoothLerpPrecision(this.angle, 0, elapsed, 0.511);
|
||||
}
|
||||
|
||||
this.updatePosition();
|
||||
|
@ -221,14 +221,14 @@ class HealthIcon extends FunkinSprite
|
|||
* Mainly forced when changing to old icon to not have a weird lerp related to changing from pixel icon to non-pixel old icon
|
||||
* @param force Force the icon immedialtely to be the target size. Defaults to false.
|
||||
*/
|
||||
function lerpIconSize(force:Bool = false):Void
|
||||
function lerpIconSize(force:Bool = false, ?elapsed:Float):Void
|
||||
{
|
||||
// Lerp the health icon back to its normal size,
|
||||
// while maintaining aspect ratio.
|
||||
if (this.width > this.height)
|
||||
{
|
||||
// Apply linear interpolation while accounting for frame rate.
|
||||
var targetSize:Int = Std.int(MathUtil.coolLerp(this.width, HEALTH_ICON_SIZE * this.size.x, 0.15));
|
||||
var targetSize:Int = Std.int(MathUtil.smoothLerpPrecision(this.width, HEALTH_ICON_SIZE * this.size.x, elapsed ?? FlxG.elapsed, 0.511));
|
||||
|
||||
if (force) targetSize = Std.int(HEALTH_ICON_SIZE * this.size.x);
|
||||
|
||||
|
@ -236,7 +236,7 @@ class HealthIcon extends FunkinSprite
|
|||
}
|
||||
else
|
||||
{
|
||||
var targetSize:Int = Std.int(MathUtil.coolLerp(this.height, HEALTH_ICON_SIZE * this.size.y, 0.15));
|
||||
var targetSize:Int = Std.int(MathUtil.smoothLerpPrecision(this.height, HEALTH_ICON_SIZE * this.size.y, elapsed ?? FlxG.elapsed, 0.511));
|
||||
|
||||
if (force) targetSize = Std.int(HEALTH_ICON_SIZE * this.size.y);
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import funkin.data.IRegistryEntry;
|
|||
import flixel.group.FlxSpriteGroup;
|
||||
import flixel.graphics.frames.FlxFramesCollection;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import flixel.addons.text.FlxTypeText;
|
||||
import funkin.util.assets.FlxAnimationUtil;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.audio.FunkinSound;
|
||||
|
@ -66,7 +65,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple
|
|||
}
|
||||
|
||||
var boxSprite:FlxSprite;
|
||||
var textDisplay:FlxTypeText;
|
||||
var textDisplay:FunkinTypeText;
|
||||
|
||||
var text(default, set):String;
|
||||
|
||||
|
@ -273,7 +272,7 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple
|
|||
|
||||
function loadText():Void
|
||||
{
|
||||
textDisplay = new FlxTypeText(0, 0, 300, '', 32);
|
||||
textDisplay = new FunkinTypeText(0, 0, 300, '', 32);
|
||||
textDisplay.fieldWidth = _data.text.width;
|
||||
textDisplay.setFormat(_data.text.fontFamily, _data.text.size, FlxColor.fromString(_data.text.color), LEFT, SHADOW,
|
||||
FlxColor.fromString(_data.text.shadowColor ?? '#00000000'), false);
|
||||
|
|
104
source/funkin/play/cutscene/dialogue/FunkinTypeText.hx
Normal file
104
source/funkin/play/cutscene/dialogue/FunkinTypeText.hx
Normal file
|
@ -0,0 +1,104 @@
|
|||
package funkin.play.cutscene.dialogue;
|
||||
|
||||
import flixel.addons.text.FlxTypeText;
|
||||
import flixel.input.keyboard.FlxKey;
|
||||
|
||||
/**
|
||||
* An FlxTypeText that better accounts for text-wrapping,
|
||||
* by overriding the functions of insertBreakLines() to check the finished state.
|
||||
* Also fixes a bug where empty strings would make the typing never 'finish'.
|
||||
*/
|
||||
class FunkinTypeText extends FlxTypeText
|
||||
{
|
||||
public var preWrapping:Bool = true;
|
||||
|
||||
public function new(X:Float, Y:Float, Width:Int, Text:String, Size:Int = 8, EmbeddedFont:Bool = true, CheckWrapping:Bool = true)
|
||||
{
|
||||
super(X, Y, Width, "", Size, EmbeddedFont);
|
||||
_finalText = Text;
|
||||
preWrapping = CheckWrapping;
|
||||
}
|
||||
|
||||
override public function start(?Delay:Float, ForceRestart:Bool = false, AutoErase:Bool = false, ?SkipKeys:Array<FlxKey>, ?Callback:Void->Void):Void
|
||||
{
|
||||
if (Delay != null)
|
||||
{
|
||||
delay = Delay;
|
||||
}
|
||||
|
||||
_typing = true;
|
||||
_erasing = false;
|
||||
paused = false;
|
||||
_waiting = false;
|
||||
|
||||
if (ForceRestart)
|
||||
{
|
||||
text = "";
|
||||
_length = 0;
|
||||
}
|
||||
|
||||
autoErase = AutoErase;
|
||||
|
||||
if (SkipKeys != null)
|
||||
{
|
||||
skipKeys = SkipKeys;
|
||||
}
|
||||
|
||||
if (Callback != null)
|
||||
{
|
||||
completeCallback = Callback;
|
||||
}
|
||||
|
||||
if (useDefaultSound)
|
||||
{
|
||||
loadDefaultSound();
|
||||
}
|
||||
|
||||
// Autocomplete if the text is empty anyway. Why bother?
|
||||
if (_finalText.length == 0)
|
||||
{
|
||||
onComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
if (preWrapping)
|
||||
{
|
||||
insertBreakLines();
|
||||
}
|
||||
}
|
||||
|
||||
override function insertBreakLines()
|
||||
{
|
||||
var saveText = text;
|
||||
|
||||
// See what it looks like when it's finished typing.
|
||||
text = prefix + _finalText;
|
||||
var prefixLength:Null<Int> = prefix.length;
|
||||
var split:String = '';
|
||||
|
||||
// trace('Breaking apart text lines...');
|
||||
|
||||
for (i in 0...textField.numLines)
|
||||
{
|
||||
var curLine = textField.getLineText(i);
|
||||
// trace('now at line $i, curLine: $curLine');
|
||||
if (prefixLength >= curLine.length)
|
||||
{
|
||||
prefixLength -= curLine.length;
|
||||
}
|
||||
else if (prefixLength != null)
|
||||
{
|
||||
split += curLine.substr(prefixLength);
|
||||
prefixLength = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
split += '\n' + curLine;
|
||||
}
|
||||
// trace('now at line $i, split: $split');
|
||||
}
|
||||
|
||||
_finalText = split;
|
||||
text = saveText;
|
||||
}
|
||||
}
|
|
@ -218,9 +218,21 @@ class FocusCameraSongEvent extends SongEvent
|
|||
'Smooth Step In' => 'smoothStepIn',
|
||||
'Smooth Step Out' => 'smoothStepOut',
|
||||
'Smooth Step In/Out' => 'smoothStepInOut',
|
||||
'Smoother Step In' => 'smootherStepIn',
|
||||
'Smoother Step Out' => 'smootherStepOut',
|
||||
'Smoother Step In/Out' => 'smootherStepInOut',
|
||||
'Elastic In' => 'elasticIn',
|
||||
'Elastic Out' => 'elasticOut',
|
||||
'Elastic In/Out' => 'elasticInOut',
|
||||
'Back In' => 'backIn',
|
||||
'Back Out' => 'backOut',
|
||||
'Back In/Out' => 'backInOut',
|
||||
'Bounce In' => 'bounceIn',
|
||||
'Bounce Out' => 'bounceOut',
|
||||
'Bounce In/Out' => 'bounceInOut',
|
||||
'Circ In' => 'circIn',
|
||||
'Circ Out' => 'circOut',
|
||||
'Circ In/Out' => 'circInOut',
|
||||
'Instant (Ignores duration)' => 'INSTANT',
|
||||
'Classic (Ignores duration)' => 'CLASSIC'
|
||||
]
|
||||
|
|
|
@ -149,9 +149,21 @@ class ScrollSpeedEvent extends SongEvent
|
|||
'Smooth Step In' => 'smoothStepIn',
|
||||
'Smooth Step Out' => 'smoothStepOut',
|
||||
'Smooth Step In/Out' => 'smoothStepInOut',
|
||||
'Smoother Step In' => 'smootherStepIn',
|
||||
'Smoother Step Out' => 'smootherStepOut',
|
||||
'Smoother Step In/Out' => 'smootherStepInOut',
|
||||
'Elastic In' => 'elasticIn',
|
||||
'Elastic Out' => 'elasticOut',
|
||||
'Elastic In/Out' => 'elasticInOut'
|
||||
'Elastic In/Out' => 'elasticInOut',
|
||||
'Back In' => 'backIn',
|
||||
'Back Out' => 'backOut',
|
||||
'Back In/Out' => 'backInOut',
|
||||
'Bounce In' => 'bounceIn',
|
||||
'Bounce Out' => 'bounceOut',
|
||||
'Bounce In/Out' => 'bounceInOut',
|
||||
'Circ In' => 'circIn',
|
||||
'Circ Out' => 'circOut',
|
||||
'Circ In/Out' => 'circInOut'
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
@ -158,9 +158,21 @@ class ZoomCameraSongEvent extends SongEvent
|
|||
'Smooth Step In' => 'smoothStepIn',
|
||||
'Smooth Step Out' => 'smoothStepOut',
|
||||
'Smooth Step In/Out' => 'smoothStepInOut',
|
||||
'Smoother Step In' => 'smootherStepIn',
|
||||
'Smoother Step Out' => 'smootherStepOut',
|
||||
'Smoother Step In/Out' => 'smootherStepInOut',
|
||||
'Elastic In' => 'elasticIn',
|
||||
'Elastic Out' => 'elasticOut',
|
||||
'Elastic In/Out' => 'elasticInOut'
|
||||
'Elastic In/Out' => 'elasticInOut',
|
||||
'Back In' => 'backIn',
|
||||
'Back Out' => 'backOut',
|
||||
'Back In/Out' => 'backInOut',
|
||||
'Bounce In' => 'bounceIn',
|
||||
'Bounce Out' => 'bounceOut',
|
||||
'Bounce In/Out' => 'bounceInOut',
|
||||
'Circ In' => 'circIn',
|
||||
'Circ Out' => 'circOut',
|
||||
'Circ In/Out' => 'circInOut'
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
|
|
@ -71,6 +71,8 @@ class NoteHoldCover extends FlxTypedSpriteGroup<FlxSprite>
|
|||
|
||||
this.visible = false;
|
||||
|
||||
holdNote.cover = null;
|
||||
|
||||
if (glow != null) glow.visible = false;
|
||||
if (sparks != null) sparks.visible = false;
|
||||
}
|
||||
|
|
|
@ -448,7 +448,6 @@ class Strumline extends FlxSpriteGroup
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* For a note's strumTime, calculate its Y position relative to the strumline.
|
||||
* NOTE: Assumes Conductor and PlayState are both initialized.
|
||||
|
@ -458,7 +457,7 @@ class Strumline extends FlxSpriteGroup
|
|||
public function calculateNoteYPos(strumTime:Float):Float
|
||||
{
|
||||
return
|
||||
Constants.PIXELS_PER_MS * (conductorInUse.songPosition - strumTime - Conductor.instance.inputOffset) * scrollSpeed * (Preferences.downscroll ? 1 : -1);
|
||||
Constants.PIXELS_PER_MS * (conductorInUse.getTimeWithDelta() - strumTime - Conductor.instance.inputOffset) * scrollSpeed * (Preferences.downscroll ? 1 : -1);
|
||||
}
|
||||
|
||||
public function updateNotes():Void
|
||||
|
|
|
@ -194,14 +194,14 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
|||
includeScript:Bool = true, validScore:Bool = false):Song
|
||||
{
|
||||
@:privateAccess
|
||||
var result:Null<Song>;
|
||||
var result:Null<Song> = null;
|
||||
|
||||
if (includeScript && SongRegistry.instance.isScriptedEntry(songId))
|
||||
{
|
||||
var songClassName:String = SongRegistry.instance.getScriptedEntryClassName(songId);
|
||||
var songClassName:Null<String> = SongRegistry.instance.getScriptedEntryClassName(songId);
|
||||
|
||||
@:privateAccess
|
||||
result = SongRegistry.instance.createScriptedEntry(songClassName);
|
||||
if (songClassName != null) result = SongRegistry.instance.createScriptedEntry(songClassName);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
package funkin.play.song;
|
||||
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.util.FileUtil;
|
||||
import openfl.net.FileReference;
|
||||
|
||||
/**
|
||||
* TODO: Refactor and remove this.
|
||||
*/
|
||||
class SongSerializer
|
||||
{
|
||||
/**
|
||||
* Access a SongChartData JSON file from a specific path, then load it.
|
||||
* @param path The file path to read from.
|
||||
*/
|
||||
public static function importSongChartDataSync(path:String):SongChartData
|
||||
{
|
||||
var fileData = FileUtil.readStringFromPath(path);
|
||||
|
||||
if (fileData == null) return null;
|
||||
|
||||
var songChartData:SongChartData = fileData.parseJSON();
|
||||
|
||||
return songChartData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Access a SongMetadata JSON file from a specific path, then load it.
|
||||
* @param path The file path to read from.
|
||||
*/
|
||||
public static function importSongMetadataSync(path:String):SongMetadata
|
||||
{
|
||||
var fileData = FileUtil.readStringFromPath(path);
|
||||
|
||||
if (fileData == null) return null;
|
||||
|
||||
var songMetadata:SongMetadata = fileData.parseJSON();
|
||||
|
||||
return songMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user to browse for a SongChartData JSON file path, then load it.
|
||||
* @param callback The function to call when the file is loaded.
|
||||
*/
|
||||
public static function importSongChartDataAsync(callback:SongChartData->Void):Void
|
||||
{
|
||||
FileUtil.browseFileReference(function(fileReference:FileReference) {
|
||||
var data = fileReference.data.toString();
|
||||
|
||||
if (data == null) return;
|
||||
|
||||
var songChartData:SongChartData = data.parseJSON();
|
||||
|
||||
if (songChartData != null) callback(songChartData);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt the user to browse for a SongMetadata JSON file path, then load it.
|
||||
* @param callback The function to call when the file is loaded.
|
||||
*/
|
||||
public static function importSongMetadataAsync(callback:SongMetadata->Void):Void
|
||||
{
|
||||
FileUtil.browseFileReference(function(fileReference:FileReference) {
|
||||
var data = fileReference.data.toString();
|
||||
|
||||
if (data == null) return;
|
||||
|
||||
var songMetadata:SongMetadata = data.parseJSON();
|
||||
|
||||
if (songMetadata != null) callback(songMetadata);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -248,8 +248,8 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
|
|||
propSprite.scrollFactor.y = dataProp.scroll[1];
|
||||
|
||||
propSprite.angle = dataProp.angle;
|
||||
propSprite.color = FlxColor.fromString(dataProp.color);
|
||||
@:privateAccess if (!isSolidColor) propSprite.blend = BlendMode.fromString(dataProp.blend);
|
||||
if (!isSolidColor) propSprite.color = FlxColor.fromString(dataProp.color);
|
||||
@:privateAccess propSprite.blend = BlendMode.fromString(dataProp.blend);
|
||||
|
||||
propSprite.zIndex = dataProp.zIndex;
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package funkin.save;
|
|||
|
||||
import flixel.util.FlxSave;
|
||||
import funkin.input.Controls.Device;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.play.scoring.Scoring;
|
||||
import funkin.play.scoring.Scoring.ScoringRank;
|
||||
import funkin.save.migrator.RawSaveData_v1_0_0;
|
||||
|
@ -127,8 +128,6 @@ class Save
|
|||
shouldHideMouse: true,
|
||||
fancyPreview: true,
|
||||
previewOnSave: true,
|
||||
saveFormat: 'PNG',
|
||||
jpegQuality: 80,
|
||||
},
|
||||
|
||||
controls:
|
||||
|
@ -173,6 +172,10 @@ class Save
|
|||
metronomeVolume: 1.0,
|
||||
hitsoundVolumePlayer: 1.0,
|
||||
hitsoundVolumeOpponent: 1.0,
|
||||
instVolume: 1.0,
|
||||
playerVoiceVolume: 1.0,
|
||||
opponentVoiceVolume: 1.0,
|
||||
playbackSpeed: 0.5,
|
||||
themeMusic: true
|
||||
},
|
||||
|
||||
|
@ -181,7 +184,10 @@ class Save
|
|||
previousFiles: [],
|
||||
moveStep: "1px",
|
||||
angleStep: 5,
|
||||
theme: StageEditorTheme.Light
|
||||
theme: StageEditorTheme.Light,
|
||||
bfChar: "bf",
|
||||
gfChar: "gf",
|
||||
dadChar: "dad"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -407,6 +413,57 @@ class Save
|
|||
return data.optionsChartEditor.hitsoundVolumeOpponent;
|
||||
}
|
||||
|
||||
public var chartEditorInstVolume(get, set):Float;
|
||||
|
||||
function get_chartEditorInstVolume():Float
|
||||
{
|
||||
if (data.optionsChartEditor.instVolume == null) data.optionsChartEditor.instVolume = 1.0;
|
||||
|
||||
return data.optionsChartEditor.instVolume;
|
||||
}
|
||||
|
||||
function set_chartEditorInstVolume(value:Float):Float
|
||||
{
|
||||
// Set and apply.
|
||||
data.optionsChartEditor.instVolume = value;
|
||||
flush();
|
||||
return data.optionsChartEditor.instVolume;
|
||||
}
|
||||
|
||||
public var chartEditorPlayerVoiceVolume(get, set):Float;
|
||||
|
||||
function get_chartEditorPlayerVoiceVolume():Float
|
||||
{
|
||||
if (data.optionsChartEditor.playerVoiceVolume == null) data.optionsChartEditor.playerVoiceVolume = 1.0;
|
||||
|
||||
return data.optionsChartEditor.playerVoiceVolume;
|
||||
}
|
||||
|
||||
function set_chartEditorPlayerVoiceVolume(value:Float):Float
|
||||
{
|
||||
// Set and apply.
|
||||
data.optionsChartEditor.playerVoiceVolume = value;
|
||||
flush();
|
||||
return data.optionsChartEditor.playerVoiceVolume;
|
||||
}
|
||||
|
||||
public var chartEditorOpponentVoiceVolume(get, set):Float;
|
||||
|
||||
function get_chartEditorOpponentVoiceVolume():Float
|
||||
{
|
||||
if (data.optionsChartEditor.opponentVoiceVolume == null) data.optionsChartEditor.opponentVoiceVolume = 1.0;
|
||||
|
||||
return data.optionsChartEditor.opponentVoiceVolume;
|
||||
}
|
||||
|
||||
function set_chartEditorOpponentVoiceVolume(value:Float):Float
|
||||
{
|
||||
// Set and apply.
|
||||
data.optionsChartEditor.opponentVoiceVolume = value;
|
||||
flush();
|
||||
return data.optionsChartEditor.opponentVoiceVolume;
|
||||
}
|
||||
|
||||
public var chartEditorThemeMusic(get, set):Bool;
|
||||
|
||||
function get_chartEditorThemeMusic():Bool
|
||||
|
@ -428,7 +485,7 @@ class Save
|
|||
|
||||
function get_chartEditorPlaybackSpeed():Float
|
||||
{
|
||||
if (data.optionsChartEditor.playbackSpeed == null) data.optionsChartEditor.playbackSpeed = 1.0;
|
||||
if (data.optionsChartEditor.playbackSpeed == null) data.optionsChartEditor.playbackSpeed = 0.5;
|
||||
|
||||
return data.optionsChartEditor.playbackSpeed;
|
||||
}
|
||||
|
@ -550,6 +607,60 @@ class Save
|
|||
return data.optionsStageEditor.theme;
|
||||
}
|
||||
|
||||
public var stageBoyfriendChar(get, set):String;
|
||||
|
||||
function get_stageBoyfriendChar():String
|
||||
{
|
||||
if (data.optionsStageEditor.bfChar == null
|
||||
|| CharacterDataParser.fetchCharacterData(data.optionsStageEditor.bfChar) == null) data.optionsStageEditor.bfChar = "bf";
|
||||
|
||||
return data.optionsStageEditor.bfChar;
|
||||
}
|
||||
|
||||
function set_stageBoyfriendChar(value:String):String
|
||||
{
|
||||
// Set and apply.
|
||||
data.optionsStageEditor.bfChar = value;
|
||||
flush();
|
||||
return data.optionsStageEditor.bfChar;
|
||||
}
|
||||
|
||||
public var stageGirlfriendChar(get, set):String;
|
||||
|
||||
function get_stageGirlfriendChar():String
|
||||
{
|
||||
if (data.optionsStageEditor.gfChar == null
|
||||
|| CharacterDataParser.fetchCharacterData(data.optionsStageEditor.gfChar ?? "") == null) data.optionsStageEditor.gfChar = "gf";
|
||||
|
||||
return data.optionsStageEditor.gfChar;
|
||||
}
|
||||
|
||||
function set_stageGirlfriendChar(value:String):String
|
||||
{
|
||||
// Set and apply.
|
||||
data.optionsStageEditor.gfChar = value;
|
||||
flush();
|
||||
return data.optionsStageEditor.gfChar;
|
||||
}
|
||||
|
||||
public var stageDadChar(get, set):String;
|
||||
|
||||
function get_stageDadChar():String
|
||||
{
|
||||
if (data.optionsStageEditor.dadChar == null
|
||||
|| CharacterDataParser.fetchCharacterData(data.optionsStageEditor.dadChar ?? "") == null) data.optionsStageEditor.dadChar = "dad";
|
||||
|
||||
return data.optionsStageEditor.dadChar;
|
||||
}
|
||||
|
||||
function set_stageDadChar(value:String):String
|
||||
{
|
||||
// Set and apply.
|
||||
data.optionsStageEditor.dadChar = value;
|
||||
flush();
|
||||
return data.optionsStageEditor.dadChar;
|
||||
}
|
||||
|
||||
/**
|
||||
* When we've seen a character unlock, add it to the list of characters seen.
|
||||
* @param character
|
||||
|
@ -1441,16 +1552,12 @@ typedef SaveDataOptions =
|
|||
* @param shouldHideMouse Should the mouse be hidden when taking a screenshot? Default: `true`
|
||||
* @param fancyPreview Show a fancy preview? Default: `true`
|
||||
* @param previewOnSave Only show the fancy preview after a screenshot is saved? Default: `true`
|
||||
* @param saveFormat The save format of the screenshot, PNG or JPEG. Default: `PNG`
|
||||
* @param jpegQuality The JPEG Quality, if we're saving to the format. Default: `80`
|
||||
*/
|
||||
var screenshot:
|
||||
{
|
||||
var shouldHideMouse:Bool;
|
||||
var fancyPreview:Bool;
|
||||
var previewOnSave:Bool;
|
||||
var saveFormat:String;
|
||||
var jpegQuality:Int;
|
||||
};
|
||||
|
||||
var controls:
|
||||
|
@ -1653,10 +1760,16 @@ typedef SaveDataChartEditorOptions =
|
|||
var ?instVolume:Float;
|
||||
|
||||
/**
|
||||
* Voices volume in the Chart Editor.
|
||||
* Player voice volume in the Chart Editor.
|
||||
* @default `1.0`
|
||||
*/
|
||||
var ?voicesVolume:Float;
|
||||
var ?playerVoiceVolume:Float;
|
||||
|
||||
/**
|
||||
* Opponent voice volume in the Chart Editor.
|
||||
* @default `1.0`
|
||||
*/
|
||||
var ?opponentVoiceVolume:Float;
|
||||
|
||||
/**
|
||||
* Playback speed in the Chart Editor.
|
||||
|
@ -1699,4 +1812,22 @@ typedef SaveDataStageEditorOptions =
|
|||
* @default `StageEditorTheme.Light`
|
||||
*/
|
||||
var ?theme:StageEditorTheme;
|
||||
|
||||
/**
|
||||
* The BF character ID used in testing stages.
|
||||
* @default bf
|
||||
*/
|
||||
var ?bfChar:String;
|
||||
|
||||
/**
|
||||
* The GF character ID used in testing stages.
|
||||
* @default gf
|
||||
*/
|
||||
var ?gfChar:String;
|
||||
|
||||
/**
|
||||
* The Dad character ID used in testing stages.
|
||||
* @default dad
|
||||
*/
|
||||
var ?dadChar:String;
|
||||
};
|
||||
|
|
|
@ -8,9 +8,10 @@ typedef AtlasAsset = flixel.util.typeLimit.OneOfTwo<String, FlxAtlasFrames>;
|
|||
/**
|
||||
* A menulist whose items share a single texture atlas.
|
||||
*/
|
||||
@:nullSafety
|
||||
class AtlasMenuList extends MenuTypedList<AtlasMenuItem>
|
||||
{
|
||||
public var atlas:FlxAtlasFrames;
|
||||
public var atlas:Null<FlxAtlasFrames>;
|
||||
|
||||
public function new(atlas, navControls:NavControls = Vertical, ?wrapMode)
|
||||
{
|
||||
|
@ -38,9 +39,10 @@ class AtlasMenuList extends MenuTypedList<AtlasMenuItem>
|
|||
/**
|
||||
* A menu list item which uses single texture atlas.
|
||||
*/
|
||||
@:nullSafety
|
||||
class AtlasMenuItem extends MenuListItem
|
||||
{
|
||||
var atlas:FlxAtlasFrames;
|
||||
var atlas:Null<FlxAtlasFrames>;
|
||||
|
||||
public var centered:Bool = false;
|
||||
|
||||
|
@ -52,7 +54,7 @@ class AtlasMenuItem extends MenuListItem
|
|||
|
||||
override function setData(name:String, ?callback:Void->Void)
|
||||
{
|
||||
frames = atlas;
|
||||
if (atlas != null) frames = atlas;
|
||||
animation.addByPrefix('idle', '$name idle', 24);
|
||||
animation.addByPrefix('selected', '$name selected', 24);
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import flixel.util.FlxStringUtil;
|
|||
* AtlasText is an improved version of Alphabet and FlxBitmapText.
|
||||
* It supports animations on the letters, and is less buggy than Alphabet.
|
||||
*/
|
||||
@:nullSafety
|
||||
class AtlasText extends FlxTypedSpriteGroup<AtlasChar>
|
||||
{
|
||||
static var fonts = new Map<AtlasFont, AtlasFontData>();
|
||||
|
@ -16,7 +17,7 @@ class AtlasText extends FlxTypedSpriteGroup<AtlasChar>
|
|||
|
||||
public var text(default, set):String = "";
|
||||
|
||||
var font:AtlasFontData;
|
||||
var font:AtlasFontData = new AtlasFontData(AtlasFont.DEFAULT);
|
||||
|
||||
public var atlas(get, never):FlxAtlasFrames;
|
||||
|
||||
|
@ -33,10 +34,10 @@ class AtlasText extends FlxTypedSpriteGroup<AtlasChar>
|
|||
inline function get_maxHeight()
|
||||
return font.maxHeight;
|
||||
|
||||
public function new(x = 0.0, y = 0.0, text:String, fontName:AtlasFont = AtlasFont.DEFAULT)
|
||||
public function new(x = 0.0, y = 0.0, text:String = "", fontName:AtlasFont = AtlasFont.DEFAULT)
|
||||
{
|
||||
if (!fonts.exists(fontName)) fonts[fontName] = new AtlasFontData(fontName);
|
||||
font = fonts[fontName];
|
||||
font = fonts[fontName] ?? new AtlasFontData(fontName);
|
||||
|
||||
super(x, y);
|
||||
|
||||
|
@ -251,6 +252,7 @@ class AtlasChar extends FlxSprite
|
|||
}
|
||||
}
|
||||
|
||||
@:nullSafety
|
||||
private class AtlasFontData
|
||||
{
|
||||
static public var upperChar = ~/^[A-Z]\d+$/;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue