Merge pull request from FunkinCrew/feature/credits

First implementation of credits state.
This commit is contained in:
Cameron Taylor 2024-04-01 22:07:18 -04:00 committed by GitHub
commit fbb157b579
12 changed files with 486 additions and 19 deletions

View file

@ -183,6 +183,7 @@
<haxedef name="haxeui_focus_out_on_click" />
<!-- Required to use haxe.ui.backend.flixel.UIState with build macros. -->
<haxedef name="haxeui_dont_impose_base_class" />
<haxedef name="HARDCODED_CREDITS" />
<!-- Skip the Intro -->
<section if="debug">

2
art

@ -1 +1 @@
Subproject commit 00463685fa570f0c853d08e250b46ef80f30bc48
Subproject commit 03e7c2a2353b184e45955c96d763b7cdf1acbc34

2
assets

@ -1 +1 @@
Subproject commit 763c833cbcde724d50ff31f5bac9f2ac3d5e61a7
Subproject commit d7e85ef60933ca93d47e1db6295aba8aa64fcbdf

View file

@ -325,12 +325,3 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
}
}
}
/**
* A pair of a file name and its contents.
*/
typedef JsonFile =
{
fileName:String,
contents:String
};

View file

@ -0,0 +1,10 @@
package funkin.data;
/**
* A pair of a file name and its contents.
*/
typedef JsonFile =
{
fileName:String,
contents:String
};

View file

@ -427,7 +427,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return ScriptedSong.listScriptClasses();
}
function loadEntryMetadataFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile>
function loadEntryMetadataFile(id:String, ?variation:String):Null<JsonFile>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}');
@ -442,7 +442,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return {fileName: entryFilePath, contents: rawJson};
}
function loadMusicDataFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile>
function loadMusicDataFile(id:String, ?variation:String):Null<JsonFile>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryFilePath:String = Paths.file('music/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.json');
@ -460,7 +460,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return openfl.Assets.exists(entryFilePath);
}
function loadEntryChartFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile>
function loadEntryChartFile(id:String, ?variation:String):Null<JsonFile>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-chart${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}');

View file

@ -0,0 +1,34 @@
package funkin.ui.credits;
/**
* The members of the Funkin' Crew, organized by their roles.
*/
typedef CreditsData =
{
var entries:Array<CreditsDataRole>;
}
/**
* The members of a specific role on the Funkin' Crew.
*/
typedef CreditsDataRole =
{
@:optional
var header:String;
@:optional
@:default([])
var body:Array<CreditsDataMember>;
@:optional
@:default(false)
var appendBackers:Bool;
}
/**
* A member of a specific person on the Funkin' Crew.
*/
typedef CreditsDataMember =
{
var line:String;
}

View file

@ -0,0 +1,134 @@
package funkin.ui.credits;
import funkin.data.JsonFile;
using StringTools;
@:nullSafety
class CreditsDataHandler
{
public static final BACKER_PUBLIC_URL:String = 'https://funkin.me/backers';
#if HARDCODED_CREDITS
static final CREDITS_DATA_PATH:String = "assets/exclude/data/credits.json";
#else
static final CREDITS_DATA_PATH:String = "assets/data/credits.json";
#end
public static function debugPrint(data:Null<CreditsData>):Void
{
if (data == null)
{
trace('CreditsData(NULL)');
return;
}
var entryCount = data.entries.length;
var lineCount = 0;
for (entry in data.entries)
{
lineCount += entry?.body?.length ?? 0;
}
trace('CreditsData($entryCount entries containing $lineCount lines)');
}
/**
* If for some reason the full credits won't load,
* use this hardcoded data for the original Funkin' Crew.
*
* @return `CreditsData`
*/
public static inline function getFallback():CreditsData
{
return {
entries: [
{
header: 'Founders',
body: [
{line: 'ninjamuffin99'},
{line: 'PhantomArcade'},
{line: 'KawaiSprite'},
{line: 'evilsk8r'},
]
},
{
header: 'Kickstarter Backers',
appendBackers: true
}
]
};
}
public static function fetchBackerEntries():Array<String>
{
// TODO: Replace this with a web request.
// We can't just grab the current Kickstarter data and include it in builds,
// because we don't want to deadname people who haven't logged into the portal yet.
// It can be async and paginated for performance!
return ['See the list of backers at $BACKER_PUBLIC_URL.'];
}
#if HARDCODED_CREDITS
/**
* The data for the credits.
* Hardcoded into game via a macro at compile time.
*/
public static final CREDITS_DATA:Null<CreditsData> = #if macro null #else CreditsDataMacro.loadCreditsData() #end;
#else
/**
* The data for the credits.
* Loaded dynamically from the game folder when needed.
* Nullable because data may fail to parse.
*/
public static var CREDITS_DATA(get, default):Null<CreditsData> = null;
static function get_CREDITS_DATA():Null<CreditsData>
{
if (CREDITS_DATA == null) CREDITS_DATA = parseCreditsData(fetchCreditsData());
return CREDITS_DATA;
}
static function fetchCreditsData():funkin.data.JsonFile
{
var rawJson:String = openfl.Assets.getText(CREDITS_DATA_PATH).trim();
return {
fileName: CREDITS_DATA_PATH,
contents: rawJson
};
}
static function parseCreditsData(file:JsonFile):Null<CreditsData>
{
#if !macro
if (file.contents == null) return null;
var parser = new json2object.JsonParser<CreditsData>();
parser.ignoreUnknownVariables = false;
trace('[CREDITS] Parsing credits data from ${CREDITS_DATA_PATH}');
parser.fromJson(file.contents, file.fileName);
if (parser.errors.length > 0)
{
printErrors(parser.errors, file.fileName);
return null;
}
return parser.value;
#else
return null;
#end
}
static function printErrors(errors:Array<json2object.Error>, id:String = ''):Void
{
trace('[CREDITS] Failed to parse credits data: ${id}');
for (error in errors)
funkin.data.DataError.printError(error);
}
#end
}

View file

@ -0,0 +1,67 @@
package funkin.ui.credits;
#if macro
import haxe.macro.Context;
#end
@:access(funkin.ui.credits.CreditsDataHandler)
class CreditsDataMacro
{
public static macro function loadCreditsData():haxe.macro.Expr.ExprOf<CreditsData>
{
#if !display
trace('Hardcoding credits data...');
var json = CreditsDataMacro.fetchJSON();
if (json == null)
{
Context.info('[WARN] Could not fetch JSON data for credits.', Context.currentPos());
return macro $v{CreditsDataHandler.getFallback()};
}
var creditsData = CreditsDataMacro.parseJSON(json);
if (creditsData == null)
{
Context.info('[WARN] Could not parse JSON data for credits.', Context.currentPos());
return macro $v{CreditsDataHandler.getFallback()};
}
CreditsDataHandler.debugPrint(creditsData);
return macro $v{creditsData};
// return macro $v{null};
#else
// `#if display` is used for code completion. In this case we return
// a minimal value to keep code completion fast.
return macro $v{CreditsDataHandler.getFallback()};
#end
}
#if macro
static function fetchJSON():Null<String>
{
return sys.io.File.getContent(CreditsDataHandler.CREDITS_DATA_PATH);
}
/**
* Parse the JSON data for the credits.
*
* @param json The string data to parse.
* @return The parsed data.
*/
static function parseJSON(json:String):Null<CreditsData>
{
try
{
// TODO: Use something with better validation but that still works at macro time.
return haxe.Json.parse(json);
}
catch (e)
{
trace('[ERROR] Failed to parse JSON data for credits.');
trace(e);
return null;
}
}
#end
}

View file

@ -0,0 +1,213 @@
package funkin.ui.credits;
import flixel.text.FlxText;
import flixel.util.FlxColor;
import funkin.audio.FunkinSound;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup;
/**
* The state used to display the credits scroll.
* AAA studios often fail to credit properly, and we're better than them!
*/
class CreditsState extends MusicBeatState
{
/**
* The height the credits should start at.
* Make this an instanced variable so it gets set by the constructor.
*/
final STARTING_HEIGHT = FlxG.height;
/**
* The padding on each side of the screen.
*/
static final SCREEN_PAD = 24;
/**
* The width of the screen the credits should maximally fill up.
* Make this an instanced variable so it gets set by the constructor.
*/
final FULL_WIDTH = FlxG.width - (SCREEN_PAD * 2);
/**
* The font to use to display the text.
* To use a font from the `assets` folder, use `Paths.font(...)`.
* Choose something that will render Unicode properly.
*/
static final CREDITS_FONT = 'Arial';
/**
* The size of the font.
*/
static final CREDITS_FONT_SIZE = 48;
static final CREDITS_HEADER_FONT_SIZE = 72;
/**
* The color of the text itself.
*/
static final CREDITS_FONT_COLOR = FlxColor.WHITE;
/**
* The color of the text's outline.
*/
static final CREDITS_FONT_STROKE_COLOR = FlxColor.BLACK;
/**
* The speed the credits scroll at, in pixels per second.
*/
static final CREDITS_SCROLL_BASE_SPEED = 25.0;
/**
* The speed the credits scroll at while the button is held, in pixels per second.
*/
static final CREDITS_SCROLL_FAST_SPEED = CREDITS_SCROLL_BASE_SPEED * 4.0;
/**
* The actual sprites and text used to display the credits.
*/
var creditsGroup:FlxSpriteGroup;
var scrollPaused:Bool = false;
public function new()
{
super();
}
public override function create():Void
{
super.create();
// Background
var bg = new FlxSprite(Paths.image('menuDesat'));
bg.scrollFactor.x = 0;
bg.scrollFactor.y = 0;
bg.setGraphicSize(Std.int(FlxG.width));
bg.updateHitbox();
bg.x = 0;
bg.y = 0;
bg.visible = true;
bg.color = 0xFFB57EDC; // Lavender
add(bg);
// TODO: Once we need to display Kickstarter backers,
// make this use a recycled pool so we don't kill peformance.
creditsGroup = new FlxSpriteGroup();
creditsGroup.x = SCREEN_PAD;
creditsGroup.y = STARTING_HEIGHT;
buildCreditsGroup();
add(creditsGroup);
// Music
FunkinSound.playMusic('freeplayRandom',
{
startingVolume: 0.0,
overrideExisting: true,
restartTrack: true,
loop: true
});
FlxG.sound.music.fadeIn(2, 0, 0.8);
}
function buildCreditsGroup():Void
{
var y = 0;
for (entry in CreditsDataHandler.CREDITS_DATA.entries)
{
if (entry.header != null)
{
creditsGroup.add(buildCreditsLine(entry.header, y, true, CreditsSide.Center));
y += CREDITS_HEADER_FONT_SIZE;
}
for (line in entry?.body ?? [])
{
creditsGroup.add(buildCreditsLine(line.line, y, false, CreditsSide.Center));
y += CREDITS_FONT_SIZE;
}
if (entry.appendBackers)
{
var backers = CreditsDataHandler.fetchBackerEntries();
for (backer in backers)
{
creditsGroup.add(buildCreditsLine(backer, y, false, CreditsSide.Center));
y += CREDITS_FONT_SIZE;
}
}
// Padding between each role.
y += CREDITS_FONT_SIZE * 2;
}
}
function buildCreditsLine(text:String, yPos:Float, header:Bool, side:CreditsSide = CreditsSide.Center):FlxText
{
// CreditsSide.Center: Full screen width
// CreditsSide.Left: Left half of screen
// CreditsSide.Right: Right half of screen
var xPos = (side == CreditsSide.Right) ? (FULL_WIDTH / 2) : 0;
var width = (side == CreditsSide.Center) ? FULL_WIDTH : (FULL_WIDTH / 2);
var size = header ? CREDITS_HEADER_FONT_SIZE : CREDITS_FONT_SIZE;
var creditsLine:FlxText = new FlxText(xPos, yPos, width, text);
creditsLine.setFormat(CREDITS_FONT, size, CREDITS_FONT_COLOR, FlxTextAlign.CENTER, FlxTextBorderStyle.OUTLINE, CREDITS_FONT_STROKE_COLOR, true);
return creditsLine;
}
public override function update(elapsed:Float):Void
{
super.update(elapsed);
if (!scrollPaused)
{
// TODO: Replace with whatever the special note button is.
if (controls.ACCEPT || FlxG.keys.pressed.SPACE)
{
// Move the whole group.
creditsGroup.y -= CREDITS_SCROLL_FAST_SPEED * elapsed;
}
else
{
// Move the whole group.
creditsGroup.y -= CREDITS_SCROLL_BASE_SPEED * elapsed;
}
}
if (controls.BACK || hasEnded())
{
exit();
}
else if (controls.PAUSE)
{
scrollPaused = !scrollPaused;
}
}
function hasEnded():Bool
{
return creditsGroup.y < -creditsGroup.height;
}
function exit():Void
{
FlxG.switchState(new funkin.ui.mainmenu.MainMenuState());
}
public override function destroy():Void
{
super.destroy();
}
}
enum CreditsSide
{
Left;
Center;
Right;
}

View file

@ -51,10 +51,7 @@ class MainMenuState extends MusicBeatState
transIn = FlxTransitionableState.defaultTransIn;
transOut = FlxTransitionableState.defaultTransOut;
if (!(FlxG?.sound?.music?.playing ?? false))
{
playMenuMusic();
}
playMenuMusic();
persistentUpdate = persistentDraw = true;
@ -109,14 +106,21 @@ class MainMenuState extends MusicBeatState
});
#if CAN_OPEN_LINKS
// In order to prevent popup blockers from triggering,
// we need to open the link as an immediate result of a keypress event,
// so we can't wait for the flicker animation to complete.
var hasPopupBlocker = #if web true #else false #end;
createMenuItem('donate', 'mainmenu/donate', selectDonate, hasPopupBlocker);
createMenuItem('merch', 'mainmenu/merch', selectMerch, hasPopupBlocker);
#end
createMenuItem('options', 'mainmenu/options', function() {
startExitState(() -> new funkin.ui.options.OptionsState());
});
createMenuItem('credits', 'mainmenu/credits', function() {
startExitState(() -> new funkin.ui.credits.CreditsState());
});
// Reset position of menu items.
var spacing = 160;
var top = (FlxG.height - (spacing * (menuItems.length - 1))) / 2;
@ -125,6 +129,9 @@ class MainMenuState extends MusicBeatState
var menuItem = menuItems.members[i];
menuItem.x = FlxG.width / 2;
menuItem.y = top + spacing * i;
menuItem.scrollFactor.x = 0.0;
// This one affects how much the menu items move when you scroll between them.
menuItem.scrollFactor.y = 0.4;
}
resetCamStuff();
@ -212,6 +219,11 @@ class MainMenuState extends MusicBeatState
{
WindowUtil.openURL(Constants.URL_ITCH);
}
function selectMerch()
{
WindowUtil.openURL(Constants.URL_MERCH);
}
#end
#if newgrounds

View file

@ -60,6 +60,11 @@ class Constants
*/
// ==============================
/**
* Link to buy merch for the game.
*/
public static final URL_MERCH:String = 'https://needlejuicerecords.com/pages/friday-night-funkin';
/**
* Preloader sitelock.
* Matching is done by `FlxStringUtil.getDomain`, so any URL on the domain will work.