mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2024-11-22 23:57:50 -05:00
Merge pull request #626 from FunkinCrew/feature/playable-pico-mode
Playable Pico Mode
This commit is contained in:
commit
11eefc4f6d
20 changed files with 1124 additions and 549 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -13,3 +13,4 @@ shitAudio/
|
|||
node_modules/
|
||||
package.json
|
||||
package-lock.json
|
||||
.aider.*
|
||||
|
|
2
assets
2
assets
|
@ -1 +1 @@
|
|||
Subproject commit 0dbd2e96ca25ab5966cef05db6c76fe7fb145abf
|
||||
Subproject commit d7ecd602df733f0625763a2d7b6056f52147b9e6
|
|
@ -1,5 +1,6 @@
|
|||
package funkin;
|
||||
|
||||
import funkin.data.freeplay.player.PlayerRegistry;
|
||||
import funkin.ui.debug.charting.ChartEditorState;
|
||||
import funkin.ui.transition.LoadingState;
|
||||
import flixel.FlxState;
|
||||
|
@ -164,6 +165,7 @@ class InitState extends FlxState
|
|||
SongRegistry.instance.loadEntries();
|
||||
LevelRegistry.instance.loadEntries();
|
||||
NoteStyleRegistry.instance.loadEntries();
|
||||
PlayerRegistry.instance.loadEntries();
|
||||
ConversationRegistry.instance.loadEntries();
|
||||
DialogueBoxRegistry.instance.loadEntries();
|
||||
SpeakerRegistry.instance.loadEntries();
|
||||
|
|
|
@ -11,10 +11,17 @@ class Paths
|
|||
{
|
||||
static var currentLevel:Null<String> = null;
|
||||
|
||||
public static function setCurrentLevel(name:String):Void
|
||||
public static function setCurrentLevel(name:Null<String>):Void
|
||||
{
|
||||
if (name == null)
|
||||
{
|
||||
currentLevel = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
currentLevel = name.toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
public static function stripLibrary(path:String):String
|
||||
{
|
||||
|
|
9
source/funkin/data/freeplay/player/CHANGELOG.md
Normal file
9
source/funkin/data/freeplay/player/CHANGELOG.md
Normal file
|
@ -0,0 +1,9 @@
|
|||
# Freeplay Playable Character Data Schema Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.0]
|
||||
Initial release.
|
188
source/funkin/data/freeplay/player/PlayerData.hx
Normal file
188
source/funkin/data/freeplay/player/PlayerData.hx
Normal file
|
@ -0,0 +1,188 @@
|
|||
package funkin.data.freeplay.player;
|
||||
|
||||
import funkin.data.animation.AnimationData;
|
||||
|
||||
@:nullSafety
|
||||
class PlayerData
|
||||
{
|
||||
/**
|
||||
* The sematic version number of the player data JSON format.
|
||||
* Supports fancy comparisons like NPM does it's neat.
|
||||
*/
|
||||
@:default(funkin.data.freeplay.player.PlayerRegistry.PLAYER_DATA_VERSION)
|
||||
public var version:String;
|
||||
|
||||
/**
|
||||
* A readable name for this playable character.
|
||||
*/
|
||||
public var name:String = 'Unknown';
|
||||
|
||||
/**
|
||||
* The character IDs this character is associated with.
|
||||
* Only songs that use these characters will show up in Freeplay.
|
||||
*/
|
||||
@:default([])
|
||||
public var ownedChars:Array<String> = [];
|
||||
|
||||
/**
|
||||
* Whether to show songs with character IDs that aren't associated with any specific character.
|
||||
*/
|
||||
@:optional
|
||||
@:default(false)
|
||||
public var showUnownedChars:Bool = false;
|
||||
|
||||
/**
|
||||
* Data for displaying this character in the Freeplay menu.
|
||||
* If null, display no DJ.
|
||||
*/
|
||||
@:optional
|
||||
public var freeplayDJ:Null<PlayerFreeplayDJData> = null;
|
||||
|
||||
/**
|
||||
* Whether this character is unlocked by default.
|
||||
* Use a ScriptedPlayableCharacter to add custom logic.
|
||||
*/
|
||||
@:optional
|
||||
@:default(true)
|
||||
public var unlocked:Bool = true;
|
||||
|
||||
public function new()
|
||||
{
|
||||
this.version = PlayerRegistry.PLAYER_DATA_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this StageData into a JSON string.
|
||||
*/
|
||||
public function serialize(pretty:Bool = true):String
|
||||
{
|
||||
// Update generatedBy and version before writing.
|
||||
updateVersionToLatest();
|
||||
|
||||
var writer = new json2object.JsonWriter<PlayerData>();
|
||||
return writer.write(this, pretty ? ' ' : null);
|
||||
}
|
||||
|
||||
public function updateVersionToLatest():Void
|
||||
{
|
||||
this.version = PlayerRegistry.PLAYER_DATA_VERSION;
|
||||
}
|
||||
}
|
||||
|
||||
class PlayerFreeplayDJData
|
||||
{
|
||||
var assetPath:String;
|
||||
var animations:Array<AnimationData>;
|
||||
|
||||
@:optional
|
||||
@:default("BOYFRIEND")
|
||||
var text1:String;
|
||||
|
||||
@:optional
|
||||
@:default("HOT BLOODED IN MORE WAYS THAN ONE")
|
||||
var text2:String;
|
||||
|
||||
@:optional
|
||||
@:default("PROTECT YO NUTS")
|
||||
var text3:String;
|
||||
|
||||
|
||||
@:jignored
|
||||
var animationMap:Map<String, AnimationData>;
|
||||
|
||||
@:jignored
|
||||
var prefixToOffsetsMap:Map<String, Array<Float>>;
|
||||
|
||||
@:optional
|
||||
var cartoon:Null<PlayerFreeplayDJCartoonData>;
|
||||
|
||||
public function new()
|
||||
{
|
||||
animationMap = new Map();
|
||||
}
|
||||
|
||||
function mapAnimations()
|
||||
{
|
||||
if (animationMap == null) animationMap = new Map();
|
||||
if (prefixToOffsetsMap == null) prefixToOffsetsMap = new Map();
|
||||
|
||||
animationMap.clear();
|
||||
prefixToOffsetsMap.clear();
|
||||
for (anim in animations)
|
||||
{
|
||||
animationMap.set(anim.name, anim);
|
||||
prefixToOffsetsMap.set(anim.prefix, anim.offsets);
|
||||
}
|
||||
}
|
||||
|
||||
public function getAtlasPath():String
|
||||
{
|
||||
return Paths.animateAtlas(assetPath);
|
||||
}
|
||||
|
||||
public function getFreeplayDJText(index:Int):String {
|
||||
switch (index) {
|
||||
case 1: return text1;
|
||||
case 2: return text2;
|
||||
case 3: return text3;
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
public function getAnimationPrefix(name:String):Null<String>
|
||||
{
|
||||
if (animationMap.size() == 0) mapAnimations();
|
||||
|
||||
var anim = animationMap.get(name);
|
||||
if (anim == null) return null;
|
||||
return anim.prefix;
|
||||
}
|
||||
|
||||
public function getAnimationOffsetsByPrefix(?prefix:String):Array<Float>
|
||||
{
|
||||
if (prefixToOffsetsMap.size() == 0) mapAnimations();
|
||||
if (prefix == null) return [0, 0];
|
||||
return prefixToOffsetsMap.get(prefix);
|
||||
}
|
||||
|
||||
public function getAnimationOffsets(name:String):Array<Float>
|
||||
{
|
||||
return getAnimationOffsetsByPrefix(getAnimationPrefix(name));
|
||||
}
|
||||
|
||||
// TODO: These should really be frame labels, ehe.
|
||||
|
||||
public function getCartoonSoundClickFrame():Int
|
||||
{
|
||||
return cartoon?.soundClickFrame ?? 80;
|
||||
}
|
||||
|
||||
public function getCartoonSoundCartoonFrame():Int
|
||||
{
|
||||
return cartoon?.soundCartoonFrame ?? 85;
|
||||
}
|
||||
|
||||
public function getCartoonLoopBlinkFrame():Int
|
||||
{
|
||||
return cartoon?.loopBlinkFrame ?? 112;
|
||||
}
|
||||
|
||||
public function getCartoonLoopFrame():Int
|
||||
{
|
||||
return cartoon?.loopFrame ?? 166;
|
||||
}
|
||||
|
||||
public function getCartoonChannelChangeFrame():Int
|
||||
{
|
||||
return cartoon?.channelChangeFrame ?? 60;
|
||||
}
|
||||
}
|
||||
|
||||
typedef PlayerFreeplayDJCartoonData =
|
||||
{
|
||||
var soundClickFrame:Int;
|
||||
var soundCartoonFrame:Int;
|
||||
var loopBlinkFrame:Int;
|
||||
var loopFrame:Int;
|
||||
var channelChangeFrame:Int;
|
||||
}
|
151
source/funkin/data/freeplay/player/PlayerRegistry.hx
Normal file
151
source/funkin/data/freeplay/player/PlayerRegistry.hx
Normal file
|
@ -0,0 +1,151 @@
|
|||
package funkin.data.freeplay.player;
|
||||
|
||||
import funkin.data.freeplay.player.PlayerData;
|
||||
import funkin.ui.freeplay.charselect.PlayableCharacter;
|
||||
import funkin.ui.freeplay.charselect.ScriptedPlayableCharacter;
|
||||
|
||||
class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData>
|
||||
{
|
||||
/**
|
||||
* The current version string for the stage data format.
|
||||
* Handle breaking changes by incrementing this value
|
||||
* and adding migration to the `migratePlayerData()` function.
|
||||
*/
|
||||
public static final PLAYER_DATA_VERSION:thx.semver.Version = "1.0.0";
|
||||
|
||||
public static final PLAYER_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
|
||||
|
||||
public static var instance(get, never):PlayerRegistry;
|
||||
static var _instance:Null<PlayerRegistry> = null;
|
||||
|
||||
static function get_instance():PlayerRegistry
|
||||
{
|
||||
if (_instance == null) _instance = new PlayerRegistry();
|
||||
return _instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* A mapping between stage character IDs and Freeplay playable character IDs.
|
||||
*/
|
||||
var ownedCharacterIds:Map<String, String> = [];
|
||||
|
||||
public function new()
|
||||
{
|
||||
super('PLAYER', 'players', PLAYER_DATA_VERSION_RULE);
|
||||
}
|
||||
|
||||
public override function loadEntries():Void
|
||||
{
|
||||
super.loadEntries();
|
||||
|
||||
for (playerId in listEntryIds())
|
||||
{
|
||||
var player = fetchEntry(playerId);
|
||||
if (player == null) continue;
|
||||
|
||||
var currentPlayerCharIds = player.getOwnedCharacterIds();
|
||||
for (characterId in currentPlayerCharIds)
|
||||
{
|
||||
ownedCharacterIds.set(characterId, playerId);
|
||||
}
|
||||
}
|
||||
|
||||
log('Loaded ${countEntries()} playable characters with ${ownedCharacterIds.size()} associations.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the playable character associated with a given stage character.
|
||||
* @param characterId The stage character ID.
|
||||
* @return The playable character.
|
||||
*/
|
||||
public function getCharacterOwnerId(characterId:String):String
|
||||
{
|
||||
return ownedCharacterIds[characterId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the given stage character is associated with a specific playable character.
|
||||
* If so, the level should only appear if that character is selected in Freeplay.
|
||||
* @param characterId The stage character ID.
|
||||
* @return Whether the character is owned by any one character.
|
||||
*/
|
||||
public function isCharacterOwned(characterId:String):Bool
|
||||
{
|
||||
return ownedCharacterIds.exists(characterId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read, parse, and validate the JSON data and produce the corresponding data object.
|
||||
*/
|
||||
public function parseEntryData(id:String):Null<PlayerData>
|
||||
{
|
||||
// JsonParser does not take type parameters,
|
||||
// otherwise this function would be in BaseRegistry.
|
||||
var parser = new json2object.JsonParser<PlayerData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
|
||||
switch (loadEntryFile(id))
|
||||
{
|
||||
case {fileName: fileName, contents: contents}:
|
||||
parser.fromJson(contents, fileName);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, id);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and validate the JSON data and produce the corresponding data object.
|
||||
*
|
||||
* NOTE: Must be implemented on the implementation class.
|
||||
* @param contents The JSON as a string.
|
||||
* @param fileName An optional file name for error reporting.
|
||||
*/
|
||||
public function parseEntryDataRaw(contents:String, ?fileName:String):Null<PlayerData>
|
||||
{
|
||||
var parser = new json2object.JsonParser<PlayerData>();
|
||||
parser.ignoreUnknownVariables = false;
|
||||
parser.fromJson(contents, fileName);
|
||||
|
||||
if (parser.errors.length > 0)
|
||||
{
|
||||
printErrors(parser.errors, fileName);
|
||||
return null;
|
||||
}
|
||||
return parser.value;
|
||||
}
|
||||
|
||||
function createScriptedEntry(clsName:String):PlayableCharacter
|
||||
{
|
||||
return ScriptedPlayableCharacter.init(clsName, "unknown");
|
||||
}
|
||||
|
||||
function getScriptedClassNames():Array<String>
|
||||
{
|
||||
return ScriptedPlayableCharacter.listScriptClasses();
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of all the playable characters from the base game, in order.
|
||||
*/
|
||||
public function listBaseGamePlayerIds():Array<String>
|
||||
{
|
||||
return ["bf", "pico"];
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of all installed playable characters that are not from the base game.
|
||||
*/
|
||||
public function listModdedPlayerIds():Array<String>
|
||||
{
|
||||
return listEntryIds().filter(function(id:String):Bool {
|
||||
return listBaseGamePlayerIds().indexOf(id) == -1;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import funkin.data.event.SongEventRegistry;
|
|||
import funkin.data.story.level.LevelRegistry;
|
||||
import funkin.data.notestyle.NoteStyleRegistry;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.data.freeplay.player.PlayerRegistry;
|
||||
import funkin.data.stage.StageRegistry;
|
||||
import funkin.data.freeplay.album.AlbumRegistry;
|
||||
import funkin.modding.module.ModuleHandler;
|
||||
|
@ -369,15 +370,18 @@ class PolymodHandler
|
|||
|
||||
// These MUST be imported at the top of the file and not referred to by fully qualified name,
|
||||
// to ensure build macros work properly.
|
||||
SongEventRegistry.loadEventCache();
|
||||
|
||||
SongRegistry.instance.loadEntries();
|
||||
LevelRegistry.instance.loadEntries();
|
||||
NoteStyleRegistry.instance.loadEntries();
|
||||
SongEventRegistry.loadEventCache();
|
||||
PlayerRegistry.instance.loadEntries();
|
||||
ConversationRegistry.instance.loadEntries();
|
||||
DialogueBoxRegistry.instance.loadEntries();
|
||||
SpeakerRegistry.instance.loadEntries();
|
||||
AlbumRegistry.instance.loadEntries();
|
||||
StageRegistry.instance.loadEntries();
|
||||
|
||||
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
|
||||
ModuleHandler.loadModuleCache();
|
||||
}
|
||||
|
|
|
@ -590,7 +590,7 @@ enum abstract ScoringRank(String)
|
|||
}
|
||||
}
|
||||
|
||||
public function getFreeplayRankIconAsset():Null<String>
|
||||
public function getFreeplayRankIconAsset():String
|
||||
{
|
||||
switch (abstract)
|
||||
{
|
||||
|
@ -607,7 +607,7 @@ enum abstract ScoringRank(String)
|
|||
case SHIT:
|
||||
return 'LOSS';
|
||||
default:
|
||||
return null;
|
||||
return 'LOSS';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import funkin.data.song.SongData.SongTimeFormat;
|
|||
import funkin.data.song.SongRegistry;
|
||||
import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
|
||||
import funkin.modding.events.ScriptEvent;
|
||||
import funkin.ui.freeplay.charselect.PlayableCharacter;
|
||||
import funkin.util.SortUtil;
|
||||
import openfl.utils.Assets;
|
||||
|
||||
|
@ -401,11 +402,11 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
|||
return null;
|
||||
}
|
||||
|
||||
public function getFirstValidVariation(?diffId:String, ?possibleVariations:Array<String>):Null<String>
|
||||
public function getFirstValidVariation(?diffId:String, ?currentCharacter:PlayableCharacter, ?possibleVariations:Array<String>):Null<String>
|
||||
{
|
||||
if (possibleVariations == null)
|
||||
{
|
||||
possibleVariations = variations;
|
||||
possibleVariations = getVariationsByCharacter(currentCharacter);
|
||||
possibleVariations.sort(SortUtil.defaultsThenAlphabetically.bind(Constants.DEFAULT_VARIATION_LIST));
|
||||
}
|
||||
if (diffId == null) diffId = listDifficulties(null, possibleVariations)[0];
|
||||
|
@ -422,24 +423,31 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
|||
/**
|
||||
* Given that this character is selected in the Freeplay menu,
|
||||
* which variations should be available?
|
||||
* @param charId The character ID to query.
|
||||
* @param char The playable character to query.
|
||||
* @return An array of available variations.
|
||||
*/
|
||||
public function getVariationsByCharId(?charId:String):Array<String>
|
||||
public function getVariationsByCharacter(?char:PlayableCharacter):Array<String>
|
||||
{
|
||||
if (charId == null) charId = Constants.DEFAULT_CHARACTER;
|
||||
if (char == null) return variations;
|
||||
|
||||
if (variations.contains(charId))
|
||||
var result = [];
|
||||
trace('Evaluating variations for ${this.id} ${char.id}: ${this.variations}');
|
||||
for (variation in variations)
|
||||
{
|
||||
return [charId];
|
||||
}
|
||||
else
|
||||
var metadata = _metadata.get(variation);
|
||||
|
||||
var playerCharId = metadata?.playData?.characters?.player;
|
||||
if (playerCharId == null) continue;
|
||||
|
||||
if (char.shouldShowCharacter(playerCharId))
|
||||
{
|
||||
// TODO: How to exclude character variations while keeping other custom variations?
|
||||
return variations;
|
||||
result.push(variation);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all the difficulties in this song.
|
||||
*
|
||||
|
@ -455,6 +463,8 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
|
|||
if (variationIds == null) variationIds = [];
|
||||
if (variationId != null) variationIds.push(variationId);
|
||||
|
||||
if (variationIds.length == 0) return [];
|
||||
|
||||
// The difficulties array contains entries like 'normal', 'nightmare-erect', and 'normal-pico',
|
||||
// so we have to map it to the actual difficulty names.
|
||||
// We also filter out difficulties that don't match the variation or that don't exist.
|
||||
|
|
|
@ -808,8 +808,11 @@ class ChartEditorDialogHandler
|
|||
}
|
||||
songVariationMetadataEntry.onClick = onClickMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel);
|
||||
#if FILE_DROP_SUPPORTED
|
||||
state.addDropHandler({component: songVariationMetadataEntry, handler: onDropFileMetadataVariation.bind(variation)
|
||||
.bind(songVariationMetadataEntryLabel)});
|
||||
state.addDropHandler(
|
||||
{
|
||||
component: songVariationMetadataEntry,
|
||||
handler: onDropFileMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel)
|
||||
});
|
||||
#end
|
||||
chartContainerB.addComponent(songVariationMetadataEntry);
|
||||
|
||||
|
|
|
@ -1,371 +0,0 @@
|
|||
package funkin.ui.freeplay;
|
||||
|
||||
import flixel.FlxSprite;
|
||||
import flixel.util.FlxSignal;
|
||||
import funkin.util.assets.FlxAnimationUtil;
|
||||
import funkin.graphics.adobeanimate.FlxAtlasSprite;
|
||||
import funkin.audio.FunkinSound;
|
||||
import flixel.util.FlxTimer;
|
||||
import funkin.audio.FunkinSound;
|
||||
import funkin.audio.FlxStreamSound;
|
||||
|
||||
class DJBoyfriend extends FlxAtlasSprite
|
||||
{
|
||||
// Represents the sprite's current status.
|
||||
// Without state machines I would have driven myself crazy years ago.
|
||||
public var currentState:DJBoyfriendState = Intro;
|
||||
|
||||
// A callback activated when the intro animation finishes.
|
||||
public var onIntroDone:FlxSignal = new FlxSignal();
|
||||
|
||||
// A callback activated when Boyfriend gets spooked.
|
||||
public var onSpook:FlxSignal = new FlxSignal();
|
||||
|
||||
// playAnim stolen from Character.hx, cuz im lazy lol!
|
||||
// TODO: Switch this class to use SwagSprite instead.
|
||||
public var animOffsets:Map<String, Array<Dynamic>>;
|
||||
|
||||
var gotSpooked:Bool = false;
|
||||
|
||||
static final SPOOK_PERIOD:Float = 60.0;
|
||||
static final TV_PERIOD:Float = 120.0;
|
||||
|
||||
// Time since dad last SPOOKED you.
|
||||
var timeSinceSpook:Float = 0;
|
||||
|
||||
public function new(x:Float, y:Float)
|
||||
{
|
||||
super(x, y, Paths.animateAtlas("freeplay/freeplay-boyfriend", "preload"));
|
||||
|
||||
animOffsets = new Map<String, Array<Dynamic>>();
|
||||
|
||||
anim.callback = function(name, number) {
|
||||
switch (name)
|
||||
{
|
||||
case "Boyfriend DJ watchin tv OG":
|
||||
if (number == 80)
|
||||
{
|
||||
FunkinSound.playOnce(Paths.sound('remote_click'));
|
||||
}
|
||||
if (number == 85)
|
||||
{
|
||||
runTvLogic();
|
||||
}
|
||||
default:
|
||||
}
|
||||
};
|
||||
|
||||
setupAnimations();
|
||||
|
||||
FlxG.debugger.track(this);
|
||||
FlxG.console.registerObject("dj", this);
|
||||
|
||||
anim.onComplete = onFinishAnim;
|
||||
|
||||
FlxG.console.registerFunction("tv", function() {
|
||||
currentState = TV;
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
[remote hand under,boyfriend top head,brim piece,arm cringe l,red lazer,dj arm in,bf fist pump arm,hand raised right,forearm left,fist shaking,bf smile eyes closed face,arm cringe r,bf clenched face,face shrug,boyfriend falling,blue tint 1,shirt sleeve,bf clenched fist,head BF relaxed,blue tint 2,hand down left,blue tint 3,blue tint 4,head less smooshed,blue tint 5,boyfriend freeplay,BF head slight turn,blue tint 6,arm shrug l,blue tint 7,shoulder raised w sleeve,blue tint 8,fist pump face,blue tint 9,foot rested light,hand turnaround,arm chill right,Boyfriend DJ,arm shrug r,head back bf,hat top piece,dad bod,face surprise snap,Boyfriend DJ fist pump,office chair,foot rested right,chest down,office chair upright,body chill,bf dj afk,head mouth open dad,BF Head defalt HAIR BLOWING,hand shrug l,face piece,foot wag,turn table,shoulder up left,turntable lights,boyfriend dj body shirt blowing,body chunk turned,hand down right,dj arm out,hand shrug r,body chest out,rave hand,palm,chill face default,head back semi bf,boyfriend bottom head,DJ arm,shoulder right dad,bf surprise,boyfriend dj body,hs1,Boyfriend DJ watchin tv OG,spinning disk,hs2,arm chill left,boyfriend dj intro,hs3,hs4,chill face extra,hs5,remote hand upright,hs6,pant over table,face surprise,bf arm peace,arm turnaround,bf eyes 1,arm slammed table,eye squit,leg BF,head mid piece,arm backing,arm swoopin in,shoe right lowering,forearm right,hand out,blue tint 10,body falling back,remote thumb press,shoulder,hair spike single,bf bent
|
||||
arm,crt,foot raised right,dad hand,chill face 1,chill face 2,clenched fist,head SMOOSHED,shoulder left dad,df1,body chunk upright,df2,df3,df4,hat front piece,df5,foot rested right 2,hand in,arm spun,shoe raised left,bf 1 finger hand,bf mouth 1,Boyfriend DJ confirm,forearm down ,hand raised left,remote thumb up]
|
||||
*/
|
||||
override public function listAnimations():Array<String>
|
||||
{
|
||||
var anims:Array<String> = [];
|
||||
@:privateAccess
|
||||
for (animKey in anim.symbolDictionary)
|
||||
{
|
||||
anims.push(animKey.name);
|
||||
}
|
||||
return anims;
|
||||
}
|
||||
|
||||
var lowPumpLoopPoint:Int = 4;
|
||||
|
||||
public override function update(elapsed:Float):Void
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
switch (currentState)
|
||||
{
|
||||
case Intro:
|
||||
// Play the intro animation then leave this state immediately.
|
||||
if (getCurrentAnimation() != 'boyfriend dj intro') playFlashAnimation('boyfriend dj intro', true);
|
||||
timeSinceSpook = 0;
|
||||
case Idle:
|
||||
// We are in this state the majority of the time.
|
||||
if (getCurrentAnimation() != 'Boyfriend DJ')
|
||||
{
|
||||
playFlashAnimation('Boyfriend DJ', true);
|
||||
}
|
||||
|
||||
if (getCurrentAnimation() == 'Boyfriend DJ' && this.isLoopFinished())
|
||||
{
|
||||
if (timeSinceSpook >= SPOOK_PERIOD && !gotSpooked)
|
||||
{
|
||||
currentState = Spook;
|
||||
}
|
||||
else if (timeSinceSpook >= TV_PERIOD)
|
||||
{
|
||||
currentState = TV;
|
||||
}
|
||||
}
|
||||
timeSinceSpook += elapsed;
|
||||
case Confirm:
|
||||
if (getCurrentAnimation() != 'Boyfriend DJ confirm') playFlashAnimation('Boyfriend DJ confirm', false);
|
||||
timeSinceSpook = 0;
|
||||
case PumpIntro:
|
||||
if (getCurrentAnimation() != 'Boyfriend DJ fist pump') playFlashAnimation('Boyfriend DJ fist pump', false);
|
||||
if (getCurrentAnimation() == 'Boyfriend DJ fist pump' && anim.curFrame >= 4)
|
||||
{
|
||||
anim.play("Boyfriend DJ fist pump", true, false, 0);
|
||||
}
|
||||
case FistPump:
|
||||
|
||||
case Spook:
|
||||
if (getCurrentAnimation() != 'bf dj afk')
|
||||
{
|
||||
onSpook.dispatch();
|
||||
playFlashAnimation('bf dj afk', false);
|
||||
gotSpooked = true;
|
||||
}
|
||||
timeSinceSpook = 0;
|
||||
case TV:
|
||||
if (getCurrentAnimation() != 'Boyfriend DJ watchin tv OG') playFlashAnimation('Boyfriend DJ watchin tv OG', true);
|
||||
timeSinceSpook = 0;
|
||||
default:
|
||||
// I shit myself.
|
||||
}
|
||||
|
||||
if (FlxG.keys.pressed.CONTROL)
|
||||
{
|
||||
if (FlxG.keys.justPressed.LEFT)
|
||||
{
|
||||
this.offsetX -= FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0);
|
||||
}
|
||||
|
||||
if (FlxG.keys.justPressed.RIGHT)
|
||||
{
|
||||
this.offsetX += FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0);
|
||||
}
|
||||
|
||||
if (FlxG.keys.justPressed.UP)
|
||||
{
|
||||
this.offsetY -= FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0);
|
||||
}
|
||||
|
||||
if (FlxG.keys.justPressed.DOWN)
|
||||
{
|
||||
this.offsetY += FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0);
|
||||
}
|
||||
|
||||
if (FlxG.keys.justPressed.SPACE)
|
||||
{
|
||||
currentState = (currentState == Idle ? TV : Idle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onFinishAnim():Void
|
||||
{
|
||||
var name = anim.curSymbol.name;
|
||||
switch (name)
|
||||
{
|
||||
case "boyfriend dj intro":
|
||||
// trace('Finished intro');
|
||||
currentState = Idle;
|
||||
onIntroDone.dispatch();
|
||||
case "Boyfriend DJ":
|
||||
// trace('Finished idle');
|
||||
case "bf dj afk":
|
||||
// trace('Finished spook');
|
||||
currentState = Idle;
|
||||
case "Boyfriend DJ confirm":
|
||||
|
||||
case "Boyfriend DJ fist pump":
|
||||
currentState = Idle;
|
||||
|
||||
case "Boyfriend DJ loss reaction 1":
|
||||
currentState = Idle;
|
||||
|
||||
case "Boyfriend DJ watchin tv OG":
|
||||
var frame:Int = FlxG.random.bool(33) ? 112 : 166;
|
||||
|
||||
// BF switches channels when the video ends, or at a 10% chance each time his idle loops.
|
||||
if (FlxG.random.bool(5))
|
||||
{
|
||||
frame = 60;
|
||||
// boyfriend switches channel code?
|
||||
// runTvLogic();
|
||||
}
|
||||
trace('Replay idle: ${frame}');
|
||||
anim.play("Boyfriend DJ watchin tv OG", true, false, frame);
|
||||
// trace('Finished confirm');
|
||||
}
|
||||
}
|
||||
|
||||
public function resetAFKTimer():Void
|
||||
{
|
||||
timeSinceSpook = 0;
|
||||
gotSpooked = false;
|
||||
}
|
||||
|
||||
var offsetX:Float = 0.0;
|
||||
var offsetY:Float = 0.0;
|
||||
|
||||
function setupAnimations():Void
|
||||
{
|
||||
// Intro
|
||||
addOffset('boyfriend dj intro', 8.0 - 1.3, 3.0 - 0.4);
|
||||
|
||||
// Idle
|
||||
addOffset('Boyfriend DJ', 0, 0);
|
||||
|
||||
// Confirm
|
||||
addOffset('Boyfriend DJ confirm', 0, 0);
|
||||
|
||||
// AFK: Spook
|
||||
addOffset('bf dj afk', 649.5, 58.5);
|
||||
|
||||
// AFK: TV
|
||||
addOffset('Boyfriend DJ watchin tv OG', 0, 0);
|
||||
}
|
||||
|
||||
var cartoonSnd:Null<FunkinSound> = null;
|
||||
|
||||
public var playingCartoon:Bool = false;
|
||||
|
||||
public function runTvLogic()
|
||||
{
|
||||
if (cartoonSnd == null)
|
||||
{
|
||||
// tv is OFF, but getting turned on
|
||||
FunkinSound.playOnce(Paths.sound('tv_on'), 1.0, function() {
|
||||
loadCartoon();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// plays it smidge after the click
|
||||
FunkinSound.playOnce(Paths.sound('channel_switch'), 1.0, function() {
|
||||
cartoonSnd.destroy();
|
||||
loadCartoon();
|
||||
});
|
||||
}
|
||||
|
||||
// loadCartoon();
|
||||
}
|
||||
|
||||
function loadCartoon()
|
||||
{
|
||||
cartoonSnd = FunkinSound.load(Paths.sound(getRandomFlashToon()), 1.0, false, true, true, function() {
|
||||
anim.play("Boyfriend DJ watchin tv OG", true, false, 60);
|
||||
});
|
||||
|
||||
// Fade out music to 40% volume over 1 second.
|
||||
// This helps make the TV a bit more audible.
|
||||
FlxG.sound.music.fadeOut(1.0, 0.1);
|
||||
|
||||
// Play the cartoon at a random time between the start and 5 seconds from the end.
|
||||
cartoonSnd.time = FlxG.random.float(0, Math.max(cartoonSnd.length - (5 * Constants.MS_PER_SEC), 0.0));
|
||||
}
|
||||
|
||||
final cartoonList:Array<String> = openfl.utils.Assets.list().filter(function(path) return path.startsWith("assets/sounds/cartoons/"));
|
||||
|
||||
function getRandomFlashToon():String
|
||||
{
|
||||
var randomFile = FlxG.random.getObject(cartoonList);
|
||||
|
||||
// Strip folder prefix
|
||||
randomFile = randomFile.replace("assets/sounds/", "");
|
||||
// Strip file extension
|
||||
randomFile = randomFile.substring(0, randomFile.length - 4);
|
||||
|
||||
return randomFile;
|
||||
}
|
||||
|
||||
public function confirm():Void
|
||||
{
|
||||
currentState = Confirm;
|
||||
}
|
||||
|
||||
public function fistPump():Void
|
||||
{
|
||||
currentState = PumpIntro;
|
||||
}
|
||||
|
||||
public function pumpFist():Void
|
||||
{
|
||||
currentState = FistPump;
|
||||
anim.play("Boyfriend DJ fist pump", true, false, 4);
|
||||
}
|
||||
|
||||
public function pumpFistBad():Void
|
||||
{
|
||||
currentState = FistPump;
|
||||
anim.play("Boyfriend DJ loss reaction 1", true, false, 4);
|
||||
}
|
||||
|
||||
public inline function addOffset(name:String, x:Float = 0, y:Float = 0)
|
||||
{
|
||||
animOffsets[name] = [x, y];
|
||||
}
|
||||
|
||||
override public function getCurrentAnimation():String
|
||||
{
|
||||
if (this.anim == null || this.anim.curSymbol == null) return "";
|
||||
return this.anim.curSymbol.name;
|
||||
}
|
||||
|
||||
public function playFlashAnimation(id:String, ?Force:Bool = false, ?Reverse:Bool = false, ?Frame:Int = 0):Void
|
||||
{
|
||||
anim.play(id, Force, Reverse, Frame);
|
||||
applyAnimOffset();
|
||||
}
|
||||
|
||||
function applyAnimOffset()
|
||||
{
|
||||
var AnimName = getCurrentAnimation();
|
||||
var daOffset = animOffsets.get(AnimName);
|
||||
if (animOffsets.exists(AnimName))
|
||||
{
|
||||
var xValue = daOffset[0];
|
||||
var yValue = daOffset[1];
|
||||
if (AnimName == "Boyfriend DJ watchin tv OG")
|
||||
{
|
||||
xValue += offsetX;
|
||||
yValue += offsetY;
|
||||
}
|
||||
|
||||
offset.set(xValue, yValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
offset.set(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public override function destroy():Void
|
||||
{
|
||||
super.destroy();
|
||||
|
||||
if (cartoonSnd != null)
|
||||
{
|
||||
cartoonSnd.destroy();
|
||||
cartoonSnd = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum DJBoyfriendState
|
||||
{
|
||||
Intro;
|
||||
Idle;
|
||||
Confirm;
|
||||
PumpIntro;
|
||||
FistPump;
|
||||
Spook;
|
||||
TV;
|
||||
}
|
373
source/funkin/ui/freeplay/FreeplayDJ.hx
Normal file
373
source/funkin/ui/freeplay/FreeplayDJ.hx
Normal file
|
@ -0,0 +1,373 @@
|
|||
package funkin.ui.freeplay;
|
||||
|
||||
import flixel.FlxSprite;
|
||||
import flixel.util.FlxSignal;
|
||||
import funkin.util.assets.FlxAnimationUtil;
|
||||
import funkin.graphics.adobeanimate.FlxAtlasSprite;
|
||||
import funkin.audio.FunkinSound;
|
||||
import flixel.util.FlxTimer;
|
||||
import funkin.data.freeplay.player.PlayerRegistry;
|
||||
import funkin.data.freeplay.player.PlayerData.PlayerFreeplayDJData;
|
||||
import funkin.audio.FunkinSound;
|
||||
import funkin.audio.FlxStreamSound;
|
||||
|
||||
class FreeplayDJ extends FlxAtlasSprite
|
||||
{
|
||||
// Represents the sprite's current status.
|
||||
// Without state machines I would have driven myself crazy years ago.
|
||||
public var currentState:DJBoyfriendState = Intro;
|
||||
|
||||
// A callback activated when the intro animation finishes.
|
||||
public var onIntroDone:FlxSignal = new FlxSignal();
|
||||
|
||||
// A callback activated when the idle easter egg plays.
|
||||
public var onIdleEasterEgg:FlxSignal = new FlxSignal();
|
||||
|
||||
var seenIdleEasterEgg:Bool = false;
|
||||
|
||||
static final IDLE_EGG_PERIOD:Float = 60.0;
|
||||
static final IDLE_CARTOON_PERIOD:Float = 120.0;
|
||||
|
||||
// Time since last special idle animation you.
|
||||
var timeIdling:Float = 0;
|
||||
|
||||
final characterId:String = Constants.DEFAULT_CHARACTER;
|
||||
final playableCharData:PlayerFreeplayDJData;
|
||||
|
||||
public function new(x:Float, y:Float, characterId:String)
|
||||
{
|
||||
this.characterId = characterId;
|
||||
|
||||
var playableChar = PlayerRegistry.instance.fetchEntry(characterId);
|
||||
playableCharData = playableChar.getFreeplayDJData();
|
||||
|
||||
super(x, y, playableCharData.getAtlasPath());
|
||||
|
||||
anim.callback = function(name, number) {
|
||||
if (name == playableCharData.getAnimationPrefix('cartoon'))
|
||||
{
|
||||
if (number == playableCharData.getCartoonSoundClickFrame())
|
||||
{
|
||||
FunkinSound.playOnce(Paths.sound('remote_click'));
|
||||
}
|
||||
if (number == playableCharData.getCartoonSoundCartoonFrame())
|
||||
{
|
||||
runTvLogic();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
FlxG.debugger.track(this);
|
||||
FlxG.console.registerObject("dj", this);
|
||||
|
||||
anim.onComplete = onFinishAnim;
|
||||
|
||||
FlxG.console.registerFunction("freeplayCartoon", function() {
|
||||
currentState = Cartoon;
|
||||
});
|
||||
}
|
||||
|
||||
override public function listAnimations():Array<String>
|
||||
{
|
||||
var anims:Array<String> = [];
|
||||
@:privateAccess
|
||||
for (animKey in anim.symbolDictionary)
|
||||
{
|
||||
anims.push(animKey.name);
|
||||
}
|
||||
return anims;
|
||||
}
|
||||
|
||||
var lowPumpLoopPoint:Int = 4;
|
||||
|
||||
public override function update(elapsed:Float):Void
|
||||
{
|
||||
super.update(elapsed);
|
||||
|
||||
switch (currentState)
|
||||
{
|
||||
case Intro:
|
||||
// Play the intro animation then leave this state immediately.
|
||||
var animPrefix = playableCharData.getAnimationPrefix('intro');
|
||||
if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, true);
|
||||
timeIdling = 0;
|
||||
case Idle:
|
||||
// We are in this state the majority of the time.
|
||||
var animPrefix = playableCharData.getAnimationPrefix('idle');
|
||||
if (getCurrentAnimation() != animPrefix)
|
||||
{
|
||||
playFlashAnimation(animPrefix, true);
|
||||
}
|
||||
|
||||
if (getCurrentAnimation() == animPrefix && this.isLoopFinished())
|
||||
{
|
||||
if (timeIdling >= IDLE_EGG_PERIOD && !seenIdleEasterEgg)
|
||||
{
|
||||
currentState = IdleEasterEgg;
|
||||
}
|
||||
else if (timeIdling >= IDLE_CARTOON_PERIOD)
|
||||
{
|
||||
currentState = Cartoon;
|
||||
}
|
||||
}
|
||||
timeIdling += elapsed;
|
||||
case Confirm:
|
||||
var animPrefix = playableCharData.getAnimationPrefix('confirm');
|
||||
if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, false);
|
||||
timeIdling = 0;
|
||||
case FistPumpIntro:
|
||||
var animPrefix = playableCharData.getAnimationPrefix('fistPump');
|
||||
if (getCurrentAnimation() != animPrefix) playFlashAnimation('Boyfriend DJ fist pump', false);
|
||||
if (getCurrentAnimation() == animPrefix && anim.curFrame >= 4)
|
||||
{
|
||||
anim.play("Boyfriend DJ fist pump", true, false, 0);
|
||||
}
|
||||
case FistPump:
|
||||
|
||||
case IdleEasterEgg:
|
||||
var animPrefix = playableCharData.getAnimationPrefix('idleEasterEgg');
|
||||
if (getCurrentAnimation() != animPrefix)
|
||||
{
|
||||
onIdleEasterEgg.dispatch();
|
||||
playFlashAnimation(animPrefix, false);
|
||||
seenIdleEasterEgg = true;
|
||||
}
|
||||
timeIdling = 0;
|
||||
case Cartoon:
|
||||
var animPrefix = playableCharData.getAnimationPrefix('cartoon');
|
||||
if (animPrefix == null) {
|
||||
currentState = IdleEasterEgg;
|
||||
} else {
|
||||
if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, true);
|
||||
timeIdling = 0;
|
||||
}
|
||||
default:
|
||||
// I shit myself.
|
||||
}
|
||||
|
||||
if (FlxG.keys.pressed.CONTROL)
|
||||
{
|
||||
if (FlxG.keys.justPressed.LEFT)
|
||||
{
|
||||
this.offsetX -= FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0);
|
||||
}
|
||||
|
||||
if (FlxG.keys.justPressed.RIGHT)
|
||||
{
|
||||
this.offsetX += FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0);
|
||||
}
|
||||
|
||||
if (FlxG.keys.justPressed.UP)
|
||||
{
|
||||
this.offsetY -= FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0);
|
||||
}
|
||||
|
||||
if (FlxG.keys.justPressed.DOWN)
|
||||
{
|
||||
this.offsetY += FlxG.keys.pressed.ALT ? 0.1 : (FlxG.keys.pressed.SHIFT ? 10.0 : 1.0);
|
||||
}
|
||||
|
||||
if (FlxG.keys.justPressed.SPACE)
|
||||
{
|
||||
currentState = (currentState == Idle ? Cartoon : Idle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onFinishAnim():Void
|
||||
{
|
||||
var name = anim.curSymbol.name;
|
||||
|
||||
if (name == playableCharData.getAnimationPrefix('intro'))
|
||||
{
|
||||
currentState = Idle;
|
||||
onIntroDone.dispatch();
|
||||
}
|
||||
else if (name == playableCharData.getAnimationPrefix('idle'))
|
||||
{
|
||||
// trace('Finished idle');
|
||||
}
|
||||
else if (name == playableCharData.getAnimationPrefix('confirm'))
|
||||
{
|
||||
// trace('Finished confirm');
|
||||
}
|
||||
else if (name == playableCharData.getAnimationPrefix('fistPump'))
|
||||
{
|
||||
// trace('Finished fist pump');
|
||||
currentState = Idle;
|
||||
}
|
||||
else if (name == playableCharData.getAnimationPrefix('idleEasterEgg'))
|
||||
{
|
||||
// trace('Finished spook');
|
||||
currentState = Idle;
|
||||
}
|
||||
else if (name == playableCharData.getAnimationPrefix('loss'))
|
||||
{
|
||||
// trace('Finished loss reaction');
|
||||
currentState = Idle;
|
||||
}
|
||||
else if (name == playableCharData.getAnimationPrefix('cartoon'))
|
||||
{
|
||||
// trace('Finished cartoon');
|
||||
|
||||
var frame:Int = FlxG.random.bool(33) ? playableCharData.getCartoonLoopBlinkFrame() : playableCharData.getCartoonLoopFrame();
|
||||
|
||||
// Character switches channels when the video ends, or at a 10% chance each time his idle loops.
|
||||
if (FlxG.random.bool(5))
|
||||
{
|
||||
frame = playableCharData.getCartoonChannelChangeFrame();
|
||||
// boyfriend switches channel code?
|
||||
// runTvLogic();
|
||||
}
|
||||
trace('Replay idle: ${frame}');
|
||||
anim.play(playableCharData.getAnimationPrefix('cartoon'), true, false, frame);
|
||||
// trace('Finished confirm');
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('Finished ${name}');
|
||||
}
|
||||
}
|
||||
|
||||
public function resetAFKTimer():Void
|
||||
{
|
||||
timeIdling = 0;
|
||||
seenIdleEasterEgg = false;
|
||||
}
|
||||
|
||||
var offsetX:Float = 0.0;
|
||||
var offsetY:Float = 0.0;
|
||||
|
||||
var cartoonSnd:Null<FunkinSound> = null;
|
||||
|
||||
public var playingCartoon:Bool = false;
|
||||
|
||||
public function runTvLogic()
|
||||
{
|
||||
if (cartoonSnd == null)
|
||||
{
|
||||
// tv is OFF, but getting turned on
|
||||
FunkinSound.playOnce(Paths.sound('tv_on'), 1.0, function() {
|
||||
loadCartoon();
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// plays it smidge after the click
|
||||
FunkinSound.playOnce(Paths.sound('channel_switch'), 1.0, function() {
|
||||
cartoonSnd.destroy();
|
||||
loadCartoon();
|
||||
});
|
||||
}
|
||||
|
||||
// loadCartoon();
|
||||
}
|
||||
|
||||
function loadCartoon()
|
||||
{
|
||||
cartoonSnd = FunkinSound.load(Paths.sound(getRandomFlashToon()), 1.0, false, true, true, function() {
|
||||
anim.play("Boyfriend DJ watchin tv OG", true, false, 60);
|
||||
});
|
||||
|
||||
// Fade out music to 40% volume over 1 second.
|
||||
// This helps make the TV a bit more audible.
|
||||
FlxG.sound.music.fadeOut(1.0, 0.1);
|
||||
|
||||
// Play the cartoon at a random time between the start and 5 seconds from the end.
|
||||
cartoonSnd.time = FlxG.random.float(0, Math.max(cartoonSnd.length - (5 * Constants.MS_PER_SEC), 0.0));
|
||||
}
|
||||
|
||||
final cartoonList:Array<String> = openfl.utils.Assets.list().filter(function(path) return path.startsWith("assets/sounds/cartoons/"));
|
||||
|
||||
function getRandomFlashToon():String
|
||||
{
|
||||
var randomFile = FlxG.random.getObject(cartoonList);
|
||||
|
||||
// Strip folder prefix
|
||||
randomFile = randomFile.replace("assets/sounds/", "");
|
||||
// Strip file extension
|
||||
randomFile = randomFile.substring(0, randomFile.length - 4);
|
||||
|
||||
return randomFile;
|
||||
}
|
||||
|
||||
public function confirm():Void
|
||||
{
|
||||
currentState = Confirm;
|
||||
}
|
||||
|
||||
public function fistPump():Void
|
||||
{
|
||||
currentState = FistPumpIntro;
|
||||
}
|
||||
|
||||
public function pumpFist():Void
|
||||
{
|
||||
currentState = FistPump;
|
||||
anim.play("Boyfriend DJ fist pump", true, false, 4);
|
||||
}
|
||||
|
||||
public function pumpFistBad():Void
|
||||
{
|
||||
currentState = FistPump;
|
||||
anim.play("Boyfriend DJ loss reaction 1", true, false, 4);
|
||||
}
|
||||
|
||||
override public function getCurrentAnimation():String
|
||||
{
|
||||
if (this.anim == null || this.anim.curSymbol == null) return "";
|
||||
return this.anim.curSymbol.name;
|
||||
}
|
||||
|
||||
public function playFlashAnimation(id:String, ?Force:Bool = false, ?Reverse:Bool = false, ?Frame:Int = 0):Void
|
||||
{
|
||||
anim.play(id, Force, Reverse, Frame);
|
||||
applyAnimOffset();
|
||||
}
|
||||
|
||||
function applyAnimOffset()
|
||||
{
|
||||
var AnimName = getCurrentAnimation();
|
||||
var daOffset = playableCharData.getAnimationOffsetsByPrefix(AnimName);
|
||||
if (daOffset != null)
|
||||
{
|
||||
var xValue = daOffset[0];
|
||||
var yValue = daOffset[1];
|
||||
if (AnimName == "Boyfriend DJ watchin tv OG")
|
||||
{
|
||||
xValue += offsetX;
|
||||
yValue += offsetY;
|
||||
}
|
||||
|
||||
trace('Successfully applied offset ($AnimName): ' + xValue + ', ' + yValue);
|
||||
offset.set(xValue, yValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
trace('No offset found ($AnimName), defaulting to: 0, 0');
|
||||
offset.set(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public override function destroy():Void
|
||||
{
|
||||
super.destroy();
|
||||
|
||||
if (cartoonSnd != null)
|
||||
{
|
||||
cartoonSnd.destroy();
|
||||
cartoonSnd = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum DJBoyfriendState
|
||||
{
|
||||
Intro;
|
||||
Idle;
|
||||
Confirm;
|
||||
FistPumpIntro;
|
||||
FistPump;
|
||||
IdleEasterEgg;
|
||||
Cartoon;
|
||||
}
|
|
@ -1,52 +1,55 @@
|
|||
package funkin.ui.freeplay;
|
||||
|
||||
import funkin.graphics.adobeanimate.FlxAtlasSprite;
|
||||
import flixel.addons.transition.FlxTransitionableState;
|
||||
import flixel.addons.ui.FlxInputText;
|
||||
import flixel.FlxCamera;
|
||||
import flixel.FlxSprite;
|
||||
import flixel.group.FlxGroup;
|
||||
import funkin.graphics.shaders.GaussianBlurShader;
|
||||
import flixel.group.FlxGroup.FlxTypedGroup;
|
||||
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
|
||||
import flixel.input.touch.FlxTouch;
|
||||
import flixel.math.FlxAngle;
|
||||
import flixel.math.FlxPoint;
|
||||
import openfl.display.BlendMode;
|
||||
import flixel.system.debug.watch.Tracker.TrackerProfile;
|
||||
import flixel.text.FlxText;
|
||||
import flixel.tweens.FlxEase;
|
||||
import flixel.tweens.FlxTween;
|
||||
import flixel.tweens.misc.ShakeTween;
|
||||
import flixel.util.FlxColor;
|
||||
import flixel.util.FlxSpriteUtil;
|
||||
import flixel.util.FlxTimer;
|
||||
import funkin.audio.FunkinSound;
|
||||
import funkin.data.story.level.LevelRegistry;
|
||||
import funkin.data.freeplay.player.PlayerRegistry;
|
||||
import funkin.data.song.SongRegistry;
|
||||
import funkin.data.story.level.LevelRegistry;
|
||||
import funkin.effects.IntervalShake;
|
||||
import funkin.graphics.adobeanimate.FlxAtlasSprite;
|
||||
import funkin.graphics.FunkinCamera;
|
||||
import funkin.graphics.FunkinSprite;
|
||||
import funkin.graphics.shaders.AngleMask;
|
||||
import funkin.graphics.shaders.GaussianBlurShader;
|
||||
import funkin.graphics.shaders.HSVShader;
|
||||
import funkin.graphics.shaders.PureColor;
|
||||
import funkin.graphics.shaders.StrokeShader;
|
||||
import funkin.input.Controls;
|
||||
import funkin.play.PlayStatePlaylist;
|
||||
import funkin.play.scoring.Scoring;
|
||||
import funkin.play.scoring.Scoring.ScoringRank;
|
||||
import funkin.play.song.Song;
|
||||
import funkin.ui.story.Level;
|
||||
import funkin.save.Save;
|
||||
import funkin.save.Save.SaveScoreData;
|
||||
import funkin.ui.AtlasText;
|
||||
import funkin.play.scoring.Scoring;
|
||||
import funkin.play.scoring.Scoring.ScoringRank;
|
||||
import funkin.ui.freeplay.charselect.PlayableCharacter;
|
||||
import funkin.ui.freeplay.SongMenuItem.FreeplayRank;
|
||||
import funkin.ui.mainmenu.MainMenuState;
|
||||
import funkin.ui.MusicBeatSubState;
|
||||
import funkin.ui.story.Level;
|
||||
import funkin.ui.transition.LoadingState;
|
||||
import funkin.ui.transition.StickerSubState;
|
||||
import funkin.util.MathUtil;
|
||||
import funkin.util.SortUtil;
|
||||
import lime.utils.Assets;
|
||||
import flixel.tweens.misc.ShakeTween;
|
||||
import funkin.effects.IntervalShake;
|
||||
import funkin.ui.freeplay.SongMenuItem.FreeplayRank;
|
||||
import openfl.display.BlendMode;
|
||||
|
||||
/**
|
||||
* Parameters used to initialize the FreeplayState.
|
||||
|
@ -92,6 +95,7 @@ typedef FromResultsParams =
|
|||
/**
|
||||
* The state for the freeplay menu, allowing the player to select any song to play.
|
||||
*/
|
||||
@:nullSafety
|
||||
class FreeplayState extends MusicBeatSubState
|
||||
{
|
||||
//
|
||||
|
@ -102,7 +106,9 @@ class FreeplayState extends MusicBeatSubState
|
|||
* The current character for this FreeplayState.
|
||||
* You can't change this without transitioning to a new FreeplayState.
|
||||
*/
|
||||
final currentCharacter:String;
|
||||
final currentCharacterId:String;
|
||||
|
||||
final currentCharacter:PlayableCharacter;
|
||||
|
||||
/**
|
||||
* For the audio preview, the duration of the fade-in effect.
|
||||
|
@ -160,10 +166,9 @@ class FreeplayState extends MusicBeatSubState
|
|||
|
||||
var grpSongs:FlxTypedGroup<Alphabet>;
|
||||
var grpCapsules:FlxTypedGroup<SongMenuItem>;
|
||||
var curCapsule:SongMenuItem;
|
||||
var curPlaying:Bool = false;
|
||||
|
||||
var dj:DJBoyfriend;
|
||||
var dj:Null<FreeplayDJ> = null;
|
||||
|
||||
var ostName:FlxText;
|
||||
var albumRoll:AlbumRoll;
|
||||
|
@ -171,7 +176,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
var letterSort:LetterSort;
|
||||
var exitMovers:ExitMoverData = new Map();
|
||||
|
||||
var stickerSubState:StickerSubState;
|
||||
var stickerSubState:Null<StickerSubState> = null;
|
||||
|
||||
public static var rememberedDifficulty:Null<String> = Constants.DEFAULT_DIFFICULTY;
|
||||
public static var rememberedSongId:Null<String> = 'tutorial';
|
||||
|
@ -205,7 +210,13 @@ class FreeplayState extends MusicBeatSubState
|
|||
|
||||
public function new(?params:FreeplayStateParams, ?stickers:StickerSubState)
|
||||
{
|
||||
currentCharacter = params?.character ?? Constants.DEFAULT_CHARACTER;
|
||||
currentCharacterId = params?.character ?? Constants.DEFAULT_CHARACTER;
|
||||
var fetchPlayableCharacter = function():PlayableCharacter {
|
||||
var result = PlayerRegistry.instance.fetchEntry(params?.character ?? Constants.DEFAULT_CHARACTER);
|
||||
if (result == null) throw 'No valid playable character with id ${params?.character}';
|
||||
return result;
|
||||
};
|
||||
currentCharacter = fetchPlayableCharacter();
|
||||
|
||||
fromResultsParams = params?.fromResults;
|
||||
|
||||
|
@ -214,12 +225,54 @@ class FreeplayState extends MusicBeatSubState
|
|||
prepForNewRank = true;
|
||||
}
|
||||
|
||||
super(FlxColor.TRANSPARENT);
|
||||
|
||||
if (stickers?.members != null)
|
||||
{
|
||||
stickerSubState = stickers;
|
||||
}
|
||||
|
||||
super(FlxColor.TRANSPARENT);
|
||||
// We build a bunch of sprites BEFORE create() so we can guarantee they aren't null later on.
|
||||
albumRoll = new AlbumRoll();
|
||||
fp = new FreeplayScore(460, 60, 7, 100);
|
||||
cardGlow = new FlxSprite(-30, -30).loadGraphic(Paths.image('freeplay/cardGlow'));
|
||||
confirmGlow = new FlxSprite(-30, 240).loadGraphic(Paths.image('freeplay/confirmGlow'));
|
||||
confirmTextGlow = new FlxSprite(-8, 115).loadGraphic(Paths.image('freeplay/glowingText'));
|
||||
rankCamera = new FunkinCamera('rankCamera', 0, 0, FlxG.width, FlxG.height);
|
||||
funnyCam = new FunkinCamera('freeplayFunny', 0, 0, FlxG.width, FlxG.height);
|
||||
funnyScroll = new BGScrollingText(0, 220, currentCharacter.getFreeplayDJText(1), FlxG.width / 2, false, 60);
|
||||
funnyScroll2 = new BGScrollingText(0, 335, currentCharacter.getFreeplayDJText(1), FlxG.width / 2, false, 60);
|
||||
grpCapsules = new FlxTypedGroup<SongMenuItem>();
|
||||
grpDifficulties = new FlxTypedSpriteGroup<DifficultySprite>(-300, 80);
|
||||
letterSort = new LetterSort(400, 75);
|
||||
grpSongs = new FlxTypedGroup<Alphabet>();
|
||||
moreWays = new BGScrollingText(0, 160, currentCharacter.getFreeplayDJText(2), FlxG.width, true, 43);
|
||||
moreWays2 = new BGScrollingText(0, 397, currentCharacter.getFreeplayDJText(2), FlxG.width, true, 43);
|
||||
pinkBack = FunkinSprite.create('freeplay/pinkBack');
|
||||
rankBg = new FunkinSprite(0, 0);
|
||||
rankVignette = new FlxSprite(0, 0).loadGraphic(Paths.image('freeplay/rankVignette'));
|
||||
sparks = new FlxSprite(0, 0);
|
||||
sparksADD = new FlxSprite(0, 0);
|
||||
txtCompletion = new AtlasText(1185, 87, '69', AtlasFont.FREEPLAY_CLEAR);
|
||||
txtNuts = new BGScrollingText(0, 285, currentCharacter.getFreeplayDJText(3), FlxG.width / 2, true, 43);
|
||||
|
||||
ostName = new FlxText(8, 8, FlxG.width - 8 - 8, 'OFFICIAL OST', 48);
|
||||
|
||||
orangeBackShit = new FunkinSprite(84, 440).makeSolidColor(Std.int(pinkBack.width), 75, 0xFFFEDA00);
|
||||
|
||||
bgDad = new FlxSprite(pinkBack.width * 0.74, 0).loadGraphic(Paths.image('freeplay/freeplayBGdad'));
|
||||
alsoOrangeLOL = new FunkinSprite(0, orangeBackShit.y).makeSolidColor(100, Std.int(orangeBackShit.height), 0xFFFFD400);
|
||||
confirmGlow2 = new FlxSprite(confirmGlow.x, confirmGlow.y).loadGraphic(Paths.image('freeplay/confirmGlow2'));
|
||||
funnyScroll3 = new BGScrollingText(0, orangeBackShit.y + 10, currentCharacter.getFreeplayDJText(1), FlxG.width / 2, 60);
|
||||
backingTextYeah = new FlxAtlasSprite(640, 370, Paths.animateAtlas("freeplay/backing-text-yeah"),
|
||||
{
|
||||
FrameRate: 24.0,
|
||||
Reversed: false,
|
||||
// ?OnComplete:Void -> Void,
|
||||
ShowPivot: false,
|
||||
Antialiasing: true,
|
||||
ScrollFactor: new FlxPoint(1, 1),
|
||||
});
|
||||
}
|
||||
|
||||
override function create():Void
|
||||
|
@ -230,12 +283,6 @@ class FreeplayState extends MusicBeatSubState
|
|||
|
||||
FlxTransitionableState.skipNextTransIn = true;
|
||||
|
||||
// dedicated camera for the state so we don't need to fuk around with camera scrolls from the mainmenu / elsewhere
|
||||
funnyCam = new FunkinCamera('freeplayFunny', 0, 0, FlxG.width, FlxG.height);
|
||||
funnyCam.bgColor = FlxColor.TRANSPARENT;
|
||||
FlxG.cameras.add(funnyCam, false);
|
||||
this.cameras = [funnyCam];
|
||||
|
||||
if (stickerSubState != null)
|
||||
{
|
||||
this.persistentUpdate = true;
|
||||
|
@ -271,7 +318,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
// programmatically adds the songs via LevelRegistry and SongRegistry
|
||||
for (levelId in LevelRegistry.instance.listSortedLevelIds())
|
||||
{
|
||||
var level:Level = LevelRegistry.instance.fetchEntry(levelId);
|
||||
var level:Null<Level> = LevelRegistry.instance.fetchEntry(levelId);
|
||||
|
||||
if (level == null)
|
||||
{
|
||||
|
@ -281,7 +328,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
|
||||
for (songId in level.getSongs())
|
||||
{
|
||||
var song:Song = SongRegistry.instance.fetchEntry(songId);
|
||||
var song:Null<Song> = SongRegistry.instance.fetchEntry(songId);
|
||||
|
||||
if (song == null)
|
||||
{
|
||||
|
@ -290,11 +337,10 @@ class FreeplayState extends MusicBeatSubState
|
|||
}
|
||||
|
||||
// Only display songs which actually have available difficulties for the current character.
|
||||
var displayedVariations = song.getVariationsByCharId(currentCharacter);
|
||||
trace(songId);
|
||||
trace(displayedVariations);
|
||||
var displayedVariations = song.getVariationsByCharacter(currentCharacter);
|
||||
trace('Displayed Variations (${songId}): $displayedVariations');
|
||||
var availableDifficultiesForSong:Array<String> = song.listDifficulties(displayedVariations, false);
|
||||
trace(availableDifficultiesForSong);
|
||||
trace('Available Difficulties: $availableDifficultiesForSong');
|
||||
if (availableDifficultiesForSong.length == 0) continue;
|
||||
|
||||
songs.push(new FreeplaySongData(levelId, songId, song, displayedVariations));
|
||||
|
@ -314,17 +360,14 @@ class FreeplayState extends MusicBeatSubState
|
|||
trace(FlxG.camera.initialZoom);
|
||||
trace(FlxCamera.defaultZoom);
|
||||
|
||||
pinkBack = FunkinSprite.create('freeplay/pinkBack');
|
||||
pinkBack.color = 0xFFFFD4E9; // sets it to pink!
|
||||
pinkBack.x -= pinkBack.width;
|
||||
|
||||
FlxTween.tween(pinkBack, {x: 0}, 0.6, {ease: FlxEase.quartOut});
|
||||
add(pinkBack);
|
||||
|
||||
orangeBackShit = new FunkinSprite(84, 440).makeSolidColor(Std.int(pinkBack.width), 75, 0xFFFEDA00);
|
||||
add(orangeBackShit);
|
||||
|
||||
alsoOrangeLOL = new FunkinSprite(0, orangeBackShit.y).makeSolidColor(100, Std.int(orangeBackShit.height), 0xFFFFD400);
|
||||
add(alsoOrangeLOL);
|
||||
|
||||
exitMovers.set([pinkBack, orangeBackShit, alsoOrangeLOL],
|
||||
|
@ -339,15 +382,11 @@ class FreeplayState extends MusicBeatSubState
|
|||
orangeBackShit.visible = false;
|
||||
alsoOrangeLOL.visible = false;
|
||||
|
||||
confirmTextGlow = new FlxSprite(-8, 115).loadGraphic(Paths.image('freeplay/glowingText'));
|
||||
confirmTextGlow.blend = BlendMode.ADD;
|
||||
confirmTextGlow.visible = false;
|
||||
|
||||
confirmGlow = new FlxSprite(-30, 240).loadGraphic(Paths.image('freeplay/confirmGlow'));
|
||||
confirmGlow.blend = BlendMode.ADD;
|
||||
|
||||
confirmGlow2 = new FlxSprite(confirmGlow.x, confirmGlow.y).loadGraphic(Paths.image('freeplay/confirmGlow2'));
|
||||
|
||||
confirmGlow.visible = false;
|
||||
confirmGlow2.visible = false;
|
||||
|
||||
|
@ -362,7 +401,6 @@ class FreeplayState extends MusicBeatSubState
|
|||
|
||||
FlxG.debugger.addTrackerProfile(new TrackerProfile(BGScrollingText, ['x', 'y', 'speed', 'size']));
|
||||
|
||||
moreWays = new BGScrollingText(0, 160, 'HOT BLOODED IN MORE WAYS THAN ONE', FlxG.width, true, 43);
|
||||
moreWays.funnyColor = 0xFFFFF383;
|
||||
moreWays.speed = 6.8;
|
||||
grpTxtScrolls.add(moreWays);
|
||||
|
@ -373,7 +411,6 @@ class FreeplayState extends MusicBeatSubState
|
|||
speed: 0.4,
|
||||
});
|
||||
|
||||
funnyScroll = new BGScrollingText(0, 220, 'BOYFRIEND', FlxG.width / 2, false, 60);
|
||||
funnyScroll.funnyColor = 0xFFFF9963;
|
||||
funnyScroll.speed = -3.8;
|
||||
grpTxtScrolls.add(funnyScroll);
|
||||
|
@ -386,7 +423,6 @@ class FreeplayState extends MusicBeatSubState
|
|||
wait: 0
|
||||
});
|
||||
|
||||
txtNuts = new BGScrollingText(0, 285, 'PROTECT YO NUTS', FlxG.width / 2, true, 43);
|
||||
txtNuts.speed = 3.5;
|
||||
grpTxtScrolls.add(txtNuts);
|
||||
exitMovers.set([txtNuts],
|
||||
|
@ -395,7 +431,6 @@ class FreeplayState extends MusicBeatSubState
|
|||
speed: 0.4,
|
||||
});
|
||||
|
||||
funnyScroll2 = new BGScrollingText(0, 335, 'BOYFRIEND', FlxG.width / 2, false, 60);
|
||||
funnyScroll2.funnyColor = 0xFFFF9963;
|
||||
funnyScroll2.speed = -3.8;
|
||||
grpTxtScrolls.add(funnyScroll2);
|
||||
|
@ -406,7 +441,6 @@ class FreeplayState extends MusicBeatSubState
|
|||
speed: 0.5,
|
||||
});
|
||||
|
||||
moreWays2 = new BGScrollingText(0, 397, 'HOT BLOODED IN MORE WAYS THAN ONE', FlxG.width, true, 43);
|
||||
moreWays2.funnyColor = 0xFFFFF383;
|
||||
moreWays2.speed = 6.8;
|
||||
grpTxtScrolls.add(moreWays2);
|
||||
|
@ -417,7 +451,6 @@ class FreeplayState extends MusicBeatSubState
|
|||
speed: 0.4
|
||||
});
|
||||
|
||||
funnyScroll3 = new BGScrollingText(0, orangeBackShit.y + 10, 'BOYFRIEND', FlxG.width / 2, 60);
|
||||
funnyScroll3.funnyColor = 0xFFFEA400;
|
||||
funnyScroll3.speed = -3.8;
|
||||
grpTxtScrolls.add(funnyScroll3);
|
||||
|
@ -428,37 +461,24 @@ class FreeplayState extends MusicBeatSubState
|
|||
speed: 0.3
|
||||
});
|
||||
|
||||
backingTextYeah = new FlxAtlasSprite(640, 370, Paths.animateAtlas("freeplay/backing-text-yeah"),
|
||||
{
|
||||
FrameRate: 24.0,
|
||||
Reversed: false,
|
||||
// ?OnComplete:Void -> Void,
|
||||
ShowPivot: false,
|
||||
Antialiasing: true,
|
||||
ScrollFactor: new FlxPoint(1, 1),
|
||||
});
|
||||
|
||||
add(backingTextYeah);
|
||||
|
||||
cardGlow = new FlxSprite(-30, -30).loadGraphic(Paths.image('freeplay/cardGlow'));
|
||||
cardGlow.blend = BlendMode.ADD;
|
||||
cardGlow.visible = false;
|
||||
|
||||
add(cardGlow);
|
||||
|
||||
dj = new DJBoyfriend(640, 366);
|
||||
if (currentCharacter?.getFreeplayDJData() != null)
|
||||
{
|
||||
dj = new FreeplayDJ(640, 366, currentCharacterId);
|
||||
exitMovers.set([dj],
|
||||
{
|
||||
x: -dj.width * 1.6,
|
||||
speed: 0.5
|
||||
});
|
||||
|
||||
// TODO: Replace this.
|
||||
if (currentCharacter == 'pico') dj.visible = false;
|
||||
|
||||
add(dj);
|
||||
}
|
||||
|
||||
bgDad = new FlxSprite(pinkBack.width * 0.74, 0).loadGraphic(Paths.image('freeplay/freeplayBGdad'));
|
||||
bgDad.shader = new AngleMask();
|
||||
bgDad.visible = false;
|
||||
|
||||
|
@ -484,17 +504,13 @@ class FreeplayState extends MusicBeatSubState
|
|||
|
||||
blackOverlayBullshitLOLXD.shader = bgDad.shader;
|
||||
|
||||
rankBg = new FunkinSprite(0, 0);
|
||||
rankBg.makeSolidColor(FlxG.width, FlxG.height, 0xD3000000);
|
||||
add(rankBg);
|
||||
|
||||
grpSongs = new FlxTypedGroup<Alphabet>();
|
||||
add(grpSongs);
|
||||
|
||||
grpCapsules = new FlxTypedGroup<SongMenuItem>();
|
||||
add(grpCapsules);
|
||||
|
||||
grpDifficulties = new FlxTypedSpriteGroup<DifficultySprite>(-300, 80);
|
||||
add(grpDifficulties);
|
||||
|
||||
exitMovers.set([grpDifficulties],
|
||||
|
@ -521,7 +537,6 @@ class FreeplayState extends MusicBeatSubState
|
|||
if (diffSprite.difficultyId == currentDifficulty) diffSprite.visible = true;
|
||||
}
|
||||
|
||||
albumRoll = new AlbumRoll();
|
||||
albumRoll.albumId = null;
|
||||
add(albumRoll);
|
||||
|
||||
|
@ -536,7 +551,6 @@ class FreeplayState extends MusicBeatSubState
|
|||
fnfFreeplay.font = 'VCR OSD Mono';
|
||||
fnfFreeplay.visible = false;
|
||||
|
||||
ostName = new FlxText(8, 8, FlxG.width - 8 - 8, 'OFFICIAL OST', 48);
|
||||
ostName.font = 'VCR OSD Mono';
|
||||
ostName.alignment = RIGHT;
|
||||
ostName.visible = false;
|
||||
|
@ -568,7 +582,6 @@ class FreeplayState extends MusicBeatSubState
|
|||
tmr.time = FlxG.random.float(20, 60);
|
||||
}, 0);
|
||||
|
||||
fp = new FreeplayScore(460, 60, 7, 100);
|
||||
fp.visible = false;
|
||||
add(fp);
|
||||
|
||||
|
@ -576,11 +589,9 @@ class FreeplayState extends MusicBeatSubState
|
|||
clearBoxSprite.visible = false;
|
||||
add(clearBoxSprite);
|
||||
|
||||
txtCompletion = new AtlasText(1185, 87, '69', AtlasFont.FREEPLAY_CLEAR);
|
||||
txtCompletion.visible = false;
|
||||
add(txtCompletion);
|
||||
|
||||
letterSort = new LetterSort(400, 75);
|
||||
add(letterSort);
|
||||
letterSort.visible = false;
|
||||
|
||||
|
@ -628,7 +639,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
|
||||
// be careful not to "add()" things in here unless it's to a group that's already added to the state
|
||||
// otherwise it won't be properly attatched to funnyCamera (relavent code should be at the bottom of create())
|
||||
dj.onIntroDone.add(function() {
|
||||
var onDJIntroDone = function() {
|
||||
// when boyfriend hits dat shiii
|
||||
|
||||
albumRoll.playIntro();
|
||||
|
@ -675,20 +686,27 @@ class FreeplayState extends MusicBeatSubState
|
|||
cardGlow.visible = true;
|
||||
FlxTween.tween(cardGlow, {alpha: 0, "scale.x": 1.2, "scale.y": 1.2}, 0.45, {ease: FlxEase.sineOut});
|
||||
|
||||
if (prepForNewRank)
|
||||
if (prepForNewRank && fromResultsParams != null)
|
||||
{
|
||||
rankAnimStart(fromResultsParams);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (dj != null)
|
||||
{
|
||||
dj.onIntroDone.add(onDJIntroDone);
|
||||
}
|
||||
else
|
||||
{
|
||||
onDJIntroDone();
|
||||
}
|
||||
|
||||
generateSongList(null, false);
|
||||
|
||||
// dedicated camera for the state so we don't need to fuk around with camera scrolls from the mainmenu / elsewhere
|
||||
funnyCam = new FunkinCamera('freeplayFunny', 0, 0, FlxG.width, FlxG.height);
|
||||
funnyCam.bgColor = FlxColor.TRANSPARENT;
|
||||
FlxG.cameras.add(funnyCam, false);
|
||||
|
||||
rankVignette = new FlxSprite(0, 0).loadGraphic(Paths.image('freeplay/rankVignette'));
|
||||
rankVignette.scale.set(2, 2);
|
||||
rankVignette.updateHitbox();
|
||||
rankVignette.blend = BlendMode.ADD;
|
||||
|
@ -700,7 +718,6 @@ class FreeplayState extends MusicBeatSubState
|
|||
bs.cameras = [funnyCam];
|
||||
});
|
||||
|
||||
rankCamera = new FunkinCamera('rankCamera', 0, 0, FlxG.width, FlxG.height);
|
||||
rankCamera.bgColor = FlxColor.TRANSPARENT;
|
||||
FlxG.cameras.add(rankCamera, false);
|
||||
rankBg.cameras = [rankCamera];
|
||||
|
@ -712,8 +729,8 @@ class FreeplayState extends MusicBeatSubState
|
|||
}
|
||||
}
|
||||
|
||||
var currentFilter:SongFilter = null;
|
||||
var currentFilteredSongs:Array<FreeplaySongData> = [];
|
||||
var currentFilter:Null<SongFilter> = null;
|
||||
var currentFilteredSongs:Array<Null<FreeplaySongData>> = [];
|
||||
|
||||
/**
|
||||
* Given the current filter, rebuild the current song list.
|
||||
|
@ -724,7 +741,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
*/
|
||||
public function generateSongList(filterStuff:Null<SongFilter>, force:Bool = false, onlyIfChanged:Bool = true):Void
|
||||
{
|
||||
var tempSongs:Array<FreeplaySongData> = songs;
|
||||
var tempSongs:Array<Null<FreeplaySongData>> = songs;
|
||||
|
||||
// Remember just the difficulty because it's important for song sorting.
|
||||
if (rememberedDifficulty != null)
|
||||
|
@ -786,11 +803,12 @@ class FreeplayState extends MusicBeatSubState
|
|||
|
||||
for (i in 0...tempSongs.length)
|
||||
{
|
||||
if (tempSongs[i] == null) continue;
|
||||
var tempSong = tempSongs[i];
|
||||
if (tempSong == null) continue;
|
||||
|
||||
var funnyMenu:SongMenuItem = grpCapsules.recycle(SongMenuItem);
|
||||
|
||||
funnyMenu.init(FlxG.width, 0, tempSongs[i]);
|
||||
funnyMenu.init(FlxG.width, 0, tempSong);
|
||||
funnyMenu.onConfirm = function() {
|
||||
capsuleOnConfirmDefault(funnyMenu);
|
||||
};
|
||||
|
@ -799,8 +817,8 @@ class FreeplayState extends MusicBeatSubState
|
|||
funnyMenu.ID = i;
|
||||
funnyMenu.capsule.alpha = 0.5;
|
||||
funnyMenu.songText.visible = false;
|
||||
funnyMenu.favIcon.visible = tempSongs[i].isFav;
|
||||
funnyMenu.favIconBlurred.visible = tempSongs[i].isFav;
|
||||
funnyMenu.favIcon.visible = tempSong.isFav;
|
||||
funnyMenu.favIconBlurred.visible = tempSong.isFav;
|
||||
funnyMenu.hsvShader = hsvShader;
|
||||
|
||||
funnyMenu.newText.animation.curAnim.curFrame = 45 - ((i * 4) % 45);
|
||||
|
@ -824,13 +842,10 @@ class FreeplayState extends MusicBeatSubState
|
|||
* @param songFilter The filter to apply
|
||||
* @return Array<FreeplaySongData>
|
||||
*/
|
||||
public function sortSongs(songsToFilter:Array<FreeplaySongData>, songFilter:SongFilter):Array<FreeplaySongData>
|
||||
public function sortSongs(songsToFilter:Array<Null<FreeplaySongData>>, songFilter:SongFilter):Array<Null<FreeplaySongData>>
|
||||
{
|
||||
var filterAlphabetically = function(a:FreeplaySongData, b:FreeplaySongData):Int {
|
||||
if (a?.songName.toLowerCase() < b?.songName.toLowerCase()) return -1;
|
||||
else if (a?.songName.toLowerCase() > b?.songName.toLowerCase()) return 1;
|
||||
else
|
||||
return 0;
|
||||
var filterAlphabetically = function(a:Null<FreeplaySongData>, b:Null<FreeplaySongData>):Int {
|
||||
return SortUtil.alphabetically(a?.songName ?? '', b?.songName ?? '');
|
||||
};
|
||||
|
||||
switch (songFilter.filterType)
|
||||
|
@ -854,7 +869,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
|
||||
songsToFilter = songsToFilter.filter(str -> {
|
||||
if (str == null) return true; // Random
|
||||
return str.songName.toLowerCase().startsWith(songFilter.filterData);
|
||||
return str.songName.toLowerCase().startsWith(songFilter.filterData ?? '');
|
||||
});
|
||||
case ALL:
|
||||
// no filter!
|
||||
|
@ -876,32 +891,28 @@ class FreeplayState extends MusicBeatSubState
|
|||
var sparks:FlxSprite;
|
||||
var sparksADD:FlxSprite;
|
||||
|
||||
function rankAnimStart(fromResults:Null<FromResultsParams>):Void
|
||||
function rankAnimStart(fromResults:FromResultsParams):Void
|
||||
{
|
||||
busy = true;
|
||||
grpCapsules.members[curSelected].sparkle.alpha = 0;
|
||||
// grpCapsules.members[curSelected].forcePosition();
|
||||
|
||||
if (fromResults != null)
|
||||
{
|
||||
rememberedSongId = fromResults.songId;
|
||||
rememberedDifficulty = fromResults.difficultyId;
|
||||
changeSelection();
|
||||
changeDiff();
|
||||
}
|
||||
|
||||
dj.fistPump();
|
||||
if (dj != null) dj.fistPump();
|
||||
// rankCamera.fade(FlxColor.BLACK, 0.5, true);
|
||||
rankCamera.fade(0xFF000000, 0.5, true, null, true);
|
||||
if (FlxG.sound.music != null) FlxG.sound.music.volume = 0;
|
||||
rankBg.alpha = 1;
|
||||
|
||||
if (fromResults?.oldRank != null)
|
||||
if (fromResults.oldRank != null)
|
||||
{
|
||||
grpCapsules.members[curSelected].fakeRanking.rank = fromResults.oldRank;
|
||||
grpCapsules.members[curSelected].fakeBlurredRanking.rank = fromResults.oldRank;
|
||||
|
||||
sparks = new FlxSprite(0, 0);
|
||||
sparks.frames = Paths.getSparrowAtlas('freeplay/sparks');
|
||||
sparks.animation.addByPrefix('sparks', 'sparks', 24, false);
|
||||
sparks.visible = false;
|
||||
|
@ -911,7 +922,6 @@ class FreeplayState extends MusicBeatSubState
|
|||
add(sparks);
|
||||
sparks.cameras = [rankCamera];
|
||||
|
||||
sparksADD = new FlxSprite(0, 0);
|
||||
sparksADD.visible = false;
|
||||
sparksADD.frames = Paths.getSparrowAtlas('freeplay/sparksadd');
|
||||
sparksADD.animation.addByPrefix('sparks add', 'sparks add', 24, false);
|
||||
|
@ -976,14 +986,14 @@ class FreeplayState extends MusicBeatSubState
|
|||
grpCapsules.members[curSelected].ranking.scale.set(20, 20);
|
||||
grpCapsules.members[curSelected].blurredRanking.scale.set(20, 20);
|
||||
|
||||
if (fromResults?.newRank != null)
|
||||
if (fromResults != null && fromResults.newRank != null)
|
||||
{
|
||||
grpCapsules.members[curSelected].ranking.animation.play(fromResults.newRank.getFreeplayRankIconAsset(), true);
|
||||
}
|
||||
|
||||
FlxTween.tween(grpCapsules.members[curSelected].ranking, {"scale.x": 1, "scale.y": 1}, 0.1);
|
||||
|
||||
if (fromResults?.newRank != null)
|
||||
if (fromResults != null && fromResults.newRank != null)
|
||||
{
|
||||
grpCapsules.members[curSelected].blurredRanking.animation.play(fromResults.newRank.getFreeplayRankIconAsset(), true);
|
||||
}
|
||||
|
@ -1074,11 +1084,11 @@ class FreeplayState extends MusicBeatSubState
|
|||
|
||||
if (fromResultsParams?.newRank == SHIT)
|
||||
{
|
||||
dj.pumpFistBad();
|
||||
if (dj != null) dj.pumpFistBad();
|
||||
}
|
||||
else
|
||||
{
|
||||
dj.pumpFist();
|
||||
if (dj != null) dj.pumpFist();
|
||||
}
|
||||
|
||||
rankCamera.zoom = 0.8;
|
||||
|
@ -1192,7 +1202,23 @@ class FreeplayState extends MusicBeatSubState
|
|||
#if debug
|
||||
if (FlxG.keys.justPressed.T)
|
||||
{
|
||||
rankAnimStart(fromResultsParams);
|
||||
rankAnimStart(fromResultsParams ??
|
||||
{
|
||||
playRankAnim: true,
|
||||
newRank: PERFECT_GOLD,
|
||||
songId: "tutorial",
|
||||
difficultyId: "hard"
|
||||
});
|
||||
}
|
||||
|
||||
if (FlxG.keys.justPressed.P)
|
||||
{
|
||||
FlxG.switchState(FreeplayState.build(
|
||||
{
|
||||
{
|
||||
character: currentCharacterId == "pico" ? "bf" : "pico",
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// if (FlxG.keys.justPressed.H)
|
||||
|
@ -1302,9 +1328,9 @@ class FreeplayState extends MusicBeatSubState
|
|||
{
|
||||
if (busy) return;
|
||||
|
||||
var upP:Bool = controls.UI_UP_P && !FlxG.keys.pressed.CONTROL;
|
||||
var downP:Bool = controls.UI_DOWN_P && !FlxG.keys.pressed.CONTROL;
|
||||
var accepted:Bool = controls.ACCEPT && !FlxG.keys.pressed.CONTROL;
|
||||
var upP:Bool = controls.UI_UP_P;
|
||||
var downP:Bool = controls.UI_DOWN_P;
|
||||
var accepted:Bool = controls.ACCEPT;
|
||||
|
||||
if (FlxG.onMobile)
|
||||
{
|
||||
|
@ -1378,7 +1404,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
}
|
||||
#end
|
||||
|
||||
if (!FlxG.keys.pressed.CONTROL && (controls.UI_UP || controls.UI_DOWN))
|
||||
if ((controls.UI_UP || controls.UI_DOWN))
|
||||
{
|
||||
if (spamming)
|
||||
{
|
||||
|
@ -1413,7 +1439,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
}
|
||||
|
||||
spamTimer += elapsed;
|
||||
dj.resetAFKTimer();
|
||||
if (dj != null) dj.resetAFKTimer();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -1424,31 +1450,31 @@ class FreeplayState extends MusicBeatSubState
|
|||
#if !html5
|
||||
if (FlxG.mouse.wheel != 0)
|
||||
{
|
||||
dj.resetAFKTimer();
|
||||
if (dj != null) dj.resetAFKTimer();
|
||||
changeSelection(-Math.round(FlxG.mouse.wheel));
|
||||
}
|
||||
#else
|
||||
if (FlxG.mouse.wheel < 0)
|
||||
{
|
||||
dj.resetAFKTimer();
|
||||
if (dj != null) dj.resetAFKTimer();
|
||||
changeSelection(-Math.round(FlxG.mouse.wheel / 8));
|
||||
}
|
||||
else if (FlxG.mouse.wheel > 0)
|
||||
{
|
||||
dj.resetAFKTimer();
|
||||
if (dj != null) dj.resetAFKTimer();
|
||||
changeSelection(-Math.round(FlxG.mouse.wheel / 8));
|
||||
}
|
||||
#end
|
||||
|
||||
if (controls.UI_LEFT_P && !FlxG.keys.pressed.CONTROL)
|
||||
if (controls.UI_LEFT_P)
|
||||
{
|
||||
dj.resetAFKTimer();
|
||||
if (dj != null) dj.resetAFKTimer();
|
||||
changeDiff(-1);
|
||||
generateSongList(currentFilter, true);
|
||||
}
|
||||
if (controls.UI_RIGHT_P && !FlxG.keys.pressed.CONTROL)
|
||||
if (controls.UI_RIGHT_P)
|
||||
{
|
||||
dj.resetAFKTimer();
|
||||
if (dj != null) dj.resetAFKTimer();
|
||||
changeDiff(1);
|
||||
generateSongList(currentFilter, true);
|
||||
}
|
||||
|
@ -1458,7 +1484,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
busy = true;
|
||||
FlxTween.globalManager.clear();
|
||||
FlxTimer.globalManager.clear();
|
||||
dj.onIntroDone.removeAll();
|
||||
if (dj != null) dj.onIntroDone.removeAll();
|
||||
|
||||
FunkinSound.playOnce(Paths.sound('cancelMenu'));
|
||||
|
||||
|
@ -1484,7 +1510,8 @@ class FreeplayState extends MusicBeatSubState
|
|||
|
||||
for (grpSpr in exitMovers.keys())
|
||||
{
|
||||
var moveData:MoveData = exitMovers.get(grpSpr);
|
||||
var moveData:Null<MoveData> = exitMovers.get(grpSpr);
|
||||
if (moveData == null) continue;
|
||||
|
||||
for (spr in grpSpr)
|
||||
{
|
||||
|
@ -1492,14 +1519,14 @@ class FreeplayState extends MusicBeatSubState
|
|||
|
||||
var funnyMoveShit:MoveData = moveData;
|
||||
|
||||
if (moveData.x == null) funnyMoveShit.x = spr.x;
|
||||
if (moveData.y == null) funnyMoveShit.y = spr.y;
|
||||
if (moveData.speed == null) funnyMoveShit.speed = 0.2;
|
||||
if (moveData.wait == null) funnyMoveShit.wait = 0;
|
||||
var moveDataX = funnyMoveShit.x ?? spr.x;
|
||||
var moveDataY = funnyMoveShit.y ?? spr.y;
|
||||
var moveDataSpeed = funnyMoveShit.speed ?? 0.2;
|
||||
var moveDataWait = funnyMoveShit.wait ?? 0;
|
||||
|
||||
FlxTween.tween(spr, {x: funnyMoveShit.x, y: funnyMoveShit.y}, funnyMoveShit.speed, {ease: FlxEase.expoIn});
|
||||
FlxTween.tween(spr, {x: moveDataX, y: moveDataY}, moveDataSpeed, {ease: FlxEase.expoIn});
|
||||
|
||||
longestTimer = Math.max(longestTimer, funnyMoveShit.speed + funnyMoveShit.wait);
|
||||
longestTimer = Math.max(longestTimer, moveDataSpeed + moveDataWait);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1572,19 +1599,18 @@ class FreeplayState extends MusicBeatSubState
|
|||
var daSong:Null<FreeplaySongData> = grpCapsules.members[curSelected].songData;
|
||||
if (daSong != null)
|
||||
{
|
||||
// TODO: Make this actually be the variation you're focused on. We don't need to fetch the song metadata just to calculate it.
|
||||
var targetSong:Song = SongRegistry.instance.fetchEntry(grpCapsules.members[curSelected].songData.songId);
|
||||
var targetSong:Null<Song> = SongRegistry.instance.fetchEntry(daSong.songId);
|
||||
if (targetSong == null)
|
||||
{
|
||||
FlxG.log.warn('WARN: could not find song with id (${grpCapsules.members[curSelected].songData.songId})');
|
||||
FlxG.log.warn('WARN: could not find song with id (${daSong.songId})');
|
||||
return;
|
||||
}
|
||||
var targetVariation:String = targetSong.getFirstValidVariation(currentDifficulty);
|
||||
var targetVariation:String = targetSong.getFirstValidVariation(currentDifficulty) ?? '';
|
||||
|
||||
// TODO: This line of code makes me sad, but you can't really fix it without a breaking migration.
|
||||
var suffixedDifficulty = (targetVariation != Constants.DEFAULT_VARIATION
|
||||
&& targetVariation != 'erect') ? '$currentDifficulty-${targetVariation}' : currentDifficulty;
|
||||
var songScore:SaveScoreData = Save.instance.getSongScore(grpCapsules.members[curSelected].songData.songId, suffixedDifficulty);
|
||||
var songScore:Null<SaveScoreData> = Save.instance.getSongScore(daSong.songId, suffixedDifficulty);
|
||||
intendedScore = songScore?.score ?? 0;
|
||||
intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes);
|
||||
rememberedDifficulty = currentDifficulty;
|
||||
|
@ -1646,7 +1672,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
}
|
||||
|
||||
// Set the album graphic and play the animation if relevant.
|
||||
var newAlbumId:String = daSong?.albumId;
|
||||
var newAlbumId:Null<String> = daSong?.albumId;
|
||||
if (albumRoll.albumId != newAlbumId)
|
||||
{
|
||||
albumRoll.albumId = newAlbumId;
|
||||
|
@ -1684,7 +1710,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
});
|
||||
|
||||
trace('Available songs: ${availableSongCapsules.map(function(cap) {
|
||||
return cap.songData.songName;
|
||||
return cap?.songData?.songName;
|
||||
})}');
|
||||
|
||||
if (availableSongCapsules.length == 0)
|
||||
|
@ -1713,29 +1739,42 @@ class FreeplayState extends MusicBeatSubState
|
|||
|
||||
PlayStatePlaylist.isStoryMode = false;
|
||||
|
||||
var targetSong:Song = SongRegistry.instance.fetchEntry(cap.songData.songId);
|
||||
if (targetSong == null)
|
||||
var targetSongId:String = cap?.songData?.songId ?? 'unknown';
|
||||
var targetSongNullable:Null<Song> = SongRegistry.instance.fetchEntry(targetSongId);
|
||||
if (targetSongNullable == null)
|
||||
{
|
||||
FlxG.log.warn('WARN: could not find song with id (${cap.songData.songId})');
|
||||
FlxG.log.warn('WARN: could not find song with id (${targetSongId})');
|
||||
return;
|
||||
}
|
||||
var targetSong:Song = targetSongNullable;
|
||||
var targetDifficultyId:String = currentDifficulty;
|
||||
var targetVariation:String = targetSong.getFirstValidVariation(targetDifficultyId);
|
||||
PlayStatePlaylist.campaignId = cap.songData.levelId;
|
||||
var targetVariation:Null<String> = targetSong.getFirstValidVariation(targetDifficultyId, currentCharacter);
|
||||
var targetLevelId:Null<String> = cap?.songData?.levelId;
|
||||
PlayStatePlaylist.campaignId = targetLevelId ?? null;
|
||||
|
||||
var targetDifficulty:SongDifficulty = targetSong.getDifficulty(targetDifficultyId, targetVariation);
|
||||
var targetDifficulty:Null<SongDifficulty> = targetSong.getDifficulty(targetDifficultyId, targetVariation);
|
||||
if (targetDifficulty == null)
|
||||
{
|
||||
FlxG.log.warn('WARN: could not find difficulty with id (${targetDifficultyId})');
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Change this with alternate instrumentals
|
||||
var targetInstId:String = targetDifficulty.characters.instrumental;
|
||||
var baseInstrumentalId:String = targetDifficulty?.characters?.instrumental ?? '';
|
||||
var altInstrumentalIds:Array<String> = targetDifficulty?.characters?.altInstrumentals ?? [];
|
||||
|
||||
var targetInstId:String = baseInstrumentalId;
|
||||
|
||||
// TODO: Make this a UI element.
|
||||
#if (debug || FORCE_DEBUG_VERSION)
|
||||
if (altInstrumentalIds.length > 0 && FlxG.keys.pressed.CONTROL)
|
||||
{
|
||||
targetInstId = altInstrumentalIds[0];
|
||||
}
|
||||
#end
|
||||
|
||||
// Visual and audio effects.
|
||||
FunkinSound.playOnce(Paths.sound('confirmMenu'));
|
||||
dj.confirm();
|
||||
if (dj != null) dj.confirm();
|
||||
|
||||
grpCapsules.members[curSelected].forcePosition();
|
||||
grpCapsules.members[curSelected].confirm();
|
||||
|
@ -1777,7 +1816,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
new FlxTimer().start(1, function(tmr:FlxTimer) {
|
||||
FunkinSound.emptyPartialQueue();
|
||||
|
||||
Paths.setCurrentLevel(cap.songData.levelId);
|
||||
Paths.setCurrentLevel(cap?.songData?.levelId);
|
||||
LoadingState.loadPlayState(
|
||||
{
|
||||
targetSong: targetSong,
|
||||
|
@ -1832,7 +1871,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
var daSongCapsule:SongMenuItem = grpCapsules.members[curSelected];
|
||||
if (daSongCapsule.songData != null)
|
||||
{
|
||||
var songScore:SaveScoreData = Save.instance.getSongScore(daSongCapsule.songData.songId, currentDifficulty);
|
||||
var songScore:Null<SaveScoreData> = Save.instance.getSongScore(daSongCapsule.songData.songId, currentDifficulty);
|
||||
intendedScore = songScore?.score ?? 0;
|
||||
intendedCompletion = songScore == null ? 0.0 : ((songScore.tallies.sick + songScore.tallies.good) / songScore.tallies.totalNotes);
|
||||
diffIdsCurrent = daSongCapsule.songData.songDifficulties;
|
||||
|
@ -1882,15 +1921,37 @@ class FreeplayState extends MusicBeatSubState
|
|||
}
|
||||
else
|
||||
{
|
||||
var potentiallyErect:String = (currentDifficulty == "erect") || (currentDifficulty == "nightmare") ? "-erect" : "";
|
||||
FunkinSound.playMusic(daSongCapsule.songData.songId,
|
||||
var previewSongId:Null<String> = daSongCapsule?.songData?.songId;
|
||||
if (previewSongId == null) return;
|
||||
|
||||
var previewSong:Null<Song> = SongRegistry.instance.fetchEntry(previewSongId);
|
||||
var songDifficulty = previewSong?.getDifficulty(currentDifficulty,
|
||||
previewSong?.getVariationsByCharacter(currentCharacter) ?? Constants.DEFAULT_VARIATION_LIST);
|
||||
var baseInstrumentalId:String = songDifficulty?.characters?.instrumental ?? '';
|
||||
var altInstrumentalIds:Array<String> = songDifficulty?.characters?.altInstrumentals ?? [];
|
||||
|
||||
var instSuffix:String = baseInstrumentalId;
|
||||
|
||||
// TODO: Make this a UI element.
|
||||
#if (debug || FORCE_DEBUG_VERSION)
|
||||
if (altInstrumentalIds.length > 0 && FlxG.keys.pressed.CONTROL)
|
||||
{
|
||||
instSuffix = altInstrumentalIds[0];
|
||||
}
|
||||
#end
|
||||
|
||||
instSuffix = (instSuffix != '') ? '-$instSuffix' : '';
|
||||
|
||||
trace('Attempting to play partial preview: ${previewSongId}:${instSuffix}');
|
||||
|
||||
FunkinSound.playMusic(previewSongId,
|
||||
{
|
||||
startingVolume: 0.0,
|
||||
overrideExisting: true,
|
||||
restartTrack: false,
|
||||
mapTimeChanges: false, // The music metadata is not alongside the audio file so this won't work.
|
||||
pathsFunction: INST,
|
||||
suffix: potentiallyErect,
|
||||
suffix: instSuffix,
|
||||
partialParams:
|
||||
{
|
||||
loadPartial: true,
|
||||
|
@ -1911,7 +1972,7 @@ class FreeplayState extends MusicBeatSubState
|
|||
public static function build(?params:FreeplayStateParams, ?stickers:StickerSubState):MusicBeatState
|
||||
{
|
||||
var result:MainMenuState;
|
||||
if (params?.fromResults.playRankAnim) result = new MainMenuState(true);
|
||||
if (params?.fromResults?.playRankAnim ?? false) result = new MainMenuState(true);
|
||||
else
|
||||
result = new MainMenuState(false);
|
||||
|
||||
|
@ -1949,8 +2010,8 @@ class DifficultySelector extends FlxSprite
|
|||
|
||||
override function update(elapsed:Float):Void
|
||||
{
|
||||
if (flipX && controls.UI_RIGHT_P && !FlxG.keys.pressed.CONTROL) moveShitDown();
|
||||
if (!flipX && controls.UI_LEFT_P && !FlxG.keys.pressed.CONTROL) moveShitDown();
|
||||
if (flipX && controls.UI_RIGHT_P) moveShitDown();
|
||||
if (!flipX && controls.UI_LEFT_P) moveShitDown();
|
||||
|
||||
super.update(elapsed);
|
||||
}
|
||||
|
@ -2098,7 +2159,12 @@ class FreeplaySongData
|
|||
this.albumId = songDifficulty.album;
|
||||
}
|
||||
|
||||
this.scoringRank = Save.instance.getSongRank(songId, currentDifficulty);
|
||||
// TODO: This line of code makes me sad, but you can't really fix it without a breaking migration.
|
||||
// `easy`, `erect`, `normal-pico`, etc.
|
||||
var suffixedDifficulty = (songDifficulty.variation != Constants.DEFAULT_VARIATION
|
||||
&& songDifficulty.variation != 'erect') ? '$currentDifficulty-${songDifficulty.variation}' : currentDifficulty;
|
||||
|
||||
this.scoringRank = Save.instance.getSongRank(songId, suffixedDifficulty);
|
||||
|
||||
this.isNew = song.isSongNew(currentDifficulty);
|
||||
}
|
||||
|
|
118
source/funkin/ui/freeplay/charselect/PlayableCharacter.hx
Normal file
118
source/funkin/ui/freeplay/charselect/PlayableCharacter.hx
Normal file
|
@ -0,0 +1,118 @@
|
|||
package funkin.ui.freeplay.charselect;
|
||||
|
||||
import funkin.data.IRegistryEntry;
|
||||
import funkin.data.freeplay.player.PlayerData;
|
||||
import funkin.data.freeplay.player.PlayerRegistry;
|
||||
|
||||
/**
|
||||
* An object used to retrieve data about a playable character (also known as "weeks").
|
||||
* Can be scripted to override each function, for custom behavior.
|
||||
*/
|
||||
class PlayableCharacter implements IRegistryEntry<PlayerData>
|
||||
{
|
||||
/**
|
||||
* The ID of the playable character.
|
||||
*/
|
||||
public final id:String;
|
||||
|
||||
/**
|
||||
* Playable character data as parsed from the JSON file.
|
||||
*/
|
||||
public final _data:PlayerData;
|
||||
|
||||
/**
|
||||
* @param id The ID of the JSON file to parse.
|
||||
*/
|
||||
public function new(id:String)
|
||||
{
|
||||
this.id = id;
|
||||
_data = _fetchData(id);
|
||||
|
||||
if (_data == null)
|
||||
{
|
||||
throw 'Could not parse playable character data for id: $id';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the readable name of the playable character.
|
||||
*/
|
||||
public function getName():String
|
||||
{
|
||||
// TODO: Maybe add localization support?
|
||||
return _data.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the list of stage character IDs associated with this playable character.
|
||||
* @return The list of associated character IDs
|
||||
*/
|
||||
public function getOwnedCharacterIds():Array<String>
|
||||
{
|
||||
return _data.ownedChars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return `true` if, when this character is selected in Freeplay,
|
||||
* songs unassociated with a specific character should appear.
|
||||
*/
|
||||
public function shouldShowUnownedChars():Bool
|
||||
{
|
||||
return _data.showUnownedChars;
|
||||
}
|
||||
|
||||
public function shouldShowCharacter(id:String):Bool
|
||||
{
|
||||
if (_data.ownedChars.contains(id))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_data.showUnownedChars)
|
||||
{
|
||||
var result = !PlayerRegistry.instance.isCharacterOwned(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function getFreeplayDJData():PlayerFreeplayDJData
|
||||
{
|
||||
return _data.freeplayDJ;
|
||||
}
|
||||
|
||||
public function getFreeplayDJText(index:Int):String
|
||||
{
|
||||
return _data.freeplayDJ.getFreeplayDJText(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether this character is unlocked.
|
||||
*/
|
||||
public function isUnlocked():Bool
|
||||
{
|
||||
return _data.unlocked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the character is destroyed.
|
||||
* TODO: Document when this gets called
|
||||
*/
|
||||
public function destroy():Void {}
|
||||
|
||||
public function toString():String
|
||||
{
|
||||
return 'PlayableCharacter($id)';
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve and parse the JSON data for a playable character by ID.
|
||||
* @param id The ID of the character
|
||||
* @return The parsed player data, or null if not found or invalid
|
||||
*/
|
||||
static function _fetchData(id:String):Null<PlayerData>
|
||||
{
|
||||
return PlayerRegistry.instance.parseEntryDataWithMigration(id, PlayerRegistry.instance.fetchEntryVersion(id));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package funkin.ui.freeplay.charselect;
|
||||
|
||||
/**
|
||||
* A script that can be tied to a PlayableCharacter.
|
||||
* Create a scripted class that extends PlayableCharacter to use this.
|
||||
*/
|
||||
@:hscriptClass
|
||||
class ScriptedPlayableCharacter extends funkin.ui.freeplay.charselect.PlayableCharacter implements polymod.hscript.HScriptedClass {}
|
|
@ -117,7 +117,10 @@ class MainMenuState extends MusicBeatState
|
|||
FlxTransitionableState.skipNextTransIn = true;
|
||||
FlxTransitionableState.skipNextTransOut = true;
|
||||
|
||||
openSubState(new FreeplayState());
|
||||
openSubState(new FreeplayState(
|
||||
{
|
||||
character: FlxG.keys.pressed.SHIFT ? 'pico' : 'bf',
|
||||
}));
|
||||
});
|
||||
|
||||
#if CAN_OPEN_LINKS
|
||||
|
|
|
@ -97,7 +97,7 @@ class SortUtil
|
|||
* @param b The second string to compare.
|
||||
* @return 1 if `a` comes before `b`, -1 if `b` comes before `a`, 0 if they are equal
|
||||
*/
|
||||
public static function alphabetically(a:String, b:String):Int
|
||||
public static function alphabetically(?a:String, ?b:String):Int
|
||||
{
|
||||
a = a.toUpperCase();
|
||||
b = b.toUpperCase();
|
||||
|
|
|
@ -24,7 +24,6 @@ class VersionUtil
|
|||
try
|
||||
{
|
||||
var versionRaw:thx.semver.Version.SemVer = version;
|
||||
trace('${versionRaw} satisfies (${versionRule})? ${version.satisfies(versionRule)}');
|
||||
return version.satisfies(versionRule);
|
||||
}
|
||||
catch (e)
|
||||
|
|
|
@ -14,6 +14,7 @@ class MapTools
|
|||
*/
|
||||
public static function size<K, T>(map:Map<K, T>):Int
|
||||
{
|
||||
if (map == null) return 0;
|
||||
return map.keys().array().length;
|
||||
}
|
||||
|
||||
|
@ -22,6 +23,7 @@ class MapTools
|
|||
*/
|
||||
public static function values<K, T>(map:Map<K, T>):Array<T>
|
||||
{
|
||||
if (map == null) return [];
|
||||
return [for (i in map.iterator()) i];
|
||||
}
|
||||
|
||||
|
@ -30,6 +32,7 @@ class MapTools
|
|||
*/
|
||||
public static function clone<K, T>(map:Map<K, T>):Map<K, T>
|
||||
{
|
||||
if (map == null) return null;
|
||||
return map.copy();
|
||||
}
|
||||
|
||||
|
@ -76,6 +79,7 @@ class MapTools
|
|||
*/
|
||||
public static function keyValues<K, T>(map:Map<K, T>):Array<K>
|
||||
{
|
||||
if (map == null) return [];
|
||||
return map.keys().array();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue