mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2025-08-31 19:04:55 +00:00
Compare commits
65 commits
07cd2122d8
...
2f2efead77
Author | SHA1 | Date | |
---|---|---|---|
|
2f2efead77 | ||
|
7415cc4069 | ||
|
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",
|
||||
|
|
2
assets
2
assets
|
@ -1 +1 @@
|
|||
Subproject commit c108a7ff0d11bf328e7b232160b8f68c71e21bca
|
||||
Subproject commit d3bca00d0619c7cd0de4343049762dfba1e0fe23
|
1919
project.hxp
1919
project.hxp
File diff suppressed because it is too large
Load diff
|
@ -92,6 +92,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 +428,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,12 +495,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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a more accurate music time for higher framerates.
|
||||
* @return Float
|
||||
*/
|
||||
public function getTimeWithDelta():Float
|
||||
{
|
||||
return this.songPosition + this.songPositionDelta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be called in-between frames, usually for input related things
|
||||
* that can potentially get processed on exact milliseconds/timestmaps.
|
||||
|
|
|
@ -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;
|
||||
|
@ -241,6 +242,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 +307,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'));
|
||||
|
|
|
@ -343,44 +343,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.
|
||||
*/
|
||||
|
|
|
@ -56,8 +56,6 @@ class DiscordClient
|
|||
{
|
||||
while (true)
|
||||
{
|
||||
trace('[DISCORD] Performing client update...');
|
||||
|
||||
#if DISCORD_DISABLE_IO_THREAD
|
||||
Discord.updateConnection();
|
||||
#end
|
||||
|
@ -76,8 +74,6 @@ class DiscordClient
|
|||
|
||||
public function setPresence(params:DiscordClientPresenceParams):Void
|
||||
{
|
||||
trace('[DISCORD] Updating presence... (${params})');
|
||||
|
||||
Discord.updatePresence(buildPresence(params));
|
||||
}
|
||||
|
||||
|
@ -101,8 +97,6 @@ class DiscordClient
|
|||
// 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";
|
||||
|
@ -204,4 +198,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,7 +2,10 @@ 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;
|
||||
|
||||
|
@ -66,6 +69,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?.onComplete != null) params.onComplete(leaderboardData.scores);
|
||||
|
||||
case FAIL(error):
|
||||
trace('[NEWGROUNDS] Failed to fetch scores!');
|
||||
trace(error);
|
||||
if (params?.onFail != null) params.onFail();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit a score for a Story Level to Newgrounds.
|
||||
*/
|
||||
|
@ -84,6 +122,74 @@ 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)
|
||||
|
@ -285,7 +391,7 @@ enum abstract Leaderboard(Int)
|
|||
{
|
||||
case "darnell":
|
||||
return DarnellBFMix;
|
||||
case "litup":
|
||||
case "lit-up":
|
||||
return LitUpBFMix;
|
||||
default:
|
||||
return Unknown;
|
||||
|
@ -379,7 +485,7 @@ enum abstract Leaderboard(Int)
|
|||
return Stress;
|
||||
case "darnell":
|
||||
return Darnell;
|
||||
case "litup":
|
||||
case "lit-up":
|
||||
return LitUp;
|
||||
case "2hot":
|
||||
return TwoHot;
|
||||
|
|
|
@ -131,32 +131,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 +370,8 @@ enum abstract Medal(Int) from Int to Int
|
|||
{
|
||||
switch (levelId)
|
||||
{
|
||||
case "tutorial":
|
||||
return StoryTutorial;
|
||||
case "week1":
|
||||
return StoryWeek1;
|
||||
case "week2":
|
||||
|
@ -344,4 +392,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
|
||||
|
|
|
@ -551,6 +551,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
|
|||
}
|
||||
FlxTween.cancelTweensOf(this);
|
||||
this._label = 'unknown';
|
||||
this._waveformData = null;
|
||||
}
|
||||
|
||||
@:access(openfl.media.Sound)
|
||||
|
|
|
@ -5,7 +5,6 @@ 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;
|
||||
|
||||
|
|
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}.';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -258,6 +258,21 @@ class PolymodHandler
|
|||
// `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 +291,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 +325,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 +338,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 +355,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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -766,11 +766,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!
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
@ -2056,9 +2098,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 +2109,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 +2148,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 +2157,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 +2395,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 +2462,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 +2496,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 +2757,8 @@ class PlayState extends MusicBeatSubState
|
|||
FlxG.switchState(() -> new ChartEditorState(
|
||||
{
|
||||
targetSongId: currentSong.id,
|
||||
targetSongDifficulty: currentDifficulty,
|
||||
targetSongVariation: currentVariation,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -3154,6 +3204,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 +3228,8 @@ class PlayState extends MusicBeatSubState
|
|||
}
|
||||
}
|
||||
|
||||
forEachPausedSound((s) -> s.destroy());
|
||||
|
||||
// Remove reference to stage and remove sprites from it to save memory.
|
||||
if (currentStage != null)
|
||||
{
|
||||
|
@ -3429,7 +3484,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 +3538,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.
|
||||
|
|
|
@ -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'
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -173,6 +173,12 @@ class MenuTypedList<T:MenuListItem> extends FlxTypedGroup<T>
|
|||
}
|
||||
}
|
||||
|
||||
public function cancelAccept()
|
||||
{
|
||||
FlxFlicker.stopFlickering(members[selectedIndex]);
|
||||
busy = false;
|
||||
}
|
||||
|
||||
public function selectItem(index:Int)
|
||||
{
|
||||
members[selectedIndex].idle();
|
||||
|
|
|
@ -62,7 +62,8 @@ class CharSelectSubState extends MusicBeatSubState
|
|||
var chooseDipshit:FlxSprite;
|
||||
var dipshitBlur:FlxSprite;
|
||||
var transitionGradient:FlxSprite;
|
||||
var curChar(default, set):String = "pico";
|
||||
var curChar(default, set):String = Constants.DEFAULT_CHARACTER;
|
||||
var rememberedChar:String;
|
||||
var nametag:Nametag;
|
||||
var camFollow:FlxObject;
|
||||
var autoFollow:Bool = false;
|
||||
|
@ -87,9 +88,10 @@ class CharSelectSubState extends MusicBeatSubState
|
|||
var bopInfo:FramesJSFLInfo;
|
||||
var blackScreen:FunkinSprite;
|
||||
|
||||
public function new()
|
||||
public function new(?params:CharSelectSubStateParams)
|
||||
{
|
||||
super();
|
||||
rememberedChar = params?.character;
|
||||
loadAvailableCharacters();
|
||||
}
|
||||
|
||||
|
@ -167,18 +169,39 @@ class CharSelectSubState extends MusicBeatSubState
|
|||
charLightGF.loadGraphic(Paths.image('charSelect/charLight'));
|
||||
add(charLightGF);
|
||||
|
||||
gfChill = new CharSelectGF();
|
||||
gfChill.switchGF("bf");
|
||||
add(gfChill);
|
||||
function setupPlayerChill(character:String)
|
||||
{
|
||||
gfChill = new CharSelectGF();
|
||||
gfChill.switchGF(character);
|
||||
add(gfChill);
|
||||
|
||||
playerChillOut = new CharSelectPlayer(0, 0);
|
||||
playerChillOut.switchChar("bf");
|
||||
playerChillOut.visible = false;
|
||||
add(playerChillOut);
|
||||
playerChillOut = new CharSelectPlayer(0, 0);
|
||||
playerChillOut.switchChar(character);
|
||||
playerChillOut.visible = false;
|
||||
add(playerChillOut);
|
||||
|
||||
playerChill = new CharSelectPlayer(0, 0);
|
||||
playerChill.switchChar("bf");
|
||||
add(playerChill);
|
||||
playerChill = new CharSelectPlayer(0, 0);
|
||||
playerChill.switchChar(character);
|
||||
add(playerChill);
|
||||
}
|
||||
|
||||
// I think I can do the character preselect thing here? This better work
|
||||
// Edit: [UH-OH!] yes! It does!
|
||||
if (rememberedChar != null && rememberedChar != Constants.DEFAULT_CHARACTER)
|
||||
{
|
||||
setupPlayerChill(rememberedChar);
|
||||
for (pos => charId in availableChars)
|
||||
{
|
||||
if (charId == rememberedChar)
|
||||
{
|
||||
setCursorPosition(pos);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@:bypassAccessor curChar = rememberedChar;
|
||||
}
|
||||
else
|
||||
setupPlayerChill(Constants.DEFAULT_CHARACTER);
|
||||
|
||||
var speakers:FlxAtlasSprite = new FlxAtlasSprite(0, 0, Paths.animateAtlas("charSelect/charSelectSpeakers"));
|
||||
speakers.anim.play("");
|
||||
|
@ -224,7 +247,7 @@ class CharSelectSubState extends MusicBeatSubState
|
|||
dipshitBacking.scrollFactor.set();
|
||||
dipshitBlur.scrollFactor.set();
|
||||
|
||||
nametag = new Nametag();
|
||||
nametag = new Nametag(curChar);
|
||||
add(nametag);
|
||||
|
||||
nametag.scrollFactor.set();
|
||||
|
@ -1063,6 +1086,25 @@ class CharSelectSubState extends MusicBeatSubState
|
|||
return gridPosition;
|
||||
}
|
||||
|
||||
// Moved this code into a function because is now used twice
|
||||
function setCursorPosition(index:Int)
|
||||
{
|
||||
var copy = 3;
|
||||
var yThing = -1;
|
||||
|
||||
while ((index + 1) > copy)
|
||||
{
|
||||
yThing++;
|
||||
copy += 3;
|
||||
}
|
||||
|
||||
var xThing = (copy - index - 2) * -1;
|
||||
|
||||
// Look, I'd write better code but I had better aneurysms, my bad - Cheems
|
||||
cursorY = yThing;
|
||||
cursorX = xThing;
|
||||
}
|
||||
|
||||
function set_curChar(value:String):String
|
||||
{
|
||||
if (curChar == value) return value;
|
||||
|
@ -1113,3 +1155,11 @@ class CharSelectSubState extends MusicBeatSubState
|
|||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parameters used to initialize the CharSelectSubState.
|
||||
*/
|
||||
typedef CharSelectSubStateParams =
|
||||
{
|
||||
?character:String, // ?fromFreeplaySelect:Bool,
|
||||
};
|
||||
|
|
|
@ -10,14 +10,17 @@ class Nametag extends FlxSprite
|
|||
var midpointY(default, set):Float = 100;
|
||||
var mosaicShader:MosaicEffect;
|
||||
|
||||
public function new(?x:Float = 0, ?y:Float = 0)
|
||||
public function new(?x:Float = 0, ?y:Float = 0, character:String)
|
||||
{
|
||||
super(x, y);
|
||||
|
||||
mosaicShader = new MosaicEffect();
|
||||
shader = mosaicShader;
|
||||
|
||||
switchChar("bf");
|
||||
// So that's why there was that cursed sight (originally defaulted to bf)
|
||||
if (character != null) switchChar(character);
|
||||
else
|
||||
switchChar(Constants.DEFAULT_CHARACTER);
|
||||
}
|
||||
|
||||
public function updatePosition():Void
|
||||
|
|
|
@ -30,6 +30,7 @@ import funkin.data.song.SongData.SongNoteData;
|
|||
import funkin.data.song.SongData.SongOffsets;
|
||||
import funkin.data.song.SongData.NoteParamData;
|
||||
import funkin.data.song.SongDataUtils;
|
||||
import funkin.data.song.SongNoteDataUtils;
|
||||
import funkin.graphics.FunkinCamera;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import funkin.input.Cursor;
|
||||
|
@ -61,6 +62,7 @@ import funkin.ui.debug.charting.commands.PasteItemsCommand;
|
|||
import funkin.ui.debug.charting.commands.RemoveEventsCommand;
|
||||
import funkin.ui.debug.charting.commands.RemoveItemsCommand;
|
||||
import funkin.ui.debug.charting.commands.RemoveNotesCommand;
|
||||
import funkin.ui.debug.charting.commands.RemoveStackedNotesCommand;
|
||||
import funkin.ui.debug.charting.commands.SelectAllItemsCommand;
|
||||
import funkin.ui.debug.charting.commands.SelectItemsCommand;
|
||||
import funkin.ui.debug.charting.commands.SetItemSelectionCommand;
|
||||
|
@ -88,6 +90,7 @@ import haxe.io.Bytes;
|
|||
import haxe.io.Path;
|
||||
import haxe.ui.backend.flixel.UIState;
|
||||
import haxe.ui.components.Button;
|
||||
import haxe.ui.components.DropDown;
|
||||
import haxe.ui.components.Label;
|
||||
import haxe.ui.components.Slider;
|
||||
import haxe.ui.containers.dialogs.CollapsibleDialog;
|
||||
|
@ -355,6 +358,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
value = 0;
|
||||
}
|
||||
|
||||
// Make sure playhead doesn't scroll outside the song.
|
||||
if (value + playheadPositionInPixels < 0) playheadPositionInPixels = -value;
|
||||
if (value + playheadPositionInPixels > songLengthInPixels) playheadPositionInPixels = songLengthInPixels - value;
|
||||
|
||||
if (value > songLengthInPixels) value = songLengthInPixels;
|
||||
|
||||
if (value == scrollPositionInPixels) return value;
|
||||
|
@ -779,6 +786,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
*/
|
||||
var currentLiveInputPlaceNoteData:Array<SongNoteData> = [];
|
||||
|
||||
/**
|
||||
* Defines how "close" two notes must be to be considered stacked, based on steps.
|
||||
* For example, setting this to `0.5` (16/32) will highlight notes half a step apart.
|
||||
* Setting it to `0` only highlights notes that are nearly perfectly aligned.
|
||||
* In the dropdown menu, the threshold is based on note snaps instead.
|
||||
* For example, `0.5` would be displayed as `1/32`, and `0` would show as `Exact`.
|
||||
*/
|
||||
public static var stackedNoteThreshold:Float = 0;
|
||||
|
||||
// Note Movement
|
||||
|
||||
/**
|
||||
|
@ -851,6 +867,32 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
return currentNoteSelection;
|
||||
}
|
||||
|
||||
var currentOverlappingNotes(default, set):Array<SongNoteData> = [];
|
||||
|
||||
function set_currentOverlappingNotes(value:Array<SongNoteData>):Array<SongNoteData>
|
||||
{
|
||||
// This value is true if all elements of the current overlapping array are also in the new array.
|
||||
var isSuperset:Bool = currentOverlappingNotes.isSubset(value);
|
||||
var isEqual:Bool = currentOverlappingNotes.isEqualUnordered(value);
|
||||
|
||||
currentOverlappingNotes = value;
|
||||
|
||||
if (!isEqual)
|
||||
{
|
||||
if (currentOverlappingNotes.length > 0 && isSuperset)
|
||||
{
|
||||
notePreview.addOverlappingNotes(currentOverlappingNotes, Std.int(songLengthInMs));
|
||||
}
|
||||
else
|
||||
{
|
||||
// The new array might add or remove elements from the old array, so we have to redraw the note preview.
|
||||
notePreviewDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
return currentOverlappingNotes;
|
||||
}
|
||||
|
||||
/**
|
||||
* The events which are currently in the user's selection.
|
||||
*/
|
||||
|
@ -1294,6 +1336,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
function set_currentSongChartData(value:SongChartData):SongChartData
|
||||
{
|
||||
songChartData.set(selectedVariation, value);
|
||||
var variationMetadata:Null<SongMetadata> = songMetadata.get(selectedVariation);
|
||||
if (variationMetadata != null)
|
||||
{
|
||||
// Add the chartdata difficulties to the metadata difficulties if they don't exist so that the editor properly loads them
|
||||
var keys:Array<String> = [for (x in songChartData.get(selectedVariation).notes.keys()) x];
|
||||
for (key in keys)
|
||||
{
|
||||
variationMetadata.playData.difficulties.pushUnique(key);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
|
@ -1742,6 +1794,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
*/
|
||||
var menubarItemDelete:MenuItem;
|
||||
|
||||
/**
|
||||
* The `Edit -> Delete Stacked Notes` menu item.
|
||||
*/
|
||||
var menubarItemDeleteStacked:MenuItem;
|
||||
|
||||
/**
|
||||
* The `Edit -> Flip Notes` menu item.
|
||||
*/
|
||||
|
@ -1787,6 +1844,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
*/
|
||||
var menuBarItemNoteSnapIncrease:MenuItem;
|
||||
|
||||
/**
|
||||
* The `Edit -> Stacked Note Threshold` menu dropdown
|
||||
*/
|
||||
var menuBarStackedNoteThreshold:DropDown;
|
||||
|
||||
/**
|
||||
* The `View -> Downscroll` menu item.
|
||||
*/
|
||||
|
@ -1978,9 +2040,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
|
||||
/**
|
||||
* The IMAGE used for the selection squares. Updated by ChartEditorThemeHandler.
|
||||
* Used two ways:
|
||||
* Used three ways:
|
||||
* 1. A sprite is given this bitmap and placed over selected notes.
|
||||
* 2. The image is split and used for a 9-slice sprite for the selection box.
|
||||
* 2. Same as above but for notes that are overlapped by another.
|
||||
* 3. The image is split and used for a 9-slice sprite for the selection box.
|
||||
*/
|
||||
var selectionSquareBitmap:Null<BitmapData> = null;
|
||||
|
||||
|
@ -2231,7 +2294,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
}
|
||||
else if (params != null && params.targetSongId != null)
|
||||
{
|
||||
this.loadSongAsTemplate(params.targetSongId);
|
||||
var targetSongDifficulty = params.targetSongDifficulty ?? null;
|
||||
var targetSongVariation = params.targetSongVariation ?? null;
|
||||
this.loadSongAsTemplate(params.targetSongId, targetSongDifficulty, targetSongVariation);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -2286,13 +2351,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
currentTheme = save.chartEditorTheme;
|
||||
metronomeVolume = save.chartEditorMetronomeVolume;
|
||||
hitsoundVolumePlayer = save.chartEditorHitsoundVolumePlayer;
|
||||
hitsoundVolumePlayer = save.chartEditorHitsoundVolumeOpponent;
|
||||
hitsoundVolumeOpponent = save.chartEditorHitsoundVolumeOpponent;
|
||||
this.welcomeMusic.active = save.chartEditorThemeMusic;
|
||||
|
||||
// audioInstTrack.volume = save.chartEditorInstVolume;
|
||||
// audioInstTrack.pitch = save.chartEditorPlaybackSpeed;
|
||||
// audioVocalTrackGroup.volume = save.chartEditorVoicesVolume;
|
||||
// audioVocalTrackGroup.pitch = save.chartEditorPlaybackSpeed;
|
||||
menubarItemVolumeInstrumental.value = Std.int(save.chartEditorInstVolume * 100);
|
||||
menubarItemVolumeVocalsPlayer.value = Std.int(save.chartEditorPlayerVoiceVolume * 100);
|
||||
menubarItemVolumeVocalsOpponent.value = Std.int(save.chartEditorOpponentVoiceVolume * 100);
|
||||
menubarItemPlaybackSpeed.value = Std.int(save.chartEditorPlaybackSpeed * 100);
|
||||
}
|
||||
|
||||
public function writePreferences(hasBackup:Bool):Void
|
||||
|
@ -2318,9 +2383,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
save.chartEditorHitsoundVolumeOpponent = hitsoundVolumeOpponent;
|
||||
save.chartEditorThemeMusic = this.welcomeMusic.active;
|
||||
|
||||
// save.chartEditorInstVolume = audioInstTrack.volume;
|
||||
// save.chartEditorVoicesVolume = audioVocalTrackGroup.volume;
|
||||
// save.chartEditorPlaybackSpeed = audioInstTrack.pitch;
|
||||
save.chartEditorInstVolume = menubarItemVolumeInstrumental.value / 100.0;
|
||||
save.chartEditorPlayerVoiceVolume = menubarItemVolumeVocalsPlayer.value / 100.0;
|
||||
save.chartEditorOpponentVoiceVolume = menubarItemVolumeVocalsOpponent.value / 100.0;
|
||||
save.chartEditorPlaybackSpeed = menubarItemPlaybackSpeed.value / 100.0;
|
||||
}
|
||||
|
||||
public function populateOpenRecentMenu():Void
|
||||
|
@ -2469,7 +2535,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
add(gridPlayhead);
|
||||
gridPlayhead.zIndex = 30;
|
||||
|
||||
var playheadWidth:Int = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + (PLAYHEAD_SCROLL_AREA_WIDTH * 2);
|
||||
var playheadWidth:Int = GRID_SIZE * (STRUMLINE_SIZE * 2 + 1) + PLAYHEAD_SCROLL_AREA_WIDTH;
|
||||
var playheadBaseYPos:Float = GRID_INITIAL_Y_POS;
|
||||
gridPlayhead.setPosition(GRID_X_POS, playheadBaseYPos);
|
||||
var playheadSprite:FunkinSprite = new FunkinSprite().makeSolidColor(playheadWidth, PLAYHEAD_HEIGHT, PLAYHEAD_COLOR);
|
||||
|
@ -2936,6 +3002,17 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
}
|
||||
};
|
||||
|
||||
menubarItemDeleteStacked.onClick = _ -> {
|
||||
if (currentEventSelection.length > 0 && currentNoteSelection.length == 0)
|
||||
{
|
||||
performCommand(new RemoveEventsCommand(currentEventSelection));
|
||||
}
|
||||
else
|
||||
{
|
||||
performCommand(new RemoveStackedNotesCommand(currentNoteSelection.length > 0 ? currentNoteSelection : null));
|
||||
}
|
||||
};
|
||||
|
||||
menubarItemFlipNotes.onClick = _ -> performCommand(new FlipNotesCommand(currentNoteSelection));
|
||||
|
||||
menubarItemSelectAllNotes.onClick = _ -> performCommand(new SelectAllItemsCommand(true, false));
|
||||
|
@ -2958,6 +3035,21 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
if (noteSnapQuantIndex >= SNAP_QUANTS.length) noteSnapQuantIndex = 0;
|
||||
};
|
||||
|
||||
final REVERSE_SNAPS = SNAP_QUANTS.reversed();
|
||||
for (snap in REVERSE_SNAPS)
|
||||
{
|
||||
menuBarStackedNoteThreshold.dataSource.add({text: '1/$snap'});
|
||||
}
|
||||
|
||||
menuBarStackedNoteThreshold.onChange = event -> {
|
||||
// NOTE: It needs to be offset by 1 because of the 'Exact' option
|
||||
// -1 value means that it is the one selected
|
||||
var selectedIdx:Int = menuBarStackedNoteThreshold.selectedIndex - 1;
|
||||
stackedNoteThreshold = selectedIdx == -1 ? 0 : BASE_QUANT / REVERSE_SNAPS[selectedIdx];
|
||||
noteDisplayDirty = true;
|
||||
notePreviewDirty = true;
|
||||
}
|
||||
|
||||
menuBarItemInputStyleNone.onClick = function(event:UIEvent) {
|
||||
currentLiveInputStyle = None;
|
||||
};
|
||||
|
@ -2977,7 +3069,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
#if sys
|
||||
menubarItemGoToBackupsFolder.onClick = _ -> this.openBackupsFolder();
|
||||
#else
|
||||
// Disable the menu item if we're not on a desktop platform.
|
||||
// Disable the menu item if we're not on a native platform.
|
||||
menubarItemGoToBackupsFolder.disabled = true;
|
||||
#end
|
||||
|
||||
|
@ -3486,7 +3578,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
{
|
||||
// This sprite is off-screen or was deleted.
|
||||
// Kill the note sprite and recycle it.
|
||||
noteSprite.noteData = null;
|
||||
noteSprite.kill();
|
||||
}
|
||||
}
|
||||
// Sort the note data array, using an algorithm that is fast on nearly-sorted data.
|
||||
|
@ -3504,7 +3596,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
// It will be displayed by gridGhostHoldNoteSprite instead.
|
||||
holdNoteSprite.kill();
|
||||
}
|
||||
else if (!holdNoteSprite.isHoldNoteVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD))
|
||||
else if (!holdNoteSprite.isHoldNoteVisible(viewAreaBottomPixels, viewAreaTopPixels))
|
||||
{
|
||||
// This hold note is off-screen.
|
||||
// Kill the hold note sprite and recycle it.
|
||||
|
@ -3528,7 +3620,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
// Update the event sprite's height and position.
|
||||
// var holdNoteHeight = holdNoteSprite.noteData.getStepLength() * GRID_SIZE;
|
||||
// holdNoteSprite.setHeightDirectly(holdNoteHeight);
|
||||
holdNoteSprite.updateHoldNotePosition(renderedNotes);
|
||||
holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
|
||||
}
|
||||
}
|
||||
// Sort the note data array, using an algorithm that is fast on nearly-sorted data.
|
||||
|
@ -3544,7 +3636,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
// Resolve an issue where dragging an event too far would cause it to be hidden.
|
||||
var isSelectedAndDragged = currentEventSelection.fastContains(eventSprite.eventData) && (dragTargetCurrentStep != 0);
|
||||
|
||||
if ((eventSprite.isEventVisible(FlxG.height - PLAYBAR_HEIGHT, MENU_BAR_HEIGHT)
|
||||
if ((eventSprite.isEventVisible(viewAreaBottomPixels, viewAreaTopPixels)
|
||||
&& currentSongChartEventData.fastContains(eventSprite.eventData))
|
||||
|| isSelectedAndDragged)
|
||||
{
|
||||
|
@ -3560,7 +3652,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
{
|
||||
// This event was deleted.
|
||||
// Kill the event sprite and recycle it.
|
||||
eventSprite.eventData = null;
|
||||
eventSprite.kill();
|
||||
}
|
||||
}
|
||||
// Sort the note data array, using an algorithm that is fast on nearly-sorted data.
|
||||
|
@ -3617,6 +3709,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
var noteLengthPixels:Float = noteSprite.noteData.getStepLength() * GRID_SIZE;
|
||||
|
||||
holdNoteSprite.noteData = noteSprite.noteData;
|
||||
holdNoteSprite.overrideStepTime = null;
|
||||
holdNoteSprite.overrideData = null;
|
||||
holdNoteSprite.noteDirection = noteSprite.noteData.getDirection();
|
||||
|
||||
holdNoteSprite.setHeightDirectly(noteLengthPixels);
|
||||
|
@ -3684,6 +3778,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
var noteLengthPixels:Float = noteData.getStepLength() * GRID_SIZE;
|
||||
|
||||
holdNoteSprite.noteData = noteData;
|
||||
holdNoteSprite.overrideStepTime = null;
|
||||
holdNoteSprite.overrideData = null;
|
||||
holdNoteSprite.noteDirection = noteData.getDirection();
|
||||
holdNoteSprite.setHeightDirectly(noteLengthPixels);
|
||||
|
||||
|
@ -3701,13 +3797,32 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
member.kill();
|
||||
}
|
||||
|
||||
// Gather stacked notes to render later
|
||||
// No need to update it every time we scroll
|
||||
if (Math.abs(currentScrollEase - scrollPositionInPixels) < .0001)
|
||||
{
|
||||
currentOverlappingNotes = SongNoteDataUtils.listStackedNotes(currentSongChartNoteData, stackedNoteThreshold);
|
||||
}
|
||||
|
||||
// Readd selection squares for selected notes.
|
||||
// Recycle selection squares if possible.
|
||||
for (noteSprite in renderedNotes.members)
|
||||
{
|
||||
if (noteSprite == null || noteSprite.noteData == null || !noteSprite.exists || !noteSprite.visible) continue;
|
||||
|
||||
// TODO: Handle selection of hold notes.
|
||||
if (isNoteSelected(noteSprite.noteData))
|
||||
{
|
||||
var holdNoteSprite:ChartEditorHoldNoteSprite = null;
|
||||
|
||||
if (noteSprite.noteData != null && noteSprite.noteData.length > 0)
|
||||
{
|
||||
for (holdNote in renderedHoldNotes.members)
|
||||
{
|
||||
if (holdNote.noteData == noteSprite.noteData && holdNoteSprite == null) holdNoteSprite = holdNote;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if the note is being dragged and offset the vertical position accordingly.
|
||||
if (dragTargetCurrentStep != 0.0)
|
||||
{
|
||||
|
@ -3716,6 +3831,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
noteSprite.overrideStepTime = (stepTime + dragTargetCurrentStep).clamp(0, songLengthInSteps - (1 * noteSnapRatio));
|
||||
// Then reapply the note sprite's position relative to the grid.
|
||||
noteSprite.updateNotePosition(renderedNotes);
|
||||
|
||||
// We only need to update the position of the hold note tails as we drag the note.
|
||||
if (holdNoteSprite != null)
|
||||
{
|
||||
holdNoteSprite.overrideStepTime = noteSprite.overrideStepTime;
|
||||
holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -3725,6 +3847,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
noteSprite.overrideStepTime = null;
|
||||
// Then reapply the note sprite's position relative to the grid.
|
||||
noteSprite.updateNotePosition(renderedNotes);
|
||||
|
||||
if (holdNoteSprite != null)
|
||||
{
|
||||
holdNoteSprite.overrideStepTime = null;
|
||||
holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3737,6 +3865,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
ChartEditorState.STRUMLINE_SIZE * 2 - 1));
|
||||
// Then reapply the note sprite's position relative to the grid.
|
||||
noteSprite.updateNotePosition(renderedNotes);
|
||||
|
||||
// We only need to update the position of the hold note tails as we drag the note.
|
||||
if (holdNoteSprite != null)
|
||||
{
|
||||
holdNoteSprite.overrideData = noteSprite.overrideData;
|
||||
holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -3746,6 +3881,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
noteSprite.overrideData = null;
|
||||
// Then reapply the note sprite's position relative to the grid.
|
||||
noteSprite.updateNotePosition(renderedNotes);
|
||||
|
||||
if (holdNoteSprite != null)
|
||||
{
|
||||
holdNoteSprite.overrideData = null;
|
||||
holdNoteSprite.noteDirection = noteSprite.noteData.getDirection();
|
||||
holdNoteSprite.updateHoldNotePosition(renderedHoldNotes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3758,14 +3900,30 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
selectionSquare.x = noteSprite.x;
|
||||
selectionSquare.y = noteSprite.y;
|
||||
selectionSquare.width = GRID_SIZE;
|
||||
selectionSquare.color = FlxColor.WHITE;
|
||||
|
||||
var stepLength = noteSprite.noteData.getStepLength();
|
||||
selectionSquare.height = (stepLength <= 0) ? GRID_SIZE : ((stepLength + 1) * GRID_SIZE);
|
||||
}
|
||||
else if (doesNoteStack(noteSprite.noteData, currentOverlappingNotes))
|
||||
{
|
||||
// TODO: Maybe use another way to display these notes
|
||||
var selectionSquare:ChartEditorSelectionSquareSprite = renderedSelectionSquares.recycle(buildSelectionSquare);
|
||||
|
||||
// Set the position and size (because we might be recycling one with bad values).
|
||||
selectionSquare.noteData = noteSprite.noteData;
|
||||
selectionSquare.eventData = null;
|
||||
selectionSquare.x = noteSprite.x;
|
||||
selectionSquare.y = noteSprite.y;
|
||||
selectionSquare.width = selectionSquare.height = GRID_SIZE;
|
||||
selectionSquare.color = FlxColor.RED;
|
||||
}
|
||||
}
|
||||
|
||||
for (eventSprite in renderedEvents.members)
|
||||
{
|
||||
if (eventSprite == null || eventSprite.eventData == null || !eventSprite.exists || !eventSprite.visible) continue;
|
||||
|
||||
if (isEventSelected(eventSprite.eventData))
|
||||
{
|
||||
// Determine if the note is being dragged and offset the position accordingly.
|
||||
|
@ -3797,6 +3955,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
selectionSquare.y = eventSprite.y;
|
||||
selectionSquare.width = eventSprite.width;
|
||||
selectionSquare.height = eventSprite.height;
|
||||
selectionSquare.color = FlxColor.WHITE;
|
||||
}
|
||||
|
||||
// Additional cleanup on notes.
|
||||
|
@ -4371,7 +4530,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null)
|
||||
{
|
||||
// Control click to select/deselect an individual note.
|
||||
if (isNoteSelected(highlightedNote.noteData))
|
||||
if (isNoteSelected(highlightedHoldNote.noteData))
|
||||
{
|
||||
performCommand(new DeselectItemsCommand([highlightedHoldNote.noteData], []));
|
||||
}
|
||||
|
@ -4663,7 +4822,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
}
|
||||
else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null)
|
||||
{
|
||||
if (isNoteSelected(highlightedNote.noteData))
|
||||
if (isNoteSelected(highlightedHoldNote.noteData))
|
||||
{
|
||||
performCommand(new DeselectItemsCommand([highlightedHoldNote.noteData], []));
|
||||
}
|
||||
|
@ -5107,7 +5266,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
playbarHeadLayout.y = FlxG.height - 48 - 8;
|
||||
|
||||
var songPos:Float = Conductor.instance.songPosition + Conductor.instance.instrumentalOffset;
|
||||
var songPosMilliseconds:String = Std.string(Math.floor(Math.abs(songPos) % 1000)).lpad('0', 2).substr(0, 2);
|
||||
var songPosMilliseconds:String = Std.string(Math.floor(Math.abs(songPos) % 1000)).lpad('0', 3).substr(0, 2);
|
||||
var songPosSeconds:String = Std.string(Math.floor((Math.abs(songPos) / 1000) % 60)).lpad('0', 2);
|
||||
var songPosMinutes:String = Std.string(Math.floor((Math.abs(songPos) / 1000) / 60)).lpad('0', 2);
|
||||
if (songPos < 0) songPosMinutes = '-' + songPosMinutes;
|
||||
|
@ -5501,18 +5660,36 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
|
||||
if (delete)
|
||||
{
|
||||
// Delete selected items.
|
||||
if (currentNoteSelection.length > 0 && currentEventSelection.length > 0)
|
||||
var noteSelection = currentNoteSelection.length > 0;
|
||||
var eventSelection = currentEventSelection.length > 0;
|
||||
|
||||
// Shift to delete stacked notes
|
||||
if (FlxG.keys.pressed.SHIFT)
|
||||
{
|
||||
performCommand(new RemoveItemsCommand(currentNoteSelection, currentEventSelection));
|
||||
if (eventSelection && !noteSelection)
|
||||
{
|
||||
performCommand(new RemoveEventsCommand(currentEventSelection));
|
||||
}
|
||||
else
|
||||
{
|
||||
performCommand(new RemoveStackedNotesCommand(noteSelection ? currentNoteSelection : null));
|
||||
}
|
||||
}
|
||||
else if (currentNoteSelection.length > 0)
|
||||
else
|
||||
{
|
||||
performCommand(new RemoveNotesCommand(currentNoteSelection));
|
||||
}
|
||||
else if (currentEventSelection.length > 0)
|
||||
{
|
||||
performCommand(new RemoveEventsCommand(currentEventSelection));
|
||||
// Delete selected items.
|
||||
if (noteSelection && eventSelection)
|
||||
{
|
||||
performCommand(new RemoveItemsCommand(currentNoteSelection, currentEventSelection));
|
||||
}
|
||||
else if (noteSelection)
|
||||
{
|
||||
performCommand(new RemoveNotesCommand(currentNoteSelection));
|
||||
}
|
||||
else if (eventSelection)
|
||||
{
|
||||
performCommand(new RemoveEventsCommand(currentEventSelection));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5673,7 +5850,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
var startTimestamp:Float = 0;
|
||||
if (playtestStartTime) startTimestamp = scrollPositionInMs + playheadPositionInMs;
|
||||
|
||||
var playbackRate:Float = ((menubarItemPlaybackSpeed.value ?? 1.0) * 2.0) / 100.0;
|
||||
var playbackRate:Float = ((menubarItemPlaybackSpeed.value / 100.0) ?? 0.5) * 2.0;
|
||||
playbackRate = Math.floor(playbackRate / 0.05) * 0.05; // Round to nearest 5%
|
||||
playbackRate = Math.max(0.05, Math.min(2.0, playbackRate)); // Clamp to 5% to 200%
|
||||
|
||||
|
@ -6144,20 +6321,30 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
|
||||
fadeInWelcomeMusic(WELCOME_MUSIC_FADE_IN_DELAY, WELCOME_MUSIC_FADE_IN_DURATION);
|
||||
|
||||
// Reapply the volume.
|
||||
var instTargetVolume:Float = menubarItemVolumeInstrumental.value ?? 1.0;
|
||||
var vocalPlayerTargetVolume:Float = menubarItemVolumeVocalsPlayer.value ?? 1.0;
|
||||
var vocalOpponentTargetVolume:Float = menubarItemVolumeVocalsOpponent.value ?? 1.0;
|
||||
// Reapply the volume and playback rate.
|
||||
var instTargetVolume:Float = (menubarItemVolumeInstrumental.value / 100.0) ?? 1.0;
|
||||
var vocalPlayerTargetVolume:Float = (menubarItemVolumeVocalsPlayer.value / 100.0) ?? 1.0;
|
||||
var vocalOpponentTargetVolume:Float = (menubarItemVolumeVocalsOpponent.value / 100.0) ?? 1.0;
|
||||
|
||||
var playbackRate = ((menubarItemPlaybackSpeed.value / 100.0) ?? 0.5) * 2.0;
|
||||
playbackRate = Math.floor(playbackRate / 0.05) * 0.05; // Round to nearest 5%
|
||||
playbackRate = Math.max(0.05, Math.min(2.0, playbackRate)); // Clamp to 5% to 200%
|
||||
|
||||
if (audioInstTrack != null)
|
||||
{
|
||||
audioInstTrack.volume = instTargetVolume;
|
||||
#if FLX_PITCH
|
||||
audioInstTrack.pitch = playbackRate;
|
||||
#end
|
||||
audioInstTrack.onComplete = null;
|
||||
}
|
||||
if (audioVocalTrackGroup != null)
|
||||
{
|
||||
audioVocalTrackGroup.playerVolume = vocalPlayerTargetVolume;
|
||||
audioVocalTrackGroup.opponentVolume = vocalOpponentTargetVolume;
|
||||
#if FLX_PITCH
|
||||
audioVocalTrackGroup.pitch = playbackRate;
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6188,6 +6375,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
// TODO: Only update the notes that have changed.
|
||||
notePreview.erase();
|
||||
notePreview.addNotes(currentSongChartNoteData, Std.int(songLengthInMs));
|
||||
notePreview.addOverlappingNotes(currentOverlappingNotes, Std.int(songLengthInMs));
|
||||
notePreview.addSelectedNotes(currentNoteSelection, Std.int(songLengthInMs));
|
||||
notePreview.addEvents(currentSongChartEventData, Std.int(songLengthInMs));
|
||||
}
|
||||
|
@ -6311,6 +6499,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
|
||||
public function postLoadInstrumental():Void
|
||||
{
|
||||
// Reapply the volume and playback rate.
|
||||
var instTargetVolume:Float = ((menubarItemVolumeInstrumental.value / 100) ?? 1.0);
|
||||
var playbackRate:Float = ((menubarItemPlaybackSpeed.value / 100.0) ?? 0.5) * 2.0;
|
||||
playbackRate = Math.floor(playbackRate / 0.05) * 0.05; // Round to nearest 5%
|
||||
playbackRate = Math.max(0.05, Math.min(2.0, playbackRate)); // Clamp to 5% to 200%
|
||||
if (audioInstTrack != null)
|
||||
{
|
||||
// Prevent the time from skipping back to 0 when the song ends.
|
||||
|
@ -6323,6 +6516,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
}
|
||||
audioVocalTrackGroup.pause();
|
||||
};
|
||||
audioInstTrack.volume = instTargetVolume;
|
||||
#if FLX_PITCH
|
||||
audioInstTrack.pitch = playbackRate;
|
||||
#end
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -6339,6 +6536,25 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
healthIconsDirty = true;
|
||||
}
|
||||
|
||||
public function postLoadVocals():Void
|
||||
{
|
||||
// Reapply the volume and playback rate.
|
||||
var vocalPlayerTargetVolume:Float = (menubarItemVolumeVocalsPlayer.value / 100.0) ?? 1.0;
|
||||
var vocalOpponentTargetVolume:Float = (menubarItemVolumeVocalsOpponent.value / 100.0) ?? 1.0;
|
||||
var playbackRate:Float = ((menubarItemPlaybackSpeed.value / 100.0) ?? 0.5) * 2.0;
|
||||
playbackRate = Math.floor(playbackRate / 0.05) * 0.05; // Round to nearest 5%
|
||||
playbackRate = Math.max(0.05, Math.min(2.0, playbackRate)); // Clamp to 5% to 200%
|
||||
|
||||
if (audioVocalTrackGroup != null)
|
||||
{
|
||||
audioVocalTrackGroup.playerVolume = vocalPlayerTargetVolume;
|
||||
audioVocalTrackGroup.opponentVolume = vocalOpponentTargetVolume;
|
||||
#if FLX_PITCH
|
||||
audioVocalTrackGroup.pitch = playbackRate;
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
||||
function hardRefreshOffsetsToolbox():Void
|
||||
{
|
||||
var offsetsToolbox:ChartEditorOffsetsToolbox = cast this.getToolbox(CHART_EDITOR_TOOLBOX_OFFSETS_LAYOUT);
|
||||
|
@ -6372,6 +6588,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
return note != null && currentNoteSelection.indexOf(note) != -1;
|
||||
}
|
||||
|
||||
function doesNoteStack(note:Null<SongNoteData>, curStackedNotes:Array<SongNoteData>):Bool
|
||||
{
|
||||
return note != null && curStackedNotes.contains(note);
|
||||
}
|
||||
|
||||
override function destroy():Void
|
||||
{
|
||||
super.destroy();
|
||||
|
@ -6521,6 +6742,16 @@ typedef ChartEditorParams =
|
|||
* If non-null, load this song immediately instead of the welcome screen.
|
||||
*/
|
||||
var ?targetSongId:String;
|
||||
|
||||
/**
|
||||
* If non-null, load this difficulty immediately instead of the default difficulty.
|
||||
*/
|
||||
var ?targetSongDifficulty:String;
|
||||
|
||||
/**
|
||||
* If non-null, load this variation immediately instead of the default variation.
|
||||
*/
|
||||
var ?targetSongVariation:String;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -37,7 +37,6 @@ class ChangeStartingBPMCommand implements ChartEditorCommand
|
|||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
state.notePreviewViewportBoundsDirty = true;
|
||||
state.scrollPositionInPixels = 0;
|
||||
|
||||
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
|
||||
|
||||
|
@ -61,7 +60,6 @@ class ChangeStartingBPMCommand implements ChartEditorCommand
|
|||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
state.notePreviewViewportBoundsDirty = true;
|
||||
state.scrollPositionInPixels = 0;
|
||||
|
||||
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ import funkin.data.song.SongData.SongEventData;
|
|||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongDataUtils;
|
||||
import funkin.data.song.SongDataUtils.SongClipboardItems;
|
||||
import funkin.data.song.SongNoteDataUtils;
|
||||
import funkin.ui.debug.charting.ChartEditorState;
|
||||
|
||||
/**
|
||||
* A command which inserts the contents of the clipboard into the chart editor.
|
||||
|
@ -13,9 +15,11 @@ import funkin.data.song.SongDataUtils.SongClipboardItems;
|
|||
class PasteItemsCommand implements ChartEditorCommand
|
||||
{
|
||||
var targetTimestamp:Float;
|
||||
// Notes we added with this command, for undo.
|
||||
// Notes we added and removed with this command, for undo.
|
||||
var addedNotes:Array<SongNoteData> = [];
|
||||
var addedEvents:Array<SongEventData> = [];
|
||||
var removedNotes:Array<SongNoteData> = [];
|
||||
var isRedo:Bool = false;
|
||||
|
||||
public function new(targetTimestamp:Float)
|
||||
{
|
||||
|
@ -41,7 +45,10 @@ class PasteItemsCommand implements ChartEditorCommand
|
|||
addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp));
|
||||
addedEvents = SongDataUtils.clampSongEventData(addedEvents, 0.0, msCutoff);
|
||||
|
||||
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(addedNotes);
|
||||
removedNotes.clear();
|
||||
var mergedNotes:Array<SongNoteData> = SongNoteDataUtils.concatOverwrite(state.currentSongChartNoteData, addedNotes, removedNotes);
|
||||
|
||||
state.currentSongChartNoteData = mergedNotes;
|
||||
state.currentSongChartEventData = state.currentSongChartEventData.concat(addedEvents);
|
||||
state.currentNoteSelection = addedNotes.copy();
|
||||
state.currentEventSelection = addedEvents.copy();
|
||||
|
@ -52,29 +59,49 @@ class PasteItemsCommand implements ChartEditorCommand
|
|||
|
||||
state.sortChartData();
|
||||
|
||||
state.success('Paste Successful', 'Successfully pasted clipboard contents.');
|
||||
var title = isRedo ? 'Redone Paste Successfully' : 'Paste Successful';
|
||||
var msgType = removedNotes.length > 0 ? 'warning' : 'success';
|
||||
var msg = if (removedNotes.length == 1)
|
||||
{
|
||||
'But 1 overlapped note was overwritten.';
|
||||
}
|
||||
else if (removedNotes.length > 1)
|
||||
{
|
||||
'But ${removedNotes.length} overlapped notes were overwritten.';
|
||||
}
|
||||
else if (isRedo)
|
||||
{
|
||||
'Successfully placed pasted note(s) back.';
|
||||
}
|
||||
else 'Successfully pasted clipboard contents.';
|
||||
|
||||
Reflect.callMethod(null, Reflect.field(ChartEditorNotificationHandler, msgType), [state, title, msg]);
|
||||
|
||||
isRedo = false;
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
{
|
||||
state.playSound(Paths.sound('chartingSounds/undo'));
|
||||
|
||||
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes);
|
||||
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes).concat(removedNotes);
|
||||
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, addedEvents);
|
||||
state.currentNoteSelection = [];
|
||||
state.currentEventSelection = [];
|
||||
state.performCommand(new SelectItemsCommand(removedNotes.copy()), false);
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
state.sortChartData();
|
||||
|
||||
isRedo = true;
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return (addedNotes.length > 0 || addedEvents.length > 0);
|
||||
return (addedNotes.length > 0 || addedEvents.length > 0 || removedNotes.length > 0);
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
package funkin.ui.debug.charting.commands;
|
||||
|
||||
import funkin.data.song.SongData.SongNoteData;
|
||||
import funkin.data.song.SongDataUtils;
|
||||
import funkin.data.song.SongNoteDataUtils;
|
||||
|
||||
/**
|
||||
* Deletes the given notes from the current chart in the chart editor if any overlap another.
|
||||
* Use when ONLY notes are being deleted.
|
||||
*/
|
||||
@:nullSafety
|
||||
@:access(funkin.ui.debug.charting.ChartEditorState)
|
||||
class RemoveStackedNotesCommand implements ChartEditorCommand
|
||||
{
|
||||
var notes:Null<Array<SongNoteData>>;
|
||||
var overlappedNotes:Array<SongNoteData>;
|
||||
var removedNotes:Array<SongNoteData>;
|
||||
|
||||
public function new(?notes:Array<SongNoteData>)
|
||||
{
|
||||
this.notes = notes;
|
||||
this.overlappedNotes = [];
|
||||
this.removedNotes = [];
|
||||
}
|
||||
|
||||
public function execute(state:ChartEditorState):Void
|
||||
{
|
||||
var isSelection:Bool = notes != null;
|
||||
var notes:Array<SongNoteData> = notes ?? state.currentSongChartNoteData;
|
||||
|
||||
if (notes.length == 0) return;
|
||||
|
||||
overlappedNotes.clear();
|
||||
removedNotes = SongNoteDataUtils.listStackedNotes(notes, ChartEditorState.stackedNoteThreshold, false, overlappedNotes);
|
||||
if (removedNotes.length == 0) return;
|
||||
|
||||
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, removedNotes);
|
||||
state.currentNoteSelection = isSelection ? overlappedNotes.copy() : [];
|
||||
state.currentEventSelection = [];
|
||||
|
||||
state.playSound(Paths.sound('chartingSounds/noteErase'));
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
{
|
||||
if (removedNotes.length == 0) return;
|
||||
|
||||
state.currentSongChartNoteData = state.currentSongChartNoteData.concat(removedNotes);
|
||||
state.currentNoteSelection = overlappedNotes.concat(removedNotes).copy();
|
||||
state.currentEventSelection = [];
|
||||
state.playSound(Paths.sound('chartingSounds/undo'));
|
||||
|
||||
state.saveDataDirty = true;
|
||||
state.noteDisplayDirty = true;
|
||||
state.notePreviewDirty = true;
|
||||
|
||||
state.sortChartData();
|
||||
}
|
||||
|
||||
public function shouldAddToHistory(state:ChartEditorState):Bool
|
||||
{
|
||||
// This command is undoable. Add to the history if we actually performed an action.
|
||||
return removedNotes.length > 0;
|
||||
}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
if (removedNotes.length == 1 && removedNotes[0] != null)
|
||||
{
|
||||
var dir:String = removedNotes[0].getDirectionName();
|
||||
return 'Remove $dir Stacked Note';
|
||||
}
|
||||
|
||||
return 'Remove ${removedNotes.length} Stacked Notes';
|
||||
}
|
||||
}
|
|
@ -123,7 +123,6 @@ class ChartEditorEventSprite extends FlxSprite
|
|||
public function correctAnimationName(name:String):String
|
||||
{
|
||||
if (this.animation.exists(name)) return name;
|
||||
trace('Warning: Invalid animation name "${name}" for song event. Using "${DEFAULT_EVENT}"');
|
||||
return DEFAULT_EVENT;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,30 @@ class ChartEditorHoldNoteSprite extends SustainTrail
|
|||
return value;
|
||||
}
|
||||
|
||||
public var overrideStepTime(default, set):Null<Float> = null;
|
||||
|
||||
function set_overrideStepTime(value:Null<Float>):Null<Float>
|
||||
{
|
||||
if (overrideStepTime == value) return overrideStepTime;
|
||||
|
||||
overrideStepTime = value;
|
||||
updateHoldNotePosition();
|
||||
return overrideStepTime;
|
||||
}
|
||||
|
||||
public var overrideData(default, set):Null<Int> = null;
|
||||
|
||||
function set_overrideData(value:Null<Int>):Null<Int>
|
||||
{
|
||||
if (overrideData == value) return overrideData;
|
||||
|
||||
overrideData = value;
|
||||
if (overrideData != null) this.noteDirection = overrideData;
|
||||
updateHoldNoteGraphic();
|
||||
updateHoldNotePosition();
|
||||
return overrideData;
|
||||
}
|
||||
|
||||
public function new(parent:ChartEditorState)
|
||||
{
|
||||
var noteStyle = NoteStyleRegistry.instance.fetchDefault();
|
||||
|
@ -211,7 +235,7 @@ class ChartEditorHoldNoteSprite extends SustainTrail
|
|||
{
|
||||
if (this.noteData == null) return;
|
||||
|
||||
var cursorColumn:Int = this.noteData.data;
|
||||
var cursorColumn:Int = (overrideData != null) ? overrideData : this.noteData.data;
|
||||
|
||||
if (cursorColumn < 0) cursorColumn = 0;
|
||||
if (cursorColumn >= (ChartEditorState.STRUMLINE_SIZE * 2 + 1))
|
||||
|
@ -232,10 +256,11 @@ class ChartEditorHoldNoteSprite extends SustainTrail
|
|||
}
|
||||
|
||||
this.x = cursorColumn * ChartEditorState.GRID_SIZE;
|
||||
updateHoldNoteGraphic();
|
||||
|
||||
// Notes far in the song will start far down, but the group they belong to will have a high negative offset.
|
||||
// noteData.getStepTime() returns a calculated value which accounts for BPM changes
|
||||
var stepTime:Float =
|
||||
var stepTime:Float = (overrideStepTime != null) ? overrideStepTime :
|
||||
inline this.noteData.getStepTime();
|
||||
if (stepTime >= 0)
|
||||
{
|
||||
|
|
|
@ -27,6 +27,7 @@ class ChartEditorNotePreview extends FlxSprite
|
|||
static final RIGHT_COLOR:FlxColor = 0xFFCC1111;
|
||||
static final EVENT_COLOR:FlxColor = 0xFF111111;
|
||||
static final SELECTED_COLOR:FlxColor = 0xFFFFFF00;
|
||||
static final OVERLAPPING_COLOR:FlxColor = 0xFF640000;
|
||||
|
||||
var previewHeight:Int;
|
||||
|
||||
|
@ -58,21 +59,22 @@ class ChartEditorNotePreview extends FlxSprite
|
|||
* @param note The data for the note.
|
||||
* @param songLengthInMs The total length of the song in milliseconds.
|
||||
*/
|
||||
public function addNote(note:SongNoteData, songLengthInMs:Int, ?isSelection:Bool = false):Void
|
||||
public function addNote(note:SongNoteData, songLengthInMs:Int, previewType:NotePreviewType = None):Void
|
||||
{
|
||||
var noteDir:Int = note.getDirection();
|
||||
var mustHit:Bool = note.getStrumlineIndex() == 0;
|
||||
drawNote(noteDir, mustHit, Std.int(note.time), songLengthInMs, isSelection);
|
||||
drawNote(noteDir, mustHit, Std.int(note.time), songLengthInMs, previewType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a song event to the preview.
|
||||
* @param event The data for the event.
|
||||
* @param songLengthInMs The total length of the song in milliseconds.
|
||||
* @param isSelection If current event is selected, which then it's forced to be yellow.
|
||||
*/
|
||||
public function addEvent(event:SongEventData, songLengthInMs:Int, ?isSelection:Bool = false):Void
|
||||
public function addEvent(event:SongEventData, songLengthInMs:Int, isSelection:Bool = false):Void
|
||||
{
|
||||
drawNote(-1, false, Std.int(event.time), songLengthInMs, isSelection);
|
||||
drawNote(-1, false, Std.int(event.time), songLengthInMs, isSelection ? Selection : None);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -84,7 +86,7 @@ class ChartEditorNotePreview extends FlxSprite
|
|||
{
|
||||
for (note in notes)
|
||||
{
|
||||
addNote(note, songLengthInMs, false);
|
||||
addNote(note, songLengthInMs, None);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -97,7 +99,20 @@ class ChartEditorNotePreview extends FlxSprite
|
|||
{
|
||||
for (note in notes)
|
||||
{
|
||||
addNote(note, songLengthInMs, true);
|
||||
addNote(note, songLengthInMs, Selection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an array of overlapping notes to the preview.
|
||||
* @param notes The data for the notes
|
||||
* @param songLengthInMs The total length of the song in milliseconds.
|
||||
*/
|
||||
public function addOverlappingNotes(notes:Array<SongNoteData>, songLengthInMs:Int):Void
|
||||
{
|
||||
for (note in notes)
|
||||
{
|
||||
addNote(note, songLengthInMs, Overlapping);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -133,9 +148,9 @@ class ChartEditorNotePreview extends FlxSprite
|
|||
* @param mustHit False if opponent, true if player.
|
||||
* @param strumTimeInMs Time in milliseconds to strum the note.
|
||||
* @param songLengthInMs Length of the song in milliseconds.
|
||||
* @param isSelection If current note is selected note, which then it's forced to be green
|
||||
* @param previewType If the note should forcibly be colored as selected or overlapping.
|
||||
*/
|
||||
public function drawNote(dir:Int, mustHit:Bool, strumTimeInMs:Int, songLengthInMs:Int, ?isSelection:Bool = false):Void
|
||||
public function drawNote(dir:Int, mustHit:Bool, strumTimeInMs:Int, songLengthInMs:Int, previewType:NotePreviewType = None):Void
|
||||
{
|
||||
var color:FlxColor = switch (dir)
|
||||
{
|
||||
|
@ -148,10 +163,15 @@ class ChartEditorNotePreview extends FlxSprite
|
|||
|
||||
var noteHeight:Int = NOTE_HEIGHT;
|
||||
|
||||
if (isSelection != null && isSelection)
|
||||
switch (previewType)
|
||||
{
|
||||
color = SELECTED_COLOR;
|
||||
noteHeight += 1;
|
||||
case Selection:
|
||||
color = SELECTED_COLOR;
|
||||
noteHeight += 1;
|
||||
case Overlapping:
|
||||
color = OVERLAPPING_COLOR;
|
||||
noteHeight += 2;
|
||||
default:
|
||||
}
|
||||
|
||||
var noteX:Float = NOTE_WIDTH * dir;
|
||||
|
@ -178,3 +198,10 @@ class ChartEditorNotePreview extends FlxSprite
|
|||
FlxSpriteUtil.drawRect(this, noteX, noteY, width, height, color);
|
||||
}
|
||||
}
|
||||
|
||||
enum NotePreviewType
|
||||
{
|
||||
None;
|
||||
Selection;
|
||||
Overlapping;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,8 @@ class ChartEditorCharacterIconSelectorMenu extends ChartEditorBaseMenu
|
|||
public var charSelectScroll:ScrollView;
|
||||
public var charIconName:Label;
|
||||
|
||||
var currentCharButton:Button;
|
||||
var currentCharButton:Null<Button> = null;
|
||||
var currentCharId:String = '';
|
||||
|
||||
public function new(chartEditorState2:ChartEditorState, charType:CharacterType, lockPosition:Bool = false)
|
||||
{
|
||||
|
@ -36,14 +37,16 @@ class ChartEditorCharacterIconSelectorMenu extends ChartEditorBaseMenu
|
|||
ease: FlxEase.quartOut,
|
||||
onComplete: function(_) {
|
||||
// Just focus the button FFS. Idk why, but the scrollbar doesn't update until after the tween finishes with this????
|
||||
currentCharButton.focus = true;
|
||||
if (currentCharButton != null) currentCharButton.focus = true;
|
||||
else
|
||||
chartEditorState.error('Failure', 'Could not find character of ${currentCharId} in registry (Is the character in the registry?)');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initialize(charType:CharacterType, lockPosition:Bool)
|
||||
{
|
||||
var currentCharId:String = switch (charType)
|
||||
currentCharId = switch (charType)
|
||||
{
|
||||
case BF: chartEditorState.currentSongMetadata.playData.characters.player;
|
||||
case GF: chartEditorState.currentSongMetadata.playData.characters.girlfriend;
|
||||
|
|
|
@ -146,6 +146,8 @@ class ChartEditorAudioHandler
|
|||
result = playVocals(state, DAD, opponentId, instId);
|
||||
// if (!result) return false;
|
||||
|
||||
state.postLoadVocals();
|
||||
|
||||
var perfE:Float = TimerUtil.start();
|
||||
|
||||
state.hardRefreshOffsetsToolbox();
|
||||
|
|
|
@ -1168,7 +1168,8 @@ class ChartEditorDialogHandler
|
|||
|
||||
var dialogNoteStyle:Null<DropDown> = dialog.findComponent('dialogNoteStyle', DropDown);
|
||||
if (dialogNoteStyle == null) throw 'Could not locate dialogNoteStyle DropDown in Add Variation dialog';
|
||||
dialogNoteStyle.value = state.currentSongMetadata.playData.noteStyle;
|
||||
var startingValueNoteStyle = ChartEditorDropdowns.populateDropdownWithNoteStyles(dialogNoteStyle, state.currentSongMetadata.playData.noteStyle);
|
||||
dialogNoteStyle.value = startingValueNoteStyle;
|
||||
|
||||
var dialogCharacterPlayer:Null<DropDown> = dialog.findComponent('dialogCharacterPlayer', DropDown);
|
||||
if (dialogCharacterPlayer == null) throw 'Could not locate dialogCharacterPlayer DropDown in Add Variation dialog';
|
||||
|
@ -1203,7 +1204,7 @@ class ChartEditorDialogHandler
|
|||
var pendingVariation:SongMetadata = new SongMetadata(dialogSongName.text, dialogSongArtist.text, dialogVariationName.text.toLowerCase());
|
||||
|
||||
pendingVariation.playData.stage = dialogStage.value.id;
|
||||
pendingVariation.playData.noteStyle = dialogNoteStyle.value;
|
||||
pendingVariation.playData.noteStyle = dialogNoteStyle.value.id;
|
||||
pendingVariation.timeChanges[0].bpm = dialogBPM.value;
|
||||
|
||||
state.songMetadata.set(pendingVariation.variation, pendingVariation);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package funkin.ui.debug.charting.handlers;
|
||||
|
||||
import funkin.data.song.SongNoteDataUtils;
|
||||
import funkin.util.VersionUtil;
|
||||
import funkin.util.DateUtil;
|
||||
import haxe.io.Path;
|
||||
|
@ -26,7 +27,7 @@ class ChartEditorImportExportHandler
|
|||
/**
|
||||
* Fetch's a song's existing chart and audio and loads it, replacing the current song.
|
||||
*/
|
||||
public static function loadSongAsTemplate(state:ChartEditorState, songId:String):Void
|
||||
public static function loadSongAsTemplate(state:ChartEditorState, songId:String, targetSongDifficulty:String = null, targetSongVariation:String = null):Void
|
||||
{
|
||||
trace('===============START');
|
||||
|
||||
|
@ -92,6 +93,14 @@ class ChartEditorImportExportHandler
|
|||
{
|
||||
trace('[WARN] Strange quantity of voice paths for difficulty ${difficultyId}: ${voiceList.length}');
|
||||
}
|
||||
// Set the difficulty of the song if one was passed in the params, and it isn't the default
|
||||
if (targetSongDifficulty != null
|
||||
&& targetSongDifficulty != state.selectedDifficulty
|
||||
&& targetSongDifficulty == diff.difficulty) state.selectedDifficulty = targetSongDifficulty;
|
||||
// Set the variation of the song if one was passed in the params, and it isn't the default
|
||||
if (targetSongVariation != null
|
||||
&& targetSongVariation != state.selectedVariation
|
||||
&& targetSongVariation == diff.variation) state.selectedVariation = targetSongVariation;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,7 +112,11 @@ class ChartEditorImportExportHandler
|
|||
|
||||
state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
|
||||
|
||||
state.success('Success', 'Loaded song (${rawSongMetadata[0].songName})');
|
||||
// Actually state the correct variation loaded
|
||||
for (metadata in rawSongMetadata)
|
||||
{
|
||||
if (metadata.variation == state.selectedVariation) state.success('Success', 'Loaded song (${metadata.songName})');
|
||||
}
|
||||
|
||||
trace('===============END');
|
||||
}
|
||||
|
@ -117,12 +130,48 @@ class ChartEditorImportExportHandler
|
|||
{
|
||||
state.songMetadata = newSongMetadata;
|
||||
state.songChartData = newSongChartData;
|
||||
state.selectedDifficulty = state.availableDifficulties[0];
|
||||
|
||||
if (!newSongMetadata.exists(state.selectedVariation))
|
||||
if (!state.songMetadata.exists(state.selectedVariation))
|
||||
{
|
||||
state.selectedVariation = Constants.DEFAULT_VARIATION;
|
||||
}
|
||||
// Use the first available difficulty as a fallback if the currently selected one cannot be found.
|
||||
if (state.availableDifficulties.indexOf(state.selectedDifficulty) < 0) state.selectedDifficulty = state.availableDifficulties[0];
|
||||
|
||||
var delay:Float = 0.5;
|
||||
for (variation => chart in state.songChartData)
|
||||
{
|
||||
var metadata:SongMetadata = state.songMetadata[variation] ?? continue;
|
||||
var stackedNotesCount:Int = 0;
|
||||
var affectedDiffs:Array<String> = [];
|
||||
|
||||
for (diff => notes in chart.notes)
|
||||
{
|
||||
if (!metadata.playData.difficulties.contains(diff)) continue;
|
||||
|
||||
var count:Int = SongNoteDataUtils.listStackedNotes(notes, 0, false).length;
|
||||
|
||||
if (count > 0)
|
||||
{
|
||||
affectedDiffs.push(diff.toTitleCase());
|
||||
stackedNotesCount += count;
|
||||
}
|
||||
}
|
||||
|
||||
if (stackedNotesCount > 0)
|
||||
{
|
||||
// Difficulty names might be out of order due to how maps work
|
||||
affectedDiffs.sort(SortUtil.defaultsThenAlphabetically.bind(['Easy', 'Normal', 'Hard', 'Erect', 'Nightmare']));
|
||||
|
||||
// Delay it so it doesn't overlap other notifications
|
||||
flixel.util.FlxTimer.wait(delay, () -> {
|
||||
state.warning('Stacked Notes Detected',
|
||||
'Found $stackedNotesCount stacked note(s) in \'${variation.toTitleCase()}\' variation, ' +
|
||||
'on ${affectedDiffs.joinPlural()} difficult${affectedDiffs.length > 1 ? 'ies' : 'y'}.');
|
||||
});
|
||||
delay *= 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
Conductor.instance.forceBPM(null); // Disable the forced BPM.
|
||||
Conductor.instance.instrumentalOffset = state.currentInstrumentalOffset; // Loads from the metadata.
|
||||
|
@ -143,6 +192,11 @@ class ChartEditorImportExportHandler
|
|||
}
|
||||
state.audioVocalTrackGroup.stop();
|
||||
state.audioVocalTrackGroup.clear();
|
||||
|
||||
// Clear the undo and redo history
|
||||
state.undoHistory = [];
|
||||
state.redoHistory = [];
|
||||
state.commandHistoryDirty = true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -295,7 +349,7 @@ class ChartEditorImportExportHandler
|
|||
public static function getLatestBackupPath():Null<String>
|
||||
{
|
||||
#if sys
|
||||
if (!sys.FileSystem.exists(BACKUPS_PATH)) sys.FileSystem.createDirectory(BACKUPS_PATH);
|
||||
FileUtil.createDirIfNotExists(BACKUPS_PATH);
|
||||
|
||||
var entries:Array<String> = sys.FileSystem.readDirectory(BACKUPS_PATH);
|
||||
entries.sort(SortUtil.alphabetically);
|
||||
|
|
|
@ -113,7 +113,10 @@ class ChartEditorToolboxHandler
|
|||
{
|
||||
var toolbox:Null<ChartEditorBaseToolbox> = cast state.activeToolboxes.get(id);
|
||||
|
||||
if (toolbox == null) return;
|
||||
if (toolbox == null)
|
||||
{
|
||||
toolbox = cast initToolbox(state, id);
|
||||
}
|
||||
|
||||
if (toolbox != null)
|
||||
{
|
||||
|
|
|
@ -1,12 +1,24 @@
|
|||
package funkin.ui.debug.charting.toolboxes;
|
||||
|
||||
import funkin.data.song.SongData.SongChartData;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import haxe.ui.components.Button;
|
||||
import haxe.ui.containers.dialogs.Dialogs;
|
||||
import haxe.ui.containers.dialogs.Dialog.DialogButton;
|
||||
import funkin.data.song.SongData.SongMetadata;
|
||||
import haxe.ui.components.DropDown;
|
||||
import haxe.ui.components.HorizontalSlider;
|
||||
import funkin.util.VersionUtil;
|
||||
import funkin.util.FileUtil;
|
||||
import openfl.net.FileReference;
|
||||
import haxe.ui.containers.dialogs.MessageBox.MessageBoxType;
|
||||
import funkin.play.song.SongSerializer;
|
||||
import haxe.ui.components.Label;
|
||||
import haxe.ui.components.NumberStepper;
|
||||
import haxe.ui.components.Slider;
|
||||
import haxe.ui.components.TextField;
|
||||
import funkin.play.stage.Stage;
|
||||
import haxe.ui.containers.Box;
|
||||
import haxe.ui.containers.TreeView;
|
||||
import haxe.ui.containers.TreeViewNode;
|
||||
import haxe.ui.events.UIEvent;
|
||||
|
@ -81,26 +93,96 @@ class ChartEditorDifficultyToolbox extends ChartEditorBaseToolbox
|
|||
|
||||
difficultyToolboxSaveMetadata.onClick = function(_:UIEvent) {
|
||||
var vari:String = chartEditorState.selectedVariation != Constants.DEFAULT_VARIATION ? '-${chartEditorState.selectedVariation}' : '';
|
||||
FileUtil.writeFileReference('${chartEditorState.currentSongId}$vari-metadata.json', chartEditorState.currentSongMetadata.serialize());
|
||||
FileUtil.writeFileReference('${chartEditorState.currentSongId}$vari-metadata.json', chartEditorState.currentSongMetadata.serialize(),
|
||||
function(notification:String) {
|
||||
switch (notification)
|
||||
{
|
||||
case "success":
|
||||
chartEditorState.success("Saved Metadata", 'Successfully wrote file (${chartEditorState.currentSongId}$vari-metadata.json).');
|
||||
case "info":
|
||||
chartEditorState.info("Canceled Save Metadata", '(${chartEditorState.currentSongId}$vari-metadata.json)');
|
||||
case "error":
|
||||
chartEditorState.error("Failure", 'Failed to write file (${chartEditorState.currentSongId}$vari-metadata.json).');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
difficultyToolboxSaveChart.onClick = function(_:UIEvent) {
|
||||
var vari:String = chartEditorState.selectedVariation != Constants.DEFAULT_VARIATION ? '-${chartEditorState.selectedVariation}' : '';
|
||||
FileUtil.writeFileReference('${chartEditorState.currentSongId}$vari-chart.json', chartEditorState.currentSongChartData.serialize());
|
||||
FileUtil.writeFileReference('${chartEditorState.currentSongId}$vari-chart.json', chartEditorState.currentSongChartData.serialize(),
|
||||
function(notification:String) {
|
||||
switch (notification)
|
||||
{
|
||||
case "success":
|
||||
chartEditorState.success("Saved Chart Data", 'Successfully wrote file (${chartEditorState.currentSongId}$vari-chart.json).');
|
||||
case "info":
|
||||
chartEditorState.info("Canceled Save Chart Data", '(${chartEditorState.currentSongId}$vari-chart.json)');
|
||||
case "error":
|
||||
chartEditorState.error("Failure", 'Failed to write file (${chartEditorState.currentSongId}$vari-chart.json).');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
difficultyToolboxLoadMetadata.onClick = function(_:UIEvent) {
|
||||
// Replace metadata for current variation.
|
||||
SongSerializer.importSongMetadataAsync(function(songMetadata) {
|
||||
chartEditorState.currentSongMetadata = songMetadata;
|
||||
FileUtil.browseFileReference(function(fileReference:FileReference) {
|
||||
var data = fileReference.data.toString();
|
||||
|
||||
if (data == null) return;
|
||||
|
||||
var songMetadataVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(data);
|
||||
|
||||
var songMetadata:Null<SongMetadata> = null;
|
||||
if (VersionUtil.validateVersion(songMetadataVersion,
|
||||
SongRegistry.SONG_METADATA_VERSION_RULE)) songMetadata = SongRegistry.instance.parseEntryMetadataRawWithMigration(data, fileReference.name,
|
||||
songMetadataVersion);
|
||||
|
||||
if (songMetadata != null)
|
||||
{
|
||||
chartEditorState.currentSongMetadata = songMetadata;
|
||||
chartEditorState.healthIconsDirty = true;
|
||||
chartEditorState.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
|
||||
chartEditorState.success('Replaced Metadata', 'Replaced metadata with file (${fileReference.name})');
|
||||
}
|
||||
else
|
||||
{
|
||||
chartEditorState.error('Failure', 'Failed to load metadata file (${fileReference.name})');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
difficultyToolboxLoadChart.onClick = function(_:UIEvent) {
|
||||
// Replace chart data for current variation.
|
||||
SongSerializer.importSongChartDataAsync(function(songChartData) {
|
||||
chartEditorState.currentSongChartData = songChartData;
|
||||
chartEditorState.noteDisplayDirty = true;
|
||||
FileUtil.browseFileReference(function(fileReference:FileReference) {
|
||||
var data = fileReference.data.toString();
|
||||
|
||||
if (data == null) return;
|
||||
|
||||
var songChartDataVersion:Null<thx.semver.Version> = VersionUtil.getVersionFromJSON(data);
|
||||
|
||||
var songChartData:Null<SongChartData> = null;
|
||||
if (VersionUtil.validateVersion(songChartDataVersion,
|
||||
SongRegistry.SONG_CHART_DATA_VERSION_RULE)) songChartData = SongRegistry.instance.parseEntryChartDataRawWithMigration(data, fileReference.name,
|
||||
songChartDataVersion);
|
||||
|
||||
if (songChartData != null)
|
||||
{
|
||||
chartEditorState.currentSongChartData = songChartData;
|
||||
chartEditorState.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT);
|
||||
updateTree();
|
||||
refresh();
|
||||
chartEditorState.success('Loaded Chart Data', 'Loaded chart data file (${fileReference.name})');
|
||||
if (chartEditorState.currentNoteSelection != []) chartEditorState.currentNoteSelection = [];
|
||||
if (chartEditorState.currentEventSelection != []) chartEditorState.currentEventSelection = [];
|
||||
chartEditorState.noteDisplayDirty = true;
|
||||
chartEditorState.notePreviewDirty = true;
|
||||
chartEditorState.noteTooltipsDirty = true;
|
||||
chartEditorState.notePreviewViewportBoundsDirty = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
chartEditorState.error('Failure', 'Failed to load chart data file (${fileReference.name})');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ package funkin.ui.debug.charting.toolboxes;
|
|||
import funkin.play.character.BaseCharacter.CharacterType;
|
||||
import funkin.play.character.CharacterData;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
import funkin.data.notestyle.NoteStyleRegistry;
|
||||
import funkin.play.notes.notestyle.NoteStyle;
|
||||
import funkin.ui.debug.charting.commands.ChangeStartingBPMCommand;
|
||||
import funkin.ui.debug.charting.util.ChartEditorDropdowns;
|
||||
import haxe.ui.components.Button;
|
||||
|
@ -112,8 +114,12 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
|
|||
inputStage.value = startingValueStage;
|
||||
|
||||
inputNoteStyle.onChange = function(event:UIEvent) {
|
||||
if (event.data?.id == null) return;
|
||||
var valid:Bool = event.data != null && event.data.id != null;
|
||||
|
||||
if (valid)
|
||||
{
|
||||
chartEditorState.currentSongNoteStyle = event.data.id;
|
||||
}
|
||||
};
|
||||
var startingValueNoteStyle = ChartEditorDropdowns.populateDropdownWithNoteStyles(inputNoteStyle, chartEditorState.currentSongMetadata.playData.noteStyle);
|
||||
inputNoteStyle.value = startingValueNoteStyle;
|
||||
|
@ -122,8 +128,7 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
|
|||
if (event.value == null || event.value <= 0) return;
|
||||
|
||||
// Use a command so we can undo/redo this action.
|
||||
var startingBPM = chartEditorState.currentSongMetadata.timeChanges[0].bpm;
|
||||
if (event.value != startingBPM)
|
||||
if (event.value != Conductor.instance.bpm)
|
||||
{
|
||||
chartEditorState.performCommand(new ChangeStartingBPMCommand(event.value));
|
||||
}
|
||||
|
@ -191,7 +196,7 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
|
|||
inputSongArtist.value = chartEditorState.currentSongMetadata.artist;
|
||||
inputSongCharter.value = chartEditorState.currentSongMetadata.charter;
|
||||
inputStage.value = chartEditorState.currentSongMetadata.playData.stage;
|
||||
inputNoteStyle.value = chartEditorState.currentSongMetadata.playData.noteStyle;
|
||||
inputNoteStyle.value = chartEditorState.currentSongNoteStyle;
|
||||
inputBPM.value = chartEditorState.currentSongMetadata.timeChanges[0].bpm;
|
||||
inputDifficultyRating.value = chartEditorState.currentSongChartDifficultyRating;
|
||||
inputScrollSpeed.value = chartEditorState.currentSongChartScrollSpeed;
|
||||
|
@ -199,6 +204,11 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
|
|||
frameVariation.text = 'Variation: ${chartEditorState.selectedVariation.toTitleCase()}';
|
||||
frameDifficulty.text = 'Difficulty: ${chartEditorState.selectedDifficulty.toTitleCase()}';
|
||||
|
||||
if (chartEditorState.currentSongMetadata.timeChanges[0].bpm != Conductor.instance.bpm)
|
||||
{
|
||||
chartEditorState.performCommand(new ChangeStartingBPMCommand(chartEditorState.currentSongMetadata.timeChanges[0].bpm));
|
||||
}
|
||||
|
||||
var currentTimeSignature = '${chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureNum}/${chartEditorState.currentSongMetadata.timeChanges[0].timeSignatureDen}';
|
||||
trace('Setting time signature to ${currentTimeSignature}');
|
||||
inputTimeSignature.value = {id: currentTimeSignature, text: currentTimeSignature};
|
||||
|
@ -212,6 +222,15 @@ class ChartEditorMetadataToolbox extends ChartEditorBaseToolbox
|
|||
{id: "mainStage", text: "Main Stage"};
|
||||
}
|
||||
|
||||
var noteStyleId:String = chartEditorState.currentSongNoteStyle;
|
||||
var noteStyle:Null<NoteStyle> = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
|
||||
if (inputNoteStyle != null)
|
||||
{
|
||||
inputNoteStyle.value = (noteStyle != null) ?
|
||||
{id: noteStyle.id, text: noteStyle.getName()} :
|
||||
{id: "Funkin", text: "Funkin'"};
|
||||
}
|
||||
|
||||
var LIMIT = 6;
|
||||
|
||||
var charDataOpponent:Null<CharacterData> = CharacterDataParser.fetchCharacterData(chartEditorState.currentSongMetadata.playData.characters.opponent);
|
||||
|
|
|
@ -179,10 +179,10 @@ class LatencyState extends MusicBeatSubState
|
|||
strumLine.releaseKey(event.noteDirection);
|
||||
}
|
||||
|
||||
override public function close():Void
|
||||
override public function destroy():Void
|
||||
{
|
||||
cleanup();
|
||||
super.close();
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
function cleanup():Void
|
||||
|
@ -314,7 +314,7 @@ class LatencyState extends MusicBeatSubState
|
|||
{
|
||||
// close();
|
||||
cleanup();
|
||||
FlxG.switchState(() -> new MainMenuState());
|
||||
FlxG.switchState(() -> new funkin.ui.options.OptionsState());
|
||||
}
|
||||
|
||||
super.update(elapsed);
|
||||
|
|
|
@ -23,12 +23,14 @@ import haxe.ui.containers.menus.MenuBar;
|
|||
import haxe.ui.containers.menus.MenuOptionBox;
|
||||
import haxe.ui.containers.menus.MenuCheckBox;
|
||||
import funkin.util.FileUtil;
|
||||
import funkin.ui.mainmenu.MainMenuState;
|
||||
import funkin.ui.debug.stageeditor.handlers.AssetDataHandler;
|
||||
import funkin.ui.debug.stageeditor.handlers.AssetDataHandler.StageEditorObjectData;
|
||||
import funkin.ui.debug.stageeditor.handlers.StageDataHandler;
|
||||
import funkin.ui.debug.stageeditor.handlers.UndoRedoHandler.UndoAction;
|
||||
import funkin.ui.debug.stageeditor.toolboxes.*;
|
||||
import funkin.ui.debug.stageeditor.components.*;
|
||||
import haxe.ui.containers.dialogs.Dialog;
|
||||
import haxe.ui.containers.dialogs.Dialogs;
|
||||
import haxe.ui.containers.dialogs.Dialog.DialogButton;
|
||||
import haxe.ui.containers.dialogs.MessageBox.MessageBoxType;
|
||||
|
@ -45,6 +47,8 @@ import funkin.audio.FunkinSound;
|
|||
import haxe.ui.notifications.NotificationType;
|
||||
import haxe.ui.notifications.NotificationManager;
|
||||
import funkin.util.logging.CrashHandler;
|
||||
import funkin.graphics.shaders.Grayscale;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
|
||||
/**
|
||||
* Da Stage Editor woo!!
|
||||
|
@ -110,7 +114,9 @@ class StageEditorState extends UIState
|
|||
var menubarItemViewCamBounds:MenuCheckBox; // view cam bounds check
|
||||
|
||||
var menubarMenuWindow:Menu;
|
||||
var menubarItemWindowObject:MenuCheckBox;
|
||||
var menubarItemWindowObjectGraphic:MenuCheckBox;
|
||||
var menubarItemWindowObjectAnims:MenuCheckBox;
|
||||
var menubarItemWindowObjectProps:MenuCheckBox;
|
||||
var menubarItemWindowCharacter:MenuCheckBox;
|
||||
var menubarItemWindowStage:MenuCheckBox;
|
||||
|
||||
|
@ -133,8 +139,11 @@ class StageEditorState extends UIState
|
|||
|
||||
function set_selectedSprite(value:StageEditorObject)
|
||||
{
|
||||
selectedSprite?.selectedShader.setAmount(0);
|
||||
this.selectedSprite = value;
|
||||
updateDialog(StageEditorDialogType.OBJECT);
|
||||
updateDialog(StageEditorDialogType.OBJECT_GRAPHIC);
|
||||
updateDialog(StageEditorDialogType.OBJECT_ANIMS);
|
||||
updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
|
||||
|
||||
if (selectedSprite != null)
|
||||
{
|
||||
|
@ -251,6 +260,7 @@ class StageEditorState extends UIState
|
|||
|
||||
public var bitmaps:Map<String, BitmapData> = []; // used for optimizing the file size!!!
|
||||
|
||||
var charDeselectShader:Grayscale = new Grayscale();
|
||||
var floorLines:Array<FlxSprite> = [];
|
||||
var posCircles:Array<FlxShapeCircle> = [];
|
||||
var camFields:FlxTypedGroup<FlxSprite>;
|
||||
|
@ -285,6 +295,17 @@ class StageEditorState extends UIState
|
|||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* The params which were passed in when the Stage Editor was initialized.
|
||||
*/
|
||||
var params:Null<StageEditorParams>;
|
||||
|
||||
public function new(?params:StageEditorParams)
|
||||
{
|
||||
super();
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
override public function create():Void
|
||||
{
|
||||
WindowManager.instance.reset();
|
||||
|
@ -304,8 +325,6 @@ class StageEditorState extends UIState
|
|||
|
||||
persistentUpdate = false;
|
||||
|
||||
// FlxG.worldBounds.set(0, 0, FlxG.width, FlxG.height);
|
||||
|
||||
bg = FlxGridOverlay.create(10, 10);
|
||||
bg.scrollFactor.set();
|
||||
add(bg);
|
||||
|
@ -322,12 +341,12 @@ class StageEditorState extends UIState
|
|||
WindowManager.instance.container = root;
|
||||
Screen.instance.addComponent(root);
|
||||
|
||||
// group shit + assets
|
||||
var gf = CharacterDataParser.fetchCharacter("gf", true);
|
||||
// Characters setup.
|
||||
var gf = CharacterDataParser.fetchCharacter(Save.instance.stageGirlfriendChar, true);
|
||||
gf.characterType = CharacterType.GF;
|
||||
var dad = CharacterDataParser.fetchCharacter("dad", true);
|
||||
var dad = CharacterDataParser.fetchCharacter(Save.instance.stageDadChar, true);
|
||||
dad.characterType = CharacterType.DAD;
|
||||
var bf = CharacterDataParser.fetchCharacter("bf", true);
|
||||
var bf = CharacterDataParser.fetchCharacter(Save.instance.stageBoyfriendChar, true);
|
||||
bf.characterType = CharacterType.BF;
|
||||
|
||||
bf.flipX = !bf.getDataFlipX();
|
||||
|
@ -338,17 +357,13 @@ class StageEditorState extends UIState
|
|||
dad.updateHitbox();
|
||||
bf.updateHitbox();
|
||||
|
||||
// only one char !!!
|
||||
// Only one character per group allowed.
|
||||
charGroups = [
|
||||
CharacterType.BF => new FlxTypedGroup<BaseCharacter>(1),
|
||||
CharacterType.GF => new FlxTypedGroup<BaseCharacter>(1),
|
||||
CharacterType.DAD => new FlxTypedGroup<BaseCharacter>(1)
|
||||
];
|
||||
|
||||
// this is the part where the stage generate function comes up
|
||||
// apparently no, said the future me
|
||||
// back to the regular program
|
||||
|
||||
gf.x = charPos[CharacterType.GF][0] - gf.characterOrigin.x + gf.globalOffsets[0];
|
||||
gf.y = charPos[CharacterType.GF][1] - gf.characterOrigin.y + gf.globalOffsets[1];
|
||||
dad.x = charPos[CharacterType.DAD][0] - dad.characterOrigin.x + dad.globalOffsets[0];
|
||||
|
@ -366,12 +381,7 @@ class StageEditorState extends UIState
|
|||
add(charGroups[CharacterType.DAD]);
|
||||
add(charGroups[CharacterType.BF]);
|
||||
|
||||
// ui
|
||||
// spriteMarker = new FlxSprite().makeGraphic(1, 1, FlxColor.CYAN);
|
||||
// spriteMarker.alpha = 0.3;
|
||||
// spriteMarker.zIndex = MAX_Z_INDEX + CHARACTER_COLORS.length + 3; // PLEASE
|
||||
// add(spriteMarker);
|
||||
|
||||
// UI Sprites setup.
|
||||
camFields = new FlxTypedGroup<FlxSprite>();
|
||||
camFields.visible = false;
|
||||
camFields.zIndex = MAX_Z_INDEX + CHARACTER_COLORS.length + 1;
|
||||
|
@ -422,6 +432,7 @@ class StageEditorState extends UIState
|
|||
|
||||
addUI();
|
||||
|
||||
// Some callbacks.
|
||||
findObjDialog = new FindObjDialog(this, selectedSprite == null ? "" : selectedSprite.name);
|
||||
|
||||
FlxG.stage.window.onDropFile.add(function(path:String):Void {
|
||||
|
@ -442,29 +453,66 @@ class StageEditorState extends UIState
|
|||
}
|
||||
});
|
||||
|
||||
onMenuItemClick("new stage");
|
||||
welcomeDialog.closable = false;
|
||||
|
||||
#if sys
|
||||
if (Save.instance.stageEditorHasBackup)
|
||||
if (params?.targetStageId != null && StageRegistry.instance.hasEntry(params?.targetStageId))
|
||||
{
|
||||
FileUtil.createDirIfNotExists(BACKUPS_PATH);
|
||||
var stageData = StageRegistry.instance.parseEntryDataWithMigration(params.targetStageId, StageRegistry.instance.fetchEntryVersion(params.targetStageId));
|
||||
|
||||
var files = sys.FileSystem.readDirectory(BACKUPS_PATH);
|
||||
|
||||
if (files.length > 0)
|
||||
if (stageData != null)
|
||||
{
|
||||
// ensures that the top most file is a backup
|
||||
files.sort(funkin.util.SortUtil.alphabetically);
|
||||
|
||||
while (!files[files.length - 1].endsWith(FileUtil.FILE_EXTENSION_INFO_FNFS.extension)
|
||||
|| !files[files.length - 1].startsWith("stage-editor-"))
|
||||
files.pop();
|
||||
// Load the stage data.
|
||||
currentFile = "";
|
||||
this.loadFromDataRaw(stageData);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Notify the error and create a new stage.
|
||||
notifyChange("Problem Loading the Stage", "The Stage File could not be loaded.", true);
|
||||
onMenuItemClick("new stage");
|
||||
}
|
||||
|
||||
if (files.length != 0) new BackupAvailableDialog(this, haxe.io.Path.join([BACKUPS_PATH, files[files.length - 1]])).showDialog(true);
|
||||
}
|
||||
#end
|
||||
else if (params?.fnfsTargetPath != null)
|
||||
{
|
||||
var bytes = FileUtil.readBytesFromPath(params.fnfsTargetPath);
|
||||
|
||||
if (bytes != null)
|
||||
{
|
||||
// Open the stage file.
|
||||
currentFile = params.fnfsTargetPath;
|
||||
this.unpackShitFromZip(bytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Notify the error and create a new stage.
|
||||
notifyChange("Problem Loading the Stage", "The Stage File could not be loaded.", true);
|
||||
onMenuItemClick("new stage");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
onMenuItemClick("new stage");
|
||||
welcomeDialog.closable = false;
|
||||
|
||||
#if sys
|
||||
if (Save.instance.stageEditorHasBackup)
|
||||
{
|
||||
FileUtil.createDirIfNotExists(BACKUPS_PATH);
|
||||
|
||||
var files = sys.FileSystem.readDirectory(BACKUPS_PATH);
|
||||
|
||||
if (files.length > 0)
|
||||
{
|
||||
// ensures that the top most file is a backup
|
||||
files.sort(funkin.util.SortUtil.alphabetically);
|
||||
|
||||
while (!files[files.length - 1].endsWith(FileUtil.FILE_EXTENSION_INFO_FNFS.extension)
|
||||
|| !files[files.length - 1].startsWith("stage-editor-"))
|
||||
files.pop();
|
||||
}
|
||||
|
||||
if (files.length != 0) new BackupAvailableDialog(this, haxe.io.Path.join([BACKUPS_PATH, files[files.length - 1]])).showDialog(true);
|
||||
}
|
||||
#end
|
||||
}
|
||||
|
||||
WindowUtil.windowExit.add(windowClose);
|
||||
CrashHandler.errorSignal.add(autosavePerCrash);
|
||||
|
@ -473,7 +521,10 @@ class StageEditorState extends UIState
|
|||
Save.instance.stageEditorHasBackup = false;
|
||||
|
||||
Cursor.show();
|
||||
FlxG.sound.playMusic(Paths.music('chartEditorLoop/chartEditorLoop'));
|
||||
FunkinSound.playMusic('chartEditorLoop',
|
||||
{
|
||||
startingVolume: 0.0
|
||||
});
|
||||
FlxG.sound.music.fadeIn(10, 0, 1);
|
||||
}
|
||||
|
||||
|
@ -502,6 +553,22 @@ class StageEditorState extends UIState
|
|||
|
||||
override public function update(elapsed:Float):Void
|
||||
{
|
||||
// Save the stage if exiting through the F4 keybind, as it moves you to the Main Menu.
|
||||
if (FlxG.keys.justPressed.F4)
|
||||
{
|
||||
@:privateAccess
|
||||
if (!autoSaveTimer.finished) autoSaveTimer.onLoopFinished();
|
||||
resetWindowTitle();
|
||||
|
||||
WindowUtil.windowExit.remove(windowClose);
|
||||
CrashHandler.errorSignal.remove(autosavePerCrash);
|
||||
CrashHandler.criticalErrorSignal.remove(autosavePerCrash);
|
||||
|
||||
Cursor.hide();
|
||||
FlxG.sound.music.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
updateBGSize();
|
||||
conductorInUse.update();
|
||||
|
||||
|
@ -517,7 +584,7 @@ class StageEditorState extends UIState
|
|||
if (testingMode)
|
||||
{
|
||||
for (char in getCharacters())
|
||||
char.alpha = 1;
|
||||
char.shader = null;
|
||||
|
||||
// spriteMarker.visible = camMarker.visible = false;
|
||||
findObjDialog.hideDialog(DialogButton.CANCEL);
|
||||
|
@ -571,13 +638,15 @@ class StageEditorState extends UIState
|
|||
if (FlxG.keys.justPressed.S) FlxG.keys.pressed.SHIFT ? onMenuItemClick("save stage as") : onMenuItemClick("save stage");
|
||||
if (FlxG.keys.justPressed.F) onMenuItemClick("find object");
|
||||
if (FlxG.keys.justPressed.O) onMenuItemClick("open stage");
|
||||
if (FlxG.keys.justPressed.N) onMenuItemClick("new stage");
|
||||
if (FlxG.keys.justPressed.Q) onMenuItemClick("exit");
|
||||
}
|
||||
|
||||
if (FlxG.keys.justPressed.TAB) onMenuItemClick("switch mode");
|
||||
if (FlxG.keys.justPressed.DELETE) onMenuItemClick("delete object");
|
||||
if (FlxG.keys.justPressed.ENTER) onMenuItemClick("test stage");
|
||||
if (FlxG.keys.justPressed.ESCAPE) onMenuItemClick("exit");
|
||||
if (FlxG.keys.justPressed.F1) onMenuItemClick("user guide");
|
||||
if (FlxG.keys.justPressed.F1 && welcomeDialog == null && userGuideDialog == null) onMenuItemClick("user guide");
|
||||
|
||||
if (FlxG.keys.justPressed.T)
|
||||
{
|
||||
|
@ -612,19 +681,20 @@ class StageEditorState extends UIState
|
|||
|
||||
if (moveMode == "assets")
|
||||
{
|
||||
if (selectedSprite != null && !FlxG.mouse.overlaps(selectedSprite) && FlxG.mouse.justPressed && !isCursorOverHaxeUI)
|
||||
{
|
||||
selectedSprite = null;
|
||||
}
|
||||
|
||||
for (spr in spriteArray)
|
||||
{
|
||||
spr.active = spr.isOnScreen();
|
||||
|
||||
if (spr.pixelsOverlapPoint(FlxG.mouse.getWorldPosition()))
|
||||
if (FlxG.mouse.overlaps(spr))
|
||||
{
|
||||
if (spr.visible && !FlxG.keys.pressed.SHIFT) nameTxt.text = spr.name;
|
||||
|
||||
if (FlxG.mouse.justPressed && allowInput && spr.visible && !FlxG.keys.pressed.SHIFT && !isCursorOverHaxeUI)
|
||||
{
|
||||
selectedSprite.selectedShader.setAmount(0);
|
||||
selectedSprite = spr;
|
||||
updateDialog(StageEditorDialogType.OBJECT);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -639,7 +709,7 @@ class StageEditorState extends UIState
|
|||
if (FlxG.mouse.pressed && allowInput && selectedSprite != null && FlxG.mouse.overlaps(selectedSprite) && FlxG.mouse.justMoved && !isCursorOverHaxeUI)
|
||||
{
|
||||
saved = false;
|
||||
updateDialog(StageEditorDialogType.OBJECT);
|
||||
updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
|
||||
|
||||
if (moveOffset.length == 0)
|
||||
{
|
||||
|
@ -671,15 +741,15 @@ class StageEditorState extends UIState
|
|||
arrowMovement(selectedSprite);
|
||||
|
||||
for (char in getCharacters())
|
||||
char.alpha = 1;
|
||||
char.shader = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedChar.alpha = 1;
|
||||
selectedChar.shader = null;
|
||||
|
||||
for (char in getCharacters())
|
||||
{
|
||||
if (char != selectedChar) char.alpha = 0.3;
|
||||
if (char != selectedChar) char.shader = charDeselectShader;
|
||||
|
||||
if (char != null && checkCharOverlaps(char)) // flxg.mouse.overlaps crashes the game
|
||||
{
|
||||
|
@ -732,12 +802,10 @@ class StageEditorState extends UIState
|
|||
nameTxt.x = FlxG.mouse.getScreenPosition(camHUD).x;
|
||||
nameTxt.y = FlxG.mouse.getScreenPosition(camHUD).y - nameTxt.height;
|
||||
|
||||
// spriteMarker.visible = (moveMode == "assets" && selectedSprite != null);
|
||||
camMarker.visible = moveMode == "chars";
|
||||
// if (selectedSprite != null) spriteMarker.setPosition(selectedSprite.x, selectedSprite.y);
|
||||
|
||||
// for (item in sprDependant)
|
||||
// item.disabled = !spriteMarker.visible;
|
||||
for (item in sprDependant)
|
||||
item.disabled = (moveMode != "assets" || selectedSprite == null);
|
||||
|
||||
menubarItemPaste.disabled = copiedSprite == null;
|
||||
menubarItemFindObj.disabled = !(moveMode == "assets");
|
||||
|
@ -755,22 +823,22 @@ class StageEditorState extends UIState
|
|||
|
||||
function autosavePerCrash(message:String)
|
||||
{
|
||||
trace("fuuuucckkkkk we crashed, reason: " + message);
|
||||
trace("Crashed the game for the reason: " + message);
|
||||
|
||||
if (!saved)
|
||||
{
|
||||
trace("dw we're making a backup!!!");
|
||||
trace("You haven't saved recently, so a backup will be made.");
|
||||
autoSaveTimer.onComplete(autoSaveTimer);
|
||||
}
|
||||
}
|
||||
|
||||
function windowClose(exitCode:Int)
|
||||
{
|
||||
trace("closing da window ye");
|
||||
trace("Closing the game window.");
|
||||
|
||||
if (!saved)
|
||||
{
|
||||
trace("dum dum why no save >:[");
|
||||
trace("You haven't saved recently, so a backup will be made.");
|
||||
autoSaveTimer.onComplete(autoSaveTimer);
|
||||
}
|
||||
}
|
||||
|
@ -873,6 +941,7 @@ class StageEditorState extends UIState
|
|||
|
||||
public function updateArray()
|
||||
{
|
||||
sortAssets();
|
||||
spriteArray = [];
|
||||
|
||||
for (thing in members)
|
||||
|
@ -881,7 +950,6 @@ class StageEditorState extends UIState
|
|||
}
|
||||
|
||||
findObjDialog.updateIndicator();
|
||||
updateDialog(StageEditorDialogType.OBJECT);
|
||||
}
|
||||
|
||||
public function sortAssets()
|
||||
|
@ -967,7 +1035,7 @@ class StageEditorState extends UIState
|
|||
menubarItemRedo.onClick = function(_) onMenuItemClick("redo");
|
||||
menubarItemCopy.onClick = function(_) onMenuItemClick("copy object");
|
||||
menubarItemCut.onClick = function(_) onMenuItemClick("cut object");
|
||||
menubarItemPaste.onClick = function(_) onMenuItemClick("paste stage");
|
||||
menubarItemPaste.onClick = function(_) onMenuItemClick("paste object");
|
||||
menubarItemDelete.onClick = function(_) onMenuItemClick("delete object");
|
||||
menubarItemNewObj.onClick = function(_) onMenuItemClick("new object");
|
||||
menubarItemFindObj.onClick = function(_) onMenuItemClick("find object");
|
||||
|
@ -993,7 +1061,7 @@ class StageEditorState extends UIState
|
|||
var shit = Std.parseInt(StringTools.replace(bottomBarMoveStepText.text, "px", ""));
|
||||
moveStep = shit;
|
||||
|
||||
updateDialog(StageEditorDialogType.OBJECT);
|
||||
updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
|
||||
updateDialog(StageEditorDialogType.CHARACTER);
|
||||
updateDialog(StageEditorDialogType.STAGE);
|
||||
}
|
||||
|
@ -1016,7 +1084,7 @@ class StageEditorState extends UIState
|
|||
Save.instance.stageEditorAngleStep = angleOptions[id];
|
||||
bottomBarAngleStepText.text = (angleOptions.contains(Save.instance.stageEditorAngleStep) ? Save.instance.stageEditorAngleStep : 5) + "°";
|
||||
|
||||
updateDialog(StageEditorDialogType.OBJECT);
|
||||
updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
|
||||
}
|
||||
|
||||
bottomBarAngleStepText.onClick = function(_) changeAngle(1);
|
||||
|
@ -1024,11 +1092,15 @@ class StageEditorState extends UIState
|
|||
|
||||
changeAngle(); // update
|
||||
|
||||
dialogs.set(StageEditorDialogType.OBJECT, new StageEditorObjectToolbox(this));
|
||||
dialogs.set(StageEditorDialogType.OBJECT_GRAPHIC, new StageEditorObjectGraphicToolbox(this));
|
||||
dialogs.set(StageEditorDialogType.OBJECT_ANIMS, new StageEditorObjectAnimsToolbox(this));
|
||||
dialogs.set(StageEditorDialogType.OBJECT_PROPERTIES, new StageEditorObjectPropertiesToolbox(this));
|
||||
dialogs.set(StageEditorDialogType.CHARACTER, new StageEditorCharacterToolbox(this));
|
||||
dialogs.set(StageEditorDialogType.STAGE, new StageEditorStageToolbox(this));
|
||||
|
||||
menubarItemWindowObject.onChange = function(_) toggleDialog(StageEditorDialogType.OBJECT, menubarItemWindowObject.selected);
|
||||
menubarItemWindowObjectGraphic.onChange = function(_) toggleDialog(StageEditorDialogType.OBJECT_GRAPHIC, menubarItemWindowObjectGraphic.selected);
|
||||
menubarItemWindowObjectAnims.onChange = function(_) toggleDialog(StageEditorDialogType.OBJECT_ANIMS, menubarItemWindowObjectAnims.selected);
|
||||
menubarItemWindowObjectProps.onChange = function(_) toggleDialog(StageEditorDialogType.OBJECT_PROPERTIES, menubarItemWindowObjectProps.selected);
|
||||
menubarItemWindowCharacter.onChange = function(_) toggleDialog(StageEditorDialogType.CHARACTER, menubarItemWindowCharacter.selected);
|
||||
menubarItemWindowStage.onChange = function(_) toggleDialog(StageEditorDialogType.STAGE, menubarItemWindowStage.selected);
|
||||
|
||||
|
@ -1111,6 +1183,7 @@ class StageEditorState extends UIState
|
|||
public var userGuideDialog:UserGuideDialog;
|
||||
public var aboutDialog:AboutDialog;
|
||||
public var loadUrlDialog:LoadFromUrlDialog;
|
||||
public var exitConfirmDialog:Dialog;
|
||||
|
||||
public function onMenuItemClick(item:String):Void
|
||||
{
|
||||
|
@ -1133,8 +1206,6 @@ class StageEditorState extends UIState
|
|||
currentFile = path;
|
||||
}, null, stageName + "." + FileUtil.FILE_EXTENSION_INFO_FNFS.extension);
|
||||
|
||||
bitmaps.clear();
|
||||
|
||||
case "save stage":
|
||||
if (currentFile == "")
|
||||
{
|
||||
|
@ -1154,8 +1225,7 @@ class StageEditorState extends UIState
|
|||
|
||||
saved = true;
|
||||
|
||||
updateRecentFiles();
|
||||
bitmaps.clear();
|
||||
reloadRecentFiles();
|
||||
|
||||
case "open stage":
|
||||
if (!saved)
|
||||
|
@ -1172,26 +1242,35 @@ class StageEditorState extends UIState
|
|||
return;
|
||||
}
|
||||
|
||||
FileUtil.browseForSaveFile([FileUtil.FILE_FILTER_FNFS], function(path:String) {
|
||||
FileUtil.browseForBinaryFile("Open Stage Data", [FileUtil.FILE_EXTENSION_INFO_FNFS], function(_) {
|
||||
if (_?.fullPath == null) return;
|
||||
|
||||
clearAssets();
|
||||
|
||||
currentFile = path;
|
||||
this.unpackShitFromZip(FileUtil.readBytesFromPath(path));
|
||||
currentFile = _.fullPath;
|
||||
this.unpackShitFromZip(FileUtil.readBytesFromPath(currentFile));
|
||||
|
||||
reloadRecentFiles();
|
||||
}, null, null, "Open Stage Data");
|
||||
}, function() {
|
||||
// This function does nothing, it's there for crash prevention.
|
||||
});
|
||||
|
||||
case "exit":
|
||||
if (!saved)
|
||||
{
|
||||
Dialogs.messageBox("You are about to leave the Editor without Saving.\n\nAre you sure? ", "Leave Editor", MessageBoxType.TYPE_YESNO, true,
|
||||
if (exitConfirmDialog == null)
|
||||
{
|
||||
exitConfirmDialog = Dialogs.messageBox("You are about to leave the Editor without Saving.\n\nAre you sure? ", "Leave Editor",
|
||||
MessageBoxType.TYPE_YESNO, true,
|
||||
function(btn:DialogButton) {
|
||||
exitConfirmDialog = null;
|
||||
if (btn == DialogButton.YES)
|
||||
{
|
||||
saved = true;
|
||||
onMenuItemClick("exit");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -1203,11 +1282,14 @@ class StageEditorState extends UIState
|
|||
CrashHandler.criticalErrorSignal.remove(autosavePerCrash);
|
||||
|
||||
Cursor.hide();
|
||||
FlxG.switchState(() -> new DebugMenuSubState());
|
||||
FlxG.switchState(() -> new MainMenuState());
|
||||
FlxG.sound.music.stop();
|
||||
|
||||
case "switch mode":
|
||||
if (!testingMode) moveMode = (moveMode == "assets" ? "chars" : "assets");
|
||||
if (testingMode) return;
|
||||
moveMode = (moveMode == "assets" ? "chars" : "assets");
|
||||
|
||||
selectedSprite?.selectedShader.setAmount((moveMode == "assets" ? 1 : 0));
|
||||
|
||||
case "switch focus":
|
||||
if (testingMode)
|
||||
|
@ -1264,6 +1346,10 @@ class StageEditorState extends UIState
|
|||
userGuideDialog = new UserGuideDialog();
|
||||
userGuideDialog.showDialog();
|
||||
|
||||
userGuideDialog.onDialogClosed = function(_) {
|
||||
userGuideDialog = null;
|
||||
}
|
||||
|
||||
case "open folder":
|
||||
#if sys
|
||||
var absoluteBackupsPath:String = haxe.io.Path.join([Sys.getCwd(), BACKUPS_PATH]);
|
||||
|
@ -1281,19 +1367,25 @@ class StageEditorState extends UIState
|
|||
a.isDebugged = testingMode;
|
||||
}
|
||||
|
||||
if (!testingMode) menubarItemWindowObject.selected = menubarItemWindowCharacter.selected = menubarItemWindowStage.selected = false;
|
||||
if (!testingMode)
|
||||
{
|
||||
menubarItemWindowObjectGraphic.selected = menubarItemWindowObjectAnims.selected = menubarItemWindowObjectProps.selected = menubarItemWindowCharacter.selected = menubarItemWindowStage.selected = false;
|
||||
}
|
||||
|
||||
selectedSprite?.selectedShader.setAmount((testingMode ? (moveMode == "assets" ? 1 : 0) : 0));
|
||||
testingMode = !testingMode;
|
||||
|
||||
case "clear assets":
|
||||
Dialogs.messageBox("This will destroy all the Objects in this Stage.\n\nAre you sure? This cannot be undone.", "Clear Assets",
|
||||
MessageBoxType.TYPE_YESNO, true, function(btn:DialogButton) {
|
||||
Dialogs.messageBox("This will destroy all Objects in this Stage.\n\nAre you sure? This cannot be undone.", "Clear Assets", MessageBoxType.TYPE_YESNO,
|
||||
true, function(btn:DialogButton) {
|
||||
if (btn == DialogButton.YES)
|
||||
{
|
||||
clearAssets();
|
||||
saved = false;
|
||||
|
||||
updateDialog(StageEditorDialogType.OBJECT);
|
||||
updateDialog(StageEditorDialogType.OBJECT_GRAPHIC);
|
||||
updateDialog(StageEditorDialogType.OBJECT_ANIMS);
|
||||
updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -1301,7 +1393,7 @@ class StageEditorState extends UIState
|
|||
if (selectedSprite != null && moveMode == "assets")
|
||||
{
|
||||
selectedSprite.screenCenter();
|
||||
updateDialog(StageEditorDialogType.OBJECT);
|
||||
updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
|
||||
saved = false;
|
||||
}
|
||||
|
||||
|
@ -1313,6 +1405,8 @@ class StageEditorState extends UIState
|
|||
}
|
||||
|
||||
case "delete object":
|
||||
if (selectedSprite == null) return;
|
||||
|
||||
this.createAndPushAction(OBJECT_DELETED);
|
||||
|
||||
spriteArray.remove(selectedSprite);
|
||||
|
@ -1323,7 +1417,6 @@ class StageEditorState extends UIState
|
|||
selectedSprite = null;
|
||||
|
||||
updateArray();
|
||||
sortAssets();
|
||||
|
||||
case "copy object":
|
||||
if (selectedSprite == null) return;
|
||||
|
@ -1347,6 +1440,7 @@ class StageEditorState extends UIState
|
|||
spr.name += " (" + i + ")";
|
||||
}
|
||||
|
||||
add(spr);
|
||||
selectedSprite = spr;
|
||||
updateArray();
|
||||
|
||||
|
@ -1355,7 +1449,9 @@ class StageEditorState extends UIState
|
|||
onMenuItemClick("delete object"); // already changes the saved var
|
||||
|
||||
case "new stage":
|
||||
if (menubarItemWindowObject.selected) menubarItemWindowObject.selected = false;
|
||||
if (menubarItemWindowObjectGraphic.selected) menubarItemWindowObjectGraphic.selected = false;
|
||||
if (menubarItemWindowObjectAnims.selected) menubarItemWindowObjectAnims.selected = false;
|
||||
if (menubarItemWindowObjectProps.selected) menubarItemWindowObjectProps.selected = false;
|
||||
if (menubarItemWindowCharacter.selected) menubarItemWindowCharacter.selected = false;
|
||||
if (menubarItemWindowStage.selected) menubarItemWindowStage.selected = false;
|
||||
|
||||
|
@ -1366,7 +1462,9 @@ class StageEditorState extends UIState
|
|||
updateWindowTitle();
|
||||
welcomeDialog = null;
|
||||
|
||||
updateDialog(StageEditorDialogType.OBJECT);
|
||||
updateDialog(StageEditorDialogType.OBJECT_GRAPHIC);
|
||||
updateDialog(StageEditorDialogType.OBJECT_ANIMS);
|
||||
updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
|
||||
updateDialog(StageEditorDialogType.CHARACTER);
|
||||
updateDialog(StageEditorDialogType.STAGE);
|
||||
}
|
||||
|
@ -1388,9 +1486,7 @@ class StageEditorState extends UIState
|
|||
|
||||
undoArray = [];
|
||||
redoArray = [];
|
||||
|
||||
updateArray();
|
||||
sortAssets();
|
||||
removeUnusedBitmaps();
|
||||
}
|
||||
|
||||
|
@ -1479,7 +1575,30 @@ enum StageEditorDialogType
|
|||
CHARACTER;
|
||||
|
||||
/**
|
||||
* The Object Options Dialog.
|
||||
* The Object Graphic Options Dialog.
|
||||
*/
|
||||
OBJECT;
|
||||
OBJECT_GRAPHIC;
|
||||
|
||||
/**
|
||||
* The Object Animations Options Dialog.
|
||||
*/
|
||||
OBJECT_ANIMS;
|
||||
|
||||
/**
|
||||
* The Object Properties Options Dialog.
|
||||
*/
|
||||
OBJECT_PROPERTIES;
|
||||
}
|
||||
|
||||
typedef StageEditorParams =
|
||||
{
|
||||
/**
|
||||
* If non-null, load this stage immediately instead of the welcome screen.
|
||||
*/
|
||||
var ?fnfsTargetPath:String;
|
||||
|
||||
/**
|
||||
* If non-null, load this stage immediately instead of the welcome screen.
|
||||
*/
|
||||
var ?targetStageId:String;
|
||||
};
|
||||
|
|
|
@ -11,7 +11,7 @@ using StringTools;
|
|||
@:xml('
|
||||
<dialog id="backupAvailableDialog" width="475" height="150" title="Hey! Listen!">
|
||||
<vbox width="100%" height="100%">
|
||||
<label text="There is a chart backup available, would you like to open it?\n" width="100%" textAlign="center" />
|
||||
<label text="There is a stage backup available, would you like to open it?\n" width="100%" textAlign="center" />
|
||||
<spacer height="6" />
|
||||
<label id="backupTimeLabel" text="Jan 1, 1970 0:00" width="100%" textAlign="center" />
|
||||
<spacer height="100%" />
|
||||
|
|
|
@ -62,7 +62,9 @@ class NewObjDialog extends Dialog
|
|||
|
||||
spr.name = field.text;
|
||||
spr.screenCenter();
|
||||
spr.zIndex = 0;
|
||||
|
||||
var sprArray = stageEditorState.spriteArray;
|
||||
spr.zIndex = sprArray.length == 0 ? 0 : (sprArray[sprArray.length - 1].zIndex + 1);
|
||||
|
||||
stageEditorState.selectedSprite = spr;
|
||||
stageEditorState.createAndPushAction(OBJECT_CREATED);
|
||||
|
|
|
@ -33,8 +33,6 @@ class WelcomeDialog extends Dialog
|
|||
|
||||
for (file in Save.instance.stageEditorPreviousFiles)
|
||||
{
|
||||
trace(file);
|
||||
|
||||
if (!FileUtil.fileExists(file)) continue; // whats the point of loading something that doesnt exist
|
||||
|
||||
var patj = new haxe.io.Path(file);
|
||||
|
@ -56,7 +54,7 @@ class WelcomeDialog extends Dialog
|
|||
|
||||
boxDrag.onClick = function(_) FileUtil.browseForSaveFile([FileUtil.FILE_FILTER_FNFS], loadFromFilePath, null, null, "Open Stage Data");
|
||||
|
||||
var defaultStages:Array<String> = StageRegistry.instance.listBaseGameEntryIds();
|
||||
var defaultStages = StageRegistry.instance.listEntryIds();
|
||||
defaultStages.sort(funkin.util.SortUtil.alphabetically);
|
||||
|
||||
for (stage in defaultStages)
|
||||
|
@ -132,7 +130,8 @@ class WelcomeDialog extends Dialog
|
|||
|
||||
function killDaDialog()
|
||||
{
|
||||
stageEditorState.updateDialog(StageEditorDialogType.OBJECT);
|
||||
stageEditorState.updateDialog(StageEditorDialogType.OBJECT_GRAPHIC);
|
||||
stageEditorState.updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
|
||||
stageEditorState.updateDialog(StageEditorDialogType.CHARACTER);
|
||||
stageEditorState.updateDialog(StageEditorDialogType.STAGE);
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package funkin.ui.debug.stageeditor.handlers;
|
||||
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import openfl.display.BitmapData;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.util.FlxColor;
|
||||
|
@ -43,14 +44,17 @@ class AssetDataHandler
|
|||
startingAnimation: obj.startingAnimation,
|
||||
animType: "sparrow", // automatically making sparrow atlases yeah
|
||||
angle: obj.angle,
|
||||
flipX: obj.flipX,
|
||||
flipY: obj.flipY,
|
||||
blend: obj.blend == null ? "" : Std.string(obj.blend),
|
||||
color: obj.color.toWebString(),
|
||||
xmlData: obj.generateXML()
|
||||
animData: ""
|
||||
}
|
||||
|
||||
if (useBitmaps)
|
||||
{
|
||||
outputData.bitmap = obj.pixels.clone();
|
||||
outputData.animData = obj.generateXML();
|
||||
return outputData;
|
||||
}
|
||||
|
||||
|
@ -59,6 +63,7 @@ class AssetDataHandler
|
|||
if (areTheseBitmapsEqual(bit, obj.pixels))
|
||||
{
|
||||
outputData.assetPath = name;
|
||||
outputData.animData = obj.generateXML(name);
|
||||
return outputData;
|
||||
}
|
||||
}
|
||||
|
@ -77,14 +82,33 @@ class AssetDataHandler
|
|||
{
|
||||
if (data.bitmap != null)
|
||||
{
|
||||
var bitToLoad = state.addBitmap(data.bitmap.clone());
|
||||
object.loadGraphic(state.bitmaps[bitToLoad]);
|
||||
if (data.animations != null && data.animations.length > 0)
|
||||
{
|
||||
var bitToLoad = state.addBitmap(data.bitmap.clone());
|
||||
object.frames = FlxAtlasFrames.fromSparrow(state.bitmaps[bitToLoad], data.animData);
|
||||
}
|
||||
else if (areTheseBitmapsEqual(data.bitmap, getDefaultGraphic()))
|
||||
{
|
||||
object.loadGraphic(getDefaultGraphic());
|
||||
}
|
||||
else
|
||||
{
|
||||
var bitToLoad = state.addBitmap(data.bitmap.clone());
|
||||
object.loadGraphic(state.bitmaps[bitToLoad]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (data.animations != null && data.animations.length > 0) // considering we're unpacking we might as well just do this instead of switch
|
||||
{
|
||||
object.frames = flixel.graphics.frames.FlxAtlasFrames.fromSparrow(state.bitmaps[data.assetPath].clone(), data.xmlData);
|
||||
if (data.animData.contains("</TextureAtlas>"))
|
||||
{
|
||||
object.frames = FlxAtlasFrames.fromSparrow(state.bitmaps[data.assetPath].clone(), data.animData);
|
||||
}
|
||||
else
|
||||
{
|
||||
object.frames = FlxAtlasFrames.fromSpriteSheetPacker(state.bitmaps[data.assetPath].clone(), data.animData);
|
||||
}
|
||||
}
|
||||
else if (data.assetPath.startsWith("#"))
|
||||
{
|
||||
|
@ -107,10 +131,6 @@ class AssetDataHandler
|
|||
object.blend = blendFromString(data.blend);
|
||||
if (!data.assetPath.startsWith("#")) object.color = FlxColor.fromString(data.color);
|
||||
|
||||
// yeah
|
||||
object.pixelPerfectRender = data.isPixel;
|
||||
object.pixelPerfectPosition = data.isPixel;
|
||||
|
||||
for (anim in data.animations)
|
||||
{
|
||||
object.addAnim(anim.name, anim.prefix, anim.offsets ?? [0, 0], anim.frameIndices ?? [], anim.frameRate ?? 24, anim.looped ?? false, anim.flipX ?? false,
|
||||
|
@ -132,7 +152,7 @@ class AssetDataHandler
|
|||
object.playAnim(object.startingAnimation);
|
||||
|
||||
flixel.util.FlxTimer.wait(StageEditorState.TIME_BEFORE_ANIM_STOP, function() {
|
||||
if (object != null && object.animation.curAnim != null) object.animation.stop();
|
||||
if (object?.animation?.curAnim != null) object.animation.stop();
|
||||
});
|
||||
|
||||
return object;
|
||||
|
@ -159,15 +179,15 @@ class AssetDataHandler
|
|||
return BlendMode.fromString(blend.toLowerCase().trim());
|
||||
}
|
||||
|
||||
public static function generateXML(obj:StageEditorObject)
|
||||
public static function generateXML(obj:StageEditorObject, bitmapName:String = "")
|
||||
{
|
||||
// the last check is for if the only frame is the standard graphic frame
|
||||
if (obj == null || obj.frames.frames.length == 0 || obj.frames.frames[0].name == null) return "";
|
||||
|
||||
var xml = [
|
||||
"<!--This XML File was automatically generated by StageEditorEngine, in order to make Funkin' be able to load it.-->",
|
||||
"<!--This XML File was automatically generated by the Stage Editor.-->",
|
||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||
'<TextureAtlas imagePath="${obj.toData(false).assetPath}.png" width="${obj.pixels.width}" height="${obj.pixels.height}">'
|
||||
'<TextureAtlas imagePath="${haxe.io.Path.withoutDirectory(bitmapName)}.png" width="${obj.pixels.width}" height="${obj.pixels.height}">'
|
||||
].join("\n");
|
||||
|
||||
for (daFrame in obj.frames.frames)
|
||||
|
@ -184,11 +204,14 @@ class AssetDataHandler
|
|||
{
|
||||
if (bitmap1.width != bitmap2.width || bitmap1.height != bitmap2.height) return false;
|
||||
|
||||
for (px in 0...bitmap1.width)
|
||||
var bytes1 = bitmap1.image.data;
|
||||
var bytes2 = bitmap2.image.data;
|
||||
|
||||
for (i in 0...bytes1.length)
|
||||
{
|
||||
for (py in 0...bitmap1.height)
|
||||
if (bytes1[i] != bytes2[i])
|
||||
{
|
||||
if (bitmap1.getPixel32(px, py) != bitmap2.getPixel32(px, py)) return false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -199,6 +222,6 @@ class AssetDataHandler
|
|||
typedef StageEditorObjectData =
|
||||
{
|
||||
> StageDataProp,
|
||||
var xmlData:String;
|
||||
var animData:String;
|
||||
var ?bitmap:BitmapData;
|
||||
}
|
||||
|
|
|
@ -47,12 +47,14 @@ class StageDataHandler
|
|||
animations: data.animations,
|
||||
startingAnimation: data.startingAnimation,
|
||||
animType: data.animType,
|
||||
flipX: data.flipX,
|
||||
flipY: data.flipY,
|
||||
angle: data.angle,
|
||||
blend: data.blend,
|
||||
color: data.assetPath.startsWith("#") ? "#FFFFFF" : data.color
|
||||
});
|
||||
|
||||
if (!xmlMap.exists(data.assetPath) && data.xmlData != "") xmlMap.set(data.assetPath, data.xmlData);
|
||||
if (!xmlMap.exists(data.assetPath) && data.animData != "") xmlMap.set(data.assetPath, data.animData);
|
||||
}
|
||||
|
||||
// step 1 phase 2: character data
|
||||
|
@ -68,6 +70,18 @@ class StageDataHandler
|
|||
endData.characters.gf.cameraOffsets = state.charCamOffsets[CharacterType.GF].copy();
|
||||
endData.characters.dad.cameraOffsets = state.charCamOffsets[CharacterType.DAD].copy();
|
||||
|
||||
endData.characters.bf.alpha = state.bf.alpha;
|
||||
endData.characters.gf.alpha = state.gf.alpha;
|
||||
endData.characters.dad.alpha = state.dad.alpha;
|
||||
|
||||
endData.characters.bf.angle = state.bf.angle;
|
||||
endData.characters.gf.angle = state.gf.angle;
|
||||
endData.characters.dad.angle = state.dad.angle;
|
||||
|
||||
endData.characters.bf.scroll = [state.bf.scrollFactor.x, state.bf.scrollFactor.y];
|
||||
endData.characters.gf.scroll = [state.gf.scrollFactor.x, state.gf.scrollFactor.y];
|
||||
endData.characters.dad.scroll = [state.dad.scrollFactor.x, state.dad.scrollFactor.y];
|
||||
|
||||
endData.characters.bf.position = [
|
||||
state.bf.feetPosition.x - state.bf.globalOffsets[0],
|
||||
state.bf.feetPosition.y - state.bf.globalOffsets[1]
|
||||
|
@ -108,15 +122,13 @@ class StageDataHandler
|
|||
}
|
||||
|
||||
// step 2 phase 2: xmls
|
||||
for (obj in endData.props)
|
||||
for (path => xml in xmlMap)
|
||||
{
|
||||
if (!xmlMap.exists(obj.assetPath)) continue; // damn
|
||||
|
||||
var bytes = Bytes.ofString(xmlMap[obj.assetPath]);
|
||||
var bytes = Bytes.ofString(xml);
|
||||
|
||||
var entry:Entry =
|
||||
{
|
||||
fileName: obj.assetPath + ".xml",
|
||||
fileName: path + ".xml",
|
||||
fileSize: bytes.length,
|
||||
fileTime: Date.now(),
|
||||
compressed: false,
|
||||
|
@ -208,8 +220,10 @@ class StageDataHandler
|
|||
scroll: objData.scroll.copy(),
|
||||
color: objData.color,
|
||||
blend: objData.blend,
|
||||
flipX: objData.flipX,
|
||||
flipY: objData.flipY,
|
||||
startingAnimation: objData.startingAnimation,
|
||||
xmlData: xmls[objData.assetPath] ?? ""
|
||||
animData: xmls[objData.assetPath] ?? ""
|
||||
});
|
||||
|
||||
state.add(spr);
|
||||
|
@ -250,6 +264,10 @@ class StageDataHandler
|
|||
char.cameraFocusPoint.x += charData.cameraOffsets[0];
|
||||
char.cameraFocusPoint.y += charData.cameraOffsets[1];
|
||||
|
||||
char.alpha = charData.alpha;
|
||||
char.angle = charData.angle;
|
||||
char.scrollFactor.set(charData.scroll[0], charData.scroll[1]);
|
||||
|
||||
state.charCamOffsets[char.characterType] = charData.cameraOffsets.copy();
|
||||
}
|
||||
}
|
||||
|
@ -292,6 +310,10 @@ class StageDataHandler
|
|||
var spr = new StageEditorObject();
|
||||
if (!objData.assetPath.startsWith("#")) state.bitmaps.set(objData.assetPath, Assets.getBitmapData(Paths.image(objData.assetPath)));
|
||||
|
||||
var usePacker:Bool = objData.animType == "packer";
|
||||
var animPath:String = Paths.file("images/" + objData.assetPath + (usePacker ? ".txt" : ".xml"));
|
||||
var animText:String = Assets.exists(animPath) ? Assets.getText(animPath) : "";
|
||||
|
||||
spr.fromData(
|
||||
{
|
||||
name: objData.name ?? "Unnamed",
|
||||
|
@ -307,15 +329,16 @@ class StageDataHandler
|
|||
scroll: objData.scroll.copy(),
|
||||
color: objData.color,
|
||||
blend: objData.blend,
|
||||
flipX: objData.flipX,
|
||||
flipY: objData.flipY,
|
||||
startingAnimation: objData.startingAnimation,
|
||||
xmlData: Assets.exists(Paths.file("images/" + objData.assetPath + ".xml")) ? Assets.getText(Paths.file("images/" + objData.assetPath + ".xml")) : ""
|
||||
animData: animText
|
||||
});
|
||||
|
||||
state.add(spr);
|
||||
}
|
||||
|
||||
state.updateArray();
|
||||
state.sortAssets();
|
||||
state.updateMarkerPos();
|
||||
}
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ class UndoRedoHandler
|
|||
state.selectedSprite.x = pos[0];
|
||||
state.selectedSprite.y = pos[1];
|
||||
|
||||
state.updateDialog(StageEditorDialogType.OBJECT);
|
||||
state.updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
|
||||
}
|
||||
|
||||
case OBJECT_CREATED: // this removes the object
|
||||
|
@ -66,7 +66,9 @@ class UndoRedoHandler
|
|||
obj.destroy();
|
||||
|
||||
state.updateArray();
|
||||
state.updateDialog(StageEditorDialogType.OBJECT);
|
||||
state.updateDialog(StageEditorDialogType.OBJECT_GRAPHIC);
|
||||
state.updateDialog(StageEditorDialogType.OBJECT_ANIMS);
|
||||
state.updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
|
||||
trace("found object");
|
||||
|
||||
continue;
|
||||
|
@ -86,7 +88,9 @@ class UndoRedoHandler
|
|||
createAndPushAction(state, OBJECT_CREATED, !redo);
|
||||
state.add(obj);
|
||||
|
||||
state.updateDialog(StageEditorDialogType.OBJECT);
|
||||
state.updateDialog(StageEditorDialogType.OBJECT_GRAPHIC);
|
||||
state.updateDialog(StageEditorDialogType.OBJECT_ANIMS);
|
||||
state.updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
|
||||
state.updateArray();
|
||||
|
||||
case OBJECT_ROTATED: // primarily copied from OBJECT_MOVED
|
||||
|
@ -102,7 +106,7 @@ class UndoRedoHandler
|
|||
{
|
||||
createAndPushAction(state, actionToDo.type, !redo);
|
||||
state.selectedSprite.angle = angle;
|
||||
state.updateDialog(StageEditorDialogType.OBJECT);
|
||||
state.updateDialog(StageEditorDialogType.OBJECT_PROPERTIES);
|
||||
}
|
||||
|
||||
default: // do nothing dumbass
|
||||
|
|
|
@ -3,7 +3,9 @@ package funkin.ui.debug.stageeditor.toolboxes;
|
|||
import haxe.ui.components.NumberStepper;
|
||||
import funkin.play.character.BaseCharacter.CharacterType;
|
||||
import funkin.play.character.CharacterData.CharacterDataParser;
|
||||
import funkin.play.character.CharacterData;
|
||||
import funkin.util.SortUtil;
|
||||
import funkin.save.Save;
|
||||
import haxe.ui.components.Button;
|
||||
import haxe.ui.components.Slider;
|
||||
import haxe.ui.containers.menus.Menu;
|
||||
|
@ -11,130 +13,117 @@ import haxe.ui.core.Screen;
|
|||
import flixel.tweens.FlxTween;
|
||||
import flixel.tweens.FlxEase;
|
||||
import haxe.ui.containers.Grid;
|
||||
import funkin.play.character.CharacterData;
|
||||
import haxe.ui.events.UIEvent;
|
||||
|
||||
using StringTools;
|
||||
|
||||
@:access(funkin.ui.debug.stageeditor.StageEditorState)
|
||||
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/character-properties.xml"))
|
||||
class StageEditorCharacterToolbox extends StageEditorDefaultToolbox
|
||||
{
|
||||
var characterPosXStepper:NumberStepper;
|
||||
var characterPosYStepper:NumberStepper;
|
||||
var characterPosReset:Button;
|
||||
public var charPosX:NumberStepper;
|
||||
public var charPosY:NumberStepper;
|
||||
public var charZIdx:NumberStepper;
|
||||
public var charScale:NumberStepper;
|
||||
public var charCamX:NumberStepper;
|
||||
public var charCamY:NumberStepper;
|
||||
public var charAlpha:NumberStepper;
|
||||
public var charAngle:NumberStepper;
|
||||
public var charScrollX:NumberStepper;
|
||||
public var charScrollY:NumberStepper;
|
||||
|
||||
var characterZIdxStepper:NumberStepper;
|
||||
var characterZIdxReset:Button;
|
||||
|
||||
var characterCamXStepper:NumberStepper;
|
||||
var characterCamYStepper:NumberStepper;
|
||||
var characterCamReset:Button;
|
||||
|
||||
var characterScaleSlider:Slider;
|
||||
var characterScaleReset:Button;
|
||||
|
||||
var characterTypeButton:Button;
|
||||
var charType:Button;
|
||||
var charMenu:StageEditorCharacterMenu;
|
||||
|
||||
override public function new(state:StageEditorState)
|
||||
{
|
||||
super(state);
|
||||
|
||||
// position
|
||||
characterPosXStepper.onChange = characterPosYStepper.onChange = function(_) {
|
||||
// Numeric callbacks.
|
||||
charPosX.onChange = charPosY.onChange = function(_) {
|
||||
repositionCharacter();
|
||||
state.saved = false;
|
||||
}
|
||||
|
||||
characterPosReset.onClick = function(_) {
|
||||
if (!StageEditorState.DEFAULT_POSITIONS.exists(state.selectedChar.characterType)) return;
|
||||
|
||||
var oldPositions = StageEditorState.DEFAULT_POSITIONS[state.selectedChar.characterType];
|
||||
characterPosXStepper.pos = oldPositions[0];
|
||||
characterPosYStepper.pos = oldPositions[1];
|
||||
}
|
||||
|
||||
// zidx
|
||||
characterZIdxStepper.max = StageEditorState.MAX_Z_INDEX;
|
||||
characterZIdxStepper.onChange = function(_) {
|
||||
state.charGroups[state.selectedChar.characterType].zIndex = Std.int(characterZIdxStepper.pos);
|
||||
state.saved = false;
|
||||
charZIdx.max = StageEditorState.MAX_Z_INDEX;
|
||||
charZIdx.onChange = function(_) {
|
||||
state.charGroups[state.selectedChar.characterType].zIndex = Std.int(charZIdx.pos);
|
||||
state.sortAssets();
|
||||
}
|
||||
|
||||
characterZIdxReset.onClick = function(_) {
|
||||
var thingies = [CharacterType.GF, CharacterType.DAD, CharacterType.BF];
|
||||
var thingIdxies = thingies.indexOf(state.selectedChar.characterType);
|
||||
|
||||
characterZIdxStepper.pos = (thingIdxies * 100);
|
||||
}
|
||||
|
||||
// camera
|
||||
characterCamXStepper.onChange = characterCamYStepper.onChange = function(_) {
|
||||
state.charCamOffsets[state.selectedChar.characterType] = [characterCamXStepper.pos, characterCamYStepper.pos];
|
||||
charCamX.onChange = charCamY.onChange = function(_) {
|
||||
state.charCamOffsets[state.selectedChar.characterType] = [charCamX.pos, charCamY.pos];
|
||||
state.updateMarkerPos();
|
||||
state.saved = false;
|
||||
}
|
||||
|
||||
characterCamReset.onClick = function(_) characterCamXStepper.pos = characterCamYStepper.pos = 0; // lol
|
||||
|
||||
// scale
|
||||
characterScaleSlider.onChange = function(_) {
|
||||
state.selectedChar.setScale(state.selectedChar.getBaseScale() * characterScaleSlider.pos);
|
||||
charScale.onChange = function(_) {
|
||||
state.selectedChar.setScale(state.selectedChar.getBaseScale() * charScale.pos);
|
||||
repositionCharacter();
|
||||
state.saved = false;
|
||||
}
|
||||
|
||||
characterScaleReset.onChange = function(_) characterScaleSlider.pos = 1;
|
||||
charAlpha.onChange = function(_) {
|
||||
state.selectedChar.alpha = charAlpha.pos;
|
||||
}
|
||||
|
||||
charAngle.onChange = function(_) {
|
||||
state.selectedChar.angle = charAngle.pos;
|
||||
}
|
||||
|
||||
charScrollX.onChange = charScrollY.onChange = function(_) {
|
||||
state.selectedChar.scrollFactor.set(charScrollX.pos, charScrollY.pos);
|
||||
}
|
||||
|
||||
// character button
|
||||
characterTypeButton.onClick = function(_) {
|
||||
charType.onClick = function(_) {
|
||||
charMenu = new StageEditorCharacterMenu(state, this);
|
||||
Screen.instance.addComponent(charMenu);
|
||||
}
|
||||
|
||||
refresh();
|
||||
|
||||
this.onDialogClosed = onClose;
|
||||
}
|
||||
|
||||
function onClose(event:UIEvent)
|
||||
{
|
||||
stageEditorState.menubarItemWindowCharacter.selected = false;
|
||||
}
|
||||
|
||||
override public function refresh()
|
||||
{
|
||||
var name = stageEditorState.selectedChar.characterType;
|
||||
var curChar = stageEditorState.selectedChar;
|
||||
|
||||
characterPosXStepper.step = characterPosYStepper.step = stageEditorState.moveStep;
|
||||
characterCamXStepper.step = characterCamYStepper.step = stageEditorState.moveStep;
|
||||
charPosX.step = charPosY.step = stageEditorState.moveStep;
|
||||
charCamX.step = charCamY.step = stageEditorState.moveStep;
|
||||
charAngle.step = funkin.save.Save.instance.stageEditorAngleStep;
|
||||
|
||||
if (characterPosXStepper.pos != stageEditorState.charPos[name][0]) characterPosXStepper.pos = stageEditorState.charPos[name][0];
|
||||
if (characterPosYStepper.pos != stageEditorState.charPos[name][1]) characterPosYStepper.pos = stageEditorState.charPos[name][1];
|
||||
// Always update the displays, since selectedChar is never null.
|
||||
|
||||
if (characterZIdxStepper.pos != stageEditorState.charGroups[stageEditorState.selectedChar.characterType].zIndex)
|
||||
characterZIdxStepper.pos = stageEditorState.charGroups[stageEditorState.selectedChar.characterType].zIndex;
|
||||
if (charPosX.pos != stageEditorState.charPos[name][0]) charPosX.pos = stageEditorState.charPos[name][0];
|
||||
if (charPosY.pos != stageEditorState.charPos[name][1]) charPosY.pos = stageEditorState.charPos[name][1];
|
||||
if (charZIdx.pos != stageEditorState.charGroups[name].zIndex) charZIdx.pos = stageEditorState.charGroups[name].zIndex;
|
||||
if (charCamX.pos != stageEditorState.charCamOffsets[name][0]) charCamX.pos = stageEditorState.charCamOffsets[name][0];
|
||||
if (charCamY.pos != stageEditorState.charCamOffsets[name][1]) charCamY.pos = stageEditorState.charCamOffsets[name][1];
|
||||
if (charScale.pos != curChar.scale.x / curChar.getBaseScale()) charScale.pos = curChar.scale.x / curChar.getBaseScale();
|
||||
if (charAlpha.pos != curChar.alpha) charAlpha.pos = curChar.alpha;
|
||||
if (charAngle.pos != curChar.angle) charAngle.pos = curChar.angle;
|
||||
if (charScrollX.pos != curChar.scrollFactor.x) charScrollX.pos = curChar.scrollFactor.x;
|
||||
if (charScrollY.pos != curChar.scrollFactor.y) charScrollY.pos = curChar.scrollFactor.y;
|
||||
|
||||
if (characterCamXStepper.pos != stageEditorState.charCamOffsets[name][0]) characterCamXStepper.pos = stageEditorState.charCamOffsets[name][0];
|
||||
if (characterCamYStepper.pos != stageEditorState.charCamOffsets[name][1]) characterCamYStepper.pos = stageEditorState.charCamOffsets[name][1];
|
||||
var prevText = charType.text;
|
||||
var charData = CharacterDataParser.fetchCharacterData(curChar.characterId);
|
||||
charType.icon = (charData == null ? null : CharacterDataParser.getCharPixelIconAsset(curChar.characterId));
|
||||
charType.text = (charData == null ? "None" : charData.name.length > 6 ? '${charData.name.substr(0, 6)}.' : '${charData.name}');
|
||||
|
||||
if (characterScaleSlider.pos != stageEditorState.selectedChar.scale.x / stageEditorState.selectedChar.getBaseScale())
|
||||
characterScaleSlider.pos = stageEditorState.selectedChar.scale.x / stageEditorState.selectedChar.getBaseScale();
|
||||
|
||||
var prevText = characterTypeButton.text;
|
||||
|
||||
var charData = CharacterDataParser.fetchCharacterData(stageEditorState.selectedChar.characterId);
|
||||
characterTypeButton.icon = (charData == null ? null : CharacterDataParser.getCharPixelIconAsset(stageEditorState.selectedChar.characterId));
|
||||
characterTypeButton.text = (charData == null ? "None" : charData.name.length > 6 ? '${charData.name.substr(0, 6)}.' : '${charData.name}');
|
||||
|
||||
if (prevText != characterTypeButton.text)
|
||||
{
|
||||
Screen.instance.removeComponent(charMenu);
|
||||
}
|
||||
if (prevText != charType.text) Screen.instance.removeComponent(charMenu);
|
||||
}
|
||||
|
||||
public function repositionCharacter()
|
||||
{
|
||||
stageEditorState.selectedChar.x = characterPosXStepper.pos - stageEditorState.selectedChar.characterOrigin.x
|
||||
+ stageEditorState.selectedChar.globalOffsets[0];
|
||||
stageEditorState.selectedChar.y = characterPosYStepper.pos - stageEditorState.selectedChar.characterOrigin.y
|
||||
+ stageEditorState.selectedChar.globalOffsets[1];
|
||||
|
||||
stageEditorState.selectedChar.setScale(stageEditorState.selectedChar.getBaseScale() * characterScaleSlider.pos);
|
||||
stageEditorState.selectedChar.x = charPosX.pos - stageEditorState.selectedChar.characterOrigin.x + stageEditorState.selectedChar.globalOffsets[0];
|
||||
stageEditorState.selectedChar.y = charPosY.pos - stageEditorState.selectedChar.characterOrigin.y + stageEditorState.selectedChar.globalOffsets[1];
|
||||
|
||||
stageEditorState.selectedChar.setScale(stageEditorState.selectedChar.getBaseScale() * charScale.pos);
|
||||
stageEditorState.updateMarkerPos();
|
||||
}
|
||||
}
|
||||
|
@ -170,7 +159,7 @@ class StageEditorCharacterMenu extends Menu // copied from chart editor
|
|||
{
|
||||
var charData:CharacterData = CharacterDataParser.fetchCharacterData(charId);
|
||||
|
||||
var charButton = new haxe.ui.components.Button();
|
||||
var charButton = new Button();
|
||||
charButton.width = 70;
|
||||
charButton.height = 70;
|
||||
charButton.padding = 8;
|
||||
|
@ -207,15 +196,39 @@ class StageEditorCharacterMenu extends Menu // copied from chart editor
|
|||
// anyways new character!!!!
|
||||
|
||||
var newChar = CharacterDataParser.fetchCharacter(charId, true);
|
||||
if (newChar == null)
|
||||
{
|
||||
state.notifyChange("Switch Character", "Couldn't find character " + charId + ". Switching to default.", true);
|
||||
newChar = CharacterDataParser.fetchCharacter(Constants.DEFAULT_CHARACTER, true);
|
||||
}
|
||||
|
||||
newChar.characterType = type;
|
||||
|
||||
newChar.resetCharacter(true);
|
||||
newChar.flipX = type == CharacterType.BF ? !newChar.getDataFlipX() : newChar.getDataFlipX();
|
||||
newChar.alpha = parent.charAlpha.pos;
|
||||
newChar.angle = parent.charAngle.pos;
|
||||
newChar.scrollFactor.x = parent.charScrollX.pos;
|
||||
newChar.scrollFactor.y = parent.charScrollY.pos;
|
||||
|
||||
state.selectedChar = newChar;
|
||||
group.add(newChar);
|
||||
|
||||
parent.repositionCharacter();
|
||||
group.zIndex = Std.int(parent.charZIdx.pos ?? 0);
|
||||
|
||||
// Save the selection.
|
||||
switch (type)
|
||||
{
|
||||
case BF:
|
||||
Save.instance.stageBoyfriendChar = charId;
|
||||
case GF:
|
||||
Save.instance.stageGirlfriendChar = charId;
|
||||
case DAD:
|
||||
Save.instance.stageDadChar = charId;
|
||||
default:
|
||||
// Do nothing.
|
||||
}
|
||||
};
|
||||
|
||||
charButton.onMouseOver = _ -> {
|
||||
|
|
|
@ -16,7 +16,7 @@ class StageEditorDefaultToolbox extends CollapsibleDialog
|
|||
|
||||
this.stageEditorState = stageEditorState;
|
||||
|
||||
closable = false;
|
||||
closable = true;
|
||||
modal = true;
|
||||
destroyOnClose = false;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,218 @@
|
|||
package funkin.ui.debug.stageeditor.toolboxes;
|
||||
|
||||
import haxe.ui.components.Button;
|
||||
import haxe.ui.components.CheckBox;
|
||||
import haxe.ui.components.DropDown;
|
||||
import haxe.ui.components.NumberStepper;
|
||||
import haxe.ui.components.TextField;
|
||||
import haxe.ui.containers.ListView;
|
||||
import haxe.ui.data.ArrayDataSource;
|
||||
import flixel.graphics.frames.FlxFrame;
|
||||
import haxe.ui.events.UIEvent;
|
||||
|
||||
using StringTools;
|
||||
|
||||
@:access(funkin.ui.debug.stageeditor.StageEditorState)
|
||||
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/object-anims.xml"))
|
||||
class StageEditorObjectAnimsToolbox extends StageEditorDefaultToolbox
|
||||
{
|
||||
var linkedObj:StageEditorObject = null;
|
||||
|
||||
var objAnims:DropDown;
|
||||
var objAnimName:TextField;
|
||||
var objFrameList:ListView;
|
||||
|
||||
var objAnimPrefix:TextField;
|
||||
var objAnimIndices:TextField;
|
||||
var objAnimLooped:CheckBox;
|
||||
var objAnimFlipX:CheckBox;
|
||||
var objAnimFlipY:CheckBox;
|
||||
var objAnimStart:CheckBox;
|
||||
var objAnimFramerate:NumberStepper;
|
||||
var objAnimOffsetX:NumberStepper;
|
||||
var objAnimOffsetY:NumberStepper;
|
||||
|
||||
var objAnimSave:Button;
|
||||
var objAnimDelete:Button;
|
||||
|
||||
override public function new(state:StageEditorState)
|
||||
{
|
||||
super(state);
|
||||
|
||||
objFrameList.onChange = function(_) {
|
||||
if (objFrameList.selectedIndex == -1) return;
|
||||
objAnimPrefix.text = objFrameList.selectedItem.name;
|
||||
}
|
||||
|
||||
objAnims.onChange = function(_) {
|
||||
var animData = linkedObj?.animDatas[objAnims.selectedItem?.text ?? ""];
|
||||
|
||||
if (linkedObj == null || objAnims.selectedIndex == -1 || animData == null)
|
||||
{
|
||||
// Reset everything.
|
||||
objAnimName.text = objAnimPrefix.text = objAnimIndices.text = "";
|
||||
objAnimLooped.selected = objAnimFlipX.selected = objAnimFlipY.selected = objAnimStart.selected = false;
|
||||
objAnimFramerate.pos = 24;
|
||||
objAnimOffsetX.pos = objAnimOffsetY.pos = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the displays.
|
||||
objAnimName.text = objAnims.selectedItem.text;
|
||||
objAnimPrefix.text = animData.prefix ?? "";
|
||||
objAnimIndices.text = (animData.frameIndices?.join(", ") ?? "");
|
||||
|
||||
objAnimLooped.selected = animData.looped ?? false;
|
||||
objAnimFlipX.selected = animData.flipX ?? false;
|
||||
objAnimFlipY.selected = animData.flipY ?? false;
|
||||
objAnimFramerate.pos = animData.frameRate ?? 24;
|
||||
objAnimStart.selected = objAnimName.text == linkedObj.startingAnimation;
|
||||
|
||||
objAnimOffsetX.pos = (animData.offsets[0] ?? 0);
|
||||
objAnimOffsetY.pos = (animData.offsets[1] ?? 0);
|
||||
}
|
||||
|
||||
objAnimSave.onClick = function(_) {
|
||||
if (linkedObj == null) return;
|
||||
|
||||
if ((objAnimName.text ?? "") == "")
|
||||
{
|
||||
state.notifyChange("Animation Saving Error", "The Animation Name is missing.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((objAnimPrefix.text ?? "") == "")
|
||||
{
|
||||
state.notifyChange("Animation Saving Error", "The Animation Prefix is missing.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
addAnimation();
|
||||
}
|
||||
|
||||
objAnimDelete.onClick = function(_) {
|
||||
if (linkedObj == null || linkedObj.animation.getNameList().length <= 0 || objAnims.selectedIndex < 0) return;
|
||||
|
||||
linkedObj.animation.pause();
|
||||
linkedObj.animation.stop();
|
||||
linkedObj.animation.curAnim = null;
|
||||
|
||||
var daAnim:String = linkedObj.animation.getNameList()[objAnims.selectedIndex];
|
||||
if (linkedObj.startingAnimation == daAnim) linkedObj.startingAnimation = "";
|
||||
|
||||
linkedObj.animation.remove(daAnim);
|
||||
linkedObj.animDatas.remove(daAnim);
|
||||
linkedObj.offset.set();
|
||||
|
||||
state.notifyChange("Animation Deletion Done", "Animation "
|
||||
+ objAnims.selectedItem.text
|
||||
+ " has been removed from the Object "
|
||||
+ linkedObj.name
|
||||
+ ".");
|
||||
|
||||
updateAnimList();
|
||||
|
||||
objAnims.selectedIndex = objAnims.dataSource.size - 1;
|
||||
}
|
||||
|
||||
this.onDialogClosed = onClose;
|
||||
}
|
||||
|
||||
function onClose(event:UIEvent)
|
||||
{
|
||||
stageEditorState.menubarItemWindowObjectAnims.selected = false;
|
||||
}
|
||||
|
||||
var previousFrames:Array<String> = [];
|
||||
var previousAnims:Array<String> = [];
|
||||
|
||||
override public function refresh()
|
||||
{
|
||||
linkedObj = stageEditorState.selectedSprite;
|
||||
|
||||
// If the selected object is null, reset the displays.
|
||||
if (linkedObj == null)
|
||||
{
|
||||
updateFrameList();
|
||||
updateAnimList();
|
||||
objAnims.selectedIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, update them accordingly.
|
||||
|
||||
if (previousFrames != [for (f in linkedObj.frames.frames) f.name]) updateFrameList();
|
||||
if (previousAnims != linkedObj.animation.getNameList().copy()) updateAnimList();
|
||||
}
|
||||
|
||||
function updateFrameList()
|
||||
{
|
||||
previousFrames = [];
|
||||
objFrameList.dataSource = new ArrayDataSource();
|
||||
|
||||
if (linkedObj == null) return;
|
||||
|
||||
for (fname in linkedObj.frames.frames)
|
||||
{
|
||||
if (fname != null) objFrameList.dataSource.add({name: fname.name});
|
||||
|
||||
previousFrames.push(fname.name);
|
||||
}
|
||||
}
|
||||
|
||||
function updateAnimList()
|
||||
{
|
||||
objAnims.dataSource.clear();
|
||||
previousAnims = [];
|
||||
|
||||
if (linkedObj == null) return;
|
||||
|
||||
for (aname in linkedObj.animation.getNameList())
|
||||
{
|
||||
objAnims.dataSource.add({text: aname});
|
||||
previousAnims.push(aname);
|
||||
}
|
||||
|
||||
if (previousAnims.contains(linkedObj.startingAnimation)) objAnims.selectedIndex = previousAnims.indexOf(linkedObj.startingAnimation);
|
||||
}
|
||||
|
||||
function addAnimation()
|
||||
{
|
||||
if (linkedObj.animation.getNameList().contains(objAnimName.text))
|
||||
{
|
||||
linkedObj.animation.remove(objAnimName.text);
|
||||
}
|
||||
|
||||
var indices:Array<Null<Int>> = [];
|
||||
|
||||
if ((objAnimIndices.text ?? "") != "")
|
||||
{
|
||||
var splitter = objAnimIndices.text.replace(" ", "").split(",");
|
||||
|
||||
for (num in splitter)
|
||||
indices.push(Std.parseInt(num));
|
||||
}
|
||||
|
||||
var shouldDoIndices:Bool = (indices.length > 0 && !indices.contains(null));
|
||||
|
||||
linkedObj.addAnim(objAnimName.text, objAnimPrefix.text, [objAnimOffsetX.pos, objAnimOffsetY.pos], (shouldDoIndices ? indices : []),
|
||||
Std.int(objAnimFramerate.pos), objAnimLooped.selected, objAnimFlipX.selected, objAnimFlipY.selected);
|
||||
|
||||
if (linkedObj.animation.getByName(objAnimName.text) == null)
|
||||
{
|
||||
stageEditorState.notifyChange("Animation Saving Error", "Could not build Animation by the provided Frames.", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (objAnimStart.selected) linkedObj.startingAnimation = objAnimName.text;
|
||||
linkedObj.playAnim(objAnimName.text);
|
||||
|
||||
stageEditorState.notifyChange("Animation Saving Done", "Animation " + objAnimName.text + " has been saved to the Object " + linkedObj.name + ".");
|
||||
updateAnimList();
|
||||
|
||||
// Stop the animation after a certain time.
|
||||
flixel.util.FlxTimer.wait(StageEditorState.TIME_BEFORE_ANIM_STOP, function() {
|
||||
if (linkedObj?.animation?.curAnim != null) linkedObj.animation.stop();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
package funkin.ui.debug.stageeditor.toolboxes;
|
||||
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import funkin.ui.debug.stageeditor.handlers.AssetDataHandler;
|
||||
import haxe.ui.components.Button;
|
||||
import haxe.ui.components.Image;
|
||||
import haxe.ui.components.NumberStepper;
|
||||
import haxe.ui.components.TextArea;
|
||||
import haxe.ui.containers.dialogs.Dialogs.FileDialogTypes;
|
||||
import haxe.ui.containers.dialogs.Dialogs;
|
||||
import haxe.ui.ToolkitAssets;
|
||||
import openfl.display.BitmapData;
|
||||
import haxe.ui.events.UIEvent;
|
||||
|
||||
@:access(funkin.ui.debug.stageeditor.StageEditorState)
|
||||
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/object-graphic.xml"))
|
||||
class StageEditorObjectGraphicToolbox extends StageEditorDefaultToolbox
|
||||
{
|
||||
var linkedObj:StageEditorObject = null;
|
||||
|
||||
var objImage:Image;
|
||||
var objLoad:Button;
|
||||
var objLoadNet:Button;
|
||||
var objReset:Button;
|
||||
var objResetFrames:Button;
|
||||
|
||||
var objFrameTxt:TextArea;
|
||||
var objLoadFrames:Button;
|
||||
var objSetSparrow:Button;
|
||||
var objSetPacker:Button;
|
||||
|
||||
var objImageWidth:NumberStepper;
|
||||
var objImageHeight:NumberStepper;
|
||||
var objSplit:Button;
|
||||
|
||||
override public function new(state:StageEditorState)
|
||||
{
|
||||
super(state);
|
||||
|
||||
// Callback for loading the image from the local hard drive.
|
||||
objLoad.onClick = function(_) {
|
||||
if (linkedObj == null) return;
|
||||
|
||||
Dialogs.openBinaryFile("Open Image File", FileDialogTypes.IMAGES, function(selectedFile) {
|
||||
if (selectedFile == null) return;
|
||||
objImage.resource = null;
|
||||
|
||||
ToolkitAssets.instance.imageFromBytes(selectedFile.bytes, function(imageInfo) {
|
||||
if (imageInfo == null) return;
|
||||
|
||||
objImage.resource = imageInfo.data;
|
||||
linkedObj.frame = imageInfo.data;
|
||||
|
||||
// This checks if the same image had already been loaded, so that we don't add it twice.
|
||||
// Kind of hacky but it is what it is.
|
||||
var bitToLoad:String = state.addBitmap(linkedObj.updateFramePixels());
|
||||
linkedObj.loadGraphic(state.bitmaps[bitToLoad]);
|
||||
linkedObj.updateHitbox();
|
||||
|
||||
state.removeUnusedBitmaps();
|
||||
|
||||
refresh();
|
||||
objImageWidth.pos = objImageWidth.max;
|
||||
objImageHeight.pos = objImageHeight.max;
|
||||
|
||||
state.notifyChange("Object Graphic Loaded", "The Image File " + selectedFile.name + " has been loaded.");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Callback for loading the image from the internet.
|
||||
objLoadNet.onClick = function(_) {
|
||||
if (linkedObj == null) return;
|
||||
|
||||
state.createURLDialog(function(bytes:lime.utils.Bytes) {
|
||||
var bitToLoad:String = state.addBitmap(BitmapData.fromBytes(bytes));
|
||||
linkedObj.loadGraphic(state.bitmaps[bitToLoad]);
|
||||
linkedObj.updateHitbox();
|
||||
|
||||
state.removeUnusedBitmaps();
|
||||
|
||||
// Update the image preview.
|
||||
refresh();
|
||||
|
||||
stageEditorState.updateDialog(OBJECT_ANIMS);
|
||||
});
|
||||
}
|
||||
|
||||
// Callback for resetting the image.
|
||||
objReset.onClick = function(_) {
|
||||
if (linkedObj == null) return;
|
||||
|
||||
linkedObj.loadGraphic(AssetDataHandler.getDefaultGraphic());
|
||||
linkedObj.updateHitbox();
|
||||
|
||||
// remove unused bitmaps
|
||||
state.removeUnusedBitmaps();
|
||||
|
||||
// Update the image preview.
|
||||
refresh();
|
||||
stageEditorState.updateDialog(OBJECT_ANIMS);
|
||||
}
|
||||
|
||||
// Callback for resetting frames.
|
||||
objResetFrames.onClick = function(_) {
|
||||
if (linkedObj == null) return;
|
||||
|
||||
linkedObj.loadGraphic(linkedObj.graphic);
|
||||
refresh();
|
||||
stageEditorState.updateDialog(OBJECT_ANIMS);
|
||||
}
|
||||
|
||||
// Callback for loading the text for the Frame Data.
|
||||
objLoadFrames.onClick = function(_) {
|
||||
Dialogs.openTextFile("Open Text File", FileDialogTypes.TEXTS, function(selectedFile) {
|
||||
if (selectedFile.text == null || (!selectedFile.name.endsWith(".xml") && !selectedFile.name.endsWith(".txt"))) return;
|
||||
|
||||
objFrameTxt.text = selectedFile.text;
|
||||
|
||||
state.notifyChange("Frame Text Loaded", "The Text File " + selectedFile.name + " has been loaded.");
|
||||
});
|
||||
}
|
||||
|
||||
// Callback for setting the frames as Sparrow.
|
||||
objSetSparrow.onClick = function(_) setObjFrames(false);
|
||||
|
||||
// Callback for setting the frames as Packer.
|
||||
objSetPacker.onClick = function(_) setObjFrames(true);
|
||||
|
||||
// Callback for splitting the graphic into frames.
|
||||
objSplit.onClick = function(_) {
|
||||
if (linkedObj == null) return;
|
||||
|
||||
linkedObj.loadGraphic(linkedObj.graphic, true, Std.int(objImageWidth.pos), Std.int(objImageHeight.pos));
|
||||
linkedObj.updateHitbox();
|
||||
|
||||
// Set the names of the frames.
|
||||
for (i in 0...linkedObj.frames.frames.length)
|
||||
{
|
||||
linkedObj.frames.framesHash.set('Frame$i', linkedObj.frames.frames[i]);
|
||||
linkedObj.frames.frames[i].name = 'Frame$i';
|
||||
}
|
||||
|
||||
// Refresh the display.
|
||||
refresh();
|
||||
|
||||
stageEditorState.updateDialog(OBJECT_ANIMS);
|
||||
}
|
||||
|
||||
this.onDialogClosed = onClose;
|
||||
}
|
||||
|
||||
function onClose(event:UIEvent)
|
||||
{
|
||||
stageEditorState.menubarItemWindowObjectGraphic.selected = false;
|
||||
}
|
||||
|
||||
override public function refresh()
|
||||
{
|
||||
linkedObj = stageEditorState.selectedSprite;
|
||||
|
||||
if (linkedObj == null)
|
||||
{
|
||||
// If there is no selected object, reset displays.
|
||||
objImage.resource = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, update only the changed fields.
|
||||
if (objImage.resource != linkedObj.frame) objImage.resource = linkedObj.frame;
|
||||
if (objImageWidth.max != linkedObj.graphic.width) objImageWidth.max = objImageWidth.pos = linkedObj.graphic.width;
|
||||
if (objImageHeight.max != linkedObj.graphic.height) objImageHeight.max = objImageHeight.pos = linkedObj.graphic.height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the linked object's frames based on its graphic and loaded text.
|
||||
* @param usePacker
|
||||
*/
|
||||
function setObjFrames(usePacker:Bool)
|
||||
{
|
||||
if (linkedObj == null || objFrameTxt.text == null || objFrameTxt.text.length == 0) return;
|
||||
|
||||
try
|
||||
{
|
||||
if (usePacker)
|
||||
{
|
||||
linkedObj.frames = FlxAtlasFrames.fromSpriteSheetPacker(linkedObj.graphic, objFrameTxt.text);
|
||||
}
|
||||
else
|
||||
{
|
||||
linkedObj.frames = FlxAtlasFrames.fromSparrow(linkedObj.graphic, objFrameTxt.text);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
stageEditorState.notifyChange("Frame Setup Error", e.toString(), true);
|
||||
return;
|
||||
}
|
||||
|
||||
linkedObj.animDatas.clear();
|
||||
linkedObj.animation.destroyAnimations();
|
||||
linkedObj.updateHitbox();
|
||||
refresh();
|
||||
|
||||
stageEditorState.notifyChange("Frame Setup Done", "Finished the Frame Setup for the Object " + linkedObj.name + ".");
|
||||
stageEditorState.updateDialog(OBJECT_ANIMS);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,295 @@
|
|||
package funkin.ui.debug.stageeditor.toolboxes;
|
||||
|
||||
import haxe.ui.containers.VBox;
|
||||
import haxe.ui.components.CheckBox;
|
||||
import haxe.ui.components.DropDown;
|
||||
import haxe.ui.components.NumberStepper;
|
||||
import haxe.ui.events.MouseEvent;
|
||||
import haxe.ui.events.UIEvent;
|
||||
import haxe.ui.util.Color;
|
||||
import flixel.util.FlxColor;
|
||||
import haxe.ui.events.UIEvent;
|
||||
|
||||
@:access(funkin.ui.debug.stageeditor.StageEditorState)
|
||||
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/object-properties.xml"))
|
||||
class StageEditorObjectPropertiesToolbox extends StageEditorDefaultToolbox
|
||||
{
|
||||
var linkedObj:StageEditorObject = null;
|
||||
|
||||
var objPosX:NumberStepper;
|
||||
var objPosY:NumberStepper;
|
||||
var objZIdx:NumberStepper;
|
||||
var objAlpha:NumberStepper;
|
||||
var objAngle:NumberStepper;
|
||||
var objScaleX:NumberStepper;
|
||||
var objScaleY:NumberStepper;
|
||||
var objScrollX:NumberStepper;
|
||||
var objScrollY:NumberStepper;
|
||||
var objDance:NumberStepper;
|
||||
|
||||
var objPixel:CheckBox;
|
||||
var objFlipX:CheckBox;
|
||||
var objFlipY:CheckBox;
|
||||
|
||||
var objBlend:DropDown;
|
||||
var objTint:DropDown;
|
||||
|
||||
override public function new(state:StageEditorState)
|
||||
{
|
||||
super(state);
|
||||
|
||||
// Initialize the custom DropDown view.
|
||||
DropDownBuilder.HANDLER_MAP.set("objTint", Type.getClassName(ObjectTintHandler));
|
||||
|
||||
// Numeric callbacks.
|
||||
objPosX.onChange = function(_) {
|
||||
if (linkedObj != null) linkedObj.x = objPosX.pos;
|
||||
}
|
||||
|
||||
objPosY.onChange = function(_) {
|
||||
if (linkedObj != null) linkedObj.y = objPosY.pos;
|
||||
}
|
||||
|
||||
objZIdx.max = StageEditorState.MAX_Z_INDEX;
|
||||
objZIdx.onChange = function(_) {
|
||||
if (linkedObj != null)
|
||||
{
|
||||
linkedObj.zIndex = Std.int(objZIdx.pos);
|
||||
state.updateArray();
|
||||
}
|
||||
}
|
||||
|
||||
objAlpha.onChange = function(_) {
|
||||
if (linkedObj != null) linkedObj.alpha = objAlpha.pos;
|
||||
}
|
||||
|
||||
objAngle.onChange = function(_) {
|
||||
if (linkedObj != null) linkedObj.angle = objAngle.pos;
|
||||
}
|
||||
|
||||
objScaleX.onChange = function(_) {
|
||||
if (linkedObj != null)
|
||||
{
|
||||
linkedObj.scale.x = objScaleX.pos;
|
||||
linkedObj.updateHitbox();
|
||||
}
|
||||
}
|
||||
|
||||
objScaleY.onChange = function(_) {
|
||||
if (linkedObj != null)
|
||||
{
|
||||
linkedObj.scale.y = objScaleY.pos;
|
||||
linkedObj.updateHitbox();
|
||||
}
|
||||
}
|
||||
|
||||
objScrollX.onChange = function(_) {
|
||||
if (linkedObj != null) linkedObj.scrollFactor.x = objScrollX.pos;
|
||||
}
|
||||
|
||||
objScrollY.onChange = function(_) {
|
||||
if (linkedObj != null) linkedObj.scrollFactor.y = objScrollY.pos;
|
||||
}
|
||||
|
||||
objDance.onChange = function(_) {
|
||||
if (linkedObj != null) linkedObj.danceEvery = Std.int(objDance.pos);
|
||||
}
|
||||
|
||||
// Boolean callbacks.
|
||||
objPixel.onChange = function(_) {
|
||||
if (linkedObj != null) linkedObj.antialiasing = objPixel.selected; // Kind of misleading, but objPixel has the 'Antialiasing' label attached to it!
|
||||
}
|
||||
|
||||
objFlipX.onChange = function(_) {
|
||||
if (linkedObj != null) linkedObj.flipX = objFlipX.selected;
|
||||
}
|
||||
|
||||
objFlipY.onChange = function(_) {
|
||||
if (linkedObj != null) linkedObj.flipY = objFlipY.selected;
|
||||
}
|
||||
|
||||
objBlend.onChange = function(_) {
|
||||
if (linkedObj != null)
|
||||
{
|
||||
linkedObj.blend = (objBlend.selectedItem?.text ?? "NONE") == "NONE" ? null : AssetDataHandler.blendFromString(objBlend.selectedItem.text);
|
||||
}
|
||||
}
|
||||
|
||||
objTint.onChange = function(_) {
|
||||
if (linkedObj != null)
|
||||
{
|
||||
linkedObj.color = FlxColor.fromString(_.value) ?? 0xFFFFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
this.onDialogClosed = onClose;
|
||||
}
|
||||
|
||||
function onClose(event:UIEvent)
|
||||
{
|
||||
stageEditorState.menubarItemWindowObjectProps.selected = false;
|
||||
}
|
||||
|
||||
override public function refresh()
|
||||
{
|
||||
linkedObj = stageEditorState.selectedSprite;
|
||||
|
||||
objPosX.step = stageEditorState.moveStep;
|
||||
objPosY.step = stageEditorState.moveStep;
|
||||
objAngle.step = funkin.save.Save.instance.stageEditorAngleStep;
|
||||
|
||||
if (linkedObj == null)
|
||||
{
|
||||
// If there is no selected object, reset displays.
|
||||
objPosX.pos = 0;
|
||||
objPosY.pos = 0;
|
||||
objZIdx.pos = 0;
|
||||
objAlpha.pos = 1;
|
||||
objAngle.pos = 0;
|
||||
objScaleX.pos = 1;
|
||||
objScaleY.pos = 1;
|
||||
objScrollX.pos = 1;
|
||||
objScrollY.pos = 1;
|
||||
objDance.pos = 0;
|
||||
|
||||
objPixel.selected = true;
|
||||
objFlipX.selected = false;
|
||||
objFlipY.selected = false;
|
||||
|
||||
objBlend.selectedIndex = 0;
|
||||
objTint.selectedItem = Color.fromString("white");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Otherwise, only update components whose linked object values have been changed.
|
||||
if (objPosX.pos != linkedObj.x) objPosX.pos = linkedObj.x;
|
||||
if (objPosY.pos != linkedObj.y) objPosY.pos = linkedObj.y;
|
||||
if (objZIdx.pos != linkedObj.zIndex) objZIdx.pos = linkedObj.zIndex;
|
||||
if (objAlpha.pos != linkedObj.alpha) objAlpha.pos = linkedObj.alpha;
|
||||
if (objAngle.pos != linkedObj.angle) objAngle.pos = linkedObj.angle;
|
||||
if (objScaleX.pos != linkedObj.scale.x) objScaleX.pos = linkedObj.scale.x;
|
||||
if (objScaleY.pos != linkedObj.scale.y) objScaleY.pos = linkedObj.scale.y;
|
||||
if (objScrollX.pos != linkedObj.scrollFactor.x) objScrollX.pos = linkedObj.scrollFactor.x;
|
||||
if (objScrollY.pos != linkedObj.scrollFactor.y) objScrollY.pos = linkedObj.scrollFactor.y;
|
||||
if (objDance.pos != linkedObj.danceEvery) objDance.pos = linkedObj.danceEvery;
|
||||
|
||||
if (objPixel.selected != linkedObj.antialiasing) objPixel.selected = linkedObj.antialiasing;
|
||||
if (objFlipX.selected != linkedObj.flipX) objFlipX.selected = linkedObj.flipX;
|
||||
if (objFlipY.selected != linkedObj.flipY) objFlipY.selected = linkedObj.flipY;
|
||||
|
||||
var blendMode:String = Std.string(linkedObj.blend) ?? "NONE";
|
||||
if (objBlend.selectedItem != blendMode.toUpperCase()) objBlend.selectedItem = blendMode.toUpperCase();
|
||||
|
||||
var objColor:Color = Color.fromComponents(linkedObj.color.red, linkedObj.color.green, linkedObj.color.blue, linkedObj.color.alpha);
|
||||
if (objTint.selectedItem != objColor) objTint.selectedItem = objColor;
|
||||
}
|
||||
}
|
||||
|
||||
// The following two classes are mostly carbon copies of ColorPickerPopup's handlers, just with an actually functioning onChange callback.
|
||||
// The code looks like a bit of a mess though.
|
||||
|
||||
@:access(haxe.ui.core.Component)
|
||||
private class ObjectTintHandler extends DropDownHandler
|
||||
{
|
||||
private var _view:ObjectTintView = null;
|
||||
|
||||
private override function get_component()
|
||||
{
|
||||
if (_view == null)
|
||||
{
|
||||
_view = new ObjectTintView();
|
||||
_view.dropdown = _dropdown;
|
||||
_view.currentColor = _cachedSelectedColor;
|
||||
_view.onChange = onColorChange;
|
||||
}
|
||||
|
||||
return _view;
|
||||
}
|
||||
|
||||
public override function prepare(_)
|
||||
{
|
||||
super.prepare(_);
|
||||
if (_view != null) _view.currentColor = _cachedSelectedColor;
|
||||
}
|
||||
|
||||
private var _cachedSelectedColor:Null<Color> = null;
|
||||
|
||||
private override function get_selectedItem():Dynamic
|
||||
{
|
||||
if (_view != null) _cachedSelectedColor = _view.currentColor;
|
||||
return _cachedSelectedColor;
|
||||
}
|
||||
|
||||
private override function set_selectedItem(value:Dynamic):Dynamic
|
||||
{
|
||||
if ((value is String)) _cachedSelectedColor = Color.fromString(value);
|
||||
else
|
||||
_cachedSelectedColor = value;
|
||||
|
||||
if (_view != null) _view.currentColor = _cachedSelectedColor;
|
||||
|
||||
_dropdown.text = _cachedSelectedColor.toHex();
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private function onColorChange(e:UIEvent)
|
||||
{
|
||||
if (_view != null)
|
||||
{
|
||||
_cachedSelectedColor = _view.currentColor;
|
||||
}
|
||||
|
||||
_dropdown.text = _cachedSelectedColor.toHex();
|
||||
|
||||
var event = new UIEvent(UIEvent.CHANGE);
|
||||
event.value = _cachedSelectedColor.toHex();
|
||||
_dropdown.dispatch(event);
|
||||
}
|
||||
}
|
||||
|
||||
@:xml('
|
||||
<vbox style="spacing:0;padding:5px;">
|
||||
<color-picker id="picker" />
|
||||
<box id="cancelApplyButtons" style="padding-top: 5px;" width="100%">
|
||||
<hbox horizontalAlign="right">
|
||||
<button id="cancelButton" text="Cancel" styleName="text-small" style="padding: 4px 8px;" />
|
||||
<button id="applyButton" text="Apply" styleName="text-small" style="padding: 4px 8px;" />
|
||||
</hbox>
|
||||
</box>
|
||||
</vbox>
|
||||
')
|
||||
private class ObjectTintView extends VBox
|
||||
{
|
||||
public var dropdown:DropDown = null;
|
||||
|
||||
public var currentColor(get, set):Null<Color>;
|
||||
|
||||
private function get_currentColor():Null<Color>
|
||||
{
|
||||
return picker.currentColor;
|
||||
}
|
||||
|
||||
private function set_currentColor(value:Null<Color>):Null<Color>
|
||||
{
|
||||
picker.currentColor = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
@:bind(cancelButton, MouseEvent.CLICK)
|
||||
private function onCancel(_)
|
||||
{
|
||||
dropdown.hideDropDown();
|
||||
}
|
||||
|
||||
@:bind(applyButton, MouseEvent.CLICK)
|
||||
private function onApply(_)
|
||||
{
|
||||
dropdown.text = currentColor.toHex();
|
||||
|
||||
var event = new UIEvent(UIEvent.CHANGE);
|
||||
event.value = currentColor.toHex();
|
||||
dropdown.dispatch(event);
|
||||
}
|
||||
}
|
|
@ -1,577 +0,0 @@
|
|||
package funkin.ui.debug.stageeditor.toolboxes;
|
||||
|
||||
import haxe.ui.components.HorizontalSlider;
|
||||
import haxe.ui.components.NumberStepper;
|
||||
import haxe.ui.components.TextField;
|
||||
import haxe.ui.components.TextArea;
|
||||
import haxe.ui.components.Button;
|
||||
import haxe.ui.components.Image;
|
||||
import haxe.ui.containers.dialogs.Dialogs.FileDialogTypes;
|
||||
import haxe.ui.ToolkitAssets;
|
||||
import haxe.ui.containers.dialogs.Dialogs;
|
||||
import funkin.ui.debug.stageeditor.handlers.AssetDataHandler;
|
||||
import flixel.graphics.frames.FlxAtlasFrames;
|
||||
import haxe.ui.components.DropDown;
|
||||
import haxe.ui.containers.ListView;
|
||||
import haxe.ui.components.CheckBox;
|
||||
import flixel.util.FlxTimer;
|
||||
import haxe.ui.data.ArrayDataSource;
|
||||
import haxe.ui.components.ColorPicker;
|
||||
import flixel.util.FlxColor;
|
||||
import haxe.ui.util.Color;
|
||||
import flixel.graphics.frames.FlxFrame;
|
||||
import funkin.util.FileUtil;
|
||||
import openfl.display.BitmapData;
|
||||
|
||||
using StringTools;
|
||||
|
||||
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/object-properties.xml"))
|
||||
class StageEditorObjectToolbox extends StageEditorDefaultToolbox
|
||||
{
|
||||
var linkedObject:StageEditorObject = null;
|
||||
|
||||
var objectImagePreview:Image;
|
||||
var objectLoadImageButton:Button;
|
||||
var objectLoadInternetButton:Button;
|
||||
var objectDownloadImageButton:Button;
|
||||
var objectResetImageButton:Button;
|
||||
var objectZIdxStepper:NumberStepper;
|
||||
var objectZIdxReset:Button;
|
||||
|
||||
var objectPosXStepper:NumberStepper;
|
||||
var objectPosYStepper:NumberStepper;
|
||||
var objectPosResetButton:Button;
|
||||
var objectAlphaSlider:HorizontalSlider;
|
||||
var objectAlphaResetButton:Button;
|
||||
var objectAngleSlider:HorizontalSlider;
|
||||
var objectAngleResetButton:Button;
|
||||
var objectScaleXStepper:NumberStepper;
|
||||
var objectScaleYStepper:NumberStepper;
|
||||
var objectScaleResetButton:Button;
|
||||
var objectSizeXStepper:NumberStepper;
|
||||
var objectSizeYStepper:NumberStepper;
|
||||
var objectSizeResetButton:Button;
|
||||
var objectScrollXSlider:HorizontalSlider;
|
||||
var objectScrollYSlider:HorizontalSlider;
|
||||
var objectScrollResetButton:Button;
|
||||
|
||||
var objectFrameText:TextArea;
|
||||
var objectFrameTextLoad:Button;
|
||||
var objectFrameTextSparrow:Button;
|
||||
var objectFrameTextPacker:Button;
|
||||
var objectFrameImageWidth:NumberStepper;
|
||||
var objectFrameImageHeight:NumberStepper;
|
||||
var objectFrameImageSetter:Button;
|
||||
var objectFrameReset:Button;
|
||||
|
||||
var objectAnimDropdown:DropDown;
|
||||
var objectAnimName:TextField;
|
||||
var objectAnimFrameList:ListView;
|
||||
var objectAnimPrefix:TextField;
|
||||
var objectAnimFrames:TextField;
|
||||
var objectAnimLooped:CheckBox;
|
||||
var objectAnimFlipX:CheckBox;
|
||||
var objectAnimFlipY:CheckBox;
|
||||
var objectAnimFramerate:NumberStepper;
|
||||
var objectAnimOffsetX:NumberStepper;
|
||||
var objectAnimOffsetY:NumberStepper;
|
||||
var objectAnimDanceBeat:NumberStepper;
|
||||
var objectAnimDanceBeatReset:Button;
|
||||
var objectAnimStart:TextField;
|
||||
var objectAnimStartReset:Button;
|
||||
|
||||
var objectMiscAntialias:CheckBox;
|
||||
var objectMiscAntialiasReset:Button;
|
||||
var objectMiscFlipReset:Button;
|
||||
var objectMiscBlendDrop:DropDown;
|
||||
var objectMiscBlendReset:Button;
|
||||
var objectMiscColor:ColorPicker;
|
||||
var objectMiscColorReset:Button;
|
||||
|
||||
override public function new(state:StageEditorState)
|
||||
{
|
||||
super(state);
|
||||
|
||||
// basic callbacks
|
||||
objectLoadImageButton.onClick = function(_) {
|
||||
if (linkedObject == null) return;
|
||||
|
||||
Dialogs.openBinaryFile("Open Image File", FileDialogTypes.IMAGES, function(selectedFile) {
|
||||
if (selectedFile == null) return;
|
||||
|
||||
objectImagePreview.resource = null;
|
||||
|
||||
ToolkitAssets.instance.imageFromBytes(selectedFile.bytes, function(imageInfo) {
|
||||
if (imageInfo == null) return;
|
||||
|
||||
objectImagePreview.resource = imageInfo.data;
|
||||
|
||||
linkedObject.frame = imageInfo.data;
|
||||
|
||||
var bit = linkedObject.updateFramePixels();
|
||||
var bitToLoad = state.addBitmap(bit);
|
||||
|
||||
linkedObject.loadGraphic(state.bitmaps[bitToLoad]);
|
||||
linkedObject.updateHitbox();
|
||||
|
||||
// update size stuff
|
||||
objectSizeXStepper.pos = linkedObject.width;
|
||||
objectSizeYStepper.pos = linkedObject.height;
|
||||
|
||||
// remove unused bitmaps
|
||||
state.removeUnusedBitmaps();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
objectLoadInternetButton.onClick = function(_) {
|
||||
if (linkedObject == null) return;
|
||||
|
||||
state.createURLDialog(function(bytes:lime.utils.Bytes) {
|
||||
linkedObject.loadGraphic(BitmapData.fromBytes(bytes));
|
||||
linkedObject.updateHitbox();
|
||||
refresh();
|
||||
});
|
||||
}
|
||||
|
||||
objectDownloadImageButton.onClick = function(_) {
|
||||
if (linkedObject == null) return;
|
||||
|
||||
FileUtil.saveFile(linkedObject.pixels.image.encode(PNG), [FileUtil.FILE_FILTER_PNG], null, null,
|
||||
linkedObject.name + "-graphic.png"); // i'on need any callbacks
|
||||
}
|
||||
|
||||
objectZIdxStepper.max = StageEditorState.MAX_Z_INDEX;
|
||||
objectZIdxStepper.onChange = function(_) {
|
||||
if (linkedObject != null)
|
||||
{
|
||||
linkedObject.zIndex = Std.int(objectZIdxStepper.pos);
|
||||
state.sortAssets();
|
||||
}
|
||||
}
|
||||
|
||||
// numeric callbacks
|
||||
objectPosXStepper.onChange = function(_) {
|
||||
if (linkedObject != null) linkedObject.x = objectPosXStepper.pos;
|
||||
};
|
||||
|
||||
objectPosYStepper.onChange = function(_) {
|
||||
if (linkedObject != null) linkedObject.y = objectPosYStepper.pos;
|
||||
};
|
||||
|
||||
objectAlphaSlider.onChange = function(_) {
|
||||
if (linkedObject != null) linkedObject.alpha = objectAlphaSlider.pos;
|
||||
};
|
||||
|
||||
objectAngleSlider.onChange = function(_) {
|
||||
if (linkedObject != null) linkedObject.angle = objectAngleSlider.pos;
|
||||
};
|
||||
|
||||
objectScaleXStepper.onChange = objectScaleYStepper.onChange = function(_) {
|
||||
if (linkedObject != null)
|
||||
{
|
||||
linkedObject.scale.set(objectScaleXStepper.pos, objectScaleYStepper.pos);
|
||||
linkedObject.updateHitbox();
|
||||
objectSizeXStepper.pos = linkedObject.width;
|
||||
objectSizeYStepper.pos = linkedObject.height;
|
||||
|
||||
linkedObject.playAnim(linkedObject.animation.name); // load offsets
|
||||
}
|
||||
};
|
||||
|
||||
objectSizeXStepper.onChange = objectSizeYStepper.onChange = function(_) {
|
||||
if (linkedObject != null)
|
||||
{
|
||||
linkedObject.setGraphicSize(Std.int(objectSizeXStepper.pos), Std.int(objectSizeYStepper.pos));
|
||||
linkedObject.updateHitbox();
|
||||
objectScaleXStepper.pos = linkedObject.scale.x;
|
||||
objectScaleYStepper.pos = linkedObject.scale.y;
|
||||
|
||||
linkedObject.playAnim(linkedObject.animation.name); // load offsets
|
||||
}
|
||||
};
|
||||
|
||||
objectScrollXSlider.onChange = objectScrollYSlider.onChange = function(_) {
|
||||
if (linkedObject != null) linkedObject.scrollFactor.set(objectScrollXSlider.pos, objectScrollYSlider.pos);
|
||||
};
|
||||
|
||||
// frame callbacks
|
||||
objectFrameTextLoad.onClick = function(_) {
|
||||
Dialogs.openTextFile("Open Text File", FileDialogTypes.TEXTS, function(selectedFile) {
|
||||
if (selectedFile.text == null || (!selectedFile.name.endsWith(".xml") && !selectedFile.name.endsWith(".txt"))) return;
|
||||
|
||||
objectFrameText.text = selectedFile.text;
|
||||
|
||||
state.notifyChange("Frame Text Loaded", "The Text File " + selectedFile.name + " has been loaded.");
|
||||
});
|
||||
}
|
||||
|
||||
objectFrameTextSparrow.onClick = function(_) {
|
||||
if (linkedObject == null || objectFrameText.text == null || objectFrameText.text == "") return;
|
||||
|
||||
try
|
||||
{
|
||||
linkedObject.frames = FlxAtlasFrames.fromSparrow(linkedObject.graphic, objectFrameText.text);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
state.notifyChange("Frame Setup Error", e.toString(), true);
|
||||
return;
|
||||
}
|
||||
|
||||
// might as well clear animations because frames SUCK
|
||||
linkedObject.animDatas.clear();
|
||||
linkedObject.animation.destroyAnimations();
|
||||
linkedObject.updateHitbox();
|
||||
refresh();
|
||||
|
||||
state.notifyChange("Frame Setup Done", "Finished the Sparrow Frame Setup for the Object " + linkedObject.name + ".");
|
||||
}
|
||||
|
||||
objectFrameTextPacker.onClick = function(_) {
|
||||
if (linkedObject == null || objectFrameText.text == null || objectFrameText.text == "") return;
|
||||
|
||||
try // crash prevention
|
||||
{
|
||||
linkedObject.frames = FlxAtlasFrames.fromSpriteSheetPacker(linkedObject.graphic, objectFrameText.text);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
state.notifyChange("Frame Setup Error", e.toString(), true);
|
||||
return;
|
||||
}
|
||||
|
||||
// might as well clear animations because frames SUCK
|
||||
linkedObject.animDatas.clear();
|
||||
linkedObject.animation.destroyAnimations();
|
||||
|
||||
linkedObject.updateHitbox();
|
||||
refresh();
|
||||
|
||||
state.notifyChange("Frame Setup Done", "Finished the Packer Frame Setup for the Object " + linkedObject.name + ".");
|
||||
}
|
||||
|
||||
objectFrameImageSetter.onClick = function(_) {
|
||||
if (linkedObject == null) return;
|
||||
|
||||
linkedObject.loadGraphic(linkedObject.graphic, true, Std.int(objectFrameImageWidth.pos), Std.int(objectFrameImageHeight.pos));
|
||||
linkedObject.updateHitbox();
|
||||
|
||||
// set da names
|
||||
for (i in 0...linkedObject.frames.frames.length)
|
||||
{
|
||||
linkedObject.frames.framesHash.set("Frame" + i, linkedObject.frames.frames[i]);
|
||||
linkedObject.frames.frames[i].name = "Frame" + i;
|
||||
}
|
||||
|
||||
// might as well clear animations because frames SUCK
|
||||
linkedObject.animDatas.clear();
|
||||
linkedObject.animation.destroyAnimations();
|
||||
refresh();
|
||||
|
||||
state.notifyChange("Frame Setup Done", "Finished the Image Frame Setup for the Object " + linkedObject.name + ".");
|
||||
}
|
||||
|
||||
// animation
|
||||
objectAnimDropdown.onChange = function(_) {
|
||||
if (linkedObject == null) return;
|
||||
|
||||
if (objectAnimDropdown.selectedIndex == -1) // RESET EVERYTHING INSTANTENEOUSLY
|
||||
{
|
||||
objectAnimName.text = "";
|
||||
objectAnimLooped.selected = objectAnimFlipX.selected = objectAnimFlipY.selected = false;
|
||||
objectAnimFramerate.pos = 24;
|
||||
objectAnimOffsetX.pos = objectAnimOffsetY.pos = 0;
|
||||
objectAnimFrames.text = "";
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var animData = linkedObject.animDatas[objectAnimDropdown.selectedItem.text];
|
||||
if (animData == null) return;
|
||||
|
||||
objectAnimName.text = objectAnimDropdown.selectedItem.text;
|
||||
objectAnimPrefix.text = animData.prefix ?? "";
|
||||
objectAnimFrames.text = (animData.frameIndices != null && animData.frameIndices.length > 0 ? animData.frameIndices.join(", ") : "");
|
||||
|
||||
objectAnimLooped.selected = animData.looped ?? false;
|
||||
objectAnimFlipX.selected = animData.flipX ?? false;
|
||||
objectAnimFlipY.selected = animData.flipY ?? false;
|
||||
objectAnimFramerate.pos = animData.frameRate ?? 24;
|
||||
|
||||
objectAnimOffsetX.pos = (animData.offsets != null && animData.offsets.length == 2 ? animData.offsets[0] : 0);
|
||||
objectAnimOffsetY.pos = (animData.offsets != null && animData.offsets.length == 2 ? animData.offsets[1] : 0);
|
||||
}
|
||||
|
||||
objectAnimSave.onClick = function(_) {
|
||||
if (linkedObject == null) return;
|
||||
|
||||
if (objectAnimName.text == null || objectAnimName.text == "")
|
||||
{
|
||||
state.notifyChange("Animation Saving Error", "Invalid Animation Name!", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (objectAnimPrefix.text == null || objectAnimPrefix.text == "")
|
||||
{
|
||||
state.notifyChange("Animation Saving Error", "Missing Animation Prefix!", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (linkedObject.animation.getNameList().contains(objectAnimName.text)) linkedObject.animation.remove(objectAnimName.text);
|
||||
|
||||
var indices = [];
|
||||
|
||||
if (objectAnimFrames.text != null && objectAnimFrames.text != "")
|
||||
{
|
||||
var splitter = objectAnimFrames.text.replace(" ", "").split(",");
|
||||
|
||||
for (num in splitter)
|
||||
{
|
||||
indices.push(Std.parseInt(num));
|
||||
}
|
||||
}
|
||||
|
||||
var shouldDoIndices:Bool = (indices.length > 0 && !indices.contains(null));
|
||||
|
||||
linkedObject.addAnim(objectAnimName.text, objectAnimPrefix.text, [objectAnimOffsetX.pos, objectAnimOffsetY.pos], (shouldDoIndices ? indices : []),
|
||||
Std.int(objectAnimFramerate.pos), objectAnimLooped.selected, objectAnimFlipX.selected, objectAnimFlipY.selected);
|
||||
|
||||
if (linkedObject.animation.getByName(objectAnimName.text) == null)
|
||||
{
|
||||
state.notifyChange("Animation Saving Error", "Invalid Frames!", true);
|
||||
return;
|
||||
}
|
||||
|
||||
linkedObject.playAnim(objectAnimName.text);
|
||||
|
||||
state.notifyChange("Animation Saving Done", "Animation " + objectAnimName.text + " has been saved to the Object " + linkedObject.name + ".");
|
||||
updateAnimList();
|
||||
|
||||
// stops the animation preview if animation is looped for too long
|
||||
FlxTimer.wait(StageEditorState.TIME_BEFORE_ANIM_STOP, function() {
|
||||
if (linkedObject != null && linkedObject.animation.curAnim != null)
|
||||
linkedObject.animation.stop(); // null check cuz if we stop an anim for a null object the game crashes :[
|
||||
});
|
||||
}
|
||||
|
||||
objectAnimDelete.onClick = function(_) {
|
||||
if (linkedObject == null || linkedObject.animation.getNameList().length <= 0 || objectAnimDropdown.selectedIndex < 0) return;
|
||||
|
||||
linkedObject.animation.pause();
|
||||
linkedObject.animation.stop();
|
||||
linkedObject.animation.curAnim = null;
|
||||
|
||||
var daAnim = linkedObject.animation.getNameList()[objectAnimDropdown.selectedIndex];
|
||||
|
||||
linkedObject.animation.remove(daAnim);
|
||||
linkedObject.animDatas.remove(daAnim);
|
||||
linkedObject.offset.set();
|
||||
|
||||
state.notifyChange("Animation Deletion Done",
|
||||
"Animation "
|
||||
+ objectAnimDropdown.selectedItem.text
|
||||
+ " has been removed from the Object "
|
||||
+ linkedObject.name
|
||||
+ ".");
|
||||
|
||||
updateAnimList();
|
||||
|
||||
objectAnimDropdown.selectedIndex = objectAnimDropdown.dataSource.size - 1;
|
||||
}
|
||||
|
||||
objectAnimDanceBeat.onChange = function(_) {
|
||||
if (linkedObject != null) linkedObject.danceEvery = Std.int(objectAnimDanceBeat.pos);
|
||||
}
|
||||
|
||||
objectAnimStart.onChange = function(_) {
|
||||
if (linkedObject != null)
|
||||
{
|
||||
if (linkedObject.animation.getNameList().contains(objectAnimStart.text)) objectAnimStart.styleString = "color: white";
|
||||
else
|
||||
objectAnimStart.styleString = "color: indianred";
|
||||
|
||||
linkedObject.startingAnimation = objectAnimStart.text;
|
||||
}
|
||||
}
|
||||
|
||||
// misc
|
||||
objectMiscAntialias.onClick = function(_) {
|
||||
if (linkedObject != null) linkedObject.antialiasing = objectMiscAntialias.selected;
|
||||
}
|
||||
|
||||
objectMiscBlendDrop.onChange = function(_) {
|
||||
if (linkedObject != null)
|
||||
linkedObject.blend = objectMiscBlendDrop.selectedItem.text == "NONE" ? null : AssetDataHandler.blendFromString(objectMiscBlendDrop.selectedItem.text);
|
||||
}
|
||||
|
||||
objectMiscColor.onChange = function(_) {
|
||||
if (linkedObject != null) linkedObject.color = FlxColor.fromRGB(objectMiscColor.currentColor.r, objectMiscColor.currentColor.g,
|
||||
objectMiscColor.currentColor.b);
|
||||
}
|
||||
|
||||
// reset button callbacks
|
||||
objectResetImageButton.onClick = function(_) {
|
||||
if (linkedObject != null)
|
||||
{
|
||||
linkedObject.loadGraphic(AssetDataHandler.getDefaultGraphic());
|
||||
linkedObject.updateHitbox();
|
||||
refresh();
|
||||
|
||||
// remove unused bitmaps
|
||||
state.removeUnusedBitmaps();
|
||||
}
|
||||
}
|
||||
|
||||
objectZIdxReset.onClick = function(_) {
|
||||
if (linkedObject != null) objectZIdxStepper.pos = 0; // corner cutting because onChange will activate with this
|
||||
}
|
||||
|
||||
objectPosResetButton.onClick = function(_) {
|
||||
if (linkedObject != null)
|
||||
{
|
||||
linkedObject.screenCenter();
|
||||
objectPosXStepper.pos = linkedObject.x;
|
||||
objectPosYStepper.pos = linkedObject.y;
|
||||
}
|
||||
}
|
||||
|
||||
objectAlphaResetButton.onClick = function(_) {
|
||||
if (linkedObject != null) linkedObject.alpha = objectAlphaSlider.pos = 1;
|
||||
}
|
||||
|
||||
objectAngleResetButton.onClick = function(_) {
|
||||
if (linkedObject != null) linkedObject.angle = objectAngleSlider.pos = 0;
|
||||
}
|
||||
|
||||
objectScaleResetButton.onClick = objectSizeResetButton.onClick = function(_) // the corner cutting goes crazy
|
||||
{
|
||||
if (linkedObject != null)
|
||||
{
|
||||
linkedObject.scale.set(1, 1);
|
||||
refresh(); // refreshes like multiple shit
|
||||
}
|
||||
}
|
||||
|
||||
objectScrollResetButton.onClick = function(_) {
|
||||
if (linkedObject != null) linkedObject.scrollFactor.x = linkedObject.scrollFactor.y = objectScrollXSlider.pos = objectScrollYSlider.pos = 1;
|
||||
}
|
||||
|
||||
objectFrameReset.onClick = function(_) {
|
||||
if (linkedObject == null) return;
|
||||
|
||||
linkedObject.loadGraphic(linkedObject.pixels);
|
||||
linkedObject.animDatas.clear();
|
||||
linkedObject.animation.destroyAnimations();
|
||||
refresh();
|
||||
}
|
||||
|
||||
objectMiscAntialiasReset.onClick = function(_) {
|
||||
if (linkedObject != null) objectMiscAntialias.selected = true;
|
||||
}
|
||||
|
||||
objectMiscBlendReset.onClick = function(_) {
|
||||
if (linkedObject != null) objectMiscBlendDrop.selectedItem = "NORMAL";
|
||||
}
|
||||
|
||||
objectMiscColorReset.onClick = function(_) {
|
||||
if (linkedObject != null) objectMiscColor.currentColor = Color.fromString("white");
|
||||
}
|
||||
|
||||
objectAnimDanceBeatReset.onClick = function(_) {
|
||||
if (linkedObject != null) objectAnimDanceBeat.pos = 0;
|
||||
}
|
||||
|
||||
objectAnimStartReset.onClick = function(_) {
|
||||
if (linkedObject != null) objectAnimStart.text = "";
|
||||
}
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
var prevFrames:Array<FlxFrame> = [];
|
||||
var prevAnims:Array<String> = [];
|
||||
|
||||
override public function refresh()
|
||||
{
|
||||
linkedObject = stageEditorState.selectedSprite;
|
||||
|
||||
objectPosXStepper.step = stageEditorState.moveStep;
|
||||
objectPosYStepper.step = stageEditorState.moveStep;
|
||||
objectAngleSlider.step = funkin.save.Save.instance.stageEditorAngleStep;
|
||||
|
||||
if (linkedObject == null)
|
||||
{
|
||||
updateFrameList();
|
||||
updateAnimList();
|
||||
return;
|
||||
}
|
||||
|
||||
// saving fps
|
||||
if (objectImagePreview.resource != linkedObject.frame) objectImagePreview.resource = linkedObject.frame;
|
||||
|
||||
if (objectZIdxStepper.pos != linkedObject.zIndex) objectZIdxStepper.pos = linkedObject.zIndex;
|
||||
if (objectPosXStepper.pos != linkedObject.x) objectPosXStepper.pos = linkedObject.x;
|
||||
if (objectPosYStepper.pos != linkedObject.y) objectPosYStepper.pos = linkedObject.y;
|
||||
if (objectAlphaSlider.pos != linkedObject.alpha) objectAlphaSlider.pos = linkedObject.alpha;
|
||||
if (objectAngleSlider.pos != linkedObject.angle) objectAngleSlider.pos = linkedObject.angle;
|
||||
if (objectScaleXStepper.pos != linkedObject.scale.x) objectScaleXStepper.pos = linkedObject.scale.x;
|
||||
if (objectScaleYStepper.pos != linkedObject.scale.y) objectScaleYStepper.pos = linkedObject.scale.y;
|
||||
if (objectSizeXStepper.pos != linkedObject.width) objectSizeXStepper.pos = linkedObject.width;
|
||||
if (objectSizeYStepper.pos != linkedObject.height) objectSizeYStepper.pos = linkedObject.height;
|
||||
if (objectScrollXSlider.pos != linkedObject.scrollFactor.x) objectScrollXSlider.pos = linkedObject.scrollFactor.x;
|
||||
if (objectScrollYSlider.pos != linkedObject.scrollFactor.y) objectScrollYSlider.pos = linkedObject.scrollFactor.y;
|
||||
if (objectMiscAntialias.selected != linkedObject.antialiasing) objectMiscAntialias.selected = linkedObject.antialiasing;
|
||||
|
||||
if (objectMiscColor.currentColor != Color.fromString(linkedObject.color.toHexString() ?? "white"))
|
||||
objectMiscColor.currentColor = Color.fromString(linkedObject.color.toHexString());
|
||||
|
||||
if (objectAnimDanceBeat.pos != linkedObject.danceEvery) objectAnimDanceBeat.pos = linkedObject.danceEvery;
|
||||
if (objectAnimStart.text != linkedObject.startingAnimation) objectAnimStart.text = linkedObject.startingAnimation;
|
||||
|
||||
var objBlend = Std.string(linkedObject.blend) ?? "NONE";
|
||||
if (objectMiscBlendDrop.selectedItem != objBlend.toUpperCase()) objectMiscBlendDrop.selectedItem = objBlend.toUpperCase();
|
||||
|
||||
// ough the max
|
||||
if (objectFrameImageWidth.max != linkedObject.pixels.width) objectFrameImageWidth.max = linkedObject.graphic.width;
|
||||
if (objectFrameImageHeight.max != linkedObject.pixels.height) objectFrameImageHeight.max = linkedObject.graphic.height;
|
||||
|
||||
// update some anim shit
|
||||
if (prevFrames != linkedObject.frames.frames.copy()) updateFrameList();
|
||||
if (prevAnims != linkedObject.animation.getNameList().copy()) updateAnimList();
|
||||
}
|
||||
|
||||
function updateFrameList()
|
||||
{
|
||||
prevFrames = [];
|
||||
objectAnimFrameList.dataSource = new ArrayDataSource();
|
||||
|
||||
if (linkedObject == null) return;
|
||||
|
||||
for (fname in linkedObject.frames.frames)
|
||||
{
|
||||
if (fname != null) objectAnimFrameList.dataSource.add({name: fname.name, tooltip: fname.name});
|
||||
|
||||
prevFrames.push(fname);
|
||||
}
|
||||
}
|
||||
|
||||
function updateAnimList()
|
||||
{
|
||||
objectAnimDropdown.dataSource.clear();
|
||||
prevAnims = [];
|
||||
if (linkedObject == null) return;
|
||||
|
||||
for (aname in linkedObject.animation.getNameList())
|
||||
{
|
||||
objectAnimDropdown.dataSource.add({text: aname});
|
||||
prevAnims.push(aname);
|
||||
}
|
||||
|
||||
if (linkedObject.animation.getNameList().contains(objectAnimStart.text)) objectAnimStart.styleString = "color: white";
|
||||
else
|
||||
objectAnimStart.styleString = "color: indianred";
|
||||
|
||||
linkedObject.startingAnimation = objectAnimStart.text;
|
||||
}
|
||||
}
|
|
@ -4,7 +4,9 @@ import haxe.ui.components.NumberStepper;
|
|||
import haxe.ui.components.TextField;
|
||||
import haxe.ui.components.DropDown;
|
||||
import funkin.util.SortUtil;
|
||||
import haxe.ui.events.UIEvent;
|
||||
|
||||
@:access(funkin.ui.debug.stageeditor.StageEditorState)
|
||||
@:build(haxe.ui.macros.ComponentMacros.build("assets/exclude/data/ui/stage-editor/toolboxes/stage-settings.xml"))
|
||||
class StageEditorStageToolbox extends StageEditorDefaultToolbox
|
||||
{
|
||||
|
@ -27,7 +29,7 @@ class StageEditorStageToolbox extends StageEditorDefaultToolbox
|
|||
state.saved = false;
|
||||
}
|
||||
|
||||
final EXCLUDE_LIBS = ["art", "default", "vlc", "videos", "songs"];
|
||||
final EXCLUDE_LIBS = ["art", "default", "vlc", "videos", "songs", "libvlc"];
|
||||
var allLibs = [];
|
||||
|
||||
@:privateAccess
|
||||
|
@ -49,6 +51,13 @@ class StageEditorStageToolbox extends StageEditorDefaultToolbox
|
|||
}
|
||||
|
||||
refresh();
|
||||
|
||||
this.onDialogClosed = onClose;
|
||||
}
|
||||
|
||||
function onClose(event:UIEvent)
|
||||
{
|
||||
stageEditorState.menubarItemWindowStage.selected = false;
|
||||
}
|
||||
|
||||
override public function refresh()
|
||||
|
|
|
@ -45,6 +45,7 @@ import funkin.util.MathUtil;
|
|||
import funkin.util.SortUtil;
|
||||
import openfl.display.BlendMode;
|
||||
import funkin.data.freeplay.style.FreeplayStyleRegistry;
|
||||
import funkin.ui.debug.charting.ChartEditorState;
|
||||
#if FEATURE_DISCORD_RPC
|
||||
import funkin.api.discord.DiscordClient;
|
||||
#end
|
||||
|
@ -191,21 +192,26 @@ class FreeplayState extends MusicBeatSubState
|
|||
|
||||
public function new(?params:FreeplayStateParams, ?stickers:StickerSubState)
|
||||
{
|
||||
currentCharacterId = params?.character ?? rememberedCharacterId;
|
||||
styleData = FreeplayStyleRegistry.instance.fetchEntry(currentCharacterId);
|
||||
|
||||
var fetchPlayableCharacter = function():PlayableCharacter {
|
||||
var targetCharId = params?.character ?? rememberedCharacterId;
|
||||
var result = PlayerRegistry.instance.fetchEntry(targetCharId);
|
||||
if (result == null) throw 'No valid playable character with id ${targetCharId}';
|
||||
if (result == null)
|
||||
{
|
||||
trace('No valid playable character with id ${targetCharId}');
|
||||
result = PlayerRegistry.instance.fetchEntry(Constants.DEFAULT_CHARACTER);
|
||||
if (result == null) throw 'WTH your default character is null?????';
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
currentCharacter = fetchPlayableCharacter();
|
||||
currentCharacterId = currentCharacter.getFreeplayStyleID();
|
||||
|
||||
currentVariation = rememberedVariation;
|
||||
currentDifficulty = rememberedDifficulty;
|
||||
styleData = FreeplayStyleRegistry.instance.fetchEntry(currentCharacter.getFreeplayStyleID());
|
||||
styleData = FreeplayStyleRegistry.instance.fetchEntry(currentCharacterId);
|
||||
rememberedCharacterId = currentCharacter?.id ?? Constants.DEFAULT_CHARACTER;
|
||||
|
||||
fromCharSelect = params?.fromCharSelect ?? false;
|
||||
fromResultsParams = params?.fromResults;
|
||||
prepForNewRank = fromResultsParams?.playRankAnim ?? false;
|
||||
|
@ -403,6 +409,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
}
|
||||
|
||||
albumRoll.albumId = null;
|
||||
albumRoll.applyExitMovers(exitMovers, exitMoversCharSel);
|
||||
add(albumRoll);
|
||||
|
||||
var overhangStuff:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, 164, FlxColor.BLACK);
|
||||
|
@ -416,7 +423,6 @@ class FreeplayState extends MusicBeatSubState
|
|||
}
|
||||
else
|
||||
{
|
||||
albumRoll.applyExitMovers(exitMovers, exitMoversCharSel);
|
||||
FlxTween.tween(overhangStuff, {y: -100}, 0.3, {ease: FlxEase.quartOut});
|
||||
FlxTween.tween(blackOverlayBullshitLOLXD, {x: 387.76}, 0.7, {ease: FlxEase.quintOut});
|
||||
}
|
||||
|
@ -433,8 +439,11 @@ class FreeplayState extends MusicBeatSubState
|
|||
charSelectHint.font = "5by7";
|
||||
charSelectHint.color = 0xFF5F5F5F;
|
||||
charSelectHint.text = 'Press [ ${controls.getDialogueNameFromControl(FREEPLAY_CHAR_SELECT, true)} ] to change characters';
|
||||
charSelectHint.y -= 100;
|
||||
FlxTween.tween(charSelectHint, {y: charSelectHint.y + 100}, 0.8, {ease: FlxEase.quartOut});
|
||||
if (!fromCharSelect)
|
||||
{
|
||||
charSelectHint.y -= 100;
|
||||
FlxTween.tween(charSelectHint, {y: charSelectHint.y + 100}, 0.8, {ease: FlxEase.quartOut});
|
||||
}
|
||||
|
||||
exitMovers.set([overhangStuff, fnfFreeplay, ostName, charSelectHint],
|
||||
{
|
||||
|
@ -497,6 +506,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
// Reminder, this is a callback function being set, rather than these being called here in create()
|
||||
letterSort.changeSelectionCallback = (str) -> {
|
||||
var curSong:Null<FreeplaySongData> = grpCapsules.members[curSelected]?.freeplayData;
|
||||
grpCapsules.members[curSelected].selected = false;
|
||||
|
||||
switch (str)
|
||||
{
|
||||
|
@ -550,7 +560,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
add(fnfFreeplay);
|
||||
add(ostName);
|
||||
|
||||
if (PlayerRegistry.instance.hasNewCharacter())
|
||||
if (PlayerRegistry.instance.countUnlockedCharacters() > 1)
|
||||
{
|
||||
add(charSelectHint);
|
||||
}
|
||||
|
@ -726,10 +736,6 @@ class FreeplayState extends MusicBeatSubState
|
|||
currentFilteredSongs = tempSongs;
|
||||
curSelected = 0;
|
||||
|
||||
// If curSelected is 0, the result will be null and fall back to the rememberedSongId.
|
||||
// We set this so if we change the filter, we'd remain on the same song if it's still in the list.
|
||||
rememberedSongId = grpCapsules.members[curSelected]?.freeplayData?.data.id ?? rememberedSongId;
|
||||
|
||||
grpCapsules.killMembers();
|
||||
|
||||
// Initialize the random capsule, with empty/blank info (which we display once bf/pico does his hand)
|
||||
|
@ -829,13 +835,12 @@ class FreeplayState extends MusicBeatSubState
|
|||
case ALL:
|
||||
// no filter!
|
||||
case FAVORITE:
|
||||
// sort favorites by week, not alphabetically
|
||||
songsToFilter = songsToFilter.filter(filteredSong -> {
|
||||
if (filteredSong == null) return true; // Random
|
||||
return filteredSong.isFav;
|
||||
});
|
||||
|
||||
songsToFilter.sort(filterAlphabetically);
|
||||
|
||||
default:
|
||||
// return all on default
|
||||
}
|
||||
|
@ -855,6 +860,9 @@ class FreeplayState extends MusicBeatSubState
|
|||
|
||||
rememberedSongId = fromResults.songId;
|
||||
rememberedDifficulty = fromResults.difficultyId;
|
||||
capsuleToRank.fakeRanking.visible = true;
|
||||
capsuleToRank.fakeRanking.alpha = 0; // If this isn't done, you'd see a tiny E being replaced for the first rank
|
||||
|
||||
changeSelection();
|
||||
changeDiff();
|
||||
|
||||
|
@ -895,6 +903,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
sparksADD.cameras = [rankCamera];
|
||||
sparksADD.color = fromResults.oldRank.getRankingFreeplayColor();
|
||||
// sparksADD.color = sparks.color;
|
||||
capsuleToRank.fakeRanking.alpha = 1.0;
|
||||
}
|
||||
|
||||
capsuleToRank.doLerp = false;
|
||||
|
@ -907,7 +916,6 @@ class FreeplayState extends MusicBeatSubState
|
|||
trace(originalPos);
|
||||
|
||||
capsuleToRank.ranking.visible = false;
|
||||
capsuleToRank.fakeRanking.visible = false;
|
||||
|
||||
rankCamera.zoom = 1.85;
|
||||
FlxTween.tween(rankCamera, {"zoom": 1.8}, 0.6, {ease: FlxEase.sineIn});
|
||||
|
@ -1179,7 +1187,8 @@ class FreeplayState extends MusicBeatSubState
|
|||
fadeShader.fade(1.0, 0.0, 0.8, {ease: FlxEase.quadIn});
|
||||
FlxG.sound.music?.fadeOut(0.9, 0);
|
||||
new FlxTimer().start(0.9, _ -> {
|
||||
FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState());
|
||||
FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState({character: currentCharacterId} // Passing the currrent Freeplay character to the CharSelect so we can start it with that character selected
|
||||
));
|
||||
});
|
||||
for (grpSpr in exitMoversCharSel.keys())
|
||||
{
|
||||
|
@ -1259,7 +1268,6 @@ class FreeplayState extends MusicBeatSubState
|
|||
capsule.doLerp = true;
|
||||
fromCharSelect = false;
|
||||
busy = false;
|
||||
albumRoll.applyExitMovers(exitMovers, exitMoversCharSel);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1343,7 +1351,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
grpCapsules.members[realShit].favIconBlurred.animation.play('fav');
|
||||
FunkinSound.playOnce(Paths.sound('fav'), 1);
|
||||
grpCapsules.members[realShit].checkClip();
|
||||
grpCapsules.members[realShit].selected = grpCapsules.members[realShit].selected; // set selected again, so it can run it's getter function to initialize movement
|
||||
grpCapsules.members[realShit].selected = true; // set selected again, so it can run it's getter function to initialize movement
|
||||
busy = true;
|
||||
|
||||
grpCapsules.members[realShit].doLerp = false;
|
||||
|
@ -1368,6 +1376,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
grpCapsules.members[realShit].favIcon.visible = false;
|
||||
grpCapsules.members[realShit].favIconBlurred.visible = false;
|
||||
grpCapsules.members[realShit].checkClip();
|
||||
grpCapsules.members[realShit].selected = true; // set selected again, so it can run it's getter function to initialize movement
|
||||
});
|
||||
|
||||
busy = true;
|
||||
|
@ -1660,6 +1669,45 @@ class FreeplayState extends MusicBeatSubState
|
|||
{
|
||||
grpCapsules.members[curSelected].onConfirm();
|
||||
}
|
||||
if (controls.DEBUG_CHART && !busy)
|
||||
{
|
||||
busy = true;
|
||||
var targetSongID = grpCapsules.members[curSelected]?.freeplayData?.data.id ?? 'unknown';
|
||||
if (targetSongID == 'unknown')
|
||||
{
|
||||
trace('CHART RANDOM SONG');
|
||||
letterSort.inputEnabled = false;
|
||||
|
||||
var availableSongCapsules:Array<SongMenuItem> = grpCapsules.members.filter(function(cap:SongMenuItem) {
|
||||
// Dead capsules are ones which were removed from the list when changing filters.
|
||||
return cap.alive && cap.freeplayData != null;
|
||||
});
|
||||
|
||||
trace('Available songs: ${availableSongCapsules.map(function(cap) {
|
||||
return cap?.freeplayData?.data.songName;
|
||||
})}');
|
||||
|
||||
if (availableSongCapsules.length == 0)
|
||||
{
|
||||
trace('No songs available!');
|
||||
busy = false;
|
||||
letterSort.inputEnabled = true;
|
||||
FunkinSound.playOnce(Paths.sound('cancelMenu'));
|
||||
return;
|
||||
}
|
||||
|
||||
var targetSong:SongMenuItem = FlxG.random.getObject(availableSongCapsules);
|
||||
|
||||
// Seeing if I can do an animation...
|
||||
curSelected = grpCapsules.members.indexOf(targetSong);
|
||||
changeSelection(0);
|
||||
targetSongID = grpCapsules.members[curSelected]?.freeplayData?.data.id ?? 'unknown';
|
||||
}
|
||||
FlxG.switchState(() -> new ChartEditorState(
|
||||
{
|
||||
targetSongId: targetSongID,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
override function beatHit():Bool
|
||||
|
@ -1718,6 +1766,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
touchTimer = 0;
|
||||
var previousVariation:String = currentVariation;
|
||||
var daSong:Null<FreeplaySongData> = grpCapsules.members[curSelected].freeplayData;
|
||||
grpCapsules.members[curSelected].selected = false;
|
||||
|
||||
// Available variations for current character. We get this since bf is usually `default` variation, and `pico` is `pico`
|
||||
// but sometimes pico can be the default variation (weekend 1 songs), and bf can be `bf` variation (darnell)
|
||||
|
@ -1765,8 +1814,8 @@ class FreeplayState extends MusicBeatSubState
|
|||
|
||||
var songScore:Null<SaveScoreData> = Save.instance.getSongScore(daSong.data.id, currentDifficulty, currentVariation);
|
||||
intendedScore = songScore?.score ?? 0;
|
||||
intendedCompletion = songScore == null ? 0.0 : Math.max(0, ((songScore.tallies.sick +
|
||||
songScore.tallies.good - songScore.tallies.missed) / songScore.tallies.totalNotes));
|
||||
intendedCompletion = songScore == null ? 0.0 : Math.max(0,
|
||||
((songScore.tallies.sick + songScore.tallies.good - songScore.tallies.missed) / songScore.tallies.totalNotes));
|
||||
rememberedDifficulty = currentDifficulty;
|
||||
grpCapsules.members[curSelected].refreshDisplay((prepForNewRank == true) ? false : true);
|
||||
}
|
||||
|
@ -1832,6 +1881,8 @@ class FreeplayState extends MusicBeatSubState
|
|||
|
||||
// Set difficulty star count.
|
||||
albumRoll.setDifficultyStars(daSong?.data.getDifficulty(currentDifficulty, currentVariation)?.difficultyRating ?? 0);
|
||||
|
||||
grpCapsules.members[curSelected].selected = true; // set selected again, so it can run it's getter function to initialize movement
|
||||
}
|
||||
|
||||
function capsuleOnConfirmRandom(randomCapsule:SongMenuItem):Void
|
||||
|
@ -2098,7 +2149,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
if (index < curSelected) capsule.targetPos.y -= 100; // another 100 for good measure
|
||||
}
|
||||
|
||||
if (grpCapsules.countLiving() > 0 && !prepForNewRank)
|
||||
if (grpCapsules.countLiving() > 0 && !prepForNewRank && !busy)
|
||||
{
|
||||
playCurSongPreview(daSongCapsule);
|
||||
grpCapsules.members[curSelected].selected = true;
|
||||
|
|
|
@ -286,7 +286,7 @@ class SongMenuItem extends FlxSpriteGroup
|
|||
var clipSize:Int = 290;
|
||||
var clipType:Int = 0;
|
||||
|
||||
if (ranking.visible)
|
||||
if (ranking.visible || fakeRanking.visible)
|
||||
{
|
||||
favIconBlurred.x = this.x + 370;
|
||||
favIcon.x = favIconBlurred.x;
|
||||
|
@ -492,8 +492,6 @@ class SongMenuItem extends FlxSpriteGroup
|
|||
spr.visible = value;
|
||||
}
|
||||
|
||||
textAppear();
|
||||
|
||||
updateSelected();
|
||||
}
|
||||
|
||||
|
@ -660,7 +658,11 @@ class SongMenuItem extends FlxSpriteGroup
|
|||
*/
|
||||
public function confirm():Void
|
||||
{
|
||||
if (songText != null) songText.flickerText();
|
||||
if (songText != null)
|
||||
{
|
||||
textAppear();
|
||||
songText.flickerText();
|
||||
}
|
||||
if (pixelIcon != null && pixelIcon.visible)
|
||||
{
|
||||
pixelIcon.animation.play('confirm');
|
||||
|
|
|
@ -104,11 +104,12 @@ class MainMenuState extends MusicBeatState
|
|||
FlxTransitionableState.skipNextTransIn = true;
|
||||
FlxTransitionableState.skipNextTransOut = true;
|
||||
|
||||
var rememberedFreeplayCharacter = FreeplayState.rememberedCharacterId;
|
||||
#if FEATURE_DEBUG_FUNCTIONS
|
||||
// Debug function: Hold SHIFT when selecting Freeplay to swap character without the char select menu
|
||||
var targetCharacter:Null<String> = (FlxG.keys.pressed.SHIFT) ? (FreeplayState.rememberedCharacterId == "pico" ? "bf" : "pico") : null;
|
||||
var targetCharacter:Null<String> = (FlxG.keys.pressed.SHIFT) ? (FreeplayState.rememberedCharacterId == "pico" ? "bf" : "pico") : rememberedFreeplayCharacter;
|
||||
#else
|
||||
var targetCharacter:Null<String> = null;
|
||||
var targetCharacter:Null<String> = rememberedFreeplayCharacter;
|
||||
#end
|
||||
|
||||
openSubState(new FreeplayState(
|
||||
|
@ -302,6 +303,9 @@ class MainMenuState extends MusicBeatState
|
|||
{
|
||||
persistentUpdate = false;
|
||||
|
||||
// Cancel the currently flickering menu item because it's about to call a state switch
|
||||
if (menuItems.busy) menuItems.cancelAccept();
|
||||
|
||||
FlxG.state.openSubState(new DebugMenuSubState());
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,9 @@ package funkin.ui.options;
|
|||
import funkin.ui.Page.PageName;
|
||||
import funkin.ui.transition.LoadingState;
|
||||
import funkin.ui.debug.latency.LatencyState;
|
||||
import flixel.math.FlxPoint;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.FlxObject;
|
||||
import flixel.FlxSubState;
|
||||
import flixel.group.FlxGroup;
|
||||
import flixel.util.FlxSignal;
|
||||
|
@ -25,6 +27,8 @@ class OptionsState extends MusicBeatState
|
|||
{
|
||||
var optionsCodex:Codex<OptionsMenuPageName>;
|
||||
|
||||
public static var rememberedSelectedIndex:Int = 0;
|
||||
|
||||
override function create():Void
|
||||
{
|
||||
persistentUpdate = true;
|
||||
|
@ -85,14 +89,23 @@ class OptionsMenu extends Page<OptionsMenuPageName>
|
|||
{
|
||||
var items:TextMenuList;
|
||||
|
||||
/**
|
||||
* Camera focus point
|
||||
*/
|
||||
var camFocusPoint:FlxObject;
|
||||
|
||||
final CAMERA_MARGIN:Int = 150;
|
||||
|
||||
public function new()
|
||||
{
|
||||
super();
|
||||
|
||||
add(items = new TextMenuList());
|
||||
|
||||
createItem("PREFERENCES", function() codex.switchPage(Preferences));
|
||||
createItem("CONTROLS", function() codex.switchPage(Controls));
|
||||
createItem("INPUT OFFSETS", function() {
|
||||
OptionsState.rememberedSelectedIndex = items.selectedIndex;
|
||||
#if web
|
||||
LoadingState.transitionToState(() -> new LatencyState());
|
||||
#else
|
||||
|
@ -135,6 +148,27 @@ class OptionsMenu extends Page<OptionsMenuPageName>
|
|||
});
|
||||
|
||||
createItem("EXIT", exit);
|
||||
|
||||
// Create an object for the camera to track.
|
||||
camFocusPoint = new FlxObject(0, 0, 140, 70);
|
||||
add(camFocusPoint);
|
||||
|
||||
// Follow the camera focus as we scroll.
|
||||
FlxG.camera.follow(camFocusPoint, null, 0.085);
|
||||
FlxG.camera.deadzone.set(0, CAMERA_MARGIN / 2, FlxG.camera.width, FlxG.camera.height - CAMERA_MARGIN + 40);
|
||||
FlxG.camera.minScrollY = -CAMERA_MARGIN / 2;
|
||||
|
||||
// Move the camera when the menu is scrolled.
|
||||
items.onChange.add(onMenuChange);
|
||||
|
||||
onMenuChange(items.members[0]);
|
||||
|
||||
items.selectItem(OptionsState.rememberedSelectedIndex);
|
||||
}
|
||||
|
||||
function onMenuChange(selected:TextMenuList.TextMenuItem)
|
||||
{
|
||||
camFocusPoint.y = selected.y;
|
||||
}
|
||||
|
||||
function createItem(name:String, callback:Void->Void, fireInstantly = false)
|
||||
|
|
|
@ -116,10 +116,15 @@ class PreferencesMenu extends Page<OptionsState.OptionsMenuPageName>
|
|||
createPrefItemCheckbox('Pause on Unfocus', 'If enabled, game automatically pauses when it loses focus.', function(value:Bool):Void {
|
||||
Preferences.autoPause = value;
|
||||
}, Preferences.autoPause);
|
||||
createPrefItemCheckbox('Launch in Fullscreen', 'Automatically launch the game in fullscreen on startup', function(value:Bool):Void {
|
||||
createPrefItemCheckbox('Launch in Fullscreen', 'Automatically launch the game in fullscreen on startup.', function(value:Bool):Void {
|
||||
Preferences.autoFullscreen = value;
|
||||
}, Preferences.autoFullscreen);
|
||||
|
||||
#if web
|
||||
createPrefItemCheckbox('Unlocked Framerate', 'If enabled, the framerate will be unlocked.', function(value:Bool):Void {
|
||||
Preferences.unlockedFramerate = value;
|
||||
}, Preferences.unlockedFramerate);
|
||||
#else
|
||||
// disabled on macos due to "error: Late swap tearing currently unsupported"
|
||||
#if !mac
|
||||
createPrefItemEnum('VSync', 'If enabled, game will attempt to match framerate with your monitor.', [
|
||||
|
@ -136,14 +141,9 @@ class PreferencesMenu extends Page<OptionsState.OptionsMenuPageName>
|
|||
case WindowVSyncMode.ADAPTIVE: "Adaptive";
|
||||
});
|
||||
#end
|
||||
#if web
|
||||
createPrefItemCheckbox('Unlocked Framerate', 'If enabled, the framerate will be unlocked.', function(value:Bool):Void {
|
||||
Preferences.unlockedFramerate = value;
|
||||
}, Preferences.unlockedFramerate);
|
||||
#else
|
||||
createPrefItemNumber('FPS', 'The maximum framerate that the game targets.', function(value:Float) {
|
||||
Preferences.framerate = Std.int(value);
|
||||
}, null, Preferences.framerate, 30, 300, 5, 0);
|
||||
}, null, Preferences.framerate, 30, 500, 5, 0);
|
||||
#end
|
||||
|
||||
createPrefItemCheckbox('Hide Mouse', 'If enabled, the mouse will be hidden when taking a screenshot.', function(value:Bool):Void {
|
||||
|
@ -155,13 +155,6 @@ class PreferencesMenu extends Page<OptionsState.OptionsMenuPageName>
|
|||
createPrefItemCheckbox('Preview on save', 'If enabled, the preview will be shown only after a screenshot is saved.', function(value:Bool):Void {
|
||||
Preferences.previewOnSave = value;
|
||||
}, Preferences.previewOnSave);
|
||||
// TODO: having oValue is weird, probably change this later? was done to accomodate VSync changes.
|
||||
createPrefItemEnum('Save Format', 'Save screenshots to this format.', ['PNG' => 'PNG', 'JPEG' => 'JPEG'], function(value:String, oValue:String):Void {
|
||||
Preferences.saveFormat = value;
|
||||
}, Preferences.saveFormat);
|
||||
createPrefItemNumber('JPEG Quality', 'The quality of JPEG screenshots.', function(value:Float) {
|
||||
Preferences.jpegQuality = Std.int(value);
|
||||
}, null, Preferences.jpegQuality, 0, 100, 5, 0);
|
||||
}
|
||||
|
||||
override function update(elapsed:Float):Void
|
||||
|
|
|
@ -231,6 +231,8 @@ class TitleState extends MusicBeatState
|
|||
|
||||
Conductor.instance.update();
|
||||
|
||||
funkin.input.Cursor.hide();
|
||||
|
||||
/* if (FlxG.onMobile)
|
||||
{
|
||||
if (gfDance != null)
|
||||
|
@ -240,12 +242,20 @@ class TitleState extends MusicBeatState
|
|||
}
|
||||
}
|
||||
*/
|
||||
if (FlxG.keys.justPressed.I)
|
||||
if (outlineShaderShit != null)
|
||||
{
|
||||
FlxTween.tween(outlineShaderShit, {funnyX: 50, funnyY: 50}, 0.6, {ease: FlxEase.quartOut});
|
||||
if (FlxG.keys.justPressed.I)
|
||||
{
|
||||
FlxTween.tween(outlineShaderShit, {funnyX: 50, funnyY: 50}, 0.6, {ease: FlxEase.quartOut});
|
||||
}
|
||||
|
||||
if (FlxG.keys.pressed.D)
|
||||
{
|
||||
outlineShaderShit.funnyX += 1;
|
||||
}
|
||||
|
||||
// outlineShaderShit.xPos.value[0] += 1;
|
||||
}
|
||||
if (FlxG.keys.pressed.D) outlineShaderShit.funnyX += 1;
|
||||
// outlineShaderShit.xPos.value[0] += 1;
|
||||
|
||||
if (FlxG.keys.justPressed.Y)
|
||||
{
|
||||
|
|
|
@ -72,6 +72,17 @@ class CLIUtil
|
|||
result.chart.shouldLoadChart = true;
|
||||
result.chart.chartPath = args.shift();
|
||||
}
|
||||
case "--stage":
|
||||
if (args.length == 0)
|
||||
{
|
||||
trace('No stage path provided.');
|
||||
printUsage();
|
||||
}
|
||||
else
|
||||
{
|
||||
result.stage.shouldLoadStage = true;
|
||||
result.stage.stagePath = args.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -83,6 +94,11 @@ class CLIUtil
|
|||
result.chart.shouldLoadChart = true;
|
||||
result.chart.chartPath = arg;
|
||||
}
|
||||
else if (arg.endsWith(Constants.EXT_STAGE))
|
||||
{
|
||||
result.stage.shouldLoadStage = true;
|
||||
result.stage.stagePath = arg;
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Unrecognized argument: ${arg}');
|
||||
|
@ -96,7 +112,7 @@ class CLIUtil
|
|||
|
||||
static function printUsage():Void
|
||||
{
|
||||
trace('Usage: Funkin.exe [--chart <chart>] [--help] [--version]');
|
||||
trace('Usage: Funkin.exe [--chart <chart>] [--stage <stage>] [--help] [--version]');
|
||||
}
|
||||
|
||||
static function buildDefaultParams():CLIParams
|
||||
|
@ -108,6 +124,11 @@ class CLIUtil
|
|||
{
|
||||
shouldLoadChart: false,
|
||||
chartPath: null
|
||||
},
|
||||
stage:
|
||||
{
|
||||
shouldLoadStage: false,
|
||||
stagePath: null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -138,6 +159,7 @@ typedef CLIParams =
|
|||
var args:Array<String>;
|
||||
|
||||
var chart:CLIChartParams;
|
||||
var stage:CLIStageParams;
|
||||
}
|
||||
|
||||
typedef CLIChartParams =
|
||||
|
@ -145,3 +167,9 @@ typedef CLIChartParams =
|
|||
var shouldLoadChart:Bool;
|
||||
var chartPath:Null<String>;
|
||||
};
|
||||
|
||||
typedef CLIStageParams =
|
||||
{
|
||||
var shouldLoadStage:Bool;
|
||||
var stagePath:Null<String>;
|
||||
};
|
||||
|
|
|
@ -537,6 +537,11 @@ class Constants
|
|||
*/
|
||||
public static final EXT_CHART = "fnfc";
|
||||
|
||||
/**
|
||||
* The file extension used when exporting stage files.
|
||||
*/
|
||||
public static final EXT_STAGE = "fnfs";
|
||||
|
||||
/**
|
||||
* The file extension used when loading audio files.
|
||||
*/
|
||||
|
|
|
@ -527,20 +527,23 @@ class FileUtil
|
|||
/**
|
||||
* Prompts the user to save a file to their computer.
|
||||
*/
|
||||
public static function writeFileReference(path:String, data:String):Void
|
||||
public static function writeFileReference(path:String, data:String, callback:String->Void)
|
||||
{
|
||||
var file = new FileReference();
|
||||
|
||||
file.addEventListener(Event.COMPLETE, function(e:Event) {
|
||||
trace('Successfully wrote file: "$path"');
|
||||
callback("success");
|
||||
});
|
||||
|
||||
file.addEventListener(Event.CANCEL, function(e:Event) {
|
||||
trace('Cancelled writing file: "$path"');
|
||||
callback("info");
|
||||
});
|
||||
|
||||
file.addEventListener(IOErrorEvent.IO_ERROR, function(e:IOErrorEvent) {
|
||||
trace('IO error writing file: "$path"');
|
||||
callback("error");
|
||||
});
|
||||
|
||||
file.save(data, path);
|
||||
|
@ -1143,12 +1146,17 @@ class FileUtil
|
|||
* Runs platform-specific code to open a path in the file explorer.
|
||||
*
|
||||
* @param pathFolder The path of the folder to open.
|
||||
* @param createIfNotExists If `true`, creates the folder if missing; otherwise, throws an error.
|
||||
*/
|
||||
public static function openFolder(pathFolder:String):Void
|
||||
public static function openFolder(pathFolder:String, createIfNotExists:Bool = true):Void
|
||||
{
|
||||
#if sys
|
||||
pathFolder = pathFolder.trim();
|
||||
if (!directoryExists(pathFolder))
|
||||
if (createIfNotExists)
|
||||
{
|
||||
createDirIfNotExists(pathFolder);
|
||||
}
|
||||
else if (!directoryExists(pathFolder))
|
||||
{
|
||||
throw 'Path is not a directory: "$pathFolder"';
|
||||
}
|
||||
|
@ -1393,9 +1401,9 @@ class FileUtilSandboxed
|
|||
FileUtil.browseFileReference(callback);
|
||||
}
|
||||
|
||||
public static function writeFileReference(path:String, data:String):Void
|
||||
public static function writeFileReference(path:String, data:String, callback:String->Void):Void
|
||||
{
|
||||
FileUtil.writeFileReference(path, data);
|
||||
FileUtil.writeFileReference(path, data, callback);
|
||||
}
|
||||
|
||||
public static function readJSONFromPath(path:String):Dynamic
|
||||
|
@ -1518,9 +1526,9 @@ class FileUtilSandboxed
|
|||
return FileUtil.makeZIPEntryFromBytes(name, data);
|
||||
}
|
||||
|
||||
public static function openFolder(pathFolder:String):Void
|
||||
public static function openFolder(pathFolder:String, createIfNotExists:Bool = true):Void
|
||||
{
|
||||
FileUtil.openFolder(sanitizePath(pathFolder));
|
||||
FileUtil.openFolder(sanitizePath(pathFolder), createIfNotExists);
|
||||
}
|
||||
|
||||
public static function openSelectFile(path:String):Void
|
||||
|
|
|
@ -85,12 +85,6 @@ class WindowUtil
|
|||
*/
|
||||
public static function initTracy():Void
|
||||
{
|
||||
// Apply a marker to indicate frame end for the Tracy profiler.
|
||||
// Do this only if Tracy is configured to prevent lag.
|
||||
openfl.Lib.current.stage.addEventListener(openfl.events.Event.EXIT_FRAME, (e:openfl.events.Event) -> {
|
||||
cpp.vm.tracy.TracyProfiler.frameMark();
|
||||
});
|
||||
|
||||
var appInfoMessage = funkin.util.logging.CrashHandler.buildSystemInfo();
|
||||
|
||||
trace("Friday Night Funkin': Connection to Tracy profiler successful.");
|
||||
|
|
|
@ -469,7 +469,7 @@ class ScreenshotPlugin extends FlxBasic
|
|||
}
|
||||
|
||||
/**
|
||||
* Convert a Bitmap to a PNG or JPEG ByteArray to save to a file.
|
||||
* Convert a Bitmap to a PNG ByteArray to save to a file.
|
||||
*/
|
||||
function encode(bitmap:Bitmap):ByteArray
|
||||
{
|
||||
|
@ -494,7 +494,7 @@ class ScreenshotPlugin extends FlxBasic
|
|||
if (previousScreenshotName != targetPath && previousScreenshotName != (targetPath + ' (${previousScreenshotCopyNum})'))
|
||||
{
|
||||
previousScreenshotName = targetPath;
|
||||
targetPath = getScreenshotPath() + targetPath + '.' + Std.string(Preferences.saveFormat).toLowerCase();
|
||||
targetPath = getScreenshotPath() + targetPath + '.png';
|
||||
previousScreenshotCopyNum = 2;
|
||||
}
|
||||
else
|
||||
|
@ -506,7 +506,7 @@ class ScreenshotPlugin extends FlxBasic
|
|||
newTargetPath = targetPath + ' (${previousScreenshotCopyNum})';
|
||||
}
|
||||
previousScreenshotName = newTargetPath;
|
||||
targetPath = getScreenshotPath() + newTargetPath + '.' + Std.string(Preferences.saveFormat).toLowerCase();
|
||||
targetPath = getScreenshotPath() + newTargetPath + '.png';
|
||||
}
|
||||
|
||||
// TODO: Make screenshot saving work on browser.
|
||||
|
@ -520,7 +520,7 @@ class ScreenshotPlugin extends FlxBasic
|
|||
|
||||
if (pngData == null)
|
||||
{
|
||||
trace('[WARN] Failed to encode ${Preferences.saveFormat} data');
|
||||
trace('[WARN] Failed to encode PNG data');
|
||||
previousScreenshotName = null;
|
||||
// Just in case
|
||||
unsavedScreenshotBuffer.shift();
|
||||
|
@ -544,7 +544,7 @@ class ScreenshotPlugin extends FlxBasic
|
|||
|
||||
if (pngData == null)
|
||||
{
|
||||
trace('[WARN] Failed to encode ${Preferences.saveFormat} data');
|
||||
trace('[WARN] Failed to encode PNG data');
|
||||
previousScreenshotName = null;
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -96,4 +96,18 @@ class ArrayTools
|
|||
// Switch it around.
|
||||
return isSuperset(superset, subset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like `join` but adds a word before the last element.
|
||||
* @param array The array to join.
|
||||
* @param separator The separator to use between elements.
|
||||
* @param andWord The word to use before the last element.
|
||||
* @return The joined string.
|
||||
*/
|
||||
public static function joinPlural(array:Array<String>, separator:String = ', ', andWord:String = 'and'):String
|
||||
{
|
||||
if (array.length == 0) return '';
|
||||
if (array.length == 1) return array[0];
|
||||
return '${array.slice(0, array.length - 1).join(separator)} $andWord ${array[array.length - 1]}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,8 +55,25 @@ class SongNoteDataArrayTools
|
|||
}
|
||||
else
|
||||
{
|
||||
// We may be close, so constrain the range (but only a little) and try again.
|
||||
highIndex -= 1;
|
||||
// Notes might have same time but not same data, do scans towards both sides
|
||||
// Scan left from midIndex
|
||||
var i = midIndex;
|
||||
while (i >= 0 && input[i].time == note.time)
|
||||
{
|
||||
if (input[i] == note) return i;
|
||||
i--;
|
||||
}
|
||||
|
||||
// Scan right from midIndex + 1
|
||||
i = midIndex + 1;
|
||||
while (i < input.length && input[i].time == note.time)
|
||||
{
|
||||
if (input[i] == note) return i;
|
||||
i++;
|
||||
}
|
||||
|
||||
// No matching note found, despite time match
|
||||
break;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
|
|
Loading…
Reference in a new issue