mirror of
https://github.com/ninjamuffin99/Funkin.git
synced 2025-11-26 06:09:02 +00:00
Merge branch 'bugfix/various-develop-fixes' into rewrite/master
This commit is contained in:
commit
1fbca9ab6a
2
assets
2
assets
|
|
@ -1 +1 @@
|
||||||
Subproject commit e19aaca19254ec2717a4f942005f32b42b6ecb18
|
Subproject commit 7210562035c7ab6d2122606ec607b3e897a5ef20
|
||||||
|
|
@ -38,6 +38,17 @@ class PlayerData
|
||||||
@:optional
|
@:optional
|
||||||
public var freeplayDJ:Null<PlayerFreeplayDJData> = null;
|
public var freeplayDJ:Null<PlayerFreeplayDJData> = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data for displaying this character in the Character Select menu.
|
||||||
|
* If null, exclude from Character Select.
|
||||||
|
*/
|
||||||
|
@:optional
|
||||||
|
public var charSelect:Null<PlayerCharSelectData> = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data for displaying this character in the results screen.
|
||||||
|
*/
|
||||||
|
@:optional
|
||||||
public var results:Null<PlayerResultsData> = null;
|
public var results:Null<PlayerResultsData> = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -97,6 +108,9 @@ class PlayerFreeplayDJData
|
||||||
@:optional
|
@:optional
|
||||||
var cartoon:Null<PlayerFreeplayDJCartoonData>;
|
var cartoon:Null<PlayerFreeplayDJCartoonData>;
|
||||||
|
|
||||||
|
@:optional
|
||||||
|
var fistPump:Null<PlayerFreeplayDJFistPumpData>;
|
||||||
|
|
||||||
public function new()
|
public function new()
|
||||||
{
|
{
|
||||||
animationMap = new Map();
|
animationMap = new Map();
|
||||||
|
|
@ -183,6 +197,58 @@ class PlayerFreeplayDJData
|
||||||
{
|
{
|
||||||
return cartoon?.channelChangeFrame ?? 60;
|
return cartoon?.channelChangeFrame ?? 60;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getFistPumpIntroStartFrame():Int
|
||||||
|
{
|
||||||
|
return fistPump?.introStartFrame ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFistPumpIntroEndFrame():Int
|
||||||
|
{
|
||||||
|
return fistPump?.introEndFrame ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFistPumpLoopStartFrame():Int
|
||||||
|
{
|
||||||
|
return fistPump?.loopStartFrame ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFistPumpLoopEndFrame():Int
|
||||||
|
{
|
||||||
|
return fistPump?.loopEndFrame ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFistPumpIntroBadStartFrame():Int
|
||||||
|
{
|
||||||
|
return fistPump?.introBadStartFrame ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFistPumpIntroBadEndFrame():Int
|
||||||
|
{
|
||||||
|
return fistPump?.introBadEndFrame ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFistPumpLoopBadStartFrame():Int
|
||||||
|
{
|
||||||
|
return fistPump?.loopBadStartFrame ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFistPumpLoopBadEndFrame():Int
|
||||||
|
{
|
||||||
|
return fistPump?.loopBadEndFrame ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class PlayerCharSelectData
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* A zero-indexed number for the character's preferred position in the grid.
|
||||||
|
* 0 = top left, 4 = center, 8 = bottom right
|
||||||
|
* In the event of a conflict, the first character alphabetically gets it,
|
||||||
|
* and others get shifted over.
|
||||||
|
*/
|
||||||
|
@:optional
|
||||||
|
public var position:Null<Int>;
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef PlayerResultsData =
|
typedef PlayerResultsData =
|
||||||
|
|
@ -242,3 +308,30 @@ typedef PlayerFreeplayDJCartoonData =
|
||||||
var loopFrame:Int;
|
var loopFrame:Int;
|
||||||
var channelChangeFrame:Int;
|
var channelChangeFrame:Int;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef PlayerFreeplayDJFistPumpData =
|
||||||
|
{
|
||||||
|
@:default(0)
|
||||||
|
var introStartFrame:Int;
|
||||||
|
|
||||||
|
@:default(4)
|
||||||
|
var introEndFrame:Int;
|
||||||
|
|
||||||
|
@:default(4)
|
||||||
|
var loopStartFrame:Int;
|
||||||
|
|
||||||
|
@:default(-1)
|
||||||
|
var loopEndFrame:Int;
|
||||||
|
|
||||||
|
@:default(0)
|
||||||
|
var introBadStartFrame:Int;
|
||||||
|
|
||||||
|
@:default(4)
|
||||||
|
var introBadEndFrame:Int;
|
||||||
|
|
||||||
|
@:default(4)
|
||||||
|
var loopBadStartFrame:Int;
|
||||||
|
|
||||||
|
@:default(-1)
|
||||||
|
var loopBadEndFrame:Int;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package funkin.data.freeplay.player;
|
||||||
import funkin.data.freeplay.player.PlayerData;
|
import funkin.data.freeplay.player.PlayerData;
|
||||||
import funkin.ui.freeplay.charselect.PlayableCharacter;
|
import funkin.ui.freeplay.charselect.PlayableCharacter;
|
||||||
import funkin.ui.freeplay.charselect.ScriptedPlayableCharacter;
|
import funkin.ui.freeplay.charselect.ScriptedPlayableCharacter;
|
||||||
|
import funkin.save.Save;
|
||||||
|
|
||||||
class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData>
|
class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData>
|
||||||
{
|
{
|
||||||
|
|
@ -53,6 +54,41 @@ class PlayerRegistry extends BaseRegistry<PlayableCharacter, PlayerData>
|
||||||
log('Loaded ${countEntries()} playable characters with ${ownedCharacterIds.size()} associations.');
|
log('Loaded ${countEntries()} playable characters with ${ownedCharacterIds.size()} associations.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function countUnlockedCharacters():Int
|
||||||
|
{
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
for (charId in listEntryIds())
|
||||||
|
{
|
||||||
|
var player = fetchEntry(charId);
|
||||||
|
if (player == null) continue;
|
||||||
|
|
||||||
|
if (player.isUnlocked()) count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function hasNewCharacter():Bool
|
||||||
|
{
|
||||||
|
var characters = Save.instance.charactersSeen.clone();
|
||||||
|
|
||||||
|
for (charId in listEntryIds())
|
||||||
|
{
|
||||||
|
var player = fetchEntry(charId);
|
||||||
|
if (player == null) continue;
|
||||||
|
|
||||||
|
if (!player.isUnlocked()) continue;
|
||||||
|
if (characters.contains(charId)) continue;
|
||||||
|
|
||||||
|
// This character is unlocked but we haven't seen them in Freeplay yet.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallthrough case.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the playable character associated with a given stage character.
|
* Get the playable character associated with a given stage character.
|
||||||
* @param characterId The stage character ID.
|
* @param characterId The stage character ID.
|
||||||
|
|
|
||||||
|
|
@ -1979,7 +1979,7 @@ class PlayState extends MusicBeatSubState
|
||||||
if (vocals == null) return;
|
if (vocals == null) return;
|
||||||
|
|
||||||
// Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.)
|
// Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.)
|
||||||
if (!FlxG.sound.music.playing) return;
|
if (!(FlxG.sound.music?.playing ?? false)) return;
|
||||||
var timeToPlayAt:Float = Conductor.instance.songPosition - Conductor.instance.instrumentalOffset;
|
var timeToPlayAt:Float = Conductor.instance.songPosition - Conductor.instance.instrumentalOffset;
|
||||||
FlxG.sound.music.pause();
|
FlxG.sound.music.pause();
|
||||||
vocals.pause();
|
vocals.pause();
|
||||||
|
|
@ -2221,10 +2221,14 @@ class PlayState extends MusicBeatSubState
|
||||||
// Calling event.cancelEvent() skips all the other logic! Neat!
|
// Calling event.cancelEvent() skips all the other logic! Neat!
|
||||||
if (event.eventCanceled) continue;
|
if (event.eventCanceled) continue;
|
||||||
|
|
||||||
|
// Skip handling the miss in botplay!
|
||||||
|
if (!isBotPlayMode)
|
||||||
|
{
|
||||||
// Judge the miss.
|
// Judge the miss.
|
||||||
// NOTE: This is what handles the scoring.
|
// NOTE: This is what handles the scoring.
|
||||||
trace('Missed note! ${note.noteData}');
|
trace('Missed note! ${note.noteData}');
|
||||||
onNoteMiss(note, event.playSound, event.healthChange);
|
onNoteMiss(note, event.playSound, event.healthChange);
|
||||||
|
}
|
||||||
|
|
||||||
note.handledMiss = true;
|
note.handledMiss = true;
|
||||||
}
|
}
|
||||||
|
|
@ -2321,9 +2325,16 @@ class PlayState extends MusicBeatSubState
|
||||||
|
|
||||||
playerStrumline.pressKey(input.noteDirection);
|
playerStrumline.pressKey(input.noteDirection);
|
||||||
|
|
||||||
|
// Don't credit or penalize inputs in Bot Play.
|
||||||
|
if (isBotPlayMode) continue;
|
||||||
|
|
||||||
var notesInDirection:Array<NoteSprite> = notesByDirection[input.noteDirection];
|
var notesInDirection:Array<NoteSprite> = notesByDirection[input.noteDirection];
|
||||||
|
|
||||||
if (!Constants.GHOST_TAPPING && notesInDirection.length == 0)
|
#if FEATURE_GHOST_TAPPING
|
||||||
|
if ((!playerStrumline.mayGhostTap()) && notesInDirection.length == 0)
|
||||||
|
#else
|
||||||
|
if (notesInDirection.length == 0)
|
||||||
|
#end
|
||||||
{
|
{
|
||||||
// Pressed a wrong key with no notes nearby.
|
// Pressed a wrong key with no notes nearby.
|
||||||
// Perform a ghost miss (anti-spam).
|
// Perform a ghost miss (anti-spam).
|
||||||
|
|
@ -2333,16 +2344,6 @@ class PlayState extends MusicBeatSubState
|
||||||
playerStrumline.playPress(input.noteDirection);
|
playerStrumline.playPress(input.noteDirection);
|
||||||
trace('PENALTY Score: ${songScore}');
|
trace('PENALTY Score: ${songScore}');
|
||||||
}
|
}
|
||||||
else if (Constants.GHOST_TAPPING && (!playerStrumline.mayGhostTap()) && notesInDirection.length == 0)
|
|
||||||
{
|
|
||||||
// Pressed a wrong key with notes visible on-screen.
|
|
||||||
// Perform a ghost miss (anti-spam).
|
|
||||||
ghostNoteMiss(input.noteDirection, notesInRange.length > 0);
|
|
||||||
|
|
||||||
// Play the strumline animation.
|
|
||||||
playerStrumline.playPress(input.noteDirection);
|
|
||||||
trace('PENALTY Score: ${songScore}');
|
|
||||||
}
|
|
||||||
else if (notesInDirection.length == 0)
|
else if (notesInDirection.length == 0)
|
||||||
{
|
{
|
||||||
// Press a key with no penalty.
|
// Press a key with no penalty.
|
||||||
|
|
|
||||||
|
|
@ -305,6 +305,8 @@ class CharacterDataParser
|
||||||
icon = "darnell";
|
icon = "darnell";
|
||||||
case "senpai-angry":
|
case "senpai-angry":
|
||||||
icon = "senpai";
|
icon = "senpai";
|
||||||
|
case "spooky-dark":
|
||||||
|
icon = "spooky";
|
||||||
case "tankman-atlas":
|
case "tankman-atlas":
|
||||||
icon = "tankman";
|
icon = "tankman";
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ class ZoomCameraSongEvent extends SongEvent
|
||||||
name: 'zoom',
|
name: 'zoom',
|
||||||
title: 'Zoom Level',
|
title: 'Zoom Level',
|
||||||
defaultValue: 1.0,
|
defaultValue: 1.0,
|
||||||
step: 0.1,
|
step: 0.05,
|
||||||
type: SongEventFieldType.FLOAT,
|
type: SongEventFieldType.FLOAT,
|
||||||
units: 'x'
|
units: 'x'
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,10 @@ class Strumline extends FlxSpriteGroup
|
||||||
|
|
||||||
final noteStyle:NoteStyle;
|
final noteStyle:NoteStyle;
|
||||||
|
|
||||||
|
#if FEATURE_GHOST_TAPPING
|
||||||
|
var ghostTapTimer:Float = 0.0;
|
||||||
|
#end
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The note data for the song. Should NOT be altered after the song starts,
|
* The note data for the song. Should NOT be altered after the song starts,
|
||||||
* so we can easily rewind.
|
* so we can easily rewind.
|
||||||
|
|
@ -179,21 +183,36 @@ class Strumline extends FlxSpriteGroup
|
||||||
super.update(elapsed);
|
super.update(elapsed);
|
||||||
|
|
||||||
updateNotes();
|
updateNotes();
|
||||||
|
|
||||||
|
#if FEATURE_GHOST_TAPPING
|
||||||
|
updateGhostTapTimer(elapsed);
|
||||||
|
#end
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if FEATURE_GHOST_TAPPING
|
||||||
/**
|
/**
|
||||||
* Returns `true` if no notes are in range of the strumline and the player can spam without penalty.
|
* Returns `true` if no notes are in range of the strumline and the player can spam without penalty.
|
||||||
*/
|
*/
|
||||||
public function mayGhostTap():Bool
|
public function mayGhostTap():Bool
|
||||||
{
|
{
|
||||||
// TODO: Refine this. Only querying "can be hit" is too tight but "is being rendered" is too loose.
|
// Any notes in range of the strumline.
|
||||||
// Also, if you just hit a note, there should be a (short) period where this is off so you can't spam.
|
if (getNotesMayHit().length > 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Any hold notes in range of the strumline.
|
||||||
|
if (getHoldNotesHitOrMissed().length > 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// If there are any notes on screen, we can't ghost tap.
|
// Note has been hit recently.
|
||||||
return notes.members.filter(function(note:NoteSprite) {
|
if (ghostTapTimer > 0.0) return false;
|
||||||
return note != null && note.alive && !note.hasBeenHit;
|
|
||||||
}).length == 0;
|
// **yippee**
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
#end
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return notes that are within `Constants.HIT_WINDOW` ms of the strumline.
|
* Return notes that are within `Constants.HIT_WINDOW` ms of the strumline.
|
||||||
|
|
@ -492,6 +511,32 @@ class Strumline extends FlxSpriteGroup
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return notes that are within, or way after, `Constants.HIT_WINDOW` ms of the strumline.
|
||||||
|
* @return An array of `NoteSprite` objects.
|
||||||
|
*/
|
||||||
|
public function getNotesOnScreen():Array<NoteSprite>
|
||||||
|
{
|
||||||
|
return notes.members.filter(function(note:NoteSprite) {
|
||||||
|
return note != null && note.alive && !note.hasBeenHit;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#if FEATURE_GHOST_TAPPING
|
||||||
|
function updateGhostTapTimer(elapsed:Float):Void
|
||||||
|
{
|
||||||
|
// If it's still our turn, don't update the ghost tap timer.
|
||||||
|
if (getNotesOnScreen().length > 0) return;
|
||||||
|
|
||||||
|
ghostTapTimer -= elapsed;
|
||||||
|
|
||||||
|
if (ghostTapTimer <= 0)
|
||||||
|
{
|
||||||
|
ghostTapTimer = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#end
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the PlayState skips a large amount of time forward or backward.
|
* Called when the PlayState skips a large amount of time forward or backward.
|
||||||
*/
|
*/
|
||||||
|
|
@ -563,6 +608,10 @@ class Strumline extends FlxSpriteGroup
|
||||||
playStatic(dir);
|
playStatic(dir);
|
||||||
}
|
}
|
||||||
resetScrollSpeed();
|
resetScrollSpeed();
|
||||||
|
|
||||||
|
#if FEATURE_GHOST_TAPPING
|
||||||
|
ghostTapTimer = 0;
|
||||||
|
#end
|
||||||
}
|
}
|
||||||
|
|
||||||
public function applyNoteData(data:Array<SongNoteData>):Void
|
public function applyNoteData(data:Array<SongNoteData>):Void
|
||||||
|
|
@ -602,6 +651,10 @@ class Strumline extends FlxSpriteGroup
|
||||||
|
|
||||||
note.holdNoteSprite.sustainLength = (note.holdNoteSprite.strumTime + note.holdNoteSprite.fullSustainLength) - conductorInUse.songPosition;
|
note.holdNoteSprite.sustainLength = (note.holdNoteSprite.strumTime + note.holdNoteSprite.fullSustainLength) - conductorInUse.songPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if FEATURE_GHOST_TAPPING
|
||||||
|
ghostTapTimer = Constants.GHOST_TAP_DELAY;
|
||||||
|
#end
|
||||||
}
|
}
|
||||||
|
|
||||||
public function killNote(note:NoteSprite):Void
|
public function killNote(note:NoteSprite):Void
|
||||||
|
|
|
||||||
|
|
@ -315,7 +315,7 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
|
||||||
|
|
||||||
public function isAnimationFinished():Bool
|
public function isAnimationFinished():Bool
|
||||||
{
|
{
|
||||||
return this.animation.finished;
|
return this.animation?.finished ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setAnimationOffsets(name:String, xOffset:Float, yOffset:Float):Void
|
public function setAnimationOffsets(name:String, xOffset:Float, yOffset:Float):Void
|
||||||
|
|
|
||||||
|
|
@ -436,8 +436,9 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
|
||||||
// Start with the per-stage character position.
|
// Start with the per-stage character position.
|
||||||
// Subtracting the origin ensures characters are positioned relative to their feet.
|
// Subtracting the origin ensures characters are positioned relative to their feet.
|
||||||
// Subtracting the global offset allows positioning on a per-character basis.
|
// Subtracting the global offset allows positioning on a per-character basis.
|
||||||
character.x = stageCharData.position[0] - character.characterOrigin.x + character.globalOffsets[0];
|
// We previously applied the global offset here but that is now done elsewhere.
|
||||||
character.y = stageCharData.position[1] - character.characterOrigin.y + character.globalOffsets[1];
|
character.x = stageCharData.position[0] - character.characterOrigin.x;
|
||||||
|
character.y = stageCharData.position[1] - character.characterOrigin.y;
|
||||||
|
|
||||||
@:privateAccess(funkin.play.stage.Bopper)
|
@:privateAccess(funkin.play.stage.Bopper)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,12 @@ class Save
|
||||||
modOptions: [],
|
modOptions: [],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
unlocks:
|
||||||
|
{
|
||||||
|
// Default to having seen the default character.
|
||||||
|
charactersSeen: ["bf"],
|
||||||
|
},
|
||||||
|
|
||||||
optionsChartEditor:
|
optionsChartEditor:
|
||||||
{
|
{
|
||||||
// Reasonable defaults.
|
// Reasonable defaults.
|
||||||
|
|
@ -393,6 +399,22 @@ class Save
|
||||||
return data.optionsChartEditor.playbackSpeed;
|
return data.optionsChartEditor.playbackSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var charactersSeen(get, never):Array<String>;
|
||||||
|
|
||||||
|
function get_charactersSeen():Array<String>
|
||||||
|
{
|
||||||
|
return data.unlocks.charactersSeen;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When we've seen a character unlock, add it to the list of characters seen.
|
||||||
|
* @param character
|
||||||
|
*/
|
||||||
|
public function addCharacterSeen(character:String):Void
|
||||||
|
{
|
||||||
|
data.unlocks.charactersSeen.push(character);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the score the user achieved for a given level on a given difficulty.
|
* Return the score the user achieved for a given level on a given difficulty.
|
||||||
*
|
*
|
||||||
|
|
@ -471,10 +493,18 @@ class Save
|
||||||
for (difficulty in difficultyList)
|
for (difficulty in difficultyList)
|
||||||
{
|
{
|
||||||
var score:Null<SaveScoreData> = getLevelScore(levelId, difficulty);
|
var score:Null<SaveScoreData> = getLevelScore(levelId, difficulty);
|
||||||
// TODO: Do we need to check accuracy/score here?
|
|
||||||
if (score != null)
|
if (score != null)
|
||||||
{
|
{
|
||||||
return true;
|
if (score.score > 0)
|
||||||
|
{
|
||||||
|
// Level has score data, which means we cleared it!
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Level has score data, but the score is 0.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -630,10 +660,18 @@ class Save
|
||||||
for (difficulty in difficultyList)
|
for (difficulty in difficultyList)
|
||||||
{
|
{
|
||||||
var score:Null<SaveScoreData> = getSongScore(songId, difficulty);
|
var score:Null<SaveScoreData> = getSongScore(songId, difficulty);
|
||||||
// TODO: Do we need to check accuracy/score here?
|
|
||||||
if (score != null)
|
if (score != null)
|
||||||
{
|
{
|
||||||
return true;
|
if (score.score > 0)
|
||||||
|
{
|
||||||
|
// Level has score data, which means we cleared it!
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Level has score data, but the score is 0.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -956,6 +994,8 @@ typedef RawSaveData =
|
||||||
*/
|
*/
|
||||||
var options:SaveDataOptions;
|
var options:SaveDataOptions;
|
||||||
|
|
||||||
|
var unlocks:SaveDataUnlocks;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The user's favorited songs in the Freeplay menu,
|
* The user's favorited songs in the Freeplay menu,
|
||||||
* as a list of song IDs.
|
* as a list of song IDs.
|
||||||
|
|
@ -980,6 +1020,15 @@ typedef SaveApiNewgroundsData =
|
||||||
var sessionId:Null<String>;
|
var sessionId:Null<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef SaveDataUnlocks =
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Every time we see the unlock animation for a character,
|
||||||
|
* add it to this list so that we don't show it again.
|
||||||
|
*/
|
||||||
|
var charactersSeen:Array<String>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An anoymous structure containing options about the user's high scores.
|
* An anoymous structure containing options about the user's high scores.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -22,14 +22,26 @@ class PixelatedIcon extends FlxSprite
|
||||||
|
|
||||||
switch (char)
|
switch (char)
|
||||||
{
|
{
|
||||||
case 'monster-christmas':
|
case "bf-christmas" | "bf-car" | "bf-pixel" | "bf-holding-gf":
|
||||||
charPath += 'monsterpixel';
|
charPath += "bfpixel";
|
||||||
case 'mom-car':
|
case "monster-christmas":
|
||||||
charPath += 'mommypixel';
|
charPath += "monsterpixel";
|
||||||
case 'darnell-blazin':
|
case "mom" | "mom-car":
|
||||||
charPath += 'darnellpixel';
|
charPath += "mommypixel";
|
||||||
case 'senpai-angry':
|
case "pico-blazin" | "pico-playable" | "pico-speaker":
|
||||||
charPath += 'senpaipixel';
|
charPath += "picopixel";
|
||||||
|
case "gf-christmas" | "gf-car" | "gf-pixel" | "gf-tankmen":
|
||||||
|
charPath += "gfpixel";
|
||||||
|
case "dad":
|
||||||
|
charPath += "dadpixel";
|
||||||
|
case "darnell-blazin":
|
||||||
|
charPath += "darnellpixel";
|
||||||
|
case "senpai-angry":
|
||||||
|
charPath += "senpaipixel";
|
||||||
|
case "spooky-dark":
|
||||||
|
charPath += "spookypixel";
|
||||||
|
case "tankman-atlas":
|
||||||
|
charPath += "tankmanpixel";
|
||||||
default:
|
default:
|
||||||
charPath += '${char}pixel';
|
charPath += '${char}pixel';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,31 @@
|
||||||
package funkin.ui.charSelect;
|
package funkin.ui.charSelect;
|
||||||
|
|
||||||
import funkin.ui.freeplay.FreeplayState;
|
import flixel.FlxObject;
|
||||||
import flixel.text.FlxText;
|
|
||||||
import funkin.ui.PixelatedIcon;
|
|
||||||
import flixel.system.debug.watch.Tracker.TrackerProfile;
|
|
||||||
import flixel.math.FlxPoint;
|
|
||||||
import flixel.tweens.FlxTween;
|
|
||||||
import openfl.display.BlendMode;
|
|
||||||
import flixel.group.FlxGroup.FlxTypedGroup;
|
|
||||||
import flixel.FlxSprite;
|
import flixel.FlxSprite;
|
||||||
|
import flixel.group.FlxGroup;
|
||||||
|
import flixel.group.FlxGroup.FlxTypedGroup;
|
||||||
import flixel.group.FlxSpriteGroup;
|
import flixel.group.FlxSpriteGroup;
|
||||||
import funkin.play.stage.Stage;
|
import flixel.math.FlxPoint;
|
||||||
|
import flixel.sound.FlxSound;
|
||||||
|
import flixel.system.debug.watch.Tracker.TrackerProfile;
|
||||||
|
import flixel.text.FlxText;
|
||||||
|
import flixel.tweens.FlxEase;
|
||||||
|
import flixel.tweens.FlxTween;
|
||||||
|
import flixel.util.FlxTimer;
|
||||||
|
import funkin.audio.FunkinSound;
|
||||||
|
import funkin.data.freeplay.player.PlayerData;
|
||||||
|
import funkin.data.freeplay.player.PlayerRegistry;
|
||||||
|
import funkin.graphics.adobeanimate.FlxAtlasSprite;
|
||||||
|
import funkin.graphics.FunkinCamera;
|
||||||
import funkin.modding.events.ScriptEvent;
|
import funkin.modding.events.ScriptEvent;
|
||||||
import funkin.modding.events.ScriptEventDispatcher;
|
import funkin.modding.events.ScriptEventDispatcher;
|
||||||
import funkin.graphics.adobeanimate.FlxAtlasSprite;
|
import funkin.play.stage.Stage;
|
||||||
import flixel.FlxObject;
|
import funkin.ui.freeplay.charselect.PlayableCharacter;
|
||||||
import openfl.display.BlendMode;
|
import funkin.ui.freeplay.FreeplayState;
|
||||||
import flixel.group.FlxGroup;
|
import funkin.ui.PixelatedIcon;
|
||||||
import funkin.util.MathUtil;
|
import funkin.util.MathUtil;
|
||||||
import flixel.util.FlxTimer;
|
import funkin.vis.dsp.SpectralAnalyzer;
|
||||||
import flixel.tweens.FlxEase;
|
import openfl.display.BlendMode;
|
||||||
import flixel.sound.FlxSound;
|
|
||||||
import funkin.audio.FunkinSound;
|
|
||||||
|
|
||||||
class CharSelectSubState extends MusicBeatSubState
|
class CharSelectSubState extends MusicBeatSubState
|
||||||
{
|
{
|
||||||
|
|
@ -67,8 +71,29 @@ class CharSelectSubState extends MusicBeatSubState
|
||||||
{
|
{
|
||||||
super();
|
super();
|
||||||
|
|
||||||
availableChars.set(4, "bf");
|
loadAvailableCharacters();
|
||||||
availableChars.set(3, "pico");
|
}
|
||||||
|
|
||||||
|
function loadAvailableCharacters():Void
|
||||||
|
{
|
||||||
|
var playerIds:Array<String> = PlayerRegistry.instance.listEntryIds();
|
||||||
|
|
||||||
|
for (playerId in playerIds)
|
||||||
|
{
|
||||||
|
var player:Null<PlayableCharacter> = PlayerRegistry.instance.fetchEntry(playerId);
|
||||||
|
if (player == null) continue;
|
||||||
|
var playerData = player.getCharSelectData();
|
||||||
|
if (playerData == null) continue;
|
||||||
|
|
||||||
|
var targetPosition:Int = playerData.position ?? 0;
|
||||||
|
while (availableChars.exists(targetPosition))
|
||||||
|
{
|
||||||
|
targetPosition += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
trace('Placing player ${playerId} at position ${targetPosition}');
|
||||||
|
availableChars.set(targetPosition, playerId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override public function create():Void
|
override public function create():Void
|
||||||
|
|
@ -269,7 +294,6 @@ class CharSelectSubState extends MusicBeatSubState
|
||||||
}
|
}
|
||||||
|
|
||||||
var grpIcons:FlxSpriteGroup;
|
var grpIcons:FlxSpriteGroup;
|
||||||
|
|
||||||
var grpXSpread(default, set):Float = 107;
|
var grpXSpread(default, set):Float = 107;
|
||||||
var grpYSpread(default, set):Float = 127;
|
var grpYSpread(default, set):Float = 127;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -190,8 +190,8 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
|
||||||
var numberStepper:NumberStepper = new NumberStepper();
|
var numberStepper:NumberStepper = new NumberStepper();
|
||||||
numberStepper.id = field.name;
|
numberStepper.id = field.name;
|
||||||
numberStepper.step = field.step ?? 1.0;
|
numberStepper.step = field.step ?? 1.0;
|
||||||
numberStepper.min = field.min ?? 0.0;
|
if (field.min != null) numberStepper.min = field.min;
|
||||||
numberStepper.max = field.max ?? 10.0;
|
if (field.min != null) numberStepper.max = field.max;
|
||||||
if (field.defaultValue != null) numberStepper.value = field.defaultValue;
|
if (field.defaultValue != null) numberStepper.value = field.defaultValue;
|
||||||
input = numberStepper;
|
input = numberStepper;
|
||||||
case FLOAT:
|
case FLOAT:
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ class FreeplayDJ extends FlxAtlasSprite
|
||||||
{
|
{
|
||||||
// Represents the sprite's current status.
|
// Represents the sprite's current status.
|
||||||
// Without state machines I would have driven myself crazy years ago.
|
// Without state machines I would have driven myself crazy years ago.
|
||||||
public var currentState:DJBoyfriendState = Intro;
|
public var currentState:FreeplayDJState = Intro;
|
||||||
|
|
||||||
// A callback activated when the intro animation finishes.
|
// A callback activated when the intro animation finishes.
|
||||||
public var onIntroDone:FlxSignal = new FlxSignal();
|
public var onIntroDone:FlxSignal = new FlxSignal();
|
||||||
|
|
@ -99,7 +99,7 @@ class FreeplayDJ extends FlxAtlasSprite
|
||||||
playFlashAnimation(animPrefix, true, false, true);
|
playFlashAnimation(animPrefix, true, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (getCurrentAnimation() == animPrefix && this.isLoopFinished())
|
if (getCurrentAnimation() == animPrefix && this.isLoopComplete())
|
||||||
{
|
{
|
||||||
if (timeIdling >= IDLE_EGG_PERIOD && !seenIdleEasterEgg)
|
if (timeIdling >= IDLE_EGG_PERIOD && !seenIdleEasterEgg)
|
||||||
{
|
{
|
||||||
|
|
@ -111,18 +111,69 @@ class FreeplayDJ extends FlxAtlasSprite
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
timeIdling += elapsed;
|
timeIdling += elapsed;
|
||||||
|
case NewUnlock:
|
||||||
|
var animPrefix = playableCharData.getAnimationPrefix('newUnlock');
|
||||||
|
if (!hasAnimation(animPrefix))
|
||||||
|
{
|
||||||
|
currentState = Idle;
|
||||||
|
}
|
||||||
|
if (getCurrentAnimation() != animPrefix)
|
||||||
|
{
|
||||||
|
playFlashAnimation(animPrefix, true, false, true);
|
||||||
|
}
|
||||||
case Confirm:
|
case Confirm:
|
||||||
var animPrefix = playableCharData.getAnimationPrefix('confirm');
|
var animPrefix = playableCharData.getAnimationPrefix('confirm');
|
||||||
if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, false);
|
if (getCurrentAnimation() != animPrefix) playFlashAnimation(animPrefix, false);
|
||||||
timeIdling = 0;
|
timeIdling = 0;
|
||||||
case FistPumpIntro:
|
case FistPumpIntro:
|
||||||
var animPrefix = playableCharData.getAnimationPrefix('fistPump');
|
var animPrefixA = playableCharData.getAnimationPrefix('fistPump');
|
||||||
if (getCurrentAnimation() != animPrefix) playFlashAnimation('Boyfriend DJ fist pump', false);
|
var animPrefixB = playableCharData.getAnimationPrefix('loss');
|
||||||
if (getCurrentAnimation() == animPrefix && anim.curFrame >= 4)
|
|
||||||
|
if (getCurrentAnimation() == animPrefixA)
|
||||||
{
|
{
|
||||||
playAnimation("Boyfriend DJ fist pump", true, false, false, 0);
|
var endFrame = playableCharData.getFistPumpIntroEndFrame();
|
||||||
|
if (endFrame > -1 && anim.curFrame >= endFrame)
|
||||||
|
{
|
||||||
|
playFlashAnimation(animPrefixA, true, false, false, playableCharData.getFistPumpIntroStartFrame());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
else if (getCurrentAnimation() == animPrefixB)
|
||||||
|
{
|
||||||
|
var endFrame = playableCharData.getFistPumpIntroBadEndFrame();
|
||||||
|
if (endFrame > -1 && anim.curFrame >= endFrame)
|
||||||
|
{
|
||||||
|
playFlashAnimation(animPrefixB, true, false, false, playableCharData.getFistPumpIntroBadStartFrame());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FlxG.log.warn("Unrecognized animation in FistPumpIntro: " + getCurrentAnimation());
|
||||||
|
}
|
||||||
|
|
||||||
case FistPump:
|
case FistPump:
|
||||||
|
var animPrefixA = playableCharData.getAnimationPrefix('fistPump');
|
||||||
|
var animPrefixB = playableCharData.getAnimationPrefix('loss');
|
||||||
|
|
||||||
|
if (getCurrentAnimation() == animPrefixA)
|
||||||
|
{
|
||||||
|
var endFrame = playableCharData.getFistPumpLoopEndFrame();
|
||||||
|
if (endFrame > -1 && anim.curFrame >= endFrame)
|
||||||
|
{
|
||||||
|
playFlashAnimation(animPrefixA, true, false, false, playableCharData.getFistPumpLoopStartFrame());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (getCurrentAnimation() == animPrefixB)
|
||||||
|
{
|
||||||
|
var endFrame = playableCharData.getFistPumpLoopBadEndFrame();
|
||||||
|
if (endFrame > -1 && anim.curFrame >= endFrame)
|
||||||
|
{
|
||||||
|
playFlashAnimation(animPrefixB, true, false, false, playableCharData.getFistPumpLoopBadStartFrame());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FlxG.log.warn("Unrecognized animation in FistPump: " + getCurrentAnimation());
|
||||||
|
}
|
||||||
|
|
||||||
case IdleEasterEgg:
|
case IdleEasterEgg:
|
||||||
var animPrefix = playableCharData.getAnimationPrefix('idleEasterEgg');
|
var animPrefix = playableCharData.getAnimationPrefix('idleEasterEgg');
|
||||||
|
|
@ -185,7 +236,14 @@ class FreeplayDJ extends FlxAtlasSprite
|
||||||
|
|
||||||
if (name == playableCharData.getAnimationPrefix('intro'))
|
if (name == playableCharData.getAnimationPrefix('intro'))
|
||||||
{
|
{
|
||||||
currentState = Idle;
|
if (PlayerRegistry.instance.hasNewCharacter())
|
||||||
|
{
|
||||||
|
currentState = NewUnlock;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentState = Idle;
|
||||||
|
}
|
||||||
onIntroDone.dispatch();
|
onIntroDone.dispatch();
|
||||||
}
|
}
|
||||||
else if (name == playableCharData.getAnimationPrefix('idle'))
|
else if (name == playableCharData.getAnimationPrefix('idle'))
|
||||||
|
|
@ -225,9 +283,17 @@ class FreeplayDJ extends FlxAtlasSprite
|
||||||
// runTvLogic();
|
// runTvLogic();
|
||||||
}
|
}
|
||||||
trace('Replay idle: ${frame}');
|
trace('Replay idle: ${frame}');
|
||||||
playAnimation(playableCharData.getAnimationPrefix('cartoon'), true, false, false, frame);
|
playFlashAnimation(playableCharData.getAnimationPrefix('cartoon'), true, false, false, frame);
|
||||||
// trace('Finished confirm');
|
// trace('Finished confirm');
|
||||||
}
|
}
|
||||||
|
else if (name == playableCharData.getAnimationPrefix('newUnlock'))
|
||||||
|
{
|
||||||
|
// Animation should loop.
|
||||||
|
}
|
||||||
|
else if (name == playableCharData.getAnimationPrefix('charSelect'))
|
||||||
|
{
|
||||||
|
onCharSelectComplete();
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
trace('Finished ${name}');
|
trace('Finished ${name}');
|
||||||
|
|
@ -240,6 +306,15 @@ class FreeplayDJ extends FlxAtlasSprite
|
||||||
seenIdleEasterEgg = false;
|
seenIdleEasterEgg = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamic function, it's actually a variable you can reassign!
|
||||||
|
* `dj.onCharSelectComplete = function() {};`
|
||||||
|
*/
|
||||||
|
public dynamic function onCharSelectComplete():Void
|
||||||
|
{
|
||||||
|
trace('onCharSelectComplete()');
|
||||||
|
}
|
||||||
|
|
||||||
var offsetX:Float = 0.0;
|
var offsetX:Float = 0.0;
|
||||||
var offsetY:Float = 0.0;
|
var offsetY:Float = 0.0;
|
||||||
|
|
||||||
|
|
@ -271,7 +346,7 @@ class FreeplayDJ extends FlxAtlasSprite
|
||||||
function loadCartoon()
|
function loadCartoon()
|
||||||
{
|
{
|
||||||
cartoonSnd = FunkinSound.load(Paths.sound(getRandomFlashToon()), 1.0, false, true, true, function() {
|
cartoonSnd = FunkinSound.load(Paths.sound(getRandomFlashToon()), 1.0, false, true, true, function() {
|
||||||
playAnimation("Boyfriend DJ watchin tv OG", true, false, false, 60);
|
playFlashAnimation(playableCharData.getAnimationPrefix('cartoon'), true, false, false, 60);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fade out music to 40% volume over 1 second.
|
// Fade out music to 40% volume over 1 second.
|
||||||
|
|
@ -301,21 +376,48 @@ class FreeplayDJ extends FlxAtlasSprite
|
||||||
currentState = Confirm;
|
currentState = Confirm;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function fistPump():Void
|
public function toCharSelect():Void
|
||||||
|
{
|
||||||
|
if (hasAnimation('charSelect'))
|
||||||
|
{
|
||||||
|
currentState = CharSelect;
|
||||||
|
var animPrefix = playableCharData.getAnimationPrefix('charSelect');
|
||||||
|
playFlashAnimation(animPrefix, true, false, false, 0);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentState = Confirm;
|
||||||
|
// Call this immediately; otherwise, we get locked out of Character Select.
|
||||||
|
onCharSelectComplete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fistPumpIntro():Void
|
||||||
{
|
{
|
||||||
currentState = FistPumpIntro;
|
currentState = FistPumpIntro;
|
||||||
|
var animPrefix = playableCharData.getAnimationPrefix('fistPump');
|
||||||
|
playFlashAnimation(animPrefix, true, false, false, playableCharData.getFistPumpIntroStartFrame());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function pumpFist():Void
|
public function fistPump():Void
|
||||||
{
|
{
|
||||||
currentState = FistPump;
|
currentState = FistPump;
|
||||||
playAnimation("Boyfriend DJ fist pump", true, false, false, 4);
|
var animPrefix = playableCharData.getAnimationPrefix('fistPump');
|
||||||
|
playFlashAnimation(animPrefix, true, false, false, playableCharData.getFistPumpLoopStartFrame());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function pumpFistBad():Void
|
public function fistPumpLossIntro():Void
|
||||||
|
{
|
||||||
|
currentState = FistPumpIntro;
|
||||||
|
var animPrefix = playableCharData.getAnimationPrefix('loss');
|
||||||
|
playFlashAnimation(animPrefix, true, false, false, playableCharData.getFistPumpIntroBadStartFrame());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fistPumpLoss():Void
|
||||||
{
|
{
|
||||||
currentState = FistPump;
|
currentState = FistPump;
|
||||||
playAnimation("Boyfriend DJ loss reaction 1", true, false, false, 4);
|
var animPrefix = playableCharData.getAnimationPrefix('loss');
|
||||||
|
playFlashAnimation(animPrefix, true, false, false, playableCharData.getFistPumpLoopBadStartFrame());
|
||||||
}
|
}
|
||||||
|
|
||||||
override public function getCurrentAnimation():String
|
override public function getCurrentAnimation():String
|
||||||
|
|
@ -366,13 +468,53 @@ class FreeplayDJ extends FlxAtlasSprite
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DJBoyfriendState
|
enum FreeplayDJState
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Character enters the frame and transitions to Idle.
|
||||||
|
*/
|
||||||
Intro;
|
Intro;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Character loops in idle.
|
||||||
|
*/
|
||||||
Idle;
|
Idle;
|
||||||
Confirm;
|
|
||||||
FistPumpIntro;
|
/**
|
||||||
FistPump;
|
* Plays an easter egg animation after a period in Idle, then reverts to Idle.
|
||||||
|
*/
|
||||||
IdleEasterEgg;
|
IdleEasterEgg;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays an elaborate easter egg animation. Does not revert until another animation is triggered.
|
||||||
|
*/
|
||||||
Cartoon;
|
Cartoon;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Player has selected a song.
|
||||||
|
*/
|
||||||
|
Confirm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Character preps to play the fist pump animation; plays after the Results screen.
|
||||||
|
* The actual frame label that gets played may vary based on the player's success.
|
||||||
|
*/
|
||||||
|
FistPumpIntro;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Character plays the fist pump animation.
|
||||||
|
* The actual frame label that gets played may vary based on the player's success.
|
||||||
|
*/
|
||||||
|
FistPump;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays an animation to indicate that the player has a new unlock in Character Select.
|
||||||
|
* Overrides all idle animations as well as the fist pump. Only Confirm and CharSelect will override this.
|
||||||
|
*/
|
||||||
|
NewUnlock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plays an animation to transition to the Character Select screen.
|
||||||
|
*/
|
||||||
|
CharSelect;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -177,9 +177,22 @@ class FreeplayState extends MusicBeatSubState
|
||||||
|
|
||||||
var stickerSubState:Null<StickerSubState> = null;
|
var stickerSubState:Null<StickerSubState> = null;
|
||||||
|
|
||||||
public static var rememberedDifficulty:Null<String> = Constants.DEFAULT_DIFFICULTY;
|
/**
|
||||||
|
* The difficulty we were on when this menu was last accessed.
|
||||||
|
*/
|
||||||
|
public static var rememberedDifficulty:String = Constants.DEFAULT_DIFFICULTY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The song we were on when this menu was last accessed.
|
||||||
|
* NOTE: `null` if the last song was `Random`.
|
||||||
|
*/
|
||||||
public static var rememberedSongId:Null<String> = 'tutorial';
|
public static var rememberedSongId:Null<String> = 'tutorial';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The character we were on when this menu was last accessed.
|
||||||
|
*/
|
||||||
|
public static var rememberedCharacterId:String = Constants.DEFAULT_CHARACTER;
|
||||||
|
|
||||||
var funnyCam:FunkinCamera;
|
var funnyCam:FunkinCamera;
|
||||||
var rankCamera:FunkinCamera;
|
var rankCamera:FunkinCamera;
|
||||||
var rankBg:FunkinSprite;
|
var rankBg:FunkinSprite;
|
||||||
|
|
@ -209,14 +222,16 @@ class FreeplayState extends MusicBeatSubState
|
||||||
|
|
||||||
public function new(?params:FreeplayStateParams, ?stickers:StickerSubState)
|
public function new(?params:FreeplayStateParams, ?stickers:StickerSubState)
|
||||||
{
|
{
|
||||||
currentCharacterId = params?.character ?? Constants.DEFAULT_CHARACTER;
|
currentCharacterId = params?.character ?? rememberedCharacterId;
|
||||||
var fetchPlayableCharacter = function():PlayableCharacter {
|
var fetchPlayableCharacter = function():PlayableCharacter {
|
||||||
var result = PlayerRegistry.instance.fetchEntry(params?.character ?? Constants.DEFAULT_CHARACTER);
|
var result = PlayerRegistry.instance.fetchEntry(params?.character ?? rememberedCharacterId);
|
||||||
if (result == null) throw 'No valid playable character with id ${params?.character}';
|
if (result == null) throw 'No valid playable character with id ${params?.character}';
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
currentCharacter = fetchPlayableCharacter();
|
currentCharacter = fetchPlayableCharacter();
|
||||||
|
|
||||||
|
rememberedCharacterId = currentCharacter?.id ?? Constants.DEFAULT_CHARACTER;
|
||||||
|
|
||||||
fromResultsParams = params?.fromResults;
|
fromResultsParams = params?.fromResults;
|
||||||
|
|
||||||
if (fromResultsParams?.playRankAnim == true)
|
if (fromResultsParams?.playRankAnim == true)
|
||||||
|
|
@ -629,8 +644,8 @@ class FreeplayState extends MusicBeatSubState
|
||||||
speed: 0.3
|
speed: 0.3
|
||||||
});
|
});
|
||||||
|
|
||||||
var diffSelLeft:DifficultySelector = new DifficultySelector(20, grpDifficulties.y - 10, false, controls);
|
var diffSelLeft:DifficultySelector = new DifficultySelector(this, 20, grpDifficulties.y - 10, false, controls);
|
||||||
var diffSelRight:DifficultySelector = new DifficultySelector(325, grpDifficulties.y - 10, true, controls);
|
var diffSelRight:DifficultySelector = new DifficultySelector(this, 325, grpDifficulties.y - 10, true, controls);
|
||||||
diffSelLeft.visible = false;
|
diffSelLeft.visible = false;
|
||||||
diffSelRight.visible = false;
|
diffSelRight.visible = false;
|
||||||
add(diffSelLeft);
|
add(diffSelLeft);
|
||||||
|
|
@ -743,10 +758,7 @@ class FreeplayState extends MusicBeatSubState
|
||||||
var tempSongs:Array<Null<FreeplaySongData>> = songs;
|
var tempSongs:Array<Null<FreeplaySongData>> = songs;
|
||||||
|
|
||||||
// Remember just the difficulty because it's important for song sorting.
|
// Remember just the difficulty because it's important for song sorting.
|
||||||
if (rememberedDifficulty != null)
|
currentDifficulty = rememberedDifficulty;
|
||||||
{
|
|
||||||
currentDifficulty = rememberedDifficulty;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterStuff != null) tempSongs = sortSongs(tempSongs, filterStuff);
|
if (filterStuff != null) tempSongs = sortSongs(tempSongs, filterStuff);
|
||||||
|
|
||||||
|
|
@ -901,7 +913,15 @@ class FreeplayState extends MusicBeatSubState
|
||||||
changeSelection();
|
changeSelection();
|
||||||
changeDiff();
|
changeDiff();
|
||||||
|
|
||||||
if (dj != null) dj.fistPump();
|
if (fromResultsParams?.newRank == SHIT)
|
||||||
|
{
|
||||||
|
if (dj != null) dj.fistPumpLossIntro();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (dj != null) dj.fistPumpIntro();
|
||||||
|
}
|
||||||
|
|
||||||
// rankCamera.fade(FlxColor.BLACK, 0.5, true);
|
// rankCamera.fade(FlxColor.BLACK, 0.5, true);
|
||||||
rankCamera.fade(0xFF000000, 0.5, true, null, true);
|
rankCamera.fade(0xFF000000, 0.5, true, null, true);
|
||||||
if (FlxG.sound.music != null) FlxG.sound.music.volume = 0;
|
if (FlxG.sound.music != null) FlxG.sound.music.volume = 0;
|
||||||
|
|
@ -1083,11 +1103,11 @@ class FreeplayState extends MusicBeatSubState
|
||||||
|
|
||||||
if (fromResultsParams?.newRank == SHIT)
|
if (fromResultsParams?.newRank == SHIT)
|
||||||
{
|
{
|
||||||
if (dj != null) dj.pumpFistBad();
|
if (dj != null) dj.fistPumpLoss();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (dj != null) dj.pumpFist();
|
if (dj != null) dj.fistPump();
|
||||||
}
|
}
|
||||||
|
|
||||||
rankCamera.zoom = 0.8;
|
rankCamera.zoom = 0.8;
|
||||||
|
|
@ -1190,7 +1210,7 @@ class FreeplayState extends MusicBeatSubState
|
||||||
/**
|
/**
|
||||||
* If true, disable interaction with the interface.
|
* If true, disable interaction with the interface.
|
||||||
*/
|
*/
|
||||||
var busy:Bool = false;
|
public var busy:Bool = false;
|
||||||
|
|
||||||
var originalPos:FlxPoint = new FlxPoint();
|
var originalPos:FlxPoint = new FlxPoint();
|
||||||
|
|
||||||
|
|
@ -1233,7 +1253,32 @@ class FreeplayState extends MusicBeatSubState
|
||||||
|
|
||||||
if (controls.FREEPLAY_CHAR_SELECT && !busy)
|
if (controls.FREEPLAY_CHAR_SELECT && !busy)
|
||||||
{
|
{
|
||||||
FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState());
|
// Check if we have ACCESS to character select!
|
||||||
|
trace('Is Pico unlocked? ${PlayerRegistry.instance.fetchEntry('pico')?.isUnlocked()}');
|
||||||
|
trace('Number of characters: ${PlayerRegistry.instance.countUnlockedCharacters()}');
|
||||||
|
|
||||||
|
if (PlayerRegistry.instance.countUnlockedCharacters() > 1)
|
||||||
|
{
|
||||||
|
if (dj != null)
|
||||||
|
{
|
||||||
|
busy = true;
|
||||||
|
// Transition to character select after animation
|
||||||
|
dj.onCharSelectComplete = function() {
|
||||||
|
FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState());
|
||||||
|
}
|
||||||
|
dj.toCharSelect();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Transition to character select immediately
|
||||||
|
FlxG.switchState(new funkin.ui.charSelect.CharSelectSubState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
trace('Not enough characters unlocked to open character select!');
|
||||||
|
FunkinSound.playOnce(Paths.sound('cancelMenu'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (controls.FREEPLAY_FAVORITE && !busy)
|
if (controls.FREEPLAY_FAVORITE && !busy)
|
||||||
|
|
@ -1326,6 +1371,8 @@ class FreeplayState extends MusicBeatSubState
|
||||||
}
|
}
|
||||||
|
|
||||||
handleInputs(elapsed);
|
handleInputs(elapsed);
|
||||||
|
|
||||||
|
if (dj != null) FlxG.watch.addQuick('dj-anim', dj.getCurrentAnimation());
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleInputs(elapsed:Float):Void
|
function handleInputs(elapsed:Float):Void
|
||||||
|
|
@ -1483,7 +1530,7 @@ class FreeplayState extends MusicBeatSubState
|
||||||
generateSongList(currentFilter, true);
|
generateSongList(currentFilter, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (controls.BACK)
|
if (controls.BACK && !busy)
|
||||||
{
|
{
|
||||||
busy = true;
|
busy = true;
|
||||||
FlxTween.globalManager.clear();
|
FlxTween.globalManager.clear();
|
||||||
|
|
@ -1891,7 +1938,7 @@ class FreeplayState extends MusicBeatSubState
|
||||||
intendedCompletion = 0.0;
|
intendedCompletion = 0.0;
|
||||||
diffIdsCurrent = diffIdsTotal;
|
diffIdsCurrent = diffIdsTotal;
|
||||||
rememberedSongId = null;
|
rememberedSongId = null;
|
||||||
rememberedDifficulty = null;
|
rememberedDifficulty = Constants.DEFAULT_DIFFICULTY;
|
||||||
albumRoll.albumId = null;
|
albumRoll.albumId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2000,10 +2047,13 @@ class DifficultySelector extends FlxSprite
|
||||||
var controls:Controls;
|
var controls:Controls;
|
||||||
var whiteShader:PureColor;
|
var whiteShader:PureColor;
|
||||||
|
|
||||||
public function new(x:Float, y:Float, flipped:Bool, controls:Controls)
|
var parent:FreeplayState;
|
||||||
|
|
||||||
|
public function new(parent:FreeplayState, x:Float, y:Float, flipped:Bool, controls:Controls)
|
||||||
{
|
{
|
||||||
super(x, y);
|
super(x, y);
|
||||||
|
|
||||||
|
this.parent = parent;
|
||||||
this.controls = controls;
|
this.controls = controls;
|
||||||
|
|
||||||
frames = Paths.getSparrowAtlas('freeplay/freeplaySelector');
|
frames = Paths.getSparrowAtlas('freeplay/freeplaySelector');
|
||||||
|
|
@ -2019,8 +2069,8 @@ class DifficultySelector extends FlxSprite
|
||||||
|
|
||||||
override function update(elapsed:Float):Void
|
override function update(elapsed:Float):Void
|
||||||
{
|
{
|
||||||
if (flipX && controls.UI_RIGHT_P) moveShitDown();
|
if (flipX && controls.UI_RIGHT_P && !parent.busy) moveShitDown();
|
||||||
if (!flipX && controls.UI_LEFT_P) moveShitDown();
|
if (!flipX && controls.UI_LEFT_P && !parent.busy) moveShitDown();
|
||||||
|
|
||||||
super.update(elapsed);
|
super.update(elapsed);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,11 @@ class PlayableCharacter implements IRegistryEntry<PlayerData>
|
||||||
return _data.freeplayDJ.getFreeplayDJText(index);
|
return _data.freeplayDJ.getFreeplayDJText(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getCharSelectData():PlayerCharSelectData
|
||||||
|
{
|
||||||
|
return _data.charSelect;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param rank Which rank to get info for
|
* @param rank Which rank to get info for
|
||||||
* @return An array of animations. For example, BF Great has two animations, one for BF and one for GF
|
* @return An array of animations. For example, BF Great has two animations, one for BF and one for GF
|
||||||
|
|
|
||||||
|
|
@ -98,14 +98,7 @@ class MainMenuState extends MusicBeatState
|
||||||
add(menuItems);
|
add(menuItems);
|
||||||
menuItems.onChange.add(onMenuItemChange);
|
menuItems.onChange.add(onMenuItemChange);
|
||||||
menuItems.onAcceptPress.add(function(_) {
|
menuItems.onAcceptPress.add(function(_) {
|
||||||
if (_.name == 'freeplay')
|
FlxFlicker.flicker(magenta, 1.1, 0.15, false, true);
|
||||||
{
|
|
||||||
magenta.visible = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
FlxFlicker.flicker(magenta, 1.1, 0.15, false, true);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
menuItems.enabled = true; // can move on intro
|
menuItems.enabled = true; // can move on intro
|
||||||
|
|
@ -117,10 +110,7 @@ class MainMenuState extends MusicBeatState
|
||||||
FlxTransitionableState.skipNextTransIn = true;
|
FlxTransitionableState.skipNextTransIn = true;
|
||||||
FlxTransitionableState.skipNextTransOut = true;
|
FlxTransitionableState.skipNextTransOut = true;
|
||||||
|
|
||||||
openSubState(new FreeplayState(
|
openSubState(new FreeplayState());
|
||||||
{
|
|
||||||
character: FlxG.keys.pressed.SHIFT ? 'pico' : 'bf',
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
#if CAN_OPEN_LINKS
|
#if CAN_OPEN_LINKS
|
||||||
|
|
@ -355,6 +345,7 @@ class MainMenuState extends MusicBeatState
|
||||||
|
|
||||||
if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.W)
|
if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.W)
|
||||||
{
|
{
|
||||||
|
FunkinSound.playOnce(Paths.sound('confirmMenu'));
|
||||||
// Give the user a score of 1 point on Weekend 1 story mode.
|
// Give the user a score of 1 point on Weekend 1 story mode.
|
||||||
// This makes the level count as cleared and displays the songs in Freeplay.
|
// This makes the level count as cleared and displays the songs in Freeplay.
|
||||||
funkin.save.Save.instance.setLevelScore('weekend1', 'easy',
|
funkin.save.Save.instance.setLevelScore('weekend1', 'easy',
|
||||||
|
|
@ -375,6 +366,29 @@ class MainMenuState extends MusicBeatState
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.L)
|
||||||
|
{
|
||||||
|
FunkinSound.playOnce(Paths.sound('confirmMenu'));
|
||||||
|
// Give the user a score of 0 points on Weekend 1 story mode.
|
||||||
|
// This makes the level count as uncleared and no longer displays the songs in Freeplay.
|
||||||
|
funkin.save.Save.instance.setLevelScore('weekend1', 'easy',
|
||||||
|
{
|
||||||
|
score: 1,
|
||||||
|
tallies:
|
||||||
|
{
|
||||||
|
sick: 0,
|
||||||
|
good: 0,
|
||||||
|
bad: 0,
|
||||||
|
shit: 0,
|
||||||
|
missed: 0,
|
||||||
|
combo: 0,
|
||||||
|
maxCombo: 0,
|
||||||
|
totalNotesHit: 0,
|
||||||
|
totalNotes: 0,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.R)
|
if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.R)
|
||||||
{
|
{
|
||||||
// Give the user a hypothetical overridden score,
|
// Give the user a hypothetical overridden score,
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ class LevelProp extends Bopper
|
||||||
this.propData = value;
|
this.propData = value;
|
||||||
|
|
||||||
this.visible = this.propData != null;
|
this.visible = this.propData != null;
|
||||||
danceEvery = this.propData?.danceEvery ?? 0.0;
|
danceEvery = this.propData?.danceEvery ?? 1.0;
|
||||||
|
|
||||||
applyData();
|
applyData();
|
||||||
}
|
}
|
||||||
|
|
@ -32,7 +32,7 @@ class LevelProp extends Bopper
|
||||||
|
|
||||||
public function playConfirm():Void
|
public function playConfirm():Void
|
||||||
{
|
{
|
||||||
playAnimation('confirm', true, true);
|
if (hasAnimation('confirm')) playAnimation('confirm', true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyData():Void
|
function applyData():Void
|
||||||
|
|
|
||||||
|
|
@ -314,25 +314,28 @@ class LoadingState extends MusicBeatSubState
|
||||||
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num7'));
|
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num7'));
|
||||||
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num8'));
|
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num8'));
|
||||||
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num9'));
|
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num9'));
|
||||||
|
|
||||||
FunkinSprite.cacheTexture(Paths.image('notes', 'shared'));
|
FunkinSprite.cacheTexture(Paths.image('notes', 'shared'));
|
||||||
FunkinSprite.cacheTexture(Paths.image('noteSplashes', 'shared'));
|
FunkinSprite.cacheTexture(Paths.image('noteSplashes', 'shared'));
|
||||||
FunkinSprite.cacheTexture(Paths.image('noteStrumline', 'shared'));
|
FunkinSprite.cacheTexture(Paths.image('noteStrumline', 'shared'));
|
||||||
FunkinSprite.cacheTexture(Paths.image('NOTE_hold_assets'));
|
FunkinSprite.cacheTexture(Paths.image('NOTE_hold_assets'));
|
||||||
|
|
||||||
FunkinSprite.cacheTexture(Paths.image('ui/countdown/funkin/ready', 'shared'));
|
FunkinSprite.cacheTexture(Paths.image('ui/countdown/funkin/ready', 'shared'));
|
||||||
FunkinSprite.cacheTexture(Paths.image('ui/countdown/funkin/set', 'shared'));
|
FunkinSprite.cacheTexture(Paths.image('ui/countdown/funkin/set', 'shared'));
|
||||||
FunkinSprite.cacheTexture(Paths.image('ui/countdown/funkin/go', 'shared'));
|
FunkinSprite.cacheTexture(Paths.image('ui/countdown/funkin/go', 'shared'));
|
||||||
FunkinSprite.cacheTexture(Paths.image('ui/countdown/pixel/ready', 'shared'));
|
FunkinSprite.cacheTexture(Paths.image('ui/countdown/pixel/ready', 'shared'));
|
||||||
FunkinSprite.cacheTexture(Paths.image('ui/countdown/pixel/set', 'shared'));
|
FunkinSprite.cacheTexture(Paths.image('ui/countdown/pixel/set', 'shared'));
|
||||||
FunkinSprite.cacheTexture(Paths.image('ui/countdown/pixel/go', 'shared'));
|
FunkinSprite.cacheTexture(Paths.image('ui/countdown/pixel/go', 'shared'));
|
||||||
FunkinSprite.cacheTexture(Paths.image('ui/popup/normal/sick'));
|
|
||||||
FunkinSprite.cacheTexture(Paths.image('ui/popup/normal/good'));
|
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/sick'));
|
||||||
FunkinSprite.cacheTexture(Paths.image('ui/popup/normal/bad'));
|
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/good'));
|
||||||
FunkinSprite.cacheTexture(Paths.image('ui/popup/normal/shit'));
|
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/bad'));
|
||||||
|
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/shit'));
|
||||||
|
|
||||||
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/sick'));
|
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/sick'));
|
||||||
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/good'));
|
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/good'));
|
||||||
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/bad'));
|
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/bad'));
|
||||||
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/shit'));
|
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/shit'));
|
||||||
FunkinSprite.cacheTexture(Paths.image('miss', 'shared')); // TODO: remove this
|
|
||||||
|
|
||||||
// List all image assets in the level's library.
|
// List all image assets in the level's library.
|
||||||
// This is crude and I want to remove it when we have a proper asset caching system.
|
// This is crude and I want to remove it when we have a proper asset caching system.
|
||||||
|
|
|
||||||
|
|
@ -524,12 +524,16 @@ class Constants
|
||||||
* OTHER
|
* OTHER
|
||||||
*/
|
*/
|
||||||
// ==============================
|
// ==============================
|
||||||
|
#if FEATURE_GHOST_TAPPING
|
||||||
|
// Hey there, Eric here.
|
||||||
|
// This feature is currently still in development. You can test it out by creating a special debug build!
|
||||||
|
// lime build windows -DFEATURE_GHOST_TAPPING
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If true, the player will not receive the ghost miss penalty if there are no notes within the hit window.
|
* Duration, in seconds, after the player's section ends before the player can spam without penalty.
|
||||||
* This is the thing people have been begging for forever lolol.
|
|
||||||
*/
|
*/
|
||||||
public static final GHOST_TAPPING:Bool = false;
|
public static final GHOST_TAP_DELAY:Float = 3 / 8;
|
||||||
|
#end
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The maximum number of previous file paths for the Chart Editor to remember.
|
* The maximum number of previous file paths for the Chart Editor to remember.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue