1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2025-03-20 17:09:21 +00:00

WIP on conductor rework

This commit is contained in:
Eric Myllyoja 2022-09-22 06:34:03 -04:00
parent 7fbdbcdfad
commit 9ebb566b2a
17 changed files with 810 additions and 76 deletions

View file

@ -1,11 +1,10 @@
package funkin;
import funkin.SongLoad.SwagSong;
import funkin.play.song.Song.SongDifficulty;
import funkin.play.song.SongData.ConductorTimeChange;
import funkin.play.song.SongData.SongTimeChange;
/**
* ...
* @author
*/
typedef BPMChangeEvent =
{
var stepTime:Int;
@ -16,12 +15,40 @@ typedef BPMChangeEvent =
class Conductor
{
/**
* Beats per minute of the song.
* The list of time changes in the song.
* There should be at least one time change (at the beginning of the song) to define the BPM.
*/
public static var bpm:Float = 100;
private static var timeChanges:Array<ConductorTimeChange> = [];
/**
* Duration of a beat in millisecond.
* The current time change.
*/
private static var currentTimeChange:ConductorTimeChange;
/**
* The current position in the song in milliseconds.
* Updated every frame based on the audio position.
*/
public static var songPosition:Float;
/**
* Beats per minute of the current song at the current time.
*/
public static var bpm(get, null):Float = 100;
static function get_bpm():Float
{
if (currentTimeChange == null)
return 100;
return currentTimeChange.bpm;
}
// OLD, replaced with timeChanges.
public static var bpmChangeMap:Array<BPMChangeEvent> = [];
/**
* Duration of a beat in millisecond. Calculated based on bpm.
*/
public static var crochet(get, null):Float;
@ -31,7 +58,7 @@ class Conductor
}
/**
* Duration of a step in milliseconds.
* Duration of a step in milliseconds. Calculated based on bpm.
*/
public static var stepCrochet(get, null):Float;
@ -40,19 +67,62 @@ class Conductor
return crochet / 4;
}
/**
* The current position in the song in milliseconds.
*/
public static var songPosition:Float;
public static var currentBeat(get, null):Float;
static function get_currentBeat():Float
{
return currentBeat;
}
public static var currentStep(get, null):Int;
static function get_currentStep():Int
{
return currentStep;
}
public static var lastSongPos:Float;
public static var visualOffset:Float = 0;
public static var audioOffset:Float = 0;
public static var offset:Float = 0;
public static var bpmChangeMap:Array<BPMChangeEvent> = [];
public function new()
{
}
public function new() {}
public static function getLastBPMChange()
{
var lastChange:BPMChangeEvent = {
stepTime: 0,
songTime: 0,
bpm: 0
}
for (i in 0...Conductor.bpmChangeMap.length)
{
if (Conductor.songPosition >= Conductor.bpmChangeMap[i].songTime)
lastChange = Conductor.bpmChangeMap[i];
if (Conductor.songPosition < Conductor.bpmChangeMap[i].songTime)
break;
}
return lastChange;
}
public static function forceBPM(bpm:Float)
{
// TODO: Get rid of this and use song metadata instead.
Conductor.bpm = bpm;
}
/**
* Update the conductor with the current song position.
* BPM, current step, etc. will be re-calculated based on the song position.
*/
public static function update(songPosition:Float)
{
Conductor.songPosition = songPosition;
Conductor.bpm = Conductor.getLastBPMChange().bpm;
}
public static function mapBPMChanges(song:SwagSong)
{
@ -80,4 +150,34 @@ class Conductor
}
trace("new BPM map BUDDY " + bpmChangeMap);
}
public static function mapTimeChanges(currentChart:SongDifficulty)
{
var songTimeChanges:Array<SongTimeChange> = currentChart.timeChanges;
timeChanges = [];
for (songTimeChange in timeChanges)
{
var prevTimeChange:ConductorTimeChange = timeChanges.length == 0 ? null : timeChanges[timeChanges.length - 1];
var currentTimeChange:ConductorTimeChange = cast songTimeChange;
if (prevTimeChange != null)
{
var deltaTime:Float = currentTimeChange.timeStamp - prevTimeChange.timeStamp;
var deltaSteps:Int = Math.round(deltaTime / (60 / prevTimeChange.bpm) * 1000 / 4);
currentTimeChange.stepTime = prevTimeChange.stepTime + deltaSteps;
}
else
{
// We know the time and steps of this time change is 0, since this is the first time change.
currentTimeChange.stepTime = 0;
}
timeChanges.push(currentTimeChange);
}
// Done.
}
}

View file

@ -27,6 +27,7 @@ import funkin.freeplayStuff.FreeplayScore;
import funkin.freeplayStuff.SongMenuItem;
import funkin.play.HealthIcon;
import funkin.play.PlayState;
import funkin.play.song.SongData.SongDataParser;
import funkin.shaderslmfao.AngleMask;
import funkin.shaderslmfao.PureColor;
import funkin.shaderslmfao.StrokeShader;
@ -97,7 +98,7 @@ class FreeplayState extends MusicBeatSubstate
}
if (StoryMenuState.weekUnlocked[2] || isDebug)
addWeek(['Bopeebo', 'Fresh', 'Dadbattle'], 1, ['dad']);
addWeek(['Bopeebo', 'Bopeebo_new', 'Fresh', 'Dadbattle'], 1, ['dad']);
if (StoryMenuState.weekUnlocked[2] || isDebug)
addWeek(['Spookeez', 'South', 'Monster'], 2, ['spooky', 'spooky', 'monster']);
@ -520,8 +521,10 @@ class FreeplayState extends MusicBeatSubstate
}*/
PlayState.currentSong = SongLoad.loadFromJson(poop, songs[curSelected].songName.toLowerCase());
PlayState.currentSong_NEW = SongDataParser.fetchSong(songs[curSelected].songName.toLowerCase());
PlayState.isStoryMode = false;
PlayState.storyDifficulty = curDifficulty;
PlayState.storyDifficulty_NEW = 'easy';
// SongLoad.curDiff = Highscore.formatSong()
SongLoad.curDiff = switch (curDifficulty)
@ -562,6 +565,7 @@ class FreeplayState extends MusicBeatSubstate
intendedScore = FlxG.random.int(0, 100000);
PlayState.storyDifficulty = curDifficulty;
PlayState.storyDifficulty_NEW = 'easy';
grpDifficulties.group.forEach(function(spr)
{

View file

@ -191,8 +191,10 @@ class InitState extends FlxTransitionableState
var dif = getDif();
PlayState.currentSong = SongLoad.loadFromJson(song, song);
PlayState.currentSong_NEW = SongDataParser.fetchSong(song);
PlayState.isStoryMode = isStoryMode;
PlayState.storyDifficulty = dif;
PlayState.storyDifficulty_NEW = 'easy';
SongLoad.curDiff = switch (dif)
{
case 0: 'easy';

View file

@ -70,7 +70,7 @@ class LatencyState extends MusicBeatSubstate
// funnyStatsGraph.hi
Conductor.bpm = 60;
Conductor.forceBPM(60);
noteGrp = new FlxTypedGroup<Note>();
add(noteGrp);

View file

@ -2,6 +2,8 @@ package funkin;
import flixel.FlxSprite;
import flixel.math.FlxMath;
import funkin.noteStuff.NoteBasic.NoteData;
import funkin.noteStuff.NoteBasic.NoteType;
import funkin.play.PlayState;
import funkin.play.Strumline.StrumlineStyle;
import funkin.shaderslmfao.ColorSwap;

View file

@ -1,17 +1,15 @@
package funkin;
import flixel.FlxSprite;
import flixel.FlxSubState;
import flixel.addons.transition.FlxTransitionableState;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.input.keyboard.FlxKey;
import flixel.system.FlxSound;
import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import funkin.Controls.Control;
import funkin.play.PlayState;
import funkin.play.song.SongData.SongDataParser;
class PauseSubState extends MusicBeatSubstate
{
@ -61,7 +59,14 @@ class PauseSubState extends MusicBeatSubstate
add(metaDataGrp);
var levelInfo:FlxText = new FlxText(20, 15, 0, "", 32);
levelInfo.text += PlayState.currentSong.song;
if (PlayState.instance.currentChart != null)
{
levelInfo.text += '${PlayState.instance.currentChart.songName} - ${PlayState.instance.currentChart.songArtist}';
}
else
{
levelInfo.text += PlayState.currentSong.song;
}
levelInfo.scrollFactor.set();
levelInfo.setFormat(Paths.font("vcr.ttf"), 32);
levelInfo.updateHitbox();
@ -180,9 +185,11 @@ class PauseSubState extends MusicBeatSubstate
close();
case "EASY" | 'NORMAL' | "HARD":
PlayState.currentSong = SongLoad.loadFromJson(PlayState.currentSong.song.toLowerCase(), PlayState.currentSong.song.toLowerCase());
PlayState.currentSong_NEW = SongDataParser.fetchSong(PlayState.currentSong.song.toLowerCase());
SongLoad.curDiff = daSelected.toLowerCase();
PlayState.storyDifficulty = curSelected;
PlayState.storyDifficulty_NEW = 'easy';
PlayState.needsReset = true;

View file

@ -2,6 +2,7 @@ package funkin;
import funkin.Section.SwagSection;
import funkin.noteStuff.NoteBasic.NoteData;
import funkin.play.PlayState;
import haxe.Json;
import lime.utils.Assets;
@ -47,7 +48,21 @@ class SongLoad
public static function loadFromJson(jsonInput:String, ?folder:String):SwagSong
{
var rawJson = Assets.getText(Paths.json('songs/${folder.toLowerCase()}/${jsonInput.toLowerCase()}')).trim();
var rawJson:Dynamic = null;
try
{
rawJson = Assets.getText(Paths.json('songs/${folder.toLowerCase()}/${jsonInput.toLowerCase()}')).trim();
}
catch (e)
{
trace('Failed to load song data: ${e}');
rawJson = null;
}
if (rawJson == null)
{
return null;
}
while (!rawJson.endsWith("}"))
{
@ -112,6 +127,11 @@ class SongLoad
public static function getSpeed(?diff:String):Float
{
if (PlayState.instance != null && PlayState.instance.currentChart != null)
{
return getSpeed_NEW(diff);
}
if (diff == null)
diff = SongLoad.curDiff;
@ -137,6 +157,14 @@ class SongLoad
return speedShit;
}
public static function getSpeed_NEW(?diff:String):Float
{
if (PlayState.instance == null || PlayState.instance.currentChart == null || PlayState.instance.currentChart.scrollSpeed == 0.0)
return 1.0;
return PlayState.instance.currentChart.scrollSpeed;
}
public static function getDefaultSwagSong():SwagSong
{
return {

View file

@ -12,6 +12,7 @@ import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.MenuItem.WeekType;
import funkin.play.PlayState;
import funkin.play.song.SongData.SongDataParser;
import lime.net.curl.CURLCode;
import openfl.Assets;
@ -372,10 +373,12 @@ class StoryMenuState extends MusicBeatState
selectedWeek = true;
PlayState.currentSong = SongLoad.loadFromJson(PlayState.storyPlaylist[0].toLowerCase(), PlayState.storyPlaylist[0].toLowerCase());
PlayState.currentSong_NEW = SongDataParser.fetchSong(PlayState.storyPlaylist[0].toLowerCase());
PlayState.storyWeek = curWeek;
PlayState.campaignScore = 0;
PlayState.storyDifficulty = curDifficulty;
PlayState.storyDifficulty_NEW = 'easy';
SongLoad.curDiff = switch (curDifficulty)
{
case 0:

View file

@ -140,7 +140,7 @@ class TitleState extends MusicBeatState
{
FlxG.sound.playMusic(Paths.music('freakyMenu'), 0);
FlxG.sound.music.fadeIn(4, 0, 0.7);
Conductor.bpm = Constants.FREAKY_MENU_BPM;
Conductor.forceBPM(Constants.FREAKY_MENU_BPM);
}
persistentUpdate = true;
@ -474,7 +474,7 @@ class TitleState extends MusicBeatState
var spec:SpectogramSprite = new SpectogramSprite(FlxG.sound.music);
add(spec);
Conductor.bpm = 190;
Conductor.forceBPM(190);
FlxG.camera.flash(FlxColor.WHITE, 1);
FlxG.sound.play(Paths.sound('confirmMenu'), 0.7);
}

View file

@ -148,7 +148,7 @@ class ChartingState extends MusicBeatState
updateGrid();
loadSong(_song.song);
Conductor.bpm = _song.bpm;
// Conductor.bpm = _song.bpm;
Conductor.mapBPMChanges(_song);
bpmTxt = new FlxText(1000, 50, 0, "", 16);
@ -549,7 +549,7 @@ class ChartingState extends MusicBeatState
{
tempBpm = nums.value;
Conductor.mapBPMChanges(_song);
Conductor.bpm = nums.value;
Conductor.forceBPM(nums.value);
}
else if (wname == 'note_susLength')
{
@ -1223,7 +1223,7 @@ class ChartingState extends MusicBeatState
if (SongLoad.getSong()[curSection].changeBPM && SongLoad.getSong()[curSection].bpm > 0)
{
Conductor.bpm = SongLoad.getSong()[curSection].bpm;
Conductor.forceBPM(SongLoad.getSong()[curSection].bpm);
FlxG.log.add('CHANGED BPM!');
}
else
@ -1233,7 +1233,7 @@ class ChartingState extends MusicBeatState
for (i in 0...curSection)
if (SongLoad.getSong()[i].changeBPM)
daBPM = SongLoad.getSong()[i].bpm;
Conductor.bpm = daBPM;
Conductor.forceBPM(daBPM);
}
/* // PORT BULLSHIT, INCASE THERE'S NO SUSTAIN DATA FOR A NOTE

View file

@ -5,6 +5,7 @@ import flixel.addons.effects.FlxTrail;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.math.FlxMath;
import flixel.util.FlxColor;
import flixel.util.FlxDirectionFlags;
import funkin.audiovis.PolygonSpectogram;
import funkin.noteStuff.NoteBasic.NoteData;
@ -35,7 +36,7 @@ class PicoFight extends MusicBeatState
FlxG.sound.playMusic(Paths.inst("blazin"));
SongLoad.loadFromJson('blazin', "blazin");
Conductor.bpm = SongLoad.songData.bpm;
Conductor.forceBPM(SongLoad.songData.bpm);
for (dumbassSection in SongLoad.songData.noteMap['hard'])
{

View file

@ -28,6 +28,10 @@ import funkin.play.Strumline.StrumlineStyle;
import funkin.play.character.BaseCharacter;
import funkin.play.character.CharacterData;
import funkin.play.scoring.Scoring;
import funkin.play.song.Song;
import funkin.play.song.SongData.SongNoteData;
import funkin.play.song.SongData.SongPlayableChar;
import funkin.play.song.SongValidator;
import funkin.play.stage.Stage;
import funkin.play.stage.StageData;
import funkin.ui.PopUpStuff;
@ -62,6 +66,8 @@ class PlayState extends MusicBeatState
*/
public static var currentSong:SwagSong = null;
public static var currentSong_NEW:Song = null;
/**
* Whether the game is currently in Story Mode. If false, we are in Free Play Mode.
*/
@ -116,6 +122,8 @@ class PlayState extends MusicBeatState
*/
public var currentStage:Stage = null;
public var currentChart(get, null):SongDifficulty;
/**
* The internal ID of the currently active Stage.
* Used to retrieve the data required to build the `currentStage`.
@ -166,6 +174,12 @@ class PlayState extends MusicBeatState
*/
private var healthLerp:Float = 1;
/**
* Forcibly disables all update logic while the game moves back to the Menu state.
* This is used only when a critical error occurs and the game cannot continue.
*/
private var criticalFailure:Bool = false;
/**
* RENDER OBJECTS
*/
@ -244,6 +258,7 @@ class PlayState extends MusicBeatState
public static var storyWeek:Int = 0;
public static var storyPlaylist:Array<String> = [];
public static var storyDifficulty:Int = 1;
public static var storyDifficulty_NEW:String = "normal";
public static var seenCutscene:Bool = false;
public static var campaignScore:Int = 0;
@ -279,8 +294,10 @@ class PlayState extends MusicBeatState
{
super.create();
if (currentSong == null)
if (currentSong == null && currentSong_NEW == null)
{
criticalFailure = true;
lime.app.Application.current.window.alert("There was a critical error while accessing the selected song. Click OK to return to the main menu.",
"Error loading PlayState");
FlxG.switchState(new MainMenuState());
@ -308,28 +325,48 @@ class PlayState extends MusicBeatState
FlxG.sound.music.stop();
// Prepare the current song to be played.
FlxG.sound.cache(Paths.inst(currentSong.song));
FlxG.sound.cache(Paths.voices(currentSong.song));
if (currentChart != null)
{
currentChart.cacheInst();
currentChart.cacheVocals();
}
else
{
FlxG.sound.cache(Paths.inst(currentSong.song));
FlxG.sound.cache(Paths.voices(currentSong.song));
}
Conductor.songPosition = -5000;
Conductor.update(-5000);
// Initialize stage stuff.
initCameras();
if (currentSong == null)
currentSong = SongLoad.loadFromJson('tutorial');
Conductor.mapBPMChanges(currentSong);
Conductor.bpm = currentSong.bpm;
switch (currentSong.song.toLowerCase())
if (currentSong == null && currentSong_NEW == null)
{
case 'senpai':
dialogue = CoolUtil.coolTextFile(Paths.txt('songs/senpai/senpaiDialogue'));
case 'roses':
dialogue = CoolUtil.coolTextFile(Paths.txt('songs/roses/rosesDialogue'));
case 'thorns':
dialogue = CoolUtil.coolTextFile(Paths.txt('songs/thorns/thornsDialogue'));
currentSong = SongLoad.loadFromJson('tutorial');
}
if (currentSong_NEW != null)
{
Conductor.mapTimeChanges(currentChart);
// Conductor.bpm = currentChart.getStartingBPM();
// TODO: Support for dialog.
}
else
{
Conductor.mapBPMChanges(currentSong);
// Conductor.bpm = currentSong.bpm;
switch (currentSong.song.toLowerCase())
{
case 'senpai':
dialogue = CoolUtil.coolTextFile(Paths.txt('songs/senpai/senpaiDialogue'));
case 'roses':
dialogue = CoolUtil.coolTextFile(Paths.txt('songs/roses/rosesDialogue'));
case 'thorns':
dialogue = CoolUtil.coolTextFile(Paths.txt('songs/thorns/thornsDialogue'));
}
}
if (dialogue != null)
@ -379,7 +416,14 @@ class PlayState extends MusicBeatState
add(grpNoteSplashes);
generateSong();
if (currentSong_NEW != null)
{
generateSong_NEW();
}
else
{
generateSong();
}
resetCamera();
@ -442,6 +486,13 @@ class PlayState extends MusicBeatState
#end
}
function get_currentChart():SongDifficulty
{
if (currentSong_NEW == null || storyDifficulty_NEW == null)
return null;
return currentSong_NEW.getDifficulty(storyDifficulty_NEW);
}
/**
* Initializes the game and HUD cameras.
*/
@ -460,6 +511,12 @@ class PlayState extends MusicBeatState
function initStage()
{
if (currentSong_NEW != null)
{
initStage_NEW();
return;
}
// TODO: Move stageId to the song file.
switch (currentSong.song.toLowerCase())
{
@ -487,9 +544,6 @@ class PlayState extends MusicBeatState
currentStageId = 'schoolEvil';
case 'guns' | 'stress' | 'ugh':
currentStageId = 'tankmanBattlefield';
case 'experimental-phase' | 'perfection':
// SERIOUSLY REVAMP THE CHART FORMAT ALREADY
currentStageId = "breakout";
default:
currentStageId = "mainStage";
}
@ -497,8 +551,33 @@ class PlayState extends MusicBeatState
loadStage(currentStageId);
}
function initStage_NEW()
{
if (currentChart == null)
{
trace('Song difficulty could not be loaded.');
}
if (currentChart.stage != null && currentChart.stage != '')
{
currentStageId = currentChart.stage;
}
else
{
currentStageId = SongValidator.DEFAULT_STAGE;
}
loadStage(currentStageId);
}
function initCharacters()
{
if (currentSong_NEW != null)
{
initCharacters_NEW();
return;
}
iconP1 = new HealthIcon(currentSong.player1, 0);
iconP1.y = healthBar.y - (iconP1.height / 2);
add(iconP1);
@ -615,6 +694,111 @@ class PlayState extends MusicBeatState
}
}
function initCharacters_NEW()
{
if (currentSong_NEW == null || currentChart == null)
{
trace('Song difficulty could not be loaded.');
}
// TODO: Switch playable character by manipulating this value.
// TODO: How to choose which one to use for story mode?
var currentPlayer = 'bf';
var currentCharData:SongPlayableChar = currentChart.getPlayableChar(currentPlayer);
//
// GIRLFRIEND
//
var girlfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharData.girlfriend);
if (girlfriend != null)
{
girlfriend.characterType = CharacterType.GF;
}
else if (currentCharData.girlfriend != '')
{
trace('WARNING: Could not load girlfriend character with ID ${currentCharData.girlfriend}, skipping...');
}
else
{
// Chosen GF was '' so we don't load one.
}
//
// DAD
//
var dad:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharData.opponent);
if (dad != null)
{
dad.characterType = CharacterType.DAD;
}
// TODO: Cut out this code/make it generic.
switch (currentCharData.opponent)
{
case 'gf':
if (isStoryMode)
{
cameraFollowPoint.x += 600;
tweenCamIn();
}
}
//
// OPPONENT HEALTH ICON
//
iconP2 = new HealthIcon(currentCharData.opponent, 1);
iconP2.y = healthBar.y - (iconP2.height / 2);
add(iconP2);
//
// BOYFRIEND
//
var boyfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentPlayer);
if (boyfriend != null)
{
boyfriend.characterType = CharacterType.BF;
}
//
// PLAYER HEALTH ICON
//
iconP1 = new HealthIcon(currentPlayer, 0);
iconP1.y = healthBar.y - (iconP1.height / 2);
add(iconP1);
//
// ADD CHARACTERS TO SCENE
//
if (currentStage != null)
{
// Characters get added to the stage, not the main scene.
if (girlfriend != null)
{
currentStage.addCharacter(girlfriend, GF);
}
if (boyfriend != null)
{
currentStage.addCharacter(boyfriend, BF);
}
if (dad != null)
{
currentStage.addCharacter(dad, DAD);
// Camera starts at dad.
cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y);
}
// Rearrange by z-indexes.
currentStage.refresh();
}
}
/**
* Removes any references to the current stage, then clears the stage cache,
* then reloads all the stages.
@ -794,7 +978,14 @@ class PlayState extends MusicBeatState
// if (FlxG.sound.music != null)
// FlxG.sound.music.play(true);
// else
FlxG.sound.playMusic(Paths.inst(currentSong.song), 1, false);
if (currentChart != null)
{
currentChart.playInst(1.0, false);
}
else
{
FlxG.sound.playMusic(Paths.inst(currentSong.song), 1, false);
}
}
FlxG.sound.music.onComplete = endSong;
@ -813,7 +1004,7 @@ class PlayState extends MusicBeatState
{
// FlxG.log.add(ChartParser.parse());
Conductor.bpm = currentSong.bpm;
Conductor.forceBPM(currentSong.bpm);
currentSong.song = currentSong.song;
@ -836,6 +1027,32 @@ class PlayState extends MusicBeatState
generatedMusic = true;
}
private function generateSong_NEW():Void
{
if (currentChart == null)
{
trace('Song difficulty could not be loaded.');
}
Conductor.forceBPM(currentChart.getStartingBPM());
// TODO: Fix grouped vocals
vocals = currentChart.buildVocals();
vocals.members[0].onComplete = function()
{
vocalsFinished = true;
}
// Create the rendered note group.
activeNotes = new FlxTypedGroup<Note>();
activeNotes.zIndex = 1000;
add(activeNotes);
regenNoteData_NEW();
generatedMusic = true;
}
function regenNoteData():Void
{
// make unspawn notes shit def empty
@ -950,6 +1167,133 @@ class PlayState extends MusicBeatState
});
}
function regenNoteData_NEW():Void
{
// Destroy inactive notes.
inactiveNotes = [];
// Destroy active notes.
activeNotes.forEach(function(nt)
{
nt.followsTime = false;
FlxTween.tween(nt, {y: FlxG.height + nt.y}, 0.5, {
ease: FlxEase.expoIn,
onComplete: function(twn)
{
nt.kill();
activeNotes.remove(nt, true);
nt.destroy();
}
});
});
var noteData:Array<SongNoteData> = currentChart.notes;
var oldNote:Note = null;
for (songNote in noteData)
{
var mustHitNote:Bool = songNote.getMustHitNote();
// TODO: Put this in the chart or something?
var strumlineStyle:StrumlineStyle = null;
switch (currentStageId)
{
case 'school':
strumlineStyle = PIXEL;
case 'schoolEvil':
strumlineStyle = PIXEL;
default:
strumlineStyle = NORMAL;
}
var newNote:Note = new Note(songNote.time, songNote.data, oldNote, false, strumlineStyle);
newNote.mustPress = mustHitNote;
newNote.data.sustainLength = songNote.length;
newNote.data.noteKind = songNote.kind;
newNote.scrollFactor.set(0, 0);
// Note positioning.
// TODO: Make this more robust.
if (newNote.mustPress)
{
if (playerStrumline != null)
{
// Align with the strumline arrow.
newNote.x = playerStrumline.getArrow(songNote.getDirection()).x;
}
else
{
// Assume strumline position.
newNote.x += FlxG.width / 2;
}
}
else
{
if (enemyStrumline != null)
{
newNote.x = enemyStrumline.getArrow(songNote.getDirection()).x;
}
else
{
// newNote.x += 0;
}
}
inactiveNotes.push(newNote);
oldNote = newNote;
// Generate X sustain notes.
var sustainSections = Math.round(songNote.length / Conductor.stepCrochet);
for (noteIndex in 0...sustainSections)
{
var noteTimeOffset:Float = Conductor.stepCrochet + (Conductor.stepCrochet * noteIndex);
var sustainNote:Note = new Note(songNote.time + noteTimeOffset, songNote.data, oldNote, true, strumlineStyle);
sustainNote.mustPress = mustHitNote;
sustainNote.data.noteKind = songNote.kind;
sustainNote.scrollFactor.set(0, 0);
if (sustainNote.mustPress)
{
if (playerStrumline != null)
{
// Align with the strumline arrow.
sustainNote.x = playerStrumline.getArrow(songNote.getDirection()).x;
}
else
{
// Assume strumline position.
sustainNote.x += FlxG.width / 2;
}
}
else
{
if (enemyStrumline != null)
{
sustainNote.x = enemyStrumline.getArrow(songNote.getDirection()).x;
}
else
{
// newNote.x += 0;
}
}
inactiveNotes.push(sustainNote);
oldNote = sustainNote;
}
}
// Sorting is an expensive operation.
// Assume it was done in the chart file.
/**
inactiveNotes.sort(function(a:Note, b:Note):Int
{
return SortUtil.byStrumtime(FlxSort.ASCENDING, a, b);
});
**/
}
function tweenCamIn():Void
{
FlxTween.tween(FlxG.camera, {zoom: 1.3 * FlxCamera.defaultZoom}, (Conductor.stepCrochet * 4 / 1000), {ease: FlxEase.elasticInOut});
@ -986,7 +1330,7 @@ class PlayState extends MusicBeatState
vocals.pause();
FlxG.sound.music.play();
Conductor.songPosition = FlxG.sound.music.time + Conductor.offset;
Conductor.update(FlxG.sound.music.time + Conductor.offset);
if (vocalsFinished)
return;
@ -999,6 +1343,9 @@ class PlayState extends MusicBeatState
{
super.update(elapsed);
if (criticalFailure)
return;
if (FlxG.keys.justPressed.U)
{
// hack for HaxeUI generation, doesn't work unless persistentUpdate is false at state creation!!
@ -1027,7 +1374,16 @@ class PlayState extends MusicBeatState
currentStage.resetStage();
regenNoteData(); // loads the note data from start
// Delete all notes and reset the arrays.
if (currentChart != null)
{
regenNoteData_NEW();
}
else
{
regenNoteData();
}
health = 1;
songScore = 0;
combo = 0;
@ -1058,7 +1414,7 @@ class PlayState extends MusicBeatState
if (Paths.SOUND_EXT == 'mp3')
Conductor.offset = -13; // DO NOT FORGET TO REMOVE THE HARDCODE! WHEN I MAKE BETTER OFFSET SYSTEM!
Conductor.songPosition = FlxG.sound.music.time + Conductor.offset; // 20 is THE MILLISECONDS??
Conductor.update(FlxG.sound.music.time + Conductor.offset);
if (!isGamePaused)
{
@ -1177,7 +1533,7 @@ class PlayState extends MusicBeatState
}
FlxG.watch.addQuick("songPos", Conductor.songPosition);
if (currentSong.song == 'Fresh')
if (currentSong != null && currentSong.song == 'Fresh')
{
switch (curBeat)
{
@ -1307,7 +1663,7 @@ class PlayState extends MusicBeatState
if (!daNote.mustPress && daNote.wasGoodHit && !daNote.tooLate)
{
if (currentSong.song != 'Tutorial')
if (currentSong != null && currentSong.song != 'Tutorial')
camZooming = true;
var event:NoteScriptEvent = new NoteScriptEvent(ScriptEvent.NOTE_HIT, daNote, combo, true);
@ -1324,7 +1680,7 @@ class PlayState extends MusicBeatState
else
{
// Volume of DAD.
if (currentSong.needsVoices)
if (currentSong != null && currentSong.needsVoices)
vocals.volume = 1;
}
}
@ -1412,8 +1768,9 @@ class PlayState extends MusicBeatState
}
daPos += 4 * (1000 * 60 / daBPM);
}
Conductor.songPosition = FlxG.sound.music.time = daPos;
Conductor.songPosition += Conductor.offset;
FlxG.sound.music.time = daPos;
Conductor.update(FlxG.sound.music.time + Conductor.offset);
updateCurStep();
resyncVocals();
}
@ -1857,7 +2214,7 @@ class PlayState extends MusicBeatState
{
if (SongLoad.getSong()[Math.floor(curStep / 16)].changeBPM)
{
Conductor.bpm = SongLoad.getSong()[Math.floor(curStep / 16)].bpm;
Conductor.forceBPM(SongLoad.getSong()[Math.floor(curStep / 16)].bpm);
FlxG.log.add('CHANGED BPM!');
}
}
@ -2118,8 +2475,14 @@ class PlayState extends MusicBeatState
function performCleanup()
{
// Uncache the song.
openfl.utils.Assets.cache.clear(Paths.inst(currentSong.song));
openfl.utils.Assets.cache.clear(Paths.voices(currentSong.song));
if (currentChart != null)
{
}
else
{
openfl.utils.Assets.cache.clear(Paths.inst(currentSong.song));
openfl.utils.Assets.cache.clear(Paths.voices(currentSong.song));
}
// Remove reference to stage and remove sprites from it to save memory.
if (currentStage != null)

View file

@ -4,9 +4,11 @@ import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxPoint;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import funkin.noteStuff.NoteBasic.NoteColor;
import funkin.noteStuff.NoteBasic.NoteDir;
import funkin.noteStuff.NoteBasic.NoteType;
import funkin.ui.PreferencesMenu;
import funkin.util.Constants;
/**

View file

@ -273,6 +273,10 @@ class BaseCharacter extends Bopper
{
if (!isOpponent)
{
if (PlayState.instance.iconP1 == null)
{
trace('[WARN] Player 1 health icon not found!');
}
PlayState.instance.iconP1.characterId = _data.healthIcon.id;
PlayState.instance.iconP1.size.set(_data.healthIcon.scale, _data.healthIcon.scale);
PlayState.instance.iconP1.offset.x = _data.healthIcon.offsets[0];
@ -281,6 +285,10 @@ class BaseCharacter extends Bopper
}
else
{
if (PlayState.instance.iconP2 == null)
{
trace('[WARN] Player 2 health icon not found!');
}
PlayState.instance.iconP2.characterId = _data.healthIcon.id;
PlayState.instance.iconP2.size.set(_data.healthIcon.scale, _data.healthIcon.scale);
PlayState.instance.iconP2.offset.x = _data.healthIcon.offsets[0];

View file

@ -1,5 +1,7 @@
package funkin.play.song;
import funkin.VoicesGroup;
import funkin.play.song.SongData.SongChartData;
import funkin.play.song.SongData.SongDataParser;
import funkin.play.song.SongData.SongEventData;
import funkin.play.song.SongData.SongMetadata;
@ -45,14 +47,18 @@ class Song // implements IPlayStateScriptedClass
cacheCharts();
}
function populateFromMetadata()
/**
* Populate the song data from the provided metadata,
* including data from individual difficulties. Does not load chart data.
*/
function populateFromMetadata():Void
{
// Variations may have different artist, time format, generatedBy, etc.
for (metadata in _metadata)
{
for (diffId in metadata.playData.difficulties)
{
var difficulty = new SongDifficulty(diffId, metadata.variation);
var difficulty:SongDifficulty = new SongDifficulty(this, diffId, metadata.variation);
variations.push(metadata.variation);
@ -83,25 +89,27 @@ class Song // implements IPlayStateScriptedClass
/**
* Parse and cache the chart for all difficulties of this song.
*/
public function cacheCharts()
public function cacheCharts():Void
{
trace('Caching ${variations.length} chart files for song $songId');
for (variation in variations)
{
var chartData = SongDataParser.parseSongChartData(songId, variation);
var chartData:SongChartData = SongDataParser.parseSongChartData(songId, variation);
var chartNotes = chartData.notes;
for (diffId in chartData.notes.keys())
for (diffId in chartNotes.keys())
{
trace(' Difficulty $diffId');
var difficulty = difficulties.get(diffId);
// Retrieve the cached difficulty data.
var difficulty:Null<SongDifficulty> = difficulties.get(diffId);
if (difficulty == null)
{
trace('Could not find difficulty $diffId for song $songId');
continue;
}
// Add the chart data to the difficulty.
difficulty.notes = chartData.notes.get(diffId);
difficulty.scrollSpeed = chartData.scrollSpeed.get(diffId);
difficulty.scrollSpeed = chartData.getScrollSpeed(diffId);
difficulty.events = chartData.events;
}
}
@ -111,7 +119,7 @@ class Song // implements IPlayStateScriptedClass
/**
* Retrieve the metadata for a specific difficulty, including the chart if it is loaded.
*/
public function getDifficulty(diffId:String):SongDifficulty
public inline function getDifficulty(diffId:String):SongDifficulty
{
return difficulties.get(diffId);
}
@ -119,7 +127,7 @@ class Song // implements IPlayStateScriptedClass
/**
* Purge the cached chart data for each difficulty of this song.
*/
public function clearCharts()
public function clearCharts():Void
{
for (diff in difficulties)
{
@ -135,6 +143,11 @@ class Song // implements IPlayStateScriptedClass
class SongDifficulty
{
/**
* The parent song for this difficulty.
*/
public final song:Song;
/**
* The difficulty ID, such as `easy` or `hard`.
*/
@ -162,8 +175,9 @@ class SongDifficulty
public var notes:Array<SongNoteData>;
public var events:Array<SongEventData>;
public function new(diffId:String, variation:String)
public function new(song:Song, diffId:String, variation:String)
{
this.song = song;
this.difficulty = diffId;
this.variation = variation;
}
@ -172,4 +186,48 @@ class SongDifficulty
{
notes = null;
}
public function getStartingBPM():Float
{
if (timeChanges.length == 0)
{
return 0;
}
return timeChanges[0].bpm;
}
public function getPlayableChar(id:String):SongPlayableChar
{
return chars.get(id);
}
public inline function cacheInst()
{
// DEBUG: Remove this.
// FlxG.sound.cache(Paths.inst(this.song.songId));
FlxG.sound.cache(Paths.inst('bopeebo'));
}
public inline function playInst(volume:Float = 1.0, looped:Bool = false)
{
// DEBUG: Remove this.
// FlxG.sound.playMusic(Paths.inst(this.song.songId), volume, looped);
FlxG.sound.playMusic(Paths.inst('bopeebo'), volume, looped);
}
public inline function cacheVocals()
{
// DEBUG: Remove this.
// FlxG.sound.cache(Paths.voices(this.song.songId));
FlxG.sound.cache(Paths.voices('bopeebo'));
}
public inline function buildVocals(charId:String = "bf"):VoicesGroup
{
// DEBUG: Remove this.
// var result:VoicesGroup = new VoicesGroup(this.song.songId, null, false);
var result:VoicesGroup = new VoicesGroup('bopeebo', null, false);
return result;
}
}

View file

@ -339,6 +339,11 @@ abstract SongNoteData(RawSongNoteData)
return Math.floor(this.d / strumlineSize);
}
public inline function getMustHitNote(strumlineSize:Int = 4):Bool
{
return getStrumlineIndex(strumlineSize) == 0;
}
public var length(get, set):Float;
public function get_length():Float
@ -522,7 +527,7 @@ abstract SongPlayableChar(RawSongPlayableChar)
}
}
typedef SongChartData =
typedef RawSongChartData =
{
var version:Version;
@ -532,6 +537,32 @@ typedef SongChartData =
var generatedBy:String;
};
@:forward
abstract SongChartData(RawSongChartData)
{
public function new(scrollSpeed:DynamicAccess<Float>, events:Array<SongEventData>, notes:DynamicAccess<Array<SongNoteData>>)
{
this = {
version: SongMigrator.CHART_VERSION,
events: events,
notes: notes,
scrollSpeed: scrollSpeed,
generatedBy: SongValidator.DEFAULT_GENERATEDBY
}
}
public function getScrollSpeed(diff:String = 'default'):Float
{
var result:Float = this.scrollSpeed.get(diff);
if (result == 0.0 && diff != 'default')
return getScrollSpeed('default');
return (result == 0.0) ? 1.0 : result;
}
}
typedef RawSongTimeChange =
{
/**
@ -569,6 +600,17 @@ typedef RawSongTimeChange =
var bt:OneOfTwo<Int, Array<Int>>;
}
typedef RawConductorTimeChange =
{
> RawSongTimeChange,
/**
* The time in the song (in steps) that this change occurs at.
* This time is somewhat weird because the rate it increases is dependent on the BPM at that point in the song.
*/
public var st:Float;
}
/**
* Add aliases to the minimalized property names of the typedef,
* to improve readability.
@ -667,6 +709,113 @@ abstract SongTimeChange(RawSongTimeChange)
}
}
abstract ConductorTimeChange(RawConductorTimeChange)
{
public function new(timeStamp:Float, beatTime:Int, bpm:Float, timeSignatureNum:Int = 4, timeSignatureDen:Int = 4, beatTuplets:Array<Int>)
{
this = {
t: timeStamp,
b: beatTime,
bpm: bpm,
n: timeSignatureNum,
d: timeSignatureDen,
bt: beatTuplets,
st: 0.0
}
}
public var timeStamp(get, set):Float;
public function get_timeStamp():Float
{
return this.t;
}
public function set_timeStamp(value:Float):Float
{
return this.t = value;
}
public var beatTime(get, set):Int;
public function get_beatTime():Int
{
return this.b;
}
public function set_beatTime(value:Int):Int
{
return this.b = value;
}
public var bpm(get, set):Float;
public function get_bpm():Float
{
return this.bpm;
}
public function set_bpm(value:Float):Float
{
return this.bpm = value;
}
public var timeSignatureNum(get, set):Int;
public function get_timeSignatureNum():Int
{
return this.n;
}
public function set_timeSignatureNum(value:Int):Int
{
return this.n = value;
}
public var timeSignatureDen(get, set):Int;
public function get_timeSignatureDen():Int
{
return this.d;
}
public function set_timeSignatureDen(value:Int):Int
{
return this.d = value;
}
public var beatTuplets(get, set):Array<Int>;
public function get_beatTuplets():Array<Int>
{
if (Std.isOfType(this.bt, Int))
{
return [this.bt];
}
else
{
return this.bt;
}
}
public function set_beatTuplets(value:Array<Int>):Array<Int>
{
return this.bt = value;
}
public var stepTime(get, set):Float;
public function get_stepTime():Float
{
return this.st;
}
public function set_stepTime(value:Float):Float
{
return this.st = value;
}
}
enum abstract SongTimeFormat(String) from String to String
{
var TICKS = "ticks";

View file

@ -5,6 +5,7 @@ import funkin.play.song.SongData.SongMetadata;
import funkin.play.song.SongData.SongPlayData;
import funkin.play.song.SongData.SongTimeChange;
import funkin.play.song.SongData.SongTimeFormat;
import funkin.util.Constants;
/**
* For SongMetadata and SongChartData objects,
@ -17,10 +18,16 @@ class SongValidator
public static final DEFAULT_TIMEFORMAT:SongTimeFormat = SongTimeFormat.MILLISECONDS;
public static final DEFAULT_DIVISIONS:Int = -1;
public static final DEFAULT_LOOP:Bool = false;
public static final DEFAULT_GENERATEDBY:String = "Unknown";
public static final DEFAULT_STAGE:String = "mainStage";
public static final DEFAULT_SCROLLSPEED:Float = 1.0;
public static var DEFAULT_GENERATEDBY(get, null):String;
static function get_DEFAULT_GENERATEDBY():String
{
return '${Constants.TITLE} - ${Constants.VERSION}';
}
/**
* Validates the fields of a SongMetadata object (excluding the version field).
*