1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2024-11-15 11:22:55 +00:00

Merge branch 'bugfix/various-develop-fixes' into rewrite/master

This commit is contained in:
Cameron Taylor 2024-08-28 16:32:50 -04:00
commit 1fbca9ab6a
20 changed files with 605 additions and 116 deletions

2
assets

@ -1 +1 @@
Subproject commit e19aaca19254ec2717a4f942005f32b42b6ecb18
Subproject commit 7210562035c7ab6d2122606ec607b3e897a5ef20

View file

@ -38,6 +38,17 @@ class PlayerData
@:optional
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;
/**
@ -97,6 +108,9 @@ class PlayerFreeplayDJData
@:optional
var cartoon:Null<PlayerFreeplayDJCartoonData>;
@:optional
var fistPump:Null<PlayerFreeplayDJFistPumpData>;
public function new()
{
animationMap = new Map();
@ -183,6 +197,58 @@ class PlayerFreeplayDJData
{
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 =
@ -242,3 +308,30 @@ typedef PlayerFreeplayDJCartoonData =
var loopFrame: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;
};

View file

@ -3,6 +3,7 @@ package funkin.data.freeplay.player;
import funkin.data.freeplay.player.PlayerData;
import funkin.ui.freeplay.charselect.PlayableCharacter;
import funkin.ui.freeplay.charselect.ScriptedPlayableCharacter;
import funkin.save.Save;
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.');
}
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.
* @param characterId The stage character ID.

View file

@ -1979,7 +1979,7 @@ class PlayState extends MusicBeatSubState
if (vocals == null) return;
// 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;
FlxG.sound.music.pause();
vocals.pause();
@ -2221,10 +2221,14 @@ class PlayState extends MusicBeatSubState
// Calling event.cancelEvent() skips all the other logic! Neat!
if (event.eventCanceled) continue;
// Skip handling the miss in botplay!
if (!isBotPlayMode)
{
// Judge the miss.
// NOTE: This is what handles the scoring.
trace('Missed note! ${note.noteData}');
onNoteMiss(note, event.playSound, event.healthChange);
}
note.handledMiss = true;
}
@ -2321,9 +2325,16 @@ class PlayState extends MusicBeatSubState
playerStrumline.pressKey(input.noteDirection);
// Don't credit or penalize inputs in Bot Play.
if (isBotPlayMode) continue;
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.
// Perform a ghost miss (anti-spam).
@ -2333,16 +2344,6 @@ class PlayState extends MusicBeatSubState
playerStrumline.playPress(input.noteDirection);
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)
{
// Press a key with no penalty.

View file

@ -305,6 +305,8 @@ class CharacterDataParser
icon = "darnell";
case "senpai-angry":
icon = "senpai";
case "spooky-dark":
icon = "spooky";
case "tankman-atlas":
icon = "tankman";
}

View file

@ -114,7 +114,7 @@ class ZoomCameraSongEvent extends SongEvent
name: 'zoom',
title: 'Zoom Level',
defaultValue: 1.0,
step: 0.1,
step: 0.05,
type: SongEventFieldType.FLOAT,
units: 'x'
},

View file

@ -94,6 +94,10 @@ class Strumline extends FlxSpriteGroup
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,
* so we can easily rewind.
@ -179,21 +183,36 @@ class Strumline extends FlxSpriteGroup
super.update(elapsed);
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.
*/
public function mayGhostTap():Bool
{
// TODO: Refine this. Only querying "can be hit" is too tight but "is being rendered" is too loose.
// Also, if you just hit a note, there should be a (short) period where this is off so you can't spam.
// Any notes in range of the strumline.
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.
return notes.members.filter(function(note:NoteSprite) {
return note != null && note.alive && !note.hasBeenHit;
}).length == 0;
// Note has been hit recently.
if (ghostTapTimer > 0.0) return false;
// **yippee**
return true;
}
#end
/**
* 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.
*/
@ -563,6 +608,10 @@ class Strumline extends FlxSpriteGroup
playStatic(dir);
}
resetScrollSpeed();
#if FEATURE_GHOST_TAPPING
ghostTapTimer = 0;
#end
}
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;
}
#if FEATURE_GHOST_TAPPING
ghostTapTimer = Constants.GHOST_TAP_DELAY;
#end
}
public function killNote(note:NoteSprite):Void

View file

@ -315,7 +315,7 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
public function isAnimationFinished():Bool
{
return this.animation.finished;
return this.animation?.finished ?? false;
}
public function setAnimationOffsets(name:String, xOffset:Float, yOffset:Float):Void

View file

@ -436,8 +436,9 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
// Start with the per-stage character position.
// Subtracting the origin ensures characters are positioned relative to their feet.
// Subtracting the global offset allows positioning on a per-character basis.
character.x = stageCharData.position[0] - character.characterOrigin.x + character.globalOffsets[0];
character.y = stageCharData.position[1] - character.characterOrigin.y + character.globalOffsets[1];
// We previously applied the global offset here but that is now done elsewhere.
character.x = stageCharData.position[0] - character.characterOrigin.x;
character.y = stageCharData.position[1] - character.characterOrigin.y;
@:privateAccess(funkin.play.stage.Bopper)
{

View file

@ -121,6 +121,12 @@ class Save
modOptions: [],
},
unlocks:
{
// Default to having seen the default character.
charactersSeen: ["bf"],
},
optionsChartEditor:
{
// Reasonable defaults.
@ -393,6 +399,22 @@ class Save
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.
*
@ -471,10 +493,18 @@ class Save
for (difficulty in difficultyList)
{
var score:Null<SaveScoreData> = getLevelScore(levelId, difficulty);
// TODO: Do we need to check accuracy/score here?
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;
@ -630,10 +660,18 @@ class Save
for (difficulty in difficultyList)
{
var score:Null<SaveScoreData> = getSongScore(songId, difficulty);
// TODO: Do we need to check accuracy/score here?
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;
@ -956,6 +994,8 @@ typedef RawSaveData =
*/
var options:SaveDataOptions;
var unlocks:SaveDataUnlocks;
/**
* The user's favorited songs in the Freeplay menu,
* as a list of song IDs.
@ -980,6 +1020,15 @@ typedef SaveApiNewgroundsData =
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.
*/

View file

@ -22,14 +22,26 @@ class PixelatedIcon extends FlxSprite
switch (char)
{
case 'monster-christmas':
charPath += 'monsterpixel';
case 'mom-car':
charPath += 'mommypixel';
case 'darnell-blazin':
charPath += 'darnellpixel';
case 'senpai-angry':
charPath += 'senpaipixel';
case "bf-christmas" | "bf-car" | "bf-pixel" | "bf-holding-gf":
charPath += "bfpixel";
case "monster-christmas":
charPath += "monsterpixel";
case "mom" | "mom-car":
charPath += "mommypixel";
case "pico-blazin" | "pico-playable" | "pico-speaker":
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:
charPath += '${char}pixel';
}

View file

@ -1,27 +1,31 @@
package funkin.ui.charSelect;
import funkin.ui.freeplay.FreeplayState;
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.FlxObject;
import flixel.FlxSprite;
import flixel.group.FlxGroup;
import flixel.group.FlxGroup.FlxTypedGroup;
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.ScriptEventDispatcher;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import flixel.FlxObject;
import openfl.display.BlendMode;
import flixel.group.FlxGroup;
import funkin.play.stage.Stage;
import funkin.ui.freeplay.charselect.PlayableCharacter;
import funkin.ui.freeplay.FreeplayState;
import funkin.ui.PixelatedIcon;
import funkin.util.MathUtil;
import flixel.util.FlxTimer;
import flixel.tweens.FlxEase;
import flixel.sound.FlxSound;
import funkin.audio.FunkinSound;
import funkin.vis.dsp.SpectralAnalyzer;
import openfl.display.BlendMode;
class CharSelectSubState extends MusicBeatSubState
{
@ -67,8 +71,29 @@ class CharSelectSubState extends MusicBeatSubState
{
super();
availableChars.set(4, "bf");
availableChars.set(3, "pico");
loadAvailableCharacters();
}
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
@ -269,7 +294,6 @@ class CharSelectSubState extends MusicBeatSubState
}
var grpIcons:FlxSpriteGroup;
var grpXSpread(default, set):Float = 107;
var grpYSpread(default, set):Float = 127;

View file

@ -190,8 +190,8 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
var numberStepper:NumberStepper = new NumberStepper();
numberStepper.id = field.name;
numberStepper.step = field.step ?? 1.0;
numberStepper.min = field.min ?? 0.0;
numberStepper.max = field.max ?? 10.0;
if (field.min != null) numberStepper.min = field.min;
if (field.min != null) numberStepper.max = field.max;
if (field.defaultValue != null) numberStepper.value = field.defaultValue;
input = numberStepper;
case FLOAT:

View file

@ -15,7 +15,7 @@ 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;
public var currentState:FreeplayDJState = Intro;
// A callback activated when the intro animation finishes.
public var onIntroDone:FlxSignal = new FlxSignal();
@ -99,7 +99,7 @@ class FreeplayDJ extends FlxAtlasSprite
playFlashAnimation(animPrefix, true, false, true);
}
if (getCurrentAnimation() == animPrefix && this.isLoopFinished())
if (getCurrentAnimation() == animPrefix && this.isLoopComplete())
{
if (timeIdling >= IDLE_EGG_PERIOD && !seenIdleEasterEgg)
{
@ -111,18 +111,69 @@ class FreeplayDJ extends FlxAtlasSprite
}
}
timeIdling += elapsed;
case NewUnlock:
var animPrefix = playableCharData.getAnimationPrefix('newUnlock');
if (!hasAnimation(animPrefix))
{
currentState = Idle;
}
if (getCurrentAnimation() != animPrefix)
{
playFlashAnimation(animPrefix, true, false, true);
}
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)
var animPrefixA = playableCharData.getAnimationPrefix('fistPump');
var animPrefixB = playableCharData.getAnimationPrefix('loss');
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:
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:
var animPrefix = playableCharData.getAnimationPrefix('idleEasterEgg');
@ -185,7 +236,14 @@ class FreeplayDJ extends FlxAtlasSprite
if (name == playableCharData.getAnimationPrefix('intro'))
{
currentState = Idle;
if (PlayerRegistry.instance.hasNewCharacter())
{
currentState = NewUnlock;
}
else
{
currentState = Idle;
}
onIntroDone.dispatch();
}
else if (name == playableCharData.getAnimationPrefix('idle'))
@ -225,9 +283,17 @@ class FreeplayDJ extends FlxAtlasSprite
// runTvLogic();
}
trace('Replay idle: ${frame}');
playAnimation(playableCharData.getAnimationPrefix('cartoon'), true, false, false, frame);
playFlashAnimation(playableCharData.getAnimationPrefix('cartoon'), true, false, false, frame);
// trace('Finished confirm');
}
else if (name == playableCharData.getAnimationPrefix('newUnlock'))
{
// Animation should loop.
}
else if (name == playableCharData.getAnimationPrefix('charSelect'))
{
onCharSelectComplete();
}
else
{
trace('Finished ${name}');
@ -240,6 +306,15 @@ class FreeplayDJ extends FlxAtlasSprite
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 offsetY:Float = 0.0;
@ -271,7 +346,7 @@ class FreeplayDJ extends FlxAtlasSprite
function loadCartoon()
{
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.
@ -301,21 +376,48 @@ class FreeplayDJ extends FlxAtlasSprite
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;
var animPrefix = playableCharData.getAnimationPrefix('fistPump');
playFlashAnimation(animPrefix, true, false, false, playableCharData.getFistPumpIntroStartFrame());
}
public function pumpFist():Void
public function fistPump():Void
{
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;
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
@ -366,13 +468,53 @@ class FreeplayDJ extends FlxAtlasSprite
}
}
enum DJBoyfriendState
enum FreeplayDJState
{
/**
* Character enters the frame and transitions to Idle.
*/
Intro;
/**
* Character loops in idle.
*/
Idle;
Confirm;
FistPumpIntro;
FistPump;
/**
* Plays an easter egg animation after a period in Idle, then reverts to Idle.
*/
IdleEasterEgg;
/**
* Plays an elaborate easter egg animation. Does not revert until another animation is triggered.
*/
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;
}

View file

@ -177,9 +177,22 @@ class FreeplayState extends MusicBeatSubState
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';
/**
* The character we were on when this menu was last accessed.
*/
public static var rememberedCharacterId:String = Constants.DEFAULT_CHARACTER;
var funnyCam:FunkinCamera;
var rankCamera:FunkinCamera;
var rankBg:FunkinSprite;
@ -209,14 +222,16 @@ class FreeplayState extends MusicBeatSubState
public function new(?params:FreeplayStateParams, ?stickers:StickerSubState)
{
currentCharacterId = params?.character ?? Constants.DEFAULT_CHARACTER;
currentCharacterId = params?.character ?? rememberedCharacterId;
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}';
return result;
};
currentCharacter = fetchPlayableCharacter();
rememberedCharacterId = currentCharacter?.id ?? Constants.DEFAULT_CHARACTER;
fromResultsParams = params?.fromResults;
if (fromResultsParams?.playRankAnim == true)
@ -629,8 +644,8 @@ class FreeplayState extends MusicBeatSubState
speed: 0.3
});
var diffSelLeft:DifficultySelector = new DifficultySelector(20, grpDifficulties.y - 10, false, controls);
var diffSelRight:DifficultySelector = new DifficultySelector(325, grpDifficulties.y - 10, true, controls);
var diffSelLeft:DifficultySelector = new DifficultySelector(this, 20, grpDifficulties.y - 10, false, controls);
var diffSelRight:DifficultySelector = new DifficultySelector(this, 325, grpDifficulties.y - 10, true, controls);
diffSelLeft.visible = false;
diffSelRight.visible = false;
add(diffSelLeft);
@ -743,10 +758,7 @@ class FreeplayState extends MusicBeatSubState
var tempSongs:Array<Null<FreeplaySongData>> = songs;
// 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);
@ -901,7 +913,15 @@ class FreeplayState extends MusicBeatSubState
changeSelection();
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(0xFF000000, 0.5, true, null, true);
if (FlxG.sound.music != null) FlxG.sound.music.volume = 0;
@ -1083,11 +1103,11 @@ class FreeplayState extends MusicBeatSubState
if (fromResultsParams?.newRank == SHIT)
{
if (dj != null) dj.pumpFistBad();
if (dj != null) dj.fistPumpLoss();
}
else
{
if (dj != null) dj.pumpFist();
if (dj != null) dj.fistPump();
}
rankCamera.zoom = 0.8;
@ -1190,7 +1210,7 @@ class FreeplayState extends MusicBeatSubState
/**
* If true, disable interaction with the interface.
*/
var busy:Bool = false;
public var busy:Bool = false;
var originalPos:FlxPoint = new FlxPoint();
@ -1233,7 +1253,32 @@ class FreeplayState extends MusicBeatSubState
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)
@ -1326,6 +1371,8 @@ class FreeplayState extends MusicBeatSubState
}
handleInputs(elapsed);
if (dj != null) FlxG.watch.addQuick('dj-anim', dj.getCurrentAnimation());
}
function handleInputs(elapsed:Float):Void
@ -1483,7 +1530,7 @@ class FreeplayState extends MusicBeatSubState
generateSongList(currentFilter, true);
}
if (controls.BACK)
if (controls.BACK && !busy)
{
busy = true;
FlxTween.globalManager.clear();
@ -1891,7 +1938,7 @@ class FreeplayState extends MusicBeatSubState
intendedCompletion = 0.0;
diffIdsCurrent = diffIdsTotal;
rememberedSongId = null;
rememberedDifficulty = null;
rememberedDifficulty = Constants.DEFAULT_DIFFICULTY;
albumRoll.albumId = null;
}
@ -2000,10 +2047,13 @@ class DifficultySelector extends FlxSprite
var controls:Controls;
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);
this.parent = parent;
this.controls = controls;
frames = Paths.getSparrowAtlas('freeplay/freeplaySelector');
@ -2019,8 +2069,8 @@ class DifficultySelector extends FlxSprite
override function update(elapsed:Float):Void
{
if (flipX && controls.UI_RIGHT_P) moveShitDown();
if (!flipX && controls.UI_LEFT_P) moveShitDown();
if (flipX && controls.UI_RIGHT_P && !parent.busy) moveShitDown();
if (!flipX && controls.UI_LEFT_P && !parent.busy) moveShitDown();
super.update(elapsed);
}

View file

@ -88,6 +88,11 @@ class PlayableCharacter implements IRegistryEntry<PlayerData>
return _data.freeplayDJ.getFreeplayDJText(index);
}
public function getCharSelectData():PlayerCharSelectData
{
return _data.charSelect;
}
/**
* @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

View file

@ -98,14 +98,7 @@ class MainMenuState extends MusicBeatState
add(menuItems);
menuItems.onChange.add(onMenuItemChange);
menuItems.onAcceptPress.add(function(_) {
if (_.name == 'freeplay')
{
magenta.visible = true;
}
else
{
FlxFlicker.flicker(magenta, 1.1, 0.15, false, true);
}
FlxFlicker.flicker(magenta, 1.1, 0.15, false, true);
});
menuItems.enabled = true; // can move on intro
@ -117,10 +110,7 @@ class MainMenuState extends MusicBeatState
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
openSubState(new FreeplayState(
{
character: FlxG.keys.pressed.SHIFT ? 'pico' : 'bf',
}));
openSubState(new FreeplayState());
});
#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)
{
FunkinSound.playOnce(Paths.sound('confirmMenu'));
// 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.
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)
{
// Give the user a hypothetical overridden score,

View file

@ -16,7 +16,7 @@ class LevelProp extends Bopper
this.propData = value;
this.visible = this.propData != null;
danceEvery = this.propData?.danceEvery ?? 0.0;
danceEvery = this.propData?.danceEvery ?? 1.0;
applyData();
}
@ -32,7 +32,7 @@ class LevelProp extends Bopper
public function playConfirm():Void
{
playAnimation('confirm', true, true);
if (hasAnimation('confirm')) playAnimation('confirm', true, true);
}
function applyData():Void

View file

@ -314,25 +314,28 @@ class LoadingState extends MusicBeatSubState
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num7'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num8'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/num9'));
FunkinSprite.cacheTexture(Paths.image('notes', 'shared'));
FunkinSprite.cacheTexture(Paths.image('noteSplashes', 'shared'));
FunkinSprite.cacheTexture(Paths.image('noteStrumline', 'shared'));
FunkinSprite.cacheTexture(Paths.image('NOTE_hold_assets'));
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/go', '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/go', 'shared'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/normal/sick'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/normal/good'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/normal/bad'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/normal/shit'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/sick'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/funkin/good'));
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/good'));
FunkinSprite.cacheTexture(Paths.image('ui/popup/pixel/bad'));
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.
// This is crude and I want to remove it when we have a proper asset caching system.

View file

@ -524,12 +524,16 @@ class Constants
* 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.
* This is the thing people have been begging for forever lolol.
* Duration, in seconds, after the player's section ends before the player can spam without penalty.
*/
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.