Merge branch 'rewrite/master' into rewrite/feature/precise-inputs-gamepad

This commit is contained in:
Cameron Taylor 2023-10-16 16:47:01 -04:00 committed by GitHub
commit dd6db0196b
65 changed files with 4689 additions and 2220 deletions

View File

@ -3,18 +3,28 @@ description: "sets up haxe shit, using HMM!"
runs:
using: "composite"
steps:
- uses: krdlab/setup-haxe@v1.5.1
with:
haxe-version: 4.3.1
- name: Config haxelib
run: |
haxelib config
shell: bash
- name: Installing Haxe lol
run: |
haxe -version
haxelib git haxelib https://github.com/HaxeFoundation/haxelib.git development
haxelib version
haxelib --global install hmm
haxelib --global run hmm install --quiet
shell: bash
- uses: krdlab/setup-haxe@v1.5.1
with:
haxe-version: 4.3.1
- name: Config haxelib
run: |
haxelib config
shell: bash
- name: Installing Haxe lol
run: |
haxe -version
haxelib git haxelib https://github.com/HaxeFoundation/haxelib.git development
haxelib version
haxelib --global install hmm
shell: bash
- name: dependency install cache
id: cache-hmm
uses: actions/cache@v3
with:
path: .haxelib
key: ${{ runner.os }}-hmm-${{ hashFiles('**/hmm.json') }}
- if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }}
name: hmm install
run: |
haxelib --global run hmm install
shell: bash

View File

@ -36,9 +36,9 @@ runs:
./butler -V
shell: bash
- name: Upload game to itch.io
env:
env:
BUTLER_API_KEY: ${{inputs.butler-key}}
run: |
./butler login
./butler push ${{inputs.build-dir}} ninja-muffin24/funkin-secret:${{inputs.target}}-${GITHUB_REF##*/}
shell: bash
./butler push ${{inputs.build-dir}} ninja-muffin24/funkin-secret:${{inputs.target}}-${GITHUB_REF_NAME}
shell: bash

5
.github/hooks/README.md vendored Normal file
View File

@ -0,0 +1,5 @@
# Git Hooks
These work even on Windows because of Git Bash.
## Setup
`git config core.hooksPath .github/hooks`

2
.github/hooks/post-checkout vendored Normal file
View File

@ -0,0 +1,2 @@
#!/bin/sh
git submodule update --init --recursive

2
.github/hooks/post-merge vendored Normal file
View File

@ -0,0 +1,2 @@
#!/bin/sh
git submodule update --init --recursive

5
.github/hooks/pre-push vendored Normal file
View File

@ -0,0 +1,5 @@
#!/bin/sh
if git diff --cached --submodule | grep -q "^+"; then
echo "WARNING: You have unpushed changes in submodules."
exit 1
fi

View File

@ -26,9 +26,11 @@ jobs:
- uses: actions/checkout@v3
with:
submodules: 'recursive'
token: ${{ secrets.GH_RO_PAT }}
- uses: ./.github/actions/setup-haxeshit
- name: Build game
run: |
sudo apt-get update
sudo apt-get install -y libx11-dev xorg-dev libgl-dev libxi-dev libxext-dev libasound2-dev libxinerama-dev libxrandr-dev libgl1-mesa-dev
haxelib run lime build html5 -release --times
ls
@ -48,28 +50,44 @@ jobs:
- uses: actions/checkout@v3
with:
submodules: 'recursive'
token: ${{ secrets.GH_RO_PAT }}
- uses: ./.github/actions/setup-haxeshit
- name: Make HXCPP cache dir
run: |
mkdir -p ${{ runner.temp }}\hxcpp_cache
- name: Restore build cache
id: cache-build-win
uses: actions/cache@v3
with:
path: |
.haxelib
export
${{ runner.temp }}\hxcpp_cache
key: ${{ runner.os }}-build-win-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
- name: Build game
run: |
haxelib run lime build windows -release -DNO_REDIRECT_ASSETS_FOLDER
dir
env:
HXCPP_COMPILE_CACHE: "${{ runner.temp }}\\hxcpp_cache"
- uses: ./.github/actions/upload-itch
with:
butler-key: ${{ secrets.BUTLER_API_KEY}}
butler-key: ${{ secrets.BUTLER_API_KEY }}
build-dir: export/release/windows/bin
target: win
test-unit-win:
needs: create-nightly-win
runs-on: windows-latest
permissions:
contents: write
actions: write
steps:
- uses: actions/checkout@v3
with:
submodules: 'recursive'
- uses: ./.github/actions/setup-haxeshit
- name: Run unit tests
run: |
cd ./tests/unit/
./start-win-native.bat
# test-unit-win:
# needs: create-nightly-win
# runs-on: windows-latest
# permissions:
# contents: write
# actions: write
# steps:
# - uses: actions/checkout@v3
# with:
# submodules: 'recursive'
# token: ${{ secrets.GH_RO_PAT }}
# - uses: ./.github/actions/setup-haxeshit
# - name: Run unit tests
# run: |
# cd ./tests/unit/
# ./start-win-native.bat

3
.gitignore vendored
View File

@ -3,4 +3,5 @@
APIStuff.hx
dump/
export/
RECOVER_*.fla
RECOVER_*.fla
shitAudio/

View File

@ -156,7 +156,6 @@
<haxedef name="HXCPP_CHECK_POINTER" />
<haxedef name="HXCPP_STACK_LINE" />
<haxedef name="HXCPP_STACK_TRACE" />
<haxedef name="openfl-enable-handle-error" />
<!-- This macro allows addition of new functionality to existing Flixel. -->
<haxeflag name="--macro" value="addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxBasic())', 'flixel.FlxBasic')" />
<!--Place custom nodes like icons here (higher priority to override the HaxeFlixel icon)-->
@ -196,6 +195,22 @@
<haxedef name="REDIRECT_ASSETS_FOLDER" />
</section>
<section>
<!--
This flag enables the popup/crashlog error handler.
However, it also messes with breakpoints on some platforms.
-->
<haxedef name="openfl-enable-handle-error" />
</section>
<section>
<!-- TODO: Add a flag to Github Actions to turn this on or something. -->
<!-- Forces the version string to include the Git hash even on release builds (which are used for performance reasons). -->
<haxedef name="FORCE_DEBUG_VERSION" />
</section>
<!-- Run a script before and after building. -->
<postbuild haxe="source/Prebuild.hx"/> -->
<postbuild haxe="source/Postbuild.hx"/> -->

2
assets

@ -1 +1 @@
Subproject commit a62e7e50d59c14d256c75da651b79dea77e1620e
Subproject commit 8104d43e584a1f25e574438d7b21a7e671358969

View File

@ -32,7 +32,7 @@
"name": "flxanimate",
"type": "git",
"dir": null,
"ref": "a9136359271cae6ea3016b7fd9023c5c42562933",
"ref": "dd2903f7dc7024335b981edf2a770760cec912e1",
"url": "https://github.com/ninjamuffin99/flxanimate"
},
{
@ -97,8 +97,8 @@
"name": "json2object",
"type": "git",
"dir": null,
"ref": "429986134031cbb1980f09d0d3d642b4b4cbcd6a",
"url": "https://github.com/elnabo/json2object"
"ref": "f4df19cfa196f85eece55c3367021fc965f1fa9a",
"url": "https://github.com/EliteMasterEric/json2object"
},
{
"name": "lime",
@ -139,7 +139,9 @@
"name": "openfl",
"type": "git",
"dir": null,
"ref": "f229d76361c7e31025a048fe7909847f75bb5d5e",
"url": "https://github.com/EliteMasterEric/openfl"
},
{

View File

@ -1,43 +1,48 @@
package funkin;
import funkin.ui.StickerSubState;
import flash.text.TextField;
import flixel.addons.display.FlxGridOverlay;
import flixel.addons.transition.FlxTransitionableState;
import flixel.addons.ui.FlxInputText;
import flixel.FlxCamera;
import flixel.FlxGame;
import flixel.FlxSprite;
import flixel.FlxState;
import flixel.addons.display.FlxGridOverlay;
import flixel.addons.transition.FlxTransitionableState;
import flixel.addons.ui.FlxInputText;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.group.FlxGroup;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.group.FlxSpriteGroup;
import flixel.input.touch.FlxTouch;
import flixel.math.FlxAngle;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import flixel.system.debug.watch.Tracker.TrackerProfile;
import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import funkin.data.song.SongRegistry;
import funkin.save.Save;
import funkin.save.Save.SaveScoreData;
import flixel.util.FlxSpriteUtil;
import flixel.util.FlxTimer;
import funkin.Controls.Control;
import funkin.data.level.LevelRegistry;
import funkin.data.song.SongRegistry;
import funkin.freeplayStuff.BGScrollingText;
import funkin.freeplayStuff.DifficultyStars;
import funkin.freeplayStuff.DJBoyfriend;
import funkin.freeplayStuff.FreeplayScore;
import funkin.freeplayStuff.LetterSort;
import funkin.freeplayStuff.SongMenuItem;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import funkin.play.HealthIcon;
import funkin.play.PlayState;
import funkin.shaderslmfao.AngleMask;
import funkin.shaderslmfao.PureColor;
import funkin.shaderslmfao.StrokeShader;
import funkin.play.PlayStatePlaylist;
import funkin.play.song.Song;
import funkin.save.Save;
import funkin.save.Save.SaveScoreData;
import funkin.shaderslmfao.AngleMask;
import funkin.shaderslmfao.HSVShader;
import funkin.shaderslmfao.PureColor;
import funkin.shaderslmfao.StrokeShader;
import funkin.ui.StickerSubState;
import lime.app.Future;
import lime.utils.Assets;
@ -45,7 +50,6 @@ class FreeplayState extends MusicBeatSubState
{
var songs:Array<FreeplaySongData> = [];
// var selector:FlxText;
var curSelected:Int = 0;
var curDifficulty:Int = 1;
@ -71,6 +75,7 @@ class FreeplayState extends MusicBeatSubState
var grpSongs:FlxTypedGroup<Alphabet>;
var grpCapsules:FlxTypedGroup<SongMenuItem>;
var curCapsule:SongMenuItem;
var curPlaying:Bool = false;
var dj:DJBoyfriend;
@ -103,8 +108,6 @@ class FreeplayState extends MusicBeatSubState
openSubState(stickerSubState);
stickerSubState.degenStickers();
// resetSubState();
}
#if discord_rpc
@ -116,44 +119,27 @@ class FreeplayState extends MusicBeatSubState
#if debug
isDebug = true;
addSong('Test', 'tutorial', 'bf-pixel');
addSong('Pyro', 'weekend1', 'darnell');
#end
var initSonglist = CoolUtil.coolTextFile(Paths.txt('freeplaySonglist'));
for (i in 0...initSonglist.length)
{
songs.push(new FreeplaySongData(initSonglist[i], 'tutorial', 'gf'));
}
if (FlxG.sound.music != null)
{
if (!FlxG.sound.music.playing) FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
}
// if (StoryMenuState.weekUnlocked[2] || isDebug)
addWeek(['Bopeebo', 'Fresh', 'Dadbattle'], 'week1', ['dad']);
// Add a null entry that represents the RANDOM option
songs.push(null);
// if (StoryMenuState.weekUnlocked[2] || isDebug)
addWeek(['Spookeez', 'South', 'Monster'], 'week2', ['spooky', 'spooky', 'monster']);
// if (StoryMenuState.weekUnlocked[3] || isDebug)
addWeek(['Pico', 'Philly-Nice', 'Blammed'], 'week3', ['pico']);
// if (StoryMenuState.weekUnlocked[4] || isDebug)
addWeek(['Satin-Panties', 'High', 'MILF'], 'week4', ['mom']);
// if (StoryMenuState.weekUnlocked[5] || isDebug)
addWeek(['Cocoa', 'Eggnog', 'Winter-Horrorland'], 'week5', ['parents-christmas', 'parents-christmas', 'monster-christmas']);
// if (StoryMenuState.weekUnlocked[6] || isDebug)
addWeek(['Senpai', 'Roses', 'Thorns'], 'week6', ['senpai', 'senpai', 'spirit']);
// if (StoryMenuState.weekUnlocked[7] || isDebug)
addWeek(['Ugh', 'Guns', 'Stress'], 'week7', ['tankman']);
addWeek(["Darnell", "lit-up", "2hot", "blazin"], 'weekend1', ['darnell']);
// programmatically adds the songs via LevelRegistry and SongRegistry
for (coolWeek in LevelRegistry.instance.listBaseGameLevelIds())
{
for (songId in LevelRegistry.instance.parseEntryData(coolWeek).songs)
{
var metadata = SongRegistry.instance.parseEntryMetadata(songId);
var char = metadata.playData.characters.opponent;
var songName = metadata.songName;
addSong(songId, songName, coolWeek, char);
}
}
// LOAD MUSIC
@ -171,7 +157,7 @@ class FreeplayState extends MusicBeatSubState
FlxTween.tween(pinkBack, {x: 0}, 0.6, {ease: FlxEase.quartOut});
add(pinkBack);
var orangeBackShit:FlxSprite = new FlxSprite(84, FlxG.height * 0.68).makeGraphic(Std.int(pinkBack.width), 50, 0xFFffd400);
var orangeBackShit:FlxSprite = new FlxSprite(84, 440).makeGraphic(Std.int(pinkBack.width), 75, 0xFFfeda00);
add(orangeBackShit);
var alsoOrangeLOL:FlxSprite = new FlxSprite(0, orangeBackShit.y).makeGraphic(100, Std.int(orangeBackShit.height), 0xFFffd400);
@ -193,9 +179,11 @@ class FreeplayState extends MusicBeatSubState
add(grpTxtScrolls);
grpTxtScrolls.visible = false;
var moreWays:BGScrollingText = new BGScrollingText(0, 200, "HOT BLOODED IN MORE WAYS THAN ONE", FlxG.width);
FlxG.debugger.addTrackerProfile(new TrackerProfile(BGScrollingText, ["x", "y", "speed", "size"]));
var moreWays:BGScrollingText = new BGScrollingText(0, 160, "HOT BLOODED IN MORE WAYS THAN ONE", FlxG.width, true, 43);
moreWays.funnyColor = 0xFFfff383;
moreWays.speed = 4;
moreWays.speed = 6.8;
grpTxtScrolls.add(moreWays);
exitMovers.set([moreWays],
@ -204,9 +192,9 @@ class FreeplayState extends MusicBeatSubState
speed: 0.4,
});
var funnyScroll:BGScrollingText = new BGScrollingText(0, 250, "BOYFRIEND", FlxG.width / 2);
var funnyScroll:BGScrollingText = new BGScrollingText(0, 220, "BOYFRIEND", FlxG.width / 2, false, 60);
funnyScroll.funnyColor = 0xFFff9963;
funnyScroll.speed = -1;
funnyScroll.speed = -3.8;
grpTxtScrolls.add(funnyScroll);
exitMovers.set([funnyScroll],
@ -217,7 +205,8 @@ class FreeplayState extends MusicBeatSubState
wait: 0
});
var txtNuts:BGScrollingText = new BGScrollingText(0, 300, "PROTECT YO NUTS", FlxG.width / 2);
var txtNuts:BGScrollingText = new BGScrollingText(0, 285, "PROTECT YO NUTS", FlxG.width / 2, true, 43);
txtNuts.speed = 3.5;
grpTxtScrolls.add(txtNuts);
exitMovers.set([txtNuts],
{
@ -225,9 +214,9 @@ class FreeplayState extends MusicBeatSubState
speed: 0.4,
});
var funnyScroll2:BGScrollingText = new BGScrollingText(0, 340, "BOYFRIEND", FlxG.width / 2);
var funnyScroll2:BGScrollingText = new BGScrollingText(0, 335, "BOYFRIEND", FlxG.width / 2, false, 60);
funnyScroll2.funnyColor = 0xFFff9963;
funnyScroll2.speed = -1.2;
funnyScroll2.speed = -3.8;
grpTxtScrolls.add(funnyScroll2);
exitMovers.set([funnyScroll2],
@ -236,9 +225,9 @@ class FreeplayState extends MusicBeatSubState
speed: 0.5,
});
var moreWays2:BGScrollingText = new BGScrollingText(0, 400, "HOT BLOODED IN MORE WAYS THAN ONE", FlxG.width);
var moreWays2:BGScrollingText = new BGScrollingText(0, 397, "HOT BLOODED IN MORE WAYS THAN ONE", FlxG.width, true, 43);
moreWays2.funnyColor = 0xFFfff383;
moreWays2.speed = 4.4;
moreWays2.speed = 6.8;
grpTxtScrolls.add(moreWays2);
exitMovers.set([moreWays2],
@ -247,9 +236,9 @@ class FreeplayState extends MusicBeatSubState
speed: 0.4
});
var funnyScroll3:BGScrollingText = new BGScrollingText(0, orangeBackShit.y, "BOYFRIEND", FlxG.width / 2);
funnyScroll3.funnyColor = 0xFFff9963;
funnyScroll3.speed = -0.8;
var funnyScroll3:BGScrollingText = new BGScrollingText(0, orangeBackShit.y + 10, "BOYFRIEND", FlxG.width / 2, 60);
funnyScroll3.funnyColor = 0xFFfea400;
funnyScroll3.speed = -3.8;
grpTxtScrolls.add(funnyScroll3);
exitMovers.set([funnyScroll3],
@ -258,7 +247,7 @@ class FreeplayState extends MusicBeatSubState
speed: 0.3
});
dj = new DJBoyfriend(0, -100);
dj = new DJBoyfriend(640, 366);
exitMovers.set([dj],
{
x: -dj.width * 1.6,
@ -314,6 +303,49 @@ class FreeplayState extends MusicBeatSubState
grpDifficulties.group.members[curDifficulty].visible = true;
var albumArt:FlxAtlasSprite = new FlxAtlasSprite(640, 360, Paths.animateAtlas("freeplay/albumRoll"));
albumArt.visible = false;
add(albumArt);
exitMovers.set([albumArt],
{
x: FlxG.width,
speed: 0.4,
wait: 0
});
var albumTitle:FlxSprite = new FlxSprite(947, 491).loadGraphic(Paths.image('freeplay/albumTitle-fnfvol1'));
var albumArtist:FlxSprite = new FlxSprite(1010, 607).loadGraphic(Paths.image('freeplay/albumArtist-kawaisprite'));
var difficultyStars:DifficultyStars = new DifficultyStars(140, 39);
difficultyStars.stars.visible = false;
albumTitle.visible = false;
albumArtist.visible = false;
exitMovers.set([albumTitle],
{
x: FlxG.width,
speed: 0.2,
wait: 0.1
});
exitMovers.set([albumArtist],
{
x: FlxG.width * 1.1,
speed: 0.2,
wait: 0.2
});
exitMovers.set([difficultyStars],
{
x: FlxG.width * 1.2,
speed: 0.2,
wait: 0.3
});
add(albumTitle);
add(albumArtist);
add(difficultyStars);
var overhangStuff:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, 64, FlxColor.BLACK);
overhangStuff.y -= overhangStuff.height;
add(overhangStuff);
@ -357,6 +389,28 @@ class FreeplayState extends MusicBeatSubState
txtCompletion.visible = false;
add(txtCompletion);
var letterSort:LetterSort = new LetterSort(400, 75);
add(letterSort);
letterSort.visible = false;
exitMovers.set([letterSort],
{
y: -100,
speed: 0.3
});
letterSort.changeSelectionCallback = (str) -> {
switch (str)
{
case "fav":
generateSongList({filterType: FAVORITE}, true);
case "ALL":
generateSongList(null, true);
default:
generateSongList({filterType: REGEXP, filterData: str}, true);
}
};
exitMovers.set([fp, txtCompletion, fnfHighscoreSpr],
{
x: FlxG.width,
@ -364,6 +418,23 @@ class FreeplayState extends MusicBeatSubState
});
dj.onIntroDone.add(function() {
// when boyfriend hits dat shiii
albumArt.visible = true;
albumArt.anim.play("");
albumArt.anim.onComplete = function() {
albumArt.anim.pause();
};
new FlxTimer().start(1, function(_) {
albumTitle.visible = true;
});
new FlxTimer().start(35 / 24, function(_) {
albumArtist.visible = true;
difficultyStars.stars.visible = true;
});
FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut});
var diffSelLeft = new DifficultySelector(20, grpDifficulties.y - 10, false, controls);
@ -372,33 +443,14 @@ class FreeplayState extends MusicBeatSubState
add(diffSelLeft);
add(diffSelRight);
letterSort.visible = true;
exitMovers.set([diffSelLeft, diffSelRight],
{
x: -diffSelLeft.width * 2,
speed: 0.26
});
var letterSort:LetterSort = new LetterSort(300, 100);
add(letterSort);
exitMovers.set([letterSort],
{
y: -100,
speed: 0.3
});
letterSort.changeSelectionCallback = (str) -> {
switch (str)
{
case "fav":
generateSongList({filterType: FAVORITE}, true);
case "ALL":
generateSongList(null, true);
default:
generateSongList({filterType: STARTSWITH, filterData: str}, true);
}
};
new FlxTimer().start(1 / 24, function(handShit) {
fnfHighscoreSpr.visible = true;
fnfFreeplay.visible = true;
@ -411,53 +463,28 @@ class FreeplayState extends MusicBeatSubState
new FlxTimer().start(1.5 / 24, function(bold) {
sillyStroke.width = 0;
sillyStroke.height = 0;
changeSelection();
});
});
pinkBack.color = 0xFFffd863;
// fnfFreeplay.visible = true;
bgDad.visible = true;
orangeBackShit.visible = true;
alsoOrangeLOL.visible = true;
grpTxtScrolls.visible = true;
});
generateSongList();
// FlxG.sound.playMusic(Paths.music('title'), 0);
// FlxG.sound.music.fadeIn(2, 0, 0.8);
// selector = new FlxText();
// selector.size = 40;
// selector.text = ">";
// add(selector);
generateSongList(null, false);
var swag:Alphabet = new Alphabet(1, 0, "swag");
// JUST DOIN THIS SHIT FOR TESTING!!!
/*
var md:String = Markdown.markdownToHtml(Assets.getText('CHANGELOG.md'));
var texFel:TextField = new TextField();
texFel.width = FlxG.width;
texFel.height = FlxG.height;
// texFel.
texFel.htmlText = md;
FlxG.stage.addChild(texFel);
trace(md);
*/
var funnyCam = new FlxCamera(0, 0, FlxG.width, FlxG.height);
funnyCam.bgColor = FlxColor.TRANSPARENT;
FlxG.cameras.add(funnyCam);
typing = new FlxInputText(100, 100);
// add(typing);
typing.callback = function(txt, action) {
// generateSongList(new EReg(txt.trim(), "ig"));
trace(action);
};
@ -468,17 +495,24 @@ class FreeplayState extends MusicBeatSubState
public function generateSongList(?filterStuff:SongFilter, force:Bool = false)
{
curSelected = 0;
curSelected = 1;
grpCapsules.clear();
for (cap in grpCapsules.members)
cap.kill();
// var regexp:EReg = regexp;
var tempSongs:Array<FreeplaySongData> = songs;
if (filterStuff != null)
{
switch (filterStuff.filterType)
{
case REGEXP:
// filterStuff.filterData has a string with the first letter of the sorting range, and the second one
// this creates a filter to return all the songs that start with a letter between those two
var filterRegexp = new EReg("^[" + filterStuff.filterData + "].*", "i");
tempSongs = tempSongs.filter(str -> {
return filterRegexp.match(str.songName);
});
case STARTSWITH:
tempSongs = tempSongs.filter(str -> {
return str.songName.toLowerCase().startsWith(filterStuff.filterData);
@ -494,74 +528,57 @@ class FreeplayState extends MusicBeatSubState
}
}
var hsvShader:HSVShader = new HSVShader();
var randomCapsule:SongMenuItem = grpCapsules.recycle(SongMenuItem);
randomCapsule.init(FlxG.width, 0, "Random");
randomCapsule.onConfirm = function() {
capsuleOnConfirmRandom(randomCapsule);
};
randomCapsule.y = randomCapsule.intendedY(0) + 10;
randomCapsule.targetPos.x = randomCapsule.x;
randomCapsule.alpha = 0.5;
randomCapsule.songText.visible = false;
randomCapsule.favIcon.visible = false;
randomCapsule.initJumpIn(0, force);
randomCapsule.hsvShader = hsvShader;
grpCapsules.add(randomCapsule);
for (i in 0...tempSongs.length)
{
var funnyMenu:SongMenuItem = new SongMenuItem(FlxG.width, (i * 150) + 160, tempSongs[i].songName);
if (tempSongs[i] == null) continue;
var funnyMenu:SongMenuItem = grpCapsules.recycle(SongMenuItem);
funnyMenu.init(FlxG.width, 0, tempSongs[i].songName);
if (tempSongs[i].songCharacter != null) funnyMenu.setCharacter(tempSongs[i].songCharacter);
funnyMenu.onConfirm = function() {
capsuleOnConfirmDefault(funnyMenu);
};
funnyMenu.y = funnyMenu.intendedY(i + 1) + 10;
funnyMenu.targetPos.x = funnyMenu.x;
funnyMenu.ID = i;
funnyMenu.alpha = 0.5;
funnyMenu.capsule.alpha = 0.5;
funnyMenu.songText.visible = false;
funnyMenu.favIcon.visible = tempSongs[i].isFav;
funnyMenu.hsvShader = hsvShader;
// fp.updateScore(0);
var maxTimer:Float = Math.min(i, 4);
new FlxTimer().start((1 / 24) * maxTimer, function(doShit) {
funnyMenu.doJumpIn = true;
});
new FlxTimer().start((0.09 * maxTimer) + 0.85, function(lerpTmr) {
funnyMenu.doLerp = true;
});
if (!force)
{
new FlxTimer().start(((0.20 * maxTimer) / (1 + maxTimer)) + 0.75, function(swagShi) {
funnyMenu.songText.visible = true;
funnyMenu.alpha = 1;
});
}
if (i < 8) funnyMenu.initJumpIn(Math.min(i, 4), force);
else
{
funnyMenu.songText.visible = true;
funnyMenu.alpha = 1;
}
funnyMenu.forcePosition();
grpCapsules.add(funnyMenu);
var songText:Alphabet = new Alphabet(0, (70 * i) + 30, tempSongs[i].songName, true, false);
songText.x += 100;
songText.isMenuItem = true;
songText.targetY = i;
// grpSongs.add(songText);
// songText.x += 40;
// DONT PUT X IN THE FIRST PARAMETER OF new ALPHABET() !!
// songText.screenCenter(X);
}
FlxG.console.registerFunction("changeSelection", changeSelection);
changeSelection();
changeDiff();
}
public function addSong(songName:String, levelId:String, songCharacter:String)
public function addSong(songId:String, songName:String, levelId:String, songCharacter:String)
{
songs.push(new FreeplaySongData(songName, levelId, songCharacter));
}
public function addWeek(songs:Array<String>, levelId:String, ?songCharacters:Array<String>)
{
if (songCharacters == null) songCharacters = ['bf'];
var num:Int = 0;
for (song in songs)
{
addSong(song, levelId, songCharacters[num]);
if (songCharacters.length != 1) num++;
}
songs.push(new FreeplaySongData(songId, songName, levelId, songCharacter));
}
var touchY:Float = 0;
@ -578,34 +595,39 @@ class FreeplayState extends MusicBeatSubState
var spamTimer:Float = 0;
var spamming:Bool = false;
var busy:Bool = false; // Set to true once the user has pressed enter to select a song.
override function update(elapsed:Float)
{
super.update(elapsed);
if (FlxG.keys.justPressed.F)
{
var realShit = curSelected;
songs[curSelected].isFav = !songs[curSelected].isFav;
if (songs[curSelected].isFav)
if (songs[curSelected] != null)
{
FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4,
{
ease: FlxEase.elasticOut,
onComplete: _ -> {
grpCapsules.members[realShit].favIcon.visible = true;
grpCapsules.members[realShit].favIcon.animation.play("fav");
}
var realShit = curSelected;
songs[curSelected].isFav = !songs[curSelected].isFav;
if (songs[curSelected].isFav)
{
FlxTween.tween(grpCapsules.members[realShit], {angle: 360}, 0.4,
{
ease: FlxEase.elasticOut,
onComplete: _ -> {
grpCapsules.members[realShit].favIcon.visible = true;
grpCapsules.members[realShit].favIcon.animation.play("fav");
}
});
}
else
{
grpCapsules.members[realShit].favIcon.animation.play('fav', false, true);
new FlxTimer().start((1 / 24) * 14, _ -> {
grpCapsules.members[realShit].favIcon.visible = false;
});
}
else
{
grpCapsules.members[realShit].favIcon.animation.play('fav', false, true);
new FlxTimer().start((1 / 24) * 14, _ -> {
grpCapsules.members[realShit].favIcon.visible = false;
});
new FlxTimer().start((1 / 24) * 24, _ -> {
FlxTween.tween(grpCapsules.members[realShit], {angle: 0}, 0.4, {ease: FlxEase.elasticOut});
});
new FlxTimer().start((1 / 24) * 24, _ -> {
FlxTween.tween(grpCapsules.members[realShit], {angle: 0}, 0.4, {ease: FlxEase.elasticOut});
});
}
}
}
@ -626,6 +648,13 @@ class FreeplayState extends MusicBeatSubState
txtCompletion.text = Math.floor(lerpCompletion * 100) + "%";
handleInputs(elapsed);
}
function handleInputs(elapsed:Float):Void
{
if (busy) return;
var upP = controls.UI_UP_P;
var downP = controls.UI_DOWN_P;
var accepted = controls.ACCEPT;
@ -648,16 +677,7 @@ class FreeplayState extends MusicBeatSubState
FlxG.watch.addQuick("LENGTH", length);
FlxG.watch.addQuick("ANGLE", Math.round(FlxAngle.asDegrees(angle)));
// trace("ANGLE", Math.round(FlxAngle.asDegrees(angle)));
}
/* switch (inputID)
{
case FlxObject.UP:
return
case FlxObject.DOWN:
}
*/
}
if (FlxG.touches.getFirst() != null)
@ -693,7 +713,6 @@ class FreeplayState extends MusicBeatSubState
touchY = touch.screenY;
if (dyTouch != 0) dyTouch < 0 ? changeSelection(1) : changeSelection(-1);
// changeSelection(1);
}
}
else
@ -816,82 +835,23 @@ class FreeplayState extends MusicBeatSubState
{
FlxG.switchState(new MainMenuState());
}
//
// close();
});
}
if (accepted)
{
// if (Assets.exists())
var poop:String = songs[curSelected].songName.toLowerCase();
// does not work properly, always just accidentally sets it to normal anyways!
/* if (!Assets.exists(Paths.json(songs[curSelected].songName + '/' + poop)))
{
// defaults to normal if HARD / EASY doesn't exist
// does not account if NORMAL doesn't exist!
FlxG.log.warn("CURRENT DIFFICULTY IS NOT CHARTED, DEFAULTING TO NORMAL!");
poop = Highscore.formatSong(songs[curSelected].songName.toLowerCase(), 1);
curDifficulty = 1;
}*/
PlayStatePlaylist.isStoryMode = false;
var songId:String = songs[curSelected].songName.toLowerCase();
var targetSong:Song = SongRegistry.instance.fetchEntry(songId);
var targetDifficulty:String = switch (curDifficulty)
{
case 0:
'easy';
case 1:
'normal';
case 2:
'hard';
default: 'normal';
};
// TODO: Implement additional difficulties into the interface properly.
if (FlxG.keys.pressed.E)
{
targetDifficulty = 'erect';
}
// TODO: Implement Pico into the interface properly.
var targetCharacter:String = 'bf';
if (FlxG.keys.pressed.P)
{
targetCharacter = 'pico';
}
PlayStatePlaylist.campaignId = songs[curSelected].levelId;
// Visual and audio effects.
FlxG.sound.play(Paths.sound('confirmMenu'));
dj.confirm();
if (targetSong != null)
{
// Load and cache the song's charts.
// TODO: Do this in the loading state.
targetSong.cacheCharts(true);
}
new FlxTimer().start(1, function(tmr:FlxTimer) {
LoadingState.loadAndSwitchState(new PlayState(
{
targetSong: targetSong,
targetDifficulty: targetDifficulty,
targetCharacter: targetCharacter,
}), true);
});
grpCapsules.members[curSelected].onConfirm();
}
}
@:haxe.warning("-WDeprecated")
override function switchTo(nextState:FlxState):Bool
{
clearDaCache(songs[curSelected].songName);
var daSong = songs[curSelected];
if (daSong != null)
{
clearDaCache(daSong.songName);
}
return super.switchTo(nextState);
}
@ -915,9 +875,18 @@ class FreeplayState extends MusicBeatSubState
default: 'normal';
};
var songScore:SaveScoreData = Save.get().getSongScore(songs[curSelected].songName, targetDifficulty);
intendedScore = songScore.score;
intendedCompletion = songScore.accuracy;
var daSong = songs[curSelected];
if (daSong != null)
{
var songScore:SaveScoreData = Save.get().getSongScore(songs[curSelected].songId, targetDifficulty);
intendedScore = songScore?.score ?? 0;
intendedCompletion = songScore?.accuracy ?? 0.0;
}
else
{
intendedScore = 0;
intendedCompletion = 0.0;
}
grpDifficulties.group.forEach(function(spr) {
spr.visible = false;
@ -939,6 +908,7 @@ class FreeplayState extends MusicBeatSubState
{
for (song in songs)
{
if (song == null) continue;
if (song.songName != actualSongTho)
{
trace('trying to remove: ' + song.songName);
@ -947,19 +917,76 @@ class FreeplayState extends MusicBeatSubState
}
}
function capsuleOnConfirmRandom(cap:SongMenuItem):Void
{
trace("RANDOM SELECTED");
busy = true;
}
function capsuleOnConfirmDefault(cap:SongMenuItem):Void
{
busy = true;
PlayStatePlaylist.isStoryMode = false;
var songId:String = cap.songTitle.toLowerCase();
var targetSong:Song = SongRegistry.instance.fetchEntry(songId);
var targetDifficulty:String = switch (curDifficulty)
{
case 0:
'easy';
case 1:
'normal';
case 2:
'hard';
default: 'normal';
};
// TODO: Implement additional difficulties into the interface properly.
if (FlxG.keys.pressed.E)
{
targetDifficulty = 'erect';
}
// TODO: Implement Pico into the interface properly.
var targetCharacter:String = 'bf';
if (FlxG.keys.pressed.P)
{
targetCharacter = 'pico';
}
PlayStatePlaylist.campaignId = songs[curSelected].levelId;
// Visual and audio effects.
FlxG.sound.play(Paths.sound('confirmMenu'));
dj.confirm();
// Load and cache the song's charts.
// TODO: Do this in the loading state.
targetSong.cacheCharts(true);
new FlxTimer().start(1, function(tmr:FlxTimer) {
Paths.setCurrentLevel(songs[curSelected].levelId);
LoadingState.loadAndSwitchState(new PlayState(
{
targetSong: targetSong,
targetDifficulty: targetDifficulty,
targetCharacter: targetCharacter,
}), true);
});
}
function changeSelection(change:Int = 0)
{
// fp.updateScore(12345);
NGio.logEvent('Fresh');
// NGio.logEvent('Fresh');
FlxG.sound.play(Paths.sound('scrollMenu'), 0.4);
// FlxG.sound.playMusic(Paths.inst(songs[curSelected].songName));
curSelected += change;
if (curSelected < 0) curSelected = grpCapsules.members.length - 1;
if (curSelected >= grpCapsules.members.length) curSelected = 0;
if (curSelected < 0) curSelected = grpCapsules.countLiving() - 1;
if (curSelected >= grpCapsules.countLiving()) curSelected = 0;
var targetDifficulty:String = switch (curDifficulty)
{
@ -972,28 +999,40 @@ class FreeplayState extends MusicBeatSubState
default: 'normal';
};
var songScore:SaveScoreData = Save.get().getSongScore(songs[curSelected].songName, targetDifficulty);
intendedScore = songScore.score;
intendedCompletion = songScore.accuracy;
#if PRELOAD_ALL
// FlxG.sound.playMusic(Paths.inst(songs[curSelected].songName), 0);
#end
var bullShit:Int = 0;
var daSong = songs[curSelected];
if (daSong != null)
{
var songScore:SaveScoreData = Save.get().getSongScore(daSong.songId, targetDifficulty);
intendedScore = songScore?.score ?? 0;
intendedCompletion = songScore?.accuracy ?? 0.0;
}
else
{
intendedScore = 0;
intendedCompletion = 0.0;
}
for (index => capsule in grpCapsules.members)
{
capsule.selected = false;
index += 1;
capsule.targetPos.y = ((index - curSelected) * 150) + 160;
capsule.selected = index == curSelected + 1;
capsule.targetPos.y = capsule.intendedY(index - curSelected);
capsule.targetPos.x = 270 + (60 * (Math.sin(index - curSelected)));
// capsule.targetPos.x = 320 + (40 * (index - curSelected));
if (index < curSelected) capsule.targetPos.y -= 100; // another 100 for good measure
}
if (grpCapsules.members.length > 0) grpCapsules.members[curSelected].selected = true;
if (grpCapsules.countLiving() > 0)
{
if (curSelected == 0)
{
FlxG.sound.playMusic(Paths.music('freeplay/freeplayRandom'), 0);
FlxG.sound.music.fadeIn(2, 0, 0.8);
}
grpCapsules.members[curSelected].selected = true;
}
}
}
@ -1033,7 +1072,10 @@ class DifficultySelector extends FlxSprite
whiteShader.colorSet = true;
scale.x = scale.y = 0.5;
new FlxTimer().start(2 / 24, function(tmr) {
scale.x = scale.y = 1;
whiteShader.colorSet = false;
updateHitbox();
});
@ -1049,20 +1091,23 @@ typedef SongFilter =
enum abstract FilterType(String)
{
var STARTSWITH;
var REGEXP;
var FAVORITE;
var ALL;
}
class FreeplaySongData
{
public var songId:String = "";
public var songName:String = "";
public var levelId:String = "";
public var songCharacter:String = "";
public var isFav:Bool = false;
public function new(song:String, levelId:String, songCharacter:String, isFav:Bool = false)
public function new(songId:String, songName:String, levelId:String, songCharacter:String, isFav:Bool = false)
{
this.songName = song;
this.songId = songId;
this.songName = songName;
this.levelId = levelId;
this.songCharacter = songCharacter;
this.isFav = isFav;

View File

@ -6,9 +6,6 @@ import openfl.utils.Assets as OpenFlAssets;
class Paths
{
public static var SOUND_EXT = #if web "mp3" #else "ogg" #end;
public static var VIDEO_EXT = "mp4";
static var currentLevel:String;
static public function setCurrentLevel(name:String)
@ -52,9 +49,9 @@ class Paths
return getPath(file, type, library);
}
public static inline function animateAtlas(path:String, library:String)
public static inline function animateAtlas(path:String, ?library:String)
{
return getLibraryPathForce('images/$path', library);
return getLibraryPath('images/$path', library);
}
inline static public function txt(key:String, ?library:String)
@ -84,7 +81,7 @@ class Paths
static public function sound(key:String, ?library:String)
{
return getPath('sounds/$key.$SOUND_EXT', SOUND, library);
return getPath('sounds/$key.${Constants.EXT_SOUND}', SOUND, library);
}
inline static public function soundRandom(key:String, min:Int, max:Int, ?library:String)
@ -94,24 +91,24 @@ class Paths
inline static public function music(key:String, ?library:String)
{
return getPath('music/$key.$SOUND_EXT', MUSIC, library);
return getPath('music/$key.${Constants.EXT_SOUND}', MUSIC, library);
}
inline static public function videos(key:String, ?library:String)
{
return getPath('videos/$key.$VIDEO_EXT', BINARY, library);
return getPath('videos/$key.${Constants.EXT_VIDEO}', BINARY, library);
}
inline static public function voices(song:String, ?suffix:String = '')
{
if (suffix == null) suffix = ""; // no suffix, for a sorta backwards compatibility with older-ish voice files
return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.$SOUND_EXT';
return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.${Constants.EXT_SOUND}';
}
inline static public function inst(song:String, ?suffix:String = '')
{
return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.$SOUND_EXT';
return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.${Constants.EXT_SOUND}';
}
inline static public function image(key:String, ?library:String)

View File

@ -150,6 +150,11 @@ class PauseSubState extends MusicBeatSubState
super.update(elapsed);
handleInputs();
}
function handleInputs():Void
{
var upP = controls.UI_UP_P;
var downP = controls.UI_DOWN_P;
var accepted = controls.ACCEPT;
@ -229,9 +234,14 @@ class PauseSubState extends MusicBeatSubState
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
if (PlayStatePlaylist.isStoryMode) openSubState(new funkin.ui.StickerSubState(null, STORY));
if (PlayStatePlaylist.isStoryMode)
{
openSubState(new funkin.ui.StickerSubState(null, STORY));
}
else
{
openSubState(new funkin.ui.StickerSubState(null, FREEPLAY));
}
case 'Exit to Chart Editor':
this.close();

View File

@ -0,0 +1,49 @@
package funkin.audio;
import flash.media.Sound;
#if flash11
import flash.utils.ByteArray;
#end
import flixel.sound.FlxSound;
import flixel.system.FlxAssets.FlxSoundAsset;
import openfl.Assets;
#if (openfl >= "8.0.0")
import openfl.utils.AssetType;
#end
/**
* a FlxSound that just overrides loadEmbedded to allow for "streamed" sounds to load with better performance!
*/
class FlxStreamSound extends FlxSound
{
public function new()
{
super();
}
override public function loadEmbedded(EmbeddedSound:FlxSoundAsset, Looped:Bool = false, AutoDestroy:Bool = false, ?OnComplete:Void->Void):FlxSound
{
if (EmbeddedSound == null) return this;
cleanup(true);
if ((EmbeddedSound is Sound))
{
_sound = EmbeddedSound;
}
else if ((EmbeddedSound is Class))
{
_sound = Type.createInstance(EmbeddedSound, []);
}
else if ((EmbeddedSound is String))
{
if (Assets.exists(EmbeddedSound, AssetType.SOUND)
|| Assets.exists(EmbeddedSound, AssetType.MUSIC)) _sound = Assets.getMusic(EmbeddedSound);
else
FlxG.log.error('Could not find a Sound asset with an ID of \'$EmbeddedSound\'.');
}
// NOTE: can't pull ID3 info from embedded sound currently
return init(Looped, AutoDestroy, OnComplete);
}
}

View File

@ -4,9 +4,6 @@ import openfl.Assets;
import funkin.util.assets.DataAssets;
import funkin.util.VersionUtil;
import haxe.Constraints.Constructible;
import json2object.Position;
import json2object.Position.Line;
import json2object.Error;
/**
* The entry's constructor function must take a single argument, the entry's ID.
@ -179,6 +176,15 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
*/
public abstract function parseEntryData(id:String):Null<J>;
/**
* Parse and validate the JSON data and produce the corresponding data object.
*
* NOTE: Must be implemented on the implementation class.
* @param contents The JSON as a string.
* @param fileName An optional file name for error reporting.
*/
public abstract function parseEntryDataRaw(contents:String, ?fileName:String):Null<J>;
/**
* Read, parse, and validate the JSON data and produce the corresponding data object,
* accounting for old versions of the data.
@ -226,79 +232,12 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
*/
abstract function createScriptedEntry(clsName:String):Null<T>;
function printErrors(errors:Array<Error>, id:String = ''):Void
function printErrors(errors:Array<json2object.Error>, id:String = ''):Void
{
trace('[${registryId}] Failed to parse entry data: ${id}');
for (error in errors)
printError(error);
}
function printError(error:Error):Void
{
switch (error)
{
case IncorrectType(vari, expected, pos):
trace(' Expected field "$vari" to be of type "$expected".');
printPos(pos);
case IncorrectEnumValue(value, expected, pos):
trace(' Invalid enum value (expected "$expected", got "$value")');
printPos(pos);
case InvalidEnumConstructor(value, expected, pos):
trace(' Invalid enum constructor (epxected "$expected", got "$value")');
printPos(pos);
case UninitializedVariable(vari, pos):
trace(' Uninitialized variable "$vari"');
printPos(pos);
case UnknownVariable(vari, pos):
trace(' Unknown variable "$vari"');
printPos(pos);
case ParserError(message, pos):
trace(' Parsing error: ${message}');
printPos(pos);
case CustomFunctionException(e, pos):
if (Std.isOfType(e, String))
{
trace(' ${e}');
}
else
{
printUnknownError(e);
}
printPos(pos);
default:
printUnknownError(error);
}
}
function printUnknownError(e:Dynamic):Void
{
switch (Type.typeof(e))
{
case TClass(c):
trace(' [${Type.getClassName(c)}] ${e.toString()}');
case TEnum(c):
trace(' [${Type.getEnumName(c)}] ${e.toString()}');
default:
trace(' [${Type.typeof(e)}] ${e.toString()}');
}
}
/**
* TODO: Figure out the nicest way to print this.
* Maybe look up how other JSON parsers format their errors?
* @see https://github.com/elnabo/json2object/blob/master/src/json2object/Position.hx
*/
function printPos(pos:Position):Void
{
if (pos.lines[0].number == pos.lines[pos.lines.length - 1].number)
{
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}');
}
else
{
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}-${pos.lines[pos.lines.length - 1].number}');
}
DataError.printError(error);
}
}

View File

@ -0,0 +1,75 @@
package funkin.data;
import json2object.Position;
import json2object.Position.Line;
import json2object.Error;
class DataError
{
public static function printError(error:Error):Void
{
switch (error)
{
case IncorrectType(vari, expected, pos):
trace(' Expected field "$vari" to be of type "$expected".');
printPos(pos);
case IncorrectEnumValue(value, expected, pos):
trace(' Invalid enum value (expected "$expected", got "$value")');
printPos(pos);
case InvalidEnumConstructor(value, expected, pos):
trace(' Invalid enum constructor (epxected "$expected", got "$value")');
printPos(pos);
case UninitializedVariable(vari, pos):
trace(' Uninitialized variable "$vari"');
printPos(pos);
case UnknownVariable(vari, pos):
trace(' Unknown variable "$vari"');
printPos(pos);
case ParserError(message, pos):
trace(' Parsing error: ${message}');
printPos(pos);
case CustomFunctionException(e, pos):
if (Std.isOfType(e, String))
{
trace(' ${e}');
}
else
{
printUnknownError(e);
}
printPos(pos);
default:
printUnknownError(error);
}
}
public static function printUnknownError(e:Dynamic):Void
{
switch (Type.typeof(e))
{
case TClass(c):
trace(' [${Type.getClassName(c)}] ${e.toString()}');
case TEnum(c):
trace(' [${Type.getEnumName(c)}] ${e.toString()}');
default:
trace(' [${Type.typeof(e)}] ${e.toString()}');
}
}
/**
* TODO: Figure out the nicest way to print this.
* Maybe look up how other JSON parsers format their errors?
* @see https://github.com/elnabo/json2object/blob/master/src/json2object/Position.hx
*/
static function printPos(pos:Position):Void
{
if (pos.lines[0].number == pos.lines[pos.lines.length - 1].number)
{
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}');
}
else
{
trace(' at ${(pos.file == '') ? 'line ' : '${pos.file}:'}${pos.lines[0].number}-${pos.lines[pos.lines.length - 1].number}');
}
}
}

View File

@ -1,7 +1,13 @@
package funkin.data;
import funkin.data.song.importer.FNFLegacyData.LegacyNote;
import hxjsonast.Json;
import hxjsonast.Tools;
import hxjsonast.Json.JObjectField;
import haxe.ds.Either;
import funkin.data.song.importer.FNFLegacyData.LegacyNoteSection;
import funkin.data.song.importer.FNFLegacyData.LegacyNoteData;
import funkin.data.song.importer.FNFLegacyData.LegacyScrollSpeeds;
/**
* `json2object` has an annotation `@:jcustomparse` which allows for mutation of parsed values.
@ -39,36 +45,40 @@ class DataParse
*/
public static function dynamicValue(json:Json, name:String):Dynamic
{
return jsonToDynamic(json);
return Tools.getValue(json);
}
/**
* Parser which outputs a Dynamic value, which must be an object with properties.
* @param json
* @param name
* @return Dynamic
* Parser which outputs a `Either<Array<LegacyNoteSection>, LegacyNoteData>`.
* Used by the FNF legacy JSON importer.
*/
public static function dynamicObject(json:Json, name:String):Dynamic
public static function eitherLegacyNoteData(json:Json, name:String):Either<Array<LegacyNoteSection>, LegacyNoteData>
{
switch (json.value)
{
case JArray(values):
return Either.Left(legacyNoteSectionArray(json, name));
case JObject(fields):
return jsonFieldsToDynamicObject(fields);
return Either.Right(cast Tools.getValue(json));
default:
throw 'Expected property $name to be an object, but it was ${json.value}.';
throw 'Expected property $name to be note data, but it was ${json.value}.';
}
}
static function jsonToDynamic(json:Json):Null<Dynamic>
/**
* Parser which outputs a `Either<Float, LegacyScrollSpeeds>`.
* Used by the FNF legacy JSON importer.
*/
public static function eitherLegacyScrollSpeeds(json:Json, name:String):Either<Float, LegacyScrollSpeeds>
{
return switch (json.value)
switch (json.value)
{
case JString(s): s;
case JNumber(n): Std.parseInt(n);
case JBool(b): b;
case JNull: null;
case JObject(fields): jsonFieldsToDynamicObject(fields);
case JArray(values): jsonArrayToDynamicArray(values);
case JNumber(f):
return Either.Left(Std.parseFloat(f));
case JObject(fields):
return Either.Right(cast Tools.getValue(json));
default:
throw 'Expected property $name to be scroll speeds, but it was ${json.value}.';
}
}
@ -82,7 +92,7 @@ class DataParse
var result:Dynamic = {};
for (field in fields)
{
Reflect.setField(result, field.name, jsonToDynamic(field.value));
Reflect.setField(result, field.name, Tools.getValue(field.value));
}
return result;
}
@ -94,6 +104,67 @@ class DataParse
*/
static function jsonArrayToDynamicArray(jsons:Array<Json>):Array<Null<Dynamic>>
{
return [for (json in jsons) jsonToDynamic(json)];
return [for (json in jsons) Tools.getValue(json)];
}
static function legacyNoteSectionArray(json:Json, name:String):Array<LegacyNoteSection>
{
switch (json.value)
{
case JArray(values):
return [for (value in values) legacyNoteSection(value, name)];
default:
throw 'Expected property to be an array, but it was ${json.value}.';
}
}
static function legacyNoteSection(json:Json, name:String):LegacyNoteSection
{
switch (json.value)
{
case JObject(fields):
return cast Tools.getValue(json);
default:
throw 'Expected property $name to be an object, but it was ${json.value}.';
}
}
public static function legacyNoteData(json:Json, name:String):LegacyNoteData
{
switch (json.value)
{
case JObject(fields):
return cast Tools.getValue(json);
default:
throw 'Expected property $name to be an object, but it was ${json.value}.';
}
}
public static function legacyNotes(json:Json, name:String):Array<LegacyNote>
{
switch (json.value)
{
case JArray(values):
return [for (value in values) legacyNote(value, name)];
default:
throw 'Expected property $name to be an array of notes, but it was ${json.value}.';
}
}
public static function legacyNote(json:Json, name:String):LegacyNote
{
switch (json.value)
{
case JArray(values):
// var time:Null<Float> = values[0] == null ? null : Tools.getValue(values[0]);
// var data:Null<Int> = values[1] == null ? null : Tools.getValue(values[1]);
// var length:Null<Float> = values[2] == null ? null : Tools.getValue(values[2]);
// var alt:Null<Bool> = values[3] == null ? null : Tools.getValue(values[3]);
// return new LegacyNote(time, data, length, alt);
return null;
default:
throw 'Expected property $name to be a note, but it was ${json.value}.';
}
}
}

View File

@ -1,8 +1,17 @@
package funkin.data;
import funkin.util.SerializerUtil;
/**
* `json2object` has an annotation `@:jcustomwrite` which allows for custom serialization of values to be written to JSON.
*
* Functions must be of the signature `(T) -> String`, where `T` is the type of the property.
*/
class DataWrite {}
class DataWrite
{
public static function dynamicValue(value:Dynamic):String
{
// Is this cheating? Yes. Do I care? No.
return SerializerUtil.toJSON(value);
}
}

View File

@ -67,7 +67,6 @@ typedef UnnamedAnimationData =
* ONLY for use by MultiSparrow characters.
* @default The assetPath of the parent sprite
*/
@:default(null)
@:optional
var assetPath:Null<String>;
@ -85,7 +84,7 @@ typedef UnnamedAnimationData =
*/
@:default(false)
@:optional
var looped:Null<Bool>;
var looped:Bool;
/**
* Whether the animation's sprites should be flipped horizontally.

View File

@ -47,6 +47,26 @@ class LevelRegistry extends BaseRegistry<Level, LevelData>
return parser.value;
}
/**
* Parse and validate the JSON data and produce the corresponding data object.
*
* NOTE: Must be implemented on the implementation class.
* @param contents The JSON as a string.
* @param fileName An optional file name for error reporting.
*/
public function parseEntryDataRaw(contents:String, ?fileName:String):Null<LevelData>
{
var parser = new json2object.JsonParser<LevelData>();
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
{
printErrors(parser.errors, fileName);
return null;
}
return parser.value;
}
function createScriptedEntry(clsName:String):Level
{
return ScriptedLevel.init(clsName, "unknown");

View File

@ -54,6 +54,26 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
return parser.value;
}
/**
* Parse and validate the JSON data and produce the corresponding data object.
*
* NOTE: Must be implemented on the implementation class.
* @param contents The JSON as a string.
* @param fileName An optional file name for error reporting.
*/
public function parseEntryDataRaw(contents:String, ?fileName:String):Null<NoteStyleData>
{
var parser = new json2object.JsonParser<NoteStyleData>();
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
{
printErrors(parser.errors, fileName);
return null;
}
return parser.value;
}
function createScriptedEntry(clsName:String):NoteStyle
{
return ScriptedNoteStyle.init(clsName, "unknown");

View File

@ -1,11 +1,10 @@
package funkin.data.song;
import flixel.util.typeLimit.OneOfTwo;
import funkin.play.song.SongMigrator;
import funkin.play.song.SongValidator;
import funkin.data.song.SongRegistry;
import thx.semver.Version;
@:nullSafety
class SongMetadata
{
/**
@ -44,35 +43,36 @@ class SongMetadata
public var timeChanges:Array<SongTimeChange>;
/**
* Defaults to `default` or `''`. Populated later.
* Defaults to `Constants.DEFAULT_VARIATION`. Populated later.
*/
@:jignored
public var variation:String = 'default';
public var variation:String;
public function new(songName:String, artist:String, variation:String = 'default')
public function new(songName:String, artist:String, ?variation:String)
{
this.version = SongMigrator.CHART_VERSION;
this.version = SongRegistry.SONG_METADATA_VERSION;
this.songName = songName;
this.artist = artist;
this.timeFormat = 'ms';
this.divisions = null;
this.timeChanges = [new SongTimeChange(0, 100)];
this.looped = false;
this.playData =
{
songVariations: [],
difficulties: ['normal'],
playableChars: ['bf' => new SongPlayableChar('gf', 'dad')],
stage: 'mainStage',
noteSkin: 'Normal'
};
this.playData = new SongPlayData();
this.playData.songVariations = [];
this.playData.difficulties = [];
this.playData.characters = new SongCharacterData('bf', 'gf', 'dad');
this.playData.stage = 'mainStage';
this.playData.noteSkin = 'funkin';
this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
// Variation ID.
this.variation = variation;
this.variation = (variation == null) ? Constants.DEFAULT_VARIATION : variation;
}
/**
* Create a copy of this SongMetadata with the same information.
* @param newVariation Set to a new variation ID to change the new metadata.
* @return The cloned SongMetadata
*/
public function clone(?newVariation:String = null):SongMetadata
{
var result:SongMetadata = new SongMetadata(this.songName, this.artist, newVariation == null ? this.variation : newVariation);
@ -87,6 +87,22 @@ class SongMetadata
return result;
}
/**
* Serialize this SongMetadata into a JSON string.
* @return The JSON string.
*/
public function serialize(pretty:Bool = true):String
{
var writer = new json2object.JsonWriter<SongMetadata>();
// I believe @:jignored should be iggnored by the writer?
// var output = this.clone();
// output.variation = null; // Not sure how to make a field optional on the reader and ignored on the writer.
return writer.write(this, pretty ? ' ' : null);
}
/**
* Produces a string representation suitable for debugging.
*/
public function toString():String
{
return 'SongMetadata(${this.songName} by ${this.artist}, variation ${this.variation})';
@ -121,7 +137,6 @@ class SongTimeChange
*/
@:optional
@:alias("b")
// @:default(funkin.data.song.SongData.SongTimeChange.DEFAULT_BEAT_TIME)
public var beatTime:Null<Float>;
/**
@ -168,6 +183,9 @@ class SongTimeChange
this.beatTuplets = beatTuplets == null ? DEFAULT_BEAT_TUPLETS : beatTuplets;
}
/**
* Produces a string representation suitable for debugging.
*/
public function toString():String
{
return 'SongTimeChange(${this.timeStamp}ms,${this.bpm}bpm)';
@ -199,7 +217,7 @@ class SongMusicData
@:optional
@:default(false)
public var looped:Bool;
public var looped:Null<Bool>;
// @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY)
public var generatedBy:String;
@ -211,14 +229,14 @@ class SongMusicData
public var timeChanges:Array<SongTimeChange>;
/**
* Defaults to `default` or `''`. Populated later.
* Defaults to `Constants.DEFAULT_VARIATION`. Populated later.
*/
@:jignored
public var variation:String = 'default';
public var variation:String;
public function new(songName:String, artist:String, variation:String = 'default')
{
this.version = SongMigrator.CHART_VERSION;
this.version = SongRegistry.SONG_CHART_DATA_VERSION;
this.songName = songName;
this.artist = artist;
this.timeFormat = 'ms';
@ -227,7 +245,7 @@ class SongMusicData
this.looped = false;
this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
// Variation ID.
this.variation = variation;
this.variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
}
public function clone(?newVariation:String = null):SongMusicData
@ -243,53 +261,106 @@ class SongMusicData
return result;
}
/**
* Produces a string representation suitable for debugging.
*/
public function toString():String
{
return 'SongMusicData(${this.songName} by ${this.artist}, variation ${this.variation})';
}
}
typedef SongPlayData =
class SongPlayData
{
/**
* The variations this song has. The associated metadata files should exist.
*/
public var songVariations:Array<String>;
/**
* The difficulties contained in this song's chart file.
*/
public var difficulties:Array<String>;
/**
* Keys are the player characters and the values give info on what opponent/GF/inst to use.
* The characters used by this song.
*/
public var playableChars:Map<String, SongPlayableChar>;
public var characters:SongCharacterData;
/**
* The stage used by this song.
*/
public var stage:String;
/**
* The note style used by this song.
* TODO: Rename to `noteStyle`? Renaming values is a breaking change to the metadata format.
*/
public var noteSkin:String;
/**
* The difficulty rating for this song as displayed in Freeplay.
* TODO: Adding this is a non-breaking change to the metadata format.
*/
// public var rating:Int;
/**
* The album ID for the album to display in Freeplay.
* TODO: Adding this is a non-breaking change to the metadata format.
*/
// public var album:String;
public function new() {}
/**
* Produces a string representation suitable for debugging.
*/
public function toString():String
{
return 'SongPlayData(${this.songVariations}, ${this.difficulties})';
}
}
class SongPlayableChar
/**
* Information about the characters used in this variation of the song.
* Create a new variation if you want to change the characters.
*/
class SongCharacterData
{
@:alias('g')
@:optional
@:default('')
public var player:String = '';
@:optional
@:default('')
public var girlfriend:String = '';
@:alias('o')
@:optional
@:default('')
public var opponent:String = '';
@:alias('i')
@:optional
@:default('')
public var inst:String = '';
public var instrumental:String = '';
public function new(girlfriend:String = '', opponent:String = '', inst:String = '')
@:optional
@:default([])
public var altInstrumentals:Array<String> = [];
public function new(player:String = '', girlfriend:String = '', opponent:String = '', instrumental:String = '')
{
this.player = player;
this.girlfriend = girlfriend;
this.opponent = opponent;
this.inst = inst;
this.instrumental = instrumental;
}
/**
* Produces a string representation suitable for debugging.
*/
public function toString():String
{
return 'SongPlayableChar(${this.girlfriend}, ${this.opponent}, ${this.inst})';
return 'SongCharacterData(${this.player}, ${this.girlfriend}, ${this.opponent}, ${this.instrumental}, [${this.altInstrumentals.join(', ')}])';
}
}
@ -305,6 +376,12 @@ class SongChartData
@:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY)
public var generatedBy:String;
/**
* Defaults to `Constants.DEFAULT_VARIATION`. Populated later.
*/
@:jignored
public var variation:String;
public function new(scrollSpeed:Map<String, Float>, events:Array<SongEventData>, notes:Map<String, Array<SongNoteData>>)
{
this.version = SongRegistry.SONG_CHART_DATA_VERSION;
@ -346,14 +423,21 @@ class SongChartData
return value;
}
public function getEvents():Array<SongEventData>
/**
* Convert this SongChartData into a JSON string.
*/
public function serialize(pretty:Bool = true):String
{
return this.events;
var writer = new json2object.JsonWriter<SongChartData>();
return writer.write(this, pretty ? ' ' : null);
}
public function setEvents(value:Array<SongEventData>):Array<SongEventData>
/**
* Produces a string representation suitable for debugging.
*/
public function toString():String
{
return this.events = value;
return 'SongChartData(${this.events.length} events, ${this.notes.size()} difficulties, ${generatedBy})';
}
}
@ -387,6 +471,7 @@ class SongEventData
@:alias("v")
@:optional
@:jcustomparse(funkin.data.DataParse.dynamicValue)
@:jcustomwrite(funkin.data.DataWrite.dynamicValue)
public var value:Dynamic = null;
/**
@ -484,6 +569,9 @@ class SongEventData
return this.time <= other.time;
}
/**
* Produces a string representation suitable for debugging.
*/
public function toString():String
{
return 'SongEventData(${this.time}ms, ${this.event}: ${this.value})';
@ -703,6 +791,9 @@ class SongNoteData
return this.time <= other.time;
}
/**
* Produces a string representation suitable for debugging.
*/
public function toString():String
{
return 'SongNoteData(${this.time}ms, ' + (this.length > 0 ? '[${this.length}ms hold]' : '') + ' ${this.data}'

View File

@ -8,6 +8,9 @@ import funkin.util.SerializerUtil;
using Lambda;
/**
* Utility functions for working with song data, including note data, event data, metadata, etc.
*/
class SongDataUtils
{
/**
@ -18,11 +21,21 @@ class SongDataUtils
* @param notes The notes to modify.
* @param offset The time difference to apply in milliseconds.
*/
public static function offsetSongNoteData(notes:Array<SongNoteData>, offset:Int):Array<SongNoteData>
public static function offsetSongNoteData(notes:Array<SongNoteData>, offset:Float):Array<SongNoteData>
{
return notes.map(function(note:SongNoteData):SongNoteData {
return new SongNoteData(note.time + offset, note.data, note.length, note.kind);
});
var offsetNote = function(note:SongNoteData):SongNoteData {
var time:Float = note.time + offset;
var data:Int = note.data;
var length:Float = note.length;
var kind:String = note.kind;
return new SongNoteData(time, data, length, kind);
};
trace(notes);
trace(notes[0]);
var result = [for (i in 0...notes.length) offsetNote(notes[i])];
trace(result);
return result;
}
/**
@ -33,7 +46,7 @@ class SongDataUtils
* @param events The events to modify.
* @param offset The time difference to apply in milliseconds.
*/
public static function offsetSongEventData(events:Array<SongEventData>, offset:Int):Array<SongEventData>
public static function offsetSongEventData(events:Array<SongEventData>, offset:Float):Array<SongEventData>
{
return events.map(function(event:SongEventData):SongEventData {
return new SongEventData(event.time + offset, event.event, event.value);
@ -149,7 +162,8 @@ class SongDataUtils
*/
public static function writeItemsToClipboard(data:SongClipboardItems):Void
{
var dataString = SerializerUtil.toJSON(data);
var writer = new json2object.JsonWriter<SongClipboardItems>();
var dataString:String = writer.write(data, ' ');
ClipboardUtil.setClipboard(dataString);
@ -167,19 +181,24 @@ class SongDataUtils
trace('Read ${notesString.length} characters from clipboard.');
var data:SongClipboardItems = notesString.parseJSON();
if (data == null)
var parser = new json2object.JsonParser<SongClipboardItems>();
parser.fromJson(notesString, 'clipboard');
if (parser.errors.length > 0)
{
trace('Failed to parse notes from clipboard.');
trace('[SongDataUtils] Error parsing note JSON data from clipboard.');
for (error in parser.errors)
DataError.printError(error);
return {
valid: false,
notes: [],
events: []
};
}
else
{
var data:SongClipboardItems = parser.value;
trace('Parsed ' + data.notes.length + ' notes and ' + data.events.length + ' from clipboard.');
data.valid = true;
return data;
}
}
@ -227,6 +246,7 @@ class SongDataUtils
typedef SongClipboardItems =
{
?valid:Bool,
notes:Array<SongNoteData>,
events:Array<SongEventData>
}

View File

@ -1,6 +1,7 @@
package funkin.data.song;
import funkin.data.song.SongData;
import funkin.data.song.migrator.SongData_v2_0_0.SongMetadata_v2_0_0;
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongMetadata;
import funkin.play.song.ScriptedSong;
@ -8,6 +9,8 @@ import funkin.play.song.Song;
import funkin.util.assets.DataAssets;
import funkin.util.VersionUtil;
using funkin.data.song.migrator.SongDataMigrator;
class SongRegistry extends BaseRegistry<Song, SongMetadata>
{
/**
@ -15,14 +18,18 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateStageData()` function.
*/
public static final SONG_METADATA_VERSION:thx.semver.Version = "2.0.0";
public static final SONG_METADATA_VERSION:thx.semver.Version = "2.1.0";
public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.1.x";
public static final SONG_CHART_DATA_VERSION:thx.semver.Version = "2.0.0";
public static final SONG_CHART_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
public static final SONG_MUSIC_DATA_VERSION:thx.semver.Version = "2.0.0";
public static final SONG_MUSIC_DATA_VERSION_RULE:thx.semver.VersionRule = "2.0.x";
public static var DEFAULT_GENERATEDBY(get, null):String;
static function get_DEFAULT_GENERATEDBY():String
@ -30,6 +37,10 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return '${Constants.TITLE} - ${Constants.VERSION}';
}
/**
* TODO: What if there was a Singleton macro which created static functions
* that redirected to the instance?
*/
public static final instance:SongRegistry = new SongRegistry();
public function new()
@ -101,12 +112,91 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return parseEntryMetadata(id);
}
public function parseEntryMetadata(id:String, variation:String = ""):Null<SongMetadata>
/**
* Parse, and validate the JSON data and produce the corresponding data object.
*/
public function parseEntryDataRaw(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
{
// JsonParser does not take type parameters,
// otherwise this function would be in BaseRegistry.
return parseEntryMetadataRaw(contents);
}
public function parseEntryMetadata(id:String, ?variation:String):Null<SongMetadata>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongMetadata>();
switch (loadEntryMetadataFile(id, variation))
{
case {fileName: fileName, contents: contents}:
parser.fromJson(contents, fileName);
default:
return null;
}
if (parser.errors.length > 0)
{
printErrors(parser.errors, id);
return null;
}
return cleanMetadata(parser.value, variation);
}
public function parseEntryMetadataRaw(contents:String, ?fileName:String = 'raw', ?variation:String):Null<SongMetadata>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongMetadata>();
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
{
printErrors(parser.errors, fileName);
return null;
}
return cleanMetadata(parser.value, variation);
}
public function parseEntryMetadataWithMigration(id:String, variation:String, version:thx.semver.Version):Null<SongMetadata>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
// If a version rule is not specified, do not check against it.
if (SONG_METADATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_METADATA_VERSION_RULE))
{
return parseEntryMetadata(id, variation);
}
else if (VersionUtil.validateVersion(version, "2.0.x"))
{
return parseEntryMetadata_v2_0_0(id, variation);
}
else
{
throw '[${registryId}] Metadata entry ${id}:${variation} does not support migration to version ${SONG_METADATA_VERSION_RULE}.';
}
}
public function parseEntryMetadataRawWithMigration(contents:String, ?fileName:String = 'raw', version:thx.semver.Version):Null<SongMetadata>
{
// If a version rule is not specified, do not check against it.
if (SONG_METADATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_METADATA_VERSION_RULE))
{
return parseEntryMetadataRaw(contents, fileName);
}
else if (VersionUtil.validateVersion(version, "2.0.x"))
{
return parseEntryMetadataRaw_v2_0_0(contents, fileName);
}
else
{
throw '[${registryId}] Metadata entry "${fileName}" does not support migration to version ${SONG_METADATA_VERSION_RULE}.';
}
}
function parseEntryMetadata_v2_0_0(id:String, ?variation:String):Null<SongMetadata>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
switch (loadEntryMetadataFile(id))
{
case {fileName: fileName, contents: contents}:
@ -114,6 +204,39 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
default:
return null;
}
if (parser.errors.length > 0)
{
printErrors(parser.errors, id);
return null;
}
return parser.value.migrate();
}
function parseEntryMetadataRaw_v2_0_0(contents:String, ?fileName:String = 'raw'):Null<SongMetadata>
{
var parser = new json2object.JsonParser<SongMetadata_v2_0_0>();
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
{
printErrors(parser.errors, fileName);
return null;
}
return parser.value.migrate();
}
public function parseMusicData(id:String, ?variation:String):Null<SongMusicData>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongMusicData>();
switch (loadMusicDataFile(id, variation))
{
case {fileName: fileName, contents: contents}:
parser.fromJson(contents, fileName);
default:
return null;
}
if (parser.errors.length > 0)
{
@ -123,48 +246,54 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return parser.value;
}
public function parseEntryMetadataWithMigration(id:String, variation:String = '', version:thx.semver.Version):Null<SongMetadata>
public function parseMusicDataRaw(contents:String, ?fileName:String = 'raw'):Null<SongMusicData>
{
// If a version rule is not specified, do not check against it.
if (SONG_METADATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_METADATA_VERSION_RULE))
var parser = new json2object.JsonParser<SongMusicData>();
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
{
return parseEntryMetadata(id);
printErrors(parser.errors, fileName);
return null;
}
return parser.value;
}
public function parseMusicDataWithMigration(id:String, ?variation:String, version:thx.semver.Version):Null<SongMusicData>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
// If a version rule is not specified, do not check against it.
if (SONG_MUSIC_DATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_MUSIC_DATA_VERSION_RULE))
{
return parseMusicData(id, variation);
}
else
{
throw '[${registryId}] Metadata entry ${id}:${variation == '' ? 'default' : variation} does not support migration to version ${SONG_METADATA_VERSION_RULE}.';
throw '[${registryId}] Chart entry ${id}:${variation} does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.';
}
}
public function parseMusicData(id:String, variation:String = ""):Null<SongMusicData>
public function parseMusicDataRawWithMigration(contents:String, ?fileName:String = 'raw', version:thx.semver.Version):Null<SongMusicData>
{
// JsonParser does not take type parameters,
// otherwise this function would be in BaseRegistry.
var parser = new json2object.JsonParser<SongMusicData>();
switch (loadMusicDataFile(id))
// If a version rule is not specified, do not check against it.
if (SONG_MUSIC_DATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_MUSIC_DATA_VERSION_RULE))
{
case {fileName: fileName, contents: contents}:
parser.fromJson(contents, fileName);
default:
return null;
return parseMusicDataRaw(contents, fileName);
}
if (parser.errors.length > 0)
else
{
printErrors(parser.errors, id);
return null;
throw '[${registryId}] Chart entry "$fileName" does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.';
}
return parser.value;
}
public function parseEntryChartData(id:String, variation:String = ''):Null<SongChartData>
public function parseEntryChartData(id:String, ?variation:String):Null<SongChartData>
{
// JsonParser does not take type parameters,
// otherwise this function would be in BaseRegistry.
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongChartData>();
switch (loadEntryChartFile(id))
switch (loadEntryChartFile(id, variation))
{
case {fileName: fileName, contents: contents}:
parser.fromJson(contents, fileName);
@ -177,11 +306,28 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
printErrors(parser.errors, id);
return null;
}
return parser.value;
return cleanChartData(parser.value, variation);
}
public function parseEntryChartDataWithMigration(id:String, variation:String = '', version:thx.semver.Version):Null<SongChartData>
public function parseEntryChartDataRaw(contents:String, ?fileName:String = 'raw', ?variation:String):Null<SongChartData>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var parser = new json2object.JsonParser<SongChartData>();
parser.fromJson(contents, fileName);
if (parser.errors.length > 0)
{
printErrors(parser.errors, fileName);
return null;
}
return cleanChartData(parser.value, variation);
}
public function parseEntryChartDataWithMigration(id:String, ?variation:String, version:thx.semver.Version):Null<SongChartData>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
// If a version rule is not specified, do not check against it.
if (SONG_CHART_DATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_CHART_DATA_VERSION_RULE))
{
@ -189,7 +335,20 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
}
else
{
throw '[${registryId}] Chart entry ${id}:${variation == '' ? 'default' : variation} does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.';
throw '[${registryId}] Chart entry ${id}:${variation} does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.';
}
}
public function parseEntryChartDataRawWithMigration(contents:String, ?fileName:String = 'raw', version:thx.semver.Version):Null<SongChartData>
{
// If a version rule is not specified, do not check against it.
if (SONG_CHART_DATA_VERSION_RULE == null || VersionUtil.validateVersion(version, SONG_CHART_DATA_VERSION_RULE))
{
return parseEntryChartDataRaw(contents, fileName);
}
else
{
throw '[${registryId}] Chart entry "${fileName}" does not support migration to version ${SONG_CHART_DATA_VERSION_RULE}.';
}
}
@ -203,9 +362,10 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return ScriptedSong.listScriptClasses();
}
function loadEntryMetadataFile(id:String, variation:String = ''):Null<BaseRegistry.JsonFile>
function loadEntryMetadataFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile>
{
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id${variation == '' ? '' : '-$variation'}-metadata');
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}');
if (!openfl.Assets.exists(entryFilePath)) return null;
var rawJson:Null<String> = openfl.Assets.getText(entryFilePath);
if (rawJson == null) return null;
@ -213,9 +373,10 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return {fileName: entryFilePath, contents: rawJson};
}
function loadMusicDataFile(id:String, variation:String = ''):Null<BaseRegistry.JsonFile>
function loadMusicDataFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile>
{
var entryFilePath:String = Paths.file('music/$id/$id${variation == '' ? '' : '-$variation'}-metadata.json');
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryFilePath:String = Paths.file('music/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.json');
if (!openfl.Assets.exists(entryFilePath)) return null;
var rawJson:String = openfl.Assets.getText(entryFilePath);
if (rawJson == null) return null;
@ -223,9 +384,10 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return {fileName: entryFilePath, contents: rawJson};
}
function loadEntryChartFile(id:String, variation:String = ''):Null<BaseRegistry.JsonFile>
function loadEntryChartFile(id:String, ?variation:String):Null<BaseRegistry.JsonFile>
{
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id${variation == '' ? '' : '-$variation'}-chart');
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryFilePath:String = Paths.json('$dataFilePath/$id/$id-chart${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}');
if (!openfl.Assets.exists(entryFilePath)) return null;
var rawJson:String = openfl.Assets.getText(entryFilePath);
if (rawJson == null) return null;
@ -233,20 +395,36 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
return {fileName: entryFilePath, contents: rawJson};
}
public function fetchEntryMetadataVersion(id:String, variation:String = ''):Null<thx.semver.Version>
public function fetchEntryMetadataVersion(id:String, ?variation:String):Null<thx.semver.Version>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryStr:Null<String> = loadEntryMetadataFile(id, variation)?.contents;
var entryVersion:thx.semver.Version = VersionUtil.getVersionFromJSON(entryStr);
return entryVersion;
}
public function fetchEntryChartVersion(id:String, variation:String = ''):Null<thx.semver.Version>
public function fetchEntryChartVersion(id:String, ?variation:String):Null<thx.semver.Version>
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
var entryStr:Null<String> = loadEntryChartFile(id, variation)?.contents;
var entryVersion:thx.semver.Version = VersionUtil.getVersionFromJSON(entryStr);
return entryVersion;
}
function cleanMetadata(metadata:SongMetadata, variation:String):SongMetadata
{
metadata.variation = variation;
return metadata;
}
function cleanChartData(chartData:SongChartData, variation:String):SongChartData
{
chartData.variation = variation;
return chartData;
}
/**
* A list of all the story weeks from the base game, in order.
* TODO: Should this be hardcoded?

View File

@ -0,0 +1,124 @@
package funkin.data.song.importer;
import haxe.ds.Either;
/**
* A data structure representing a song in the old chart format.
* This only works for charts compatible with Week 7, so you'll need a custom program
* to handle importing charts from mods or other engines.
*/
class FNFLegacyData
{
public var song:LegacySongData;
}
class LegacySongData
{
public var player1:String; // Boyfriend
public var player2:String; // Opponent
@:jcustomparse(funkin.data.DataParse.eitherLegacyScrollSpeeds)
public var speed:Either<Float, LegacyScrollSpeeds>;
public var stageDefault:String;
public var bpm:Float;
@:jcustomparse(funkin.data.DataParse.eitherLegacyNoteData)
public var notes:Either<Array<LegacyNoteSection>, LegacyNoteData>;
public var song:String; // Song name
public function new() {}
public function toString():String
{
var notesStr:String = switch (notes)
{
case Left(sections): 'single difficulty w/ ${sections.length} sections';
case Right(data):
var difficultyCount:Int = 0;
if (data.easy != null) difficultyCount++;
if (data.normal != null) difficultyCount++;
if (data.hard != null) difficultyCount++;
'${difficultyCount} difficulties';
};
return 'LegacySongData($player1, $player2, $notesStr)';
}
}
typedef LegacyScrollSpeeds =
{
public var ?easy:Float;
public var ?normal:Float;
public var ?hard:Float;
};
typedef LegacyNoteData =
{
/**
* The easy difficulty.
*/
public var ?easy:Array<LegacyNoteSection>;
/**
* The normal difficulty.
*/
public var ?normal:Array<LegacyNoteSection>;
/**
* The hard difficulty.
*/
public var ?hard:Array<LegacyNoteSection>;
};
typedef LegacyNoteSection =
{
/**
* Whether the section is a must-hit section.
* If true, 0-3 are boyfriends notes, 4-7 are opponents notes.
* If false, 0-3 are opponents notes, 4-7 are boyfriends notes.
*/
public var mustHitSection:Bool;
/**
* Array of note data:
* - Direction
* - Time (ms)
* - Sustain Duration (ms)
* - Note kind (true = "alt", or string)
*/
public var sectionNotes:Array<LegacyNote>;
public var ?typeOfSection:Int;
public var ?lengthInSteps:Int;
// BPM changes
public var ?changeBPM:Bool;
public var ?bpm:Float;
}
/**
* Notes in the old format are stored as an Array<Dynamic>
* We use a custom parser to manage this.
*/
@:jcustomparse(funkin.data.DataParse.legacyNote)
class LegacyNote
{
public var time:Float;
public var data:Int;
public var length:Float;
public var alt:Bool;
public function new(time:Float, data:Int, ?length:Float, ?alt:Bool)
{
this.time = time;
this.data = data;
this.length = length ?? 0.0;
this.alt = alt ?? false;
}
public inline function getKind():String
{
return this.alt ? 'alt' : 'normal';
}
}

View File

@ -0,0 +1,202 @@
package funkin.data.song.importer; // import is a reserved word dumbass
import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.importer.FNFLegacyData;
import funkin.data.song.importer.FNFLegacyData.LegacyNoteSection;
class FNFLegacyImporter
{
public static function parseLegacyDataRaw(input:String, fileName:String = 'raw'):FNFLegacyData
{
var parser = new json2object.JsonParser<FNFLegacyData>();
parser.fromJson(input, fileName);
if (parser.errors.length > 0)
{
trace('[FNFLegacyImporter] Error parsing JSON data from ' + fileName + ':');
for (error in parser.errors)
DataError.printError(error);
return null;
}
return parser.value;
}
/**
* @param data The raw parsed JSON data to migrate, as a Dynamic.
* @param difficulty
* @return SongMetadata
*/
public static function migrateMetadata(songData:FNFLegacyData, difficulty:String = 'normal'):SongMetadata
{
trace('Migrating song metadata from FNF Legacy.');
var songMetadata:SongMetadata = new SongMetadata('Import', 'Kawai Sprite', 'default');
var hadError:Bool = false;
// Set generatedBy string for debugging.
songMetadata.generatedBy = 'Chart Editor Import (FNF Legacy)';
songMetadata.playData.stage = songData?.song?.stageDefault ?? 'mainStage';
songMetadata.songName = songData?.song?.song ?? 'Import';
songMetadata.playData.difficulties = [];
if (songData?.song?.notes != null)
{
switch (songData.song.notes)
{
case Left(notes):
// One difficulty of notes.
songMetadata.playData.difficulties.push(difficulty);
case Right(difficulties):
if (difficulties.easy != null) songMetadata.playData.difficulties.push('easy');
if (difficulties.normal != null) songMetadata.playData.difficulties.push('normal');
if (difficulties.hard != null) songMetadata.playData.difficulties.push('hard');
}
}
songMetadata.playData.songVariations = [];
songMetadata.timeChanges = rebuildTimeChanges(songData);
songMetadata.playData.characters = new SongCharacterData(songData?.song?.player1 ?? 'bf', 'gf', songData?.song?.player2 ?? 'dad', 'mom');
return songMetadata;
}
public static function migrateChartData(songData:FNFLegacyData, difficulty:String = 'normal'):SongChartData
{
trace('Migrating song chart data from FNF Legacy.');
var songChartData:SongChartData = new SongChartData([difficulty => 1.0], [], [difficulty => []]);
if (songData?.song?.notes != null)
{
switch (songData.song.notes)
{
case Left(notes):
// One difficulty of notes.
songChartData.notes.set(difficulty, migrateNoteSections(notes));
case Right(difficulties):
var baseDifficulty = null;
if (difficulties.easy != null) songChartData.notes.set('easy', migrateNoteSections(difficulties.easy));
if (difficulties.normal != null) songChartData.notes.set('normal', migrateNoteSections(difficulties.normal));
if (difficulties.hard != null) songChartData.notes.set('hard', migrateNoteSections(difficulties.hard));
}
}
// Import event data.
songChartData.events = rebuildEventData(songData);
switch (songData.song.speed)
{
case Left(speed):
// All difficulties will use the one scroll speed.
songChartData.scrollSpeed.set('default', speed);
case Right(speeds):
if (speeds.easy != null) songChartData.scrollSpeed.set('easy', speeds.easy);
if (speeds.normal != null) songChartData.scrollSpeed.set('normal', speeds.normal);
if (speeds.hard != null) songChartData.scrollSpeed.set('hard', speeds.hard);
}
return songChartData;
}
/**
* FNF Legacy doesn't have song events, but without them the song won't look right,
* so we insert camera events when the character changes.
*/
static function rebuildEventData(songData:FNFLegacyData):Array<SongEventData>
{
var result:Array<SongEventData> = [];
var noteSections = [];
switch (songData.song.notes)
{
case Left(notes):
// All difficulties will use the one scroll speed.
noteSections = notes;
case Right(difficulties):
if (difficulties.normal != null) noteSections = difficulties.normal;
if (difficulties.hard != null) noteSections = difficulties.normal;
if (difficulties.easy != null) noteSections = difficulties.normal;
}
if (noteSections == null || noteSections.length == 0) return result;
// Add camera events.
var lastSectionWasMustHit:Null<Bool> = null;
for (section in noteSections)
{
// Skip empty sections.
if (section.sectionNotes.length == 0) continue;
if (section.mustHitSection != lastSectionWasMustHit)
{
lastSectionWasMustHit = section.mustHitSection;
var firstNote:LegacyNote = section.sectionNotes[0];
result.push(new SongEventData(firstNote.time, 'FocusCamera', {char: section.mustHitSection ? 0 : 1}));
}
}
return result;
}
/**
* Port over time changes from FNF Legacy.
* If a section contains a BPM change, it will be applied at the timestamp of the first note in that section.
*/
static function rebuildTimeChanges(songData:FNFLegacyData):Array<SongTimeChange>
{
var result:Array<SongTimeChange> = [];
result.push(new SongTimeChange(0, songData?.song?.bpm ?? Constants.DEFAULT_BPM));
var noteSections = [];
switch (songData.song.notes)
{
case Left(notes):
// All difficulties will use the one scroll speed.
noteSections = notes;
case Right(difficulties):
if (difficulties.normal != null) noteSections = difficulties.normal;
if (difficulties.hard != null) noteSections = difficulties.normal;
if (difficulties.easy != null) noteSections = difficulties.normal;
}
if (noteSections == null || noteSections.length == 0) return result;
for (noteSection in noteSections)
{
if (noteSection.changeBPM ?? false)
{
var firstNote:LegacyNote = noteSection.sectionNotes[0];
if (firstNote != null) result.push(new SongTimeChange(firstNote.time, noteSection.bpm));
}
}
return result;
}
static function migrateNoteSections(input:Array<LegacyNoteSection>):Array<SongNoteData>
{
var result:Array<SongNoteData> = [];
for (section in input)
{
for (note in section.sectionNotes)
{
result.push(new SongNoteData(note.time, note.data, note.length, note.getKind()));
}
}
return result;
}
}

View File

@ -0,0 +1,66 @@
package funkin.data.song.migrator;
import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongData.SongPlayData;
import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.migrator.SongData_v2_0_0.SongMetadata_v2_0_0;
import funkin.data.song.migrator.SongData_v2_0_0.SongPlayData_v2_0_0;
import funkin.data.song.migrator.SongData_v2_0_0.SongPlayableChar_v2_0_0;
/**
* This class contains functions to migrate older data formats to the current one.
*
* Utilizes static extensions with overloaded inline functions to make migration as easy as `.migrate()`.
* @see https://try.haxe.org/#e1c1cf22
*/
class SongDataMigrator
{
public static overload extern inline function migrate(input:SongData_v2_0_0.SongMetadata_v2_0_0):SongMetadata
{
return migrate_SongMetadata_v2_0_0(input);
}
public static function migrate_SongMetadata_v2_0_0(input:SongData_v2_0_0.SongMetadata_v2_0_0):SongMetadata
{
var result:SongMetadata = new SongMetadata(input.songName, input.artist, input.variation);
result.version = input.version;
result.timeFormat = input.timeFormat;
result.divisions = input.divisions;
result.timeChanges = input.timeChanges;
result.looped = input.looped;
result.playData = migrate_SongPlayData_v2_0_0(input.playData);
result.generatedBy = input.generatedBy;
return result;
}
public static overload extern inline function migrate(input:SongData_v2_0_0.SongPlayData_v2_0_0):SongPlayData
{
return migrate_SongPlayData_v2_0_0(input);
}
public static function migrate_SongPlayData_v2_0_0(input:SongData_v2_0_0.SongPlayData_v2_0_0):SongPlayData
{
var result:SongPlayData = new SongPlayData();
result.songVariations = input.songVariations;
result.difficulties = input.difficulties;
result.stage = input.stage;
result.noteSkin = input.noteSkin;
// Fetch the first playable character and migrate it.
var firstCharKey:Null<String> = input.playableChars.size() == 0 ? null : input.playableChars.keys().array()[0];
var firstCharData:Null<SongPlayableChar_v2_0_0> = input.playableChars.get(firstCharKey);
if (firstCharData == null)
{
// Fill in a default playable character.
result.characters = new SongCharacterData('bf', 'gf', 'dad');
}
else
{
result.characters = new SongCharacterData(firstCharKey, firstCharData.girlfriend, firstCharData.opponent, firstCharData.inst);
}
return result;
}
}

View File

@ -0,0 +1,122 @@
package funkin.data.song.migrator;
import thx.semver.Version;
import funkin.data.song.SongData;
class SongMetadata_v2_0_0
{
// ==========
// MODIFIED VALUES
// ===========
/**
* In metadata `v2.1.0`, `SongPlayData` was refactored.
*/
public var playData:SongPlayData_v2_0_0;
/**
* In metadata `v2.1.0`, `variation` was set to `ignore` when writing.
*/
@:optional
@:default('default')
public var variation:String;
// ==========
// UNMODIFIED VALUES
// ==========
public var version:Version;
@:default("Unknown")
public var songName:String;
@:default("Unknown")
public var artist:String;
@:optional
@:default(96)
public var divisions:Null<Int>; // Optional field
@:optional
@:default(false)
public var looped:Bool;
public var generatedBy:String;
public var timeFormat:SongData.SongTimeFormat;
public var timeChanges:Array<SongData.SongTimeChange>;
public function new() {}
/**
* Produces a string representation suitable for debugging.
*/
public function toString():String
{
return 'SongMetadata[LEGACY:v2.0.0](${this.songName} by ${this.artist}, variation ${this.variation})';
}
}
class SongPlayData_v2_0_0
{
// ==========
// MODIFIED VALUES
// ===========
/**
* In metadata version `v2.1.0`, this was refactored to a single `SongCharacterData` object.
*/
public var playableChars:Map<String, SongPlayableChar_v2_0_0>;
// ==========
// UNMODIFIED VALUES
// ==========
public var songVariations:Array<String>;
public var difficulties:Array<String>;
public var stage:String;
public var noteSkin:String;
public function new() {}
/**
* Produces a string representation suitable for debugging.
*/
public function toString():String
{
return 'SongPlayData[LEGACY:v2.0.0](${this.songVariations}, ${this.difficulties})';
}
}
class SongPlayableChar_v2_0_0
{
@:alias('g')
@:optional
@:default('')
public var girlfriend:String = '';
@:alias('o')
@:optional
@:default('')
public var opponent:String = '';
@:alias('i')
@:optional
@:default('')
public var inst:String = '';
public function new(girlfriend:String = '', opponent:String = '', inst:String = '')
{
this.girlfriend = girlfriend;
this.opponent = opponent;
this.inst = inst;
}
/**
* Produces a string representation suitable for debugging.
*/
public function toString():String
{
return 'SongPlayableChar[LEGACY:v2.0.0](${this.girlfriend}, ${this.opponent}, ${this.inst})';
}
}

View File

@ -7,6 +7,7 @@ import flixel.math.FlxMath;
import flixel.text.FlxText;
import flixel.util.FlxColor;
import flixel.util.FlxSort;
import flixel.util.FlxTimer;
// its kinda like marqeee html lol!
class BGScrollingText extends FlxSpriteGroup
@ -16,36 +17,53 @@ class BGScrollingText extends FlxSpriteGroup
public var widthShit:Float = FlxG.width;
public var placementOffset:Float = 20;
public var speed:Float = 1;
public var size(default, set):Int = 48;
public var funnyColor(default, set):Int = 0xFFFFFFFF;
public function new(x:Float, y:Float, text:String, widthShit:Float = 100)
public function new(x:Float, y:Float, text:String, widthShit:Float = 100, ?bold:Bool = false, ?size:Int = 48)
{
super(x, y);
this.widthShit = widthShit;
if (size != null) this.size = size;
grpTexts = new FlxTypedSpriteGroup<FlxText>();
add(grpTexts);
var testText:FlxText = new FlxText(0, 0, 0, text, 48);
var testText:FlxText = new FlxText(0, 0, 0, text, this.size);
testText.font = "5by7";
testText.bold = bold;
testText.updateHitbox();
grpTexts.add(testText);
var needed:Int = Math.ceil(widthShit / testText.frameWidth);
var needed:Int = Math.ceil(widthShit / testText.frameWidth) + 1;
for (i in 0...needed)
{
var lmfao:Int = i + 1;
var coolText:FlxText = new FlxText((lmfao * testText.frameWidth) + (lmfao * 20), 0, 0, text, 48);
var coolText:FlxText = new FlxText((lmfao * testText.frameWidth) + (lmfao * 20), 0, 0, text, this.size);
coolText.font = "5by7";
coolText.bold = bold;
coolText.updateHitbox();
grpTexts.add(coolText);
}
}
function set_size(value:Int):Int
{
if (grpTexts != null)
{
grpTexts.forEach(function(txt:FlxText) {
txt.size = value;
});
}
this.size = value;
return value;
}
function set_funnyColor(col:Int):Int
{
grpTexts.forEach(function(txt) {
@ -55,7 +73,7 @@ class BGScrollingText extends FlxSpriteGroup
return col;
}
override function update(elapsed:Float)
override public function update(elapsed:Float)
{
for (txt in grpTexts.group)
{
@ -66,14 +84,16 @@ class BGScrollingText extends FlxSpriteGroup
if (txt.x < -txt.frameWidth)
{
txt.x = grpTexts.group.members[grpTexts.length - 1].x + grpTexts.group.members[grpTexts.length - 1].frameWidth + placementOffset;
sortTextShit();
}
}
else
{
if (txt.x > widthShit)
if (txt.x > txt.frameWidth * 2)
{
txt.x = grpTexts.group.members[0].x - grpTexts.group.members[0].frameWidth - placementOffset;
sortTextShit();
}
}

View File

@ -0,0 +1,49 @@
package funkin.freeplayStuff;
import openfl.filters.BitmapFilterQuality;
import flixel.text.FlxText;
import flixel.group.FlxSpriteGroup;
import funkin.shaderslmfao.GaussianBlurShader;
class CapsuleText extends FlxSpriteGroup
{
public var blurredText:FlxText;
var whiteText:FlxText;
public var text(default, set):String;
public function new(x:Float, y:Float, songTitle:String, size:Float)
{
super(x, y);
blurredText = initText(songTitle, size);
blurredText.shader = new GaussianBlurShader(1);
whiteText = initText(songTitle, size);
// whiteText.shader = new GaussianBlurShader(0.3);
text = songTitle;
blurredText.color = 0xFF00ccff;
whiteText.color = 0xFFFFFFFF;
add(blurredText);
add(whiteText);
}
function initText(songTitle, size:Float):FlxText
{
var text:FlxText = new FlxText(0, 0, 0, songTitle, Std.int(size));
text.font = "5by7";
return text;
}
function set_text(value:String):String
{
blurredText.text = value;
whiteText.text = value;
whiteText.textField.filters = [
new openfl.filters.GlowFilter(0x00ccff, 1, 5, 5, 210, BitmapFilterQuality.MEDIUM),
// new openfl.filters.BlurFilter(5, 5, BitmapFilterQuality.LOW)
];
return value;
}
}

View File

@ -3,8 +3,12 @@ package funkin.freeplayStuff;
import flixel.FlxSprite;
import flixel.util.FlxSignal;
import funkin.util.assets.FlxAnimationUtil;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import flixel.system.FlxSound;
import flixel.util.FlxTimer;
import funkin.audio.FlxStreamSound;
class DJBoyfriend extends FlxSprite
class DJBoyfriend extends FlxAtlasSprite
{
// Represents the sprite's current status.
// Without state machines I would have driven myself crazy years ago.
@ -20,20 +24,55 @@ class DJBoyfriend extends FlxSprite
// TODO: Switch this class to use SwagSprite instead.
public var animOffsets:Map<String, Array<Dynamic>>;
static final SPOOK_PERIOD:Float = 180.0;
var gotSpooked:Bool = false;
static final SPOOK_PERIOD:Float = 120.0;
static final TV_PERIOD:Float = 180.0;
// Time since dad last SPOOKED you.
var timeSinceSpook:Float = 0;
public function new(x:Float, y:Float)
{
super(x, y);
super(x, y, Paths.animateAtlas("freeplay/freeplay-boyfriend", "preload"));
animOffsets = new Map<String, Array<Dynamic>>();
setupAnimations();
anim.callback = function(name, number) {
switch (name)
{
case "Boyfriend DJ watchin tv OG":
if (number == 85) runTvLogic();
default:
}
};
animation.finishCallback = onFinishAnim;
setupAnimations();
trace(listAnimations());
FlxG.debugger.track(this);
FlxG.console.registerObject("dj", this);
anim.onComplete = onFinishAnim;
FlxG.console.registerFunction("tv", function() {
currentState = TV;
});
}
/*
[remote hand under,boyfriend top head,brim piece,arm cringe l,red lazer,dj arm in,bf fist pump arm,hand raised right,forearm left,fist shaking,bf smile eyes closed face,arm cringe r,bf clenched face,face shrug,boyfriend falling,blue tint 1,shirt sleeve,bf clenched fist,head BF relaxed,blue tint 2,hand down left,blue tint 3,blue tint 4,head less smooshed,blue tint 5,boyfriend freeplay,BF head slight turn,blue tint 6,arm shrug l,blue tint 7,shoulder raised w sleeve,blue tint 8,fist pump face,blue tint 9,foot rested light,hand turnaround,arm chill right,Boyfriend DJ,arm shrug r,head back bf,hat top piece,dad bod,face surprise snap,Boyfriend DJ fist pump,office chair,foot rested right,chest down,office chair upright,body chill,bf dj afk,head mouth open dad,BF Head defalt HAIR BLOWING,hand shrug l,face piece,foot wag,turn table,shoulder up left,turntable lights,boyfriend dj body shirt blowing,body chunk turned,hand down right,dj arm out,hand shrug r,body chest out,rave hand,palm,chill face default,head back semi bf,boyfriend bottom head,DJ arm,shoulder right dad,bf surprise,boyfriend dj body,hs1,Boyfriend DJ watchin tv OG,spinning disk,hs2,arm chill left,boyfriend dj intro,hs3,hs4,chill face extra,hs5,remote hand upright,hs6,pant over table,face surprise,bf arm peace,arm turnaround,bf eyes 1,arm slammed table,eye squit,leg BF,head mid piece,arm backing,arm swoopin in,shoe right lowering,forearm right,hand out,blue tint 10,body falling back,remote thumb press,shoulder,hair spike single,bf bent
arm,crt,foot raised right,dad hand,chill face 1,chill face 2,clenched fist,head SMOOSHED,shoulder left dad,df1,body chunk upright,df2,df3,df4,hat front piece,df5,foot rested right 2,hand in,arm spun,shoe raised left,bf 1 finger hand,bf mouth 1,Boyfriend DJ confirm,forearm down ,hand raised left,remote thumb up]
*/
override public function listAnimations():Array<String>
{
var anims:Array<String> = [];
@:privateAccess
for (animKey in anim.symbolDictionary)
{
anims.push(animKey.name);
}
return anims;
}
public override function update(elapsed:Float):Void
@ -44,51 +83,68 @@ class DJBoyfriend extends FlxSprite
{
case Intro:
// Play the intro animation then leave this state immediately.
if (getCurrentAnimation() != 'intro') playAnimation('intro', true);
if (getCurrentAnimation() != 'boyfriend dj intro') playFlashAnimation('boyfriend dj intro', true);
timeSinceSpook = 0;
case Idle:
// We are in this state the majority of the time.
if (getCurrentAnimation() != 'idle' || animation.finished)
if (getCurrentAnimation() != 'Boyfriend DJ' || anim.finished)
{
if (timeSinceSpook > SPOOK_PERIOD)
if (timeSinceSpook > SPOOK_PERIOD && !gotSpooked)
{
currentState = Spook;
}
else if (timeSinceSpook > TV_PERIOD)
{
currentState = TV;
}
else
{
playAnimation('idle', false);
playFlashAnimation('Boyfriend DJ', false);
}
}
timeSinceSpook += elapsed;
case Confirm:
if (getCurrentAnimation() != 'confirm') playAnimation('confirm', false);
if (getCurrentAnimation() != 'Boyfriend DJ confirm') playFlashAnimation('Boyfriend DJ confirm', false);
timeSinceSpook = 0;
case Spook:
if (getCurrentAnimation() != 'spook')
if (getCurrentAnimation() != 'bf dj afk')
{
onSpook.dispatch();
playAnimation('spook', false);
playFlashAnimation('bf dj afk', false);
}
timeSinceSpook = 0;
case TV:
if (getCurrentAnimation() != 'Boyfriend DJ watchin tv OG') playFlashAnimation('Boyfriend DJ watchin tv OG', true);
timeSinceSpook = 0;
default:
// I shit myself.
}
}
function onFinishAnim(name:String):Void
function onFinishAnim():Void
{
var name = anim.curSymbol.name;
switch (name)
{
case "intro":
case "boyfriend dj intro":
// trace('Finished intro');
currentState = Idle;
onIntroDone.dispatch();
case "idle":
case "Boyfriend DJ":
// trace('Finished idle');
case "spook":
case "bf dj afk":
// trace('Finished spook');
currentState = Idle;
case "confirm":
case "Boyfriend DJ confirm":
case "Boyfriend DJ watchin tv OG":
var frame:Int = FlxG.random.bool(33) ? 112 : 166;
if (FlxG.random.bool(10))
{
frame = 60;
// boyfriend switches channel code?
}
anim.play("Boyfriend DJ watchin tv OG", true, false, frame);
// trace('Finished confirm');
}
}
@ -100,19 +156,66 @@ class DJBoyfriend extends FlxSprite
function setupAnimations():Void
{
frames = FlxAnimationUtil.combineFramesCollections(Paths.getSparrowAtlas('freeplay/bfFreeplay'), Paths.getSparrowAtlas('freeplay/bf-freeplay-afk'));
// frames = FlxAnimationUtil.combineFramesCollections(Paths.getSparrowAtlas('freeplay/bfFreeplay'), Paths.getSparrowAtlas('freeplay/bf-freeplay-afk'));
animation.addByPrefix('intro', "boyfriend dj intro", 24, false);
addOffset('intro', 0, 0);
// animation.addByPrefix('intro', "boyfriend dj intro", 24, false);
addOffset('boyfriend dj intro', 8, 3);
animation.addByPrefix('idle', "Boyfriend DJ0", 24, false);
addOffset('idle', -4, -426);
// animation.addByPrefix('idle', "Boyfriend DJ0", 24, false);
addOffset('Boyfriend DJ', 0, 0);
animation.addByPrefix('confirm', "Boyfriend DJ confirm", 24, false);
addOffset('confirm', 40, -451);
// animation.addByPrefix('confirm', "Boyfriend DJ confirm", 24, false);
addOffset('Boyfriend DJ confirm', 0, 0);
animation.addByPrefix('spook', "bf dj afk0", 24, false);
addOffset('spook', -3, -272);
// animation.addByPrefix('spook', "bf dj afk0", 24, false);
addOffset('bf dj afk', 0, 0);
}
var cartoonSnd:FlxStreamSound;
public var playingCartoon:Bool = false;
public function runTvLogic()
{
if (cartoonSnd == null)
{
// tv is OFF, but getting turned on
FlxG.sound.play(Paths.sound('tv_on'));
cartoonSnd = new FlxStreamSound();
FlxG.sound.defaultSoundGroup.add(cartoonSnd);
}
else
{
// plays it smidge after the click
new FlxTimer().start(0.1, function(_) {
FlxG.sound.play(Paths.sound('channel_switch'));
});
}
// cartoonSnd.loadEmbedded(Paths.sound("cartoons/peck"));
// cartoonSnd.play();
loadCartoon();
}
function loadCartoon()
{
cartoonSnd.loadEmbedded(Paths.sound(getRandomFlashToon()), false, false, function() {
anim.play("Boyfriend DJ watchin tv OG", true, false, 60);
});
cartoonSnd.play(true, FlxG.random.float(0, cartoonSnd.length));
}
var cartoonList:Array<String> = openfl.utils.Assets.list().filter(function(path) return path.startsWith("assets/sounds/cartoons/"));
function getRandomFlashToon():String
{
var randomFile = FlxG.random.getObject(cartoonList);
randomFile = randomFile.replace("assets/sounds/", "");
randomFile = randomFile.substring(0, randomFile.length - 4);
return randomFile;
}
public function confirm():Void
@ -125,15 +228,15 @@ class DJBoyfriend extends FlxSprite
animOffsets[name] = [x, y];
}
public function getCurrentAnimation():String
override public function getCurrentAnimation():String
{
if (this.animation == null || this.animation.curAnim == null) return "";
return this.animation.curAnim.name;
if (this.anim == null || this.anim.curSymbol == null) return "";
return this.anim.curSymbol.name;
}
public function playAnimation(AnimName:String, Force:Bool = false, Reversed:Bool = false, Frame:Int = 0):Void
public function playFlashAnimation(id:String, ?Force:Bool = false, ?Reverse:Bool = false, ?Frame:Int = 0):Void
{
animation.play(AnimName, Force, Reversed, Frame);
anim.play(id, Force, Reverse, Frame);
applyAnimOffset();
}
@ -156,4 +259,5 @@ enum DJBoyfriendState
Idle;
Confirm;
Spook;
TV;
}

View File

@ -0,0 +1,106 @@
package funkin.freeplayStuff;
import flixel.group.FlxSpriteGroup;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import funkin.shaderslmfao.HSVShader;
class DifficultyStars extends FlxSpriteGroup
{
/**
* Internal handler var for difficulty... ranges from 0... to 15
* 0 is 1 star... 15 is 0 stars!
*/
var curDifficulty(default, set):Int = 0;
/**
* Range between 0 and 15
*/
public var difficulty(default, set):Int = 1;
public var stars:FlxAtlasSprite;
var flames:FreeplayFlames;
var hsvShader:HSVShader;
public function new(x:Float, y:Float)
{
super(x, y);
hsvShader = new HSVShader();
flames = new FreeplayFlames(0, 0);
add(flames);
stars = new FlxAtlasSprite(0, 0, Paths.animateAtlas("freeplay/freeplayStars"));
stars.anim.play("diff stars");
add(stars);
stars.shader = hsvShader;
for (memb in flames.members)
memb.shader = hsvShader;
}
override function update(elapsed:Float):Void
{
super.update(elapsed);
// "loops" the current animation
// for clarity, the animation file looks like
// frame : stars
// 0-99: 1 star
// 100-199: 2 stars
// ......
// 1300-1499: 15 stars
// 1500 : 0 stars
if (curDifficulty < 15 && stars.anim.curFrame >= (curDifficulty + 1) * 100)
{
stars.anim.play("diff stars", true, false, curDifficulty * 100);
}
}
function set_difficulty(value:Int):Int
{
difficulty = value;
if (difficulty <= 0)
{
difficulty = 0;
curDifficulty = 15;
}
else if (difficulty <= 15)
{
difficulty = value;
curDifficulty = difficulty - 1;
}
else
{
difficulty = 15;
curDifficulty = difficulty - 1;
}
if (difficulty > 10) flames.flameCount = difficulty - 10;
else
flames.flameCount = 0;
return difficulty;
}
function set_curDifficulty(value:Int):Int
{
curDifficulty = value;
if (curDifficulty == 15)
{
stars.anim.play("diff stars", true, false, 1500);
stars.anim.pause();
}
else
{
stars.anim.curFrame = Std.int(curDifficulty * 100);
stars.anim.play("diff stars", true, false, curDifficulty * 100);
}
return curDifficulty;
}
}

View File

@ -0,0 +1,117 @@
package funkin.freeplayStuff;
import flixel.group.FlxSpriteGroup;
import flixel.FlxSprite;
import flixel.util.FlxTimer;
class FreeplayFlames extends FlxSpriteGroup
{
var flameX(default, set):Float = 917;
var flameY(default, set):Float = 103;
var flameSpreadX(default, set):Float = 29;
var flameSpreadY(default, set):Float = 6;
public var flameCount(default, set):Int = 0;
var flameTimer:Float = 0.25;
public function new(x:Float, y:Float)
{
super(x, y);
for (i in 0...5)
{
var flame:FlxSprite = new FlxSprite(flameX + (flameSpreadX * i), flameY + (flameSpreadY * i));
flame.frames = Paths.getSparrowAtlas("freeplay/freeplayFlame");
flame.animation.addByPrefix("flame", "fire loop", FlxG.random.int(23, 25), false);
flame.animation.play("flame");
flame.visible = false;
flameCount = 0;
// sets the loop... maybe better way to do this lol!
flame.animation.finishCallback = function(_) {
flame.animation.play("flame", true, false, 2);
};
add(flame);
}
}
var properPositions:Bool = false;
override public function update(elapsed:Float):Void
{
super.update(elapsed);
// doesn't work in create()/new() for some reason
// so putting it here bwah!
if (!properPositions)
{
setFlamePositions();
properPositions = true;
}
}
function set_flameCount(value:Int):Int
{
this.flameCount = value;
var visibleCount:Int = 0;
for (i in 0...5)
{
if (members[i] == null) continue;
var flame:FlxSprite = members[i];
if (i < flameCount)
{
if (!flame.visible)
{
new FlxTimer().start(flameTimer * visibleCount, function(_) {
flame.animation.play("flame", true);
flame.visible = true;
});
visibleCount++;
}
}
else
{
flame.visible = false;
}
}
return this.flameCount;
}
function setFlamePositions()
{
for (i in 0...5)
{
var flame:FlxSprite = members[i];
flame.x = flameX + (flameSpreadX * i);
flame.y = flameY + (flameSpreadY * i);
}
}
function set_flameX(value:Float):Float
{
this.flameX = value;
setFlamePositions();
return this.flameX;
}
function set_flameY(value:Float):Float
{
this.flameY = value;
setFlamePositions();
return this.flameY;
}
function set_flameSpreadX(value:Float):Float
{
this.flameSpreadX = value;
setFlamePositions();
return this.flameSpreadX;
}
function set_flameSpreadY(value:Float):Float
{
this.flameSpreadY = value;
setFlamePositions();
return this.flameSpreadY;
}
}

View File

@ -4,38 +4,68 @@ import flixel.FlxSprite;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.group.FlxGroup;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.tweens.FlxTween;
import flixel.tweens.FlxEase;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
class LetterSort extends FlxTypedSpriteGroup<FreeplayLetter>
class LetterSort extends FlxTypedSpriteGroup<FlxSprite>
{
public var letters:Array<FreeplayLetter> = [];
var curSelection:Int = 0;
// starts at 2, cuz that's the middle letter on start (accounting for fav and #, it should begin at ALL filter)
var curSelection:Int = 2;
public var changeSelectionCallback:String->Void;
var leftArrow:FlxSprite;
var rightArrow:FlxSprite;
var grpSeperators:Array<FlxSprite> = [];
public function new(x, y)
{
super(x, y);
var leftArrow:FreeplayLetter = new FreeplayLetter(-20, 0);
leftArrow.animation.play("arrow");
leftArrow = new FlxSprite(-20, 15).loadGraphic(Paths.image("freeplay/miniArrow"));
// leftArrow.animation.play("arrow");
leftArrow.flipX = true;
add(leftArrow);
for (i in 0...6)
for (i in 0...5)
{
var letter:FreeplayLetter = new FreeplayLetter(i * 80, 0, i);
letter.x += 50;
letter.y += 50;
letter.ogY = y;
// letter.visible = false;
add(letter);
letters.push(letter);
if (i == 3) letter.alpha = 0.6;
if (i != 2) letter.scale.x = letter.scale.y = 0.8;
var sep:FreeplayLetter = new FreeplayLetter((i * 80) + 50, 0);
sep.animation.play("seperator");
var darkness:Float = Math.abs(i - 2) / 6;
letter.color = letter.color.getDarkened(darkness);
// don't put the last seperator
if (i == 4) continue;
var sep:FlxSprite = new FlxSprite((i * 80) + 55, 20).loadGraphic(Paths.image("freeplay/seperator"));
// sep.animation.play("seperator");
sep.color = letter.color.getDarkened(darkness);
add(sep);
grpSeperators.push(sep);
}
// changeSelection(-3);
rightArrow = new FlxSprite(380, 15).loadGraphic(Paths.image("freeplay/miniArrow"));
// rightArrow.animation.play("arrow");
add(rightArrow);
changeSelection(0);
}
override function update(elapsed:Float)
@ -48,53 +78,168 @@ class LetterSort extends FlxTypedSpriteGroup<FreeplayLetter>
public function changeSelection(diff:Int = 0)
{
for (letter in letters)
letter.changeLetter(diff);
var ezTimer:Int->FlxSprite->Float->Void = function(frameNum:Int, spr:FlxSprite, offsetNum:Float) {
new FlxTimer().start(frameNum / 24, function(_) {
spr.offset.x = offsetNum;
});
};
if (changeSelectionCallback != null) changeSelectionCallback(letters[3].arr[letters[3].curLetter]); // bullshit and long lol!
var positions:Array<Float> = [-10, -22, 2, 0];
if (diff < 0)
{
for (sep in grpSeperators)
{
ezTimer(0, sep, positions[0]);
ezTimer(1, sep, positions[1]);
ezTimer(2, sep, positions[2]);
ezTimer(3, sep, positions[3]);
}
for (index => letter in letters)
{
letter.offset.x = positions[0];
new FlxTimer().start(1 / 24, function(_) {
letter.offset.x = positions[1];
if (index == 0) letter.visible = false;
});
new FlxTimer().start(2 / 24, function(_) {
letter.offset.x = positions[2];
if (index == 0.) letter.visible = true;
});
if (index == 2)
{
ezTimer(3, letter, 0);
// letter.offset.x = 0;
continue;
}
ezTimer(3, letter, positions[3]);
}
leftArrow.offset.x = 3;
new FlxTimer().start(2 / 24, function(_) {
leftArrow.offset.x = 0;
});
}
else if (diff > 0)
{
for (sep in grpSeperators)
{
ezTimer(0, sep, -positions[0]);
ezTimer(1, sep, -positions[1]);
ezTimer(2, sep, -positions[2]);
ezTimer(3, sep, -positions[3]);
}
// same timing and functions and shit as the left one... except to the right!!
for (index => letter in letters)
{
letter.offset.x = -positions[0];
new FlxTimer().start(1 / 24, function(_) {
letter.offset.x = -positions[1];
if (index == 0) letter.visible = false;
});
new FlxTimer().start(2 / 24, function(_) {
letter.offset.x = -positions[2];
if (index == 0) letter.visible = true;
});
if (index == 2)
{
ezTimer(3, letter, 0);
// letter.offset.x = 0;
continue;
}
ezTimer(3, letter, -positions[3]);
}
rightArrow.offset.x = -3;
new FlxTimer().start(2 / 24, function(_) {
rightArrow.offset.x = 0;
});
}
curSelection += diff;
if (curSelection < 0) curSelection = letters[0].arr.length - 1;
if (curSelection >= letters[0].arr.length) curSelection = 0;
for (letter in letters)
letter.changeLetter(diff, curSelection);
if (changeSelectionCallback != null) changeSelectionCallback(letters[2].arr[letters[2].curLetter]); // bullshit and long lol!
}
}
class FreeplayLetter extends FlxSprite
class FreeplayLetter extends FlxAtlasSprite
{
public var arr:Array<String> = [];
public var curLetter:Int = 0;
public var ogY:Float = 0;
public function new(x:Float, y:Float, ?letterInd:Int)
{
super(x, y);
super(x, y, Paths.animateAtlas("freeplay/sortedLetters"));
// frames = Paths.getSparrowAtlas("freeplay/letterStuff");
// this.anim.play("AB");
// trace(this.anim.symbolDictionary);
frames = Paths.getSparrowAtlas("freeplay/letterStuff");
var alphabet:String = "abcdefghijklmnopqrstuvwxyz";
arr = alphabet.split("");
arr.insert(0, "#");
var alphabet:String = "AB-CD-EH-I L-MN-OR-s-t-UZ";
arr = alphabet.split("-");
arr.insert(0, "ALL");
arr.insert(0, "fav");
arr.insert(0, "#");
for (str in arr)
{
animation.addByPrefix(str, str + " "); // string followed by a space! intentional!
}
// trace(arr);
animation.addByPrefix("arrow", "mini arrow");
animation.addByPrefix("seperator", "seperator");
// for (str in arr)
// {
// animation.addByPrefix(str, str + " "); // string followed by a space! intentional!
// }
// animation.addByPrefix("arrow", "mini arrow");
// animation.addByPrefix("seperator", "seperator");
if (letterInd != null)
{
animation.play(arr[letterInd]);
this.anim.play(arr[letterInd] + " move");
this.anim.pause();
curLetter = letterInd;
}
}
public function changeLetter(diff:Int = 0)
public function changeLetter(diff:Int = 0, ?curSelection:Int)
{
curLetter += diff;
if (curLetter < 0) curLetter = arr.length - 1;
if (curLetter >= arr.length) curLetter = 0;
animation.play(arr[curLetter]);
var animName:String = arr[curLetter] + " move";
switch (arr[curLetter])
{
case "I L":
animName = "IL move";
case "s":
animName = "S move";
case "t":
animName = "T move";
}
this.anim.play(animName);
if (curSelection != curLetter)
{
this.anim.pause();
}
// updateHitbox();
}
}

View File

@ -1,5 +1,8 @@
package funkin.freeplayStuff;
import funkin.shaderslmfao.HSVShader;
import funkin.shaderslmfao.GaussianBlurShader;
import flixel.group.FlxGroup;
import flixel.FlxSprite;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
@ -7,17 +10,29 @@ import flixel.group.FlxSpriteGroup;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import flixel.text.FlxText;
import flixel.util.FlxTimer;
import funkin.shaderslmfao.Grayscale;
class SongMenuItem extends FlxSpriteGroup
{
var capsule:FlxSprite;
public var capsule:FlxSprite;
public var selected(default, set):Bool = false;
var pixelIcon:FlxSprite;
public var selected(default, set):Bool;
public var songTitle:String = "Test";
public var songText:FlxText;
public var songText:CapsuleText;
public var favIcon:FlxSprite;
public var ranking:FlxSprite;
var ranks:Array<String> = ["fail", "average", "great", "excellent", "perfect"];
// lol...
var diffRanks:Array<String> = [
"00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "14", "15"
];
public var targetPos:FlxPoint = new FlxPoint();
public var doLerp:Bool = false;
@ -25,7 +40,12 @@ class SongMenuItem extends FlxSpriteGroup
public var doJumpOut:Bool = false;
public function new(x:Float, y:Float, song:String)
public var onConfirm:Void->Void;
public var diffGrayscale:Grayscale;
public var hsvShader(default, set):HSVShader;
public function new(x:Float, y:Float, song:String, ?character:String)
{
super(x, y);
@ -38,19 +58,144 @@ class SongMenuItem extends FlxSpriteGroup
// capsule.animation
add(capsule);
songText = new FlxText(120, 40, 0, songTitle, 40);
songText.font = "5by7";
songText.color = 0xFF43C1EA;
add(songText);
// doesn't get added, simply is here to help with visibility of things for the pop in!
grpHide = new FlxGroup();
favIcon = new FlxSprite(440, 40);
var rank:String = FlxG.random.getObject(ranks);
ranking = new FlxSprite(capsule.width * 0.84, 30);
ranking.loadGraphic(Paths.image("freeplay/ranks/" + rank));
ranking.scale.x = ranking.scale.y = realScaled;
ranking.alpha = 0.75;
ranking.origin.set(capsule.origin.x - ranking.x, capsule.origin.y - ranking.y);
add(ranking);
grpHide.add(ranking);
diffGrayscale = new Grayscale(1);
var diffRank = new FlxSprite(145, 90).loadGraphic(Paths.image("freeplay/diffRankings/diff" + FlxG.random.getObject(diffRanks)));
diffRank.shader = diffGrayscale;
diffRank.visible = false;
add(diffRank);
diffRank.origin.set(capsule.origin.x - diffRank.x, capsule.origin.y - diffRank.y);
grpHide.add(diffRank);
switch (rank)
{
case "perfect":
ranking.x -= 10;
}
songText = new CapsuleText(capsule.width * 0.26, 45, songTitle, Std.int(40 * realScaled));
add(songText);
grpHide.add(songText);
pixelIcon = new FlxSprite(155, 15);
pixelIcon.makeGraphic(32, 32, 0x00000000);
pixelIcon.antialiasing = false;
pixelIcon.active = false;
add(pixelIcon);
grpHide.add(pixelIcon);
if (character != null) setCharacter(character);
favIcon = new FlxSprite(400, 40);
favIcon.frames = Paths.getSparrowAtlas('freeplay/favHeart');
favIcon.animation.addByPrefix('fav', "favorite heart", 24, false);
favIcon.animation.play('fav');
favIcon.setGraphicSize(60, 60);
favIcon.setGraphicSize(50, 50);
favIcon.visible = false;
add(favIcon);
// grpHide.add(favIcon);
selected = selected; // just to kickstart the set_selected
setVisibleGrp(false);
}
function set_hsvShader(value:HSVShader):HSVShader
{
this.hsvShader = value;
capsule.shader = hsvShader;
songText.shader = hsvShader;
return value;
}
function textAppear()
{
songText.scale.x = 1.7;
songText.scale.y = 0.2;
new FlxTimer().start(1 / 24, function(_) {
songText.scale.x = 0.4;
songText.scale.y = 1.4;
});
new FlxTimer().start(2 / 24, function(_) {
songText.scale.x = songText.scale.y = 1;
});
}
function setVisibleGrp(value:Bool)
{
for (spr in grpHide.members)
{
spr.visible = value;
}
if (value) textAppear();
selectedAlpha();
}
public function init(x:Float, y:Float, song:String, ?character:String)
{
this.x = x;
this.y = y;
this.songTitle = song;
songText.text = this.songTitle;
if (character != null) setCharacter(character);
selected = selected;
}
/**
* Set the character displayed next to this song in the freeplay menu.
* @param char The character ID used by this song.
* If the character has no freeplay icon, a warning will be thrown and nothing will display.
*/
public function setCharacter(char:String)
{
var charPath:String = "freeplay/icons/";
trace(char);
switch (char)
{
case "monster-christmas":
charPath += "monsterpixel";
case "mom-car":
charPath += "mommypixel";
case "dad":
charPath += "daddypixel";
case "darnell-blazin":
charPath += "darnellpixel";
case "senpai-angry":
charPath += "senpaipixel";
default:
charPath += char + "pixel";
}
if (!openfl.utils.Assets.exists(Paths.image(charPath)))
{
trace('[WARN] Character ${char} has no freeplay icon.');
return;
}
pixelIcon.loadGraphic(Paths.image(charPath));
pixelIcon.scale.x = pixelIcon.scale.y = 2;
pixelIcon.origin.x = 100;
// pixelIcon.origin.x = capsule.origin.x;
// pixelIcon.offset.x -= pixelIcon.origin.x;
}
var frameInTicker:Float = 0;
@ -63,6 +208,63 @@ class SongMenuItem extends FlxSpriteGroup
var xPosLerpLol:Array<Float> = [0.9, 0.4, 0.16, 0.16, 0.22, 0.22, 0.245]; // NUMBERS ARE JANK CUZ THE SCALING OR WHATEVER
var xPosOutLerpLol:Array<Float> = [0.245, 0.75, 0.98, 0.98, 1.2]; // NUMBERS ARE JANK CUZ THE SCALING OR WHATEVER
public var realScaled:Float = 0.8;
public function initJumpIn(maxTimer:Float, ?force:Bool):Void
{
frameInTypeBeat = 0;
new FlxTimer().start((1 / 24) * maxTimer, function(doShit) {
doJumpIn = true;
});
new FlxTimer().start((0.09 * maxTimer) + 0.85, function(lerpTmr) {
doLerp = true;
});
if (force)
{
visible = true;
capsule.alpha = 1;
setVisibleGrp(true);
}
else
{
new FlxTimer().start((xFrames.length / 24) * 2.5, function(_) {
visible = true;
capsule.alpha = 1;
setVisibleGrp(true);
});
}
}
var grpHide:FlxGroup;
public function forcePosition()
{
visible = true;
capsule.alpha = 1;
selectedAlpha();
doLerp = true;
doJumpIn = false;
doJumpOut = false;
frameInTypeBeat = xFrames.length;
frameOutTypeBeat = 0;
capsule.scale.x = xFrames[frameInTypeBeat - 1];
capsule.scale.y = 1 / xFrames[frameInTypeBeat - 1];
// x = FlxG.width * xPosLerpLol[Std.int(Math.min(frameInTypeBeat - 1, xPosLerpLol.length - 1))];
x = targetPos.x;
y = targetPos.y;
capsule.scale.x *= realScaled;
capsule.scale.y *= realScaled;
setVisibleGrp(true);
}
override function update(elapsed:Float)
{
if (doJumpIn)
@ -73,10 +275,13 @@ class SongMenuItem extends FlxSpriteGroup
{
frameInTicker = 0;
scale.x = xFrames[frameInTypeBeat];
scale.y = 1 / xFrames[frameInTypeBeat];
capsule.scale.x = xFrames[frameInTypeBeat];
capsule.scale.y = 1 / xFrames[frameInTypeBeat];
x = FlxG.width * xPosLerpLol[Std.int(Math.min(frameInTypeBeat, xPosLerpLol.length - 1))];
capsule.scale.x *= realScaled;
capsule.scale.y *= realScaled;
frameInTypeBeat += 1;
}
}
@ -89,10 +294,13 @@ class SongMenuItem extends FlxSpriteGroup
{
frameOutTicker = 0;
scale.x = xFrames[frameOutTypeBeat];
scale.y = 1 / xFrames[frameOutTypeBeat];
capsule.scale.x = xFrames[frameOutTypeBeat];
capsule.scale.y = 1 / xFrames[frameOutTypeBeat];
x = FlxG.width * xPosOutLerpLol[Std.int(Math.min(frameOutTypeBeat, xPosOutLerpLol.length - 1))];
capsule.scale.x *= realScaled;
capsule.scale.y *= realScaled;
frameOutTypeBeat += 1;
}
}
@ -106,14 +314,29 @@ class SongMenuItem extends FlxSpriteGroup
super.update(elapsed);
}
public function intendedY(index:Int):Float
{
return (index * ((height * realScaled) + 10)) + 120;
}
/**
* Merely a helper function to call set_selected, to make sure that the alpha is correct on the rankings/selections
*/
public function selectedAlpha():Void
{
selected = selected;
}
function set_selected(value:Bool):Bool
{
// trace(value);
// cute one liners, lol!
diffGrayscale.setAmount(value ? 0 : 0.8);
songText.alpha = value ? 1 : 0.6;
songText.blurredText.visible = value ? true : false;
capsule.offset.x = value ? 0 : -5;
capsule.animation.play(value ? "selected" : "unselected");
ranking.alpha = value ? 1 : 0.7;
ranking.color = value ? 0xFFFFFFFF : 0xFFAAAAAA;
return value;
}
}

View File

@ -47,7 +47,7 @@ import funkin.play.song.Song;
import funkin.data.song.SongRegistry;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongPlayableChar;
import funkin.data.song.SongData.SongCharacterData;
import funkin.play.stage.Stage;
import funkin.play.stage.StageData.StageDataParser;
import funkin.ui.PopUpStuff;
@ -512,41 +512,7 @@ class PlayState extends MusicBeatSubState
NoteSplash.buildSplashFrames();
// Returns null if the song failed to load or doesn't have the selected difficulty.
if (currentSong == null || currentChart == null)
{
// We have encountered a critical error. Prevent Flixel from trying to run any gameplay logic.
criticalFailure = true;
// Choose an error message.
var message:String = 'There was a critical error. Click OK to return to the main menu.';
if (currentSong == null)
{
message = 'The was a critical error loading this song\'s chart. Click OK to return to the main menu.';
}
else if (currentDifficulty == null)
{
message = 'The was a critical error selecting a difficulty for this song. Click OK to return to the main menu.';
}
else if (currentSong.getDifficulty(currentDifficulty) == null)
{
message = 'The was a critical error retrieving data for this song on "$currentDifficulty" difficulty. Click OK to return to the main menu.';
}
// Display a popup. This blocks the application until the user clicks OK.
lime.app.Application.current.window.alert(message, 'Error loading PlayState');
// Force the user back to the main menu.
if (isSubState)
{
this.close();
}
else
{
FlxG.switchState(new MainMenuState());
}
return;
}
if (!assertChartExists()) return;
if (false)
{
@ -575,8 +541,8 @@ class PlayState extends MusicBeatSubState
// Prepare the current song's instrumental and vocals to be played.
if (!overrideMusic && currentChart != null)
{
currentChart.cacheInst(currentPlayerId);
currentChart.cacheVocals(currentPlayerId);
currentChart.cacheInst();
currentChart.cacheVocals();
}
// Prepare the Conductor.
@ -661,6 +627,47 @@ class PlayState extends MusicBeatSubState
initialized = true;
}
function assertChartExists():Bool
{
// Returns null if the song failed to load or doesn't have the selected difficulty.
if (currentSong == null || currentChart == null)
{
// We have encountered a critical error. Prevent Flixel from trying to run any gameplay logic.
criticalFailure = true;
// Choose an error message.
var message:String = 'There was a critical error. Click OK to return to the main menu.';
if (currentSong == null)
{
message = 'The was a critical error loading this song\'s chart. Click OK to return to the main menu.';
}
else if (currentDifficulty == null)
{
message = 'The was a critical error selecting a difficulty for this song. Click OK to return to the main menu.';
}
else if (currentSong.getDifficulty(currentDifficulty) == null)
{
message = 'The was a critical error retrieving data for this song on "$currentDifficulty" difficulty. Click OK to return to the main menu.';
}
// Display a popup. This blocks the application until the user clicks OK.
lime.app.Application.current.window.alert(message, 'Error loading PlayState');
// Force the user back to the main menu.
if (isSubState)
{
this.close();
}
else
{
FlxG.switchState(new MainMenuState());
}
return false;
}
return true;
}
public override function update(elapsed:Float):Void
{
if (criticalFailure) return;
@ -673,6 +680,8 @@ class PlayState extends MusicBeatSubState
// Handle restarting the song when needed (player death or pressing Retry)
if (needsReset)
{
if (!assertChartExists()) return;
dispatchEvent(new ScriptEvent(ScriptEvent.SONG_RETRY));
resetCamera();
@ -687,8 +696,10 @@ class PlayState extends MusicBeatSubState
// Reset music properly.
FlxG.sound.music.pause();
vocals.pause();
FlxG.sound.music.time = (startTimestamp);
vocals = currentChart.buildVocals();
vocals.pause();
vocals.time = 0;
FlxG.sound.music.volume = 1;
@ -734,7 +745,7 @@ class PlayState extends MusicBeatSubState
// DO NOT FORGET TO REMOVE THE HARDCODE! WHEN I MAKE BETTER OFFSET SYSTEM!
// :nerd: um ackshually it's not 13 it's 11.97278911564
if (Paths.SOUND_EXT == 'mp3') Conductor.offset = Constants.MP3_DELAY_MS;
if (Constants.EXT_SOUND == 'mp3') Conductor.offset = Constants.MP3_DELAY_MS;
Conductor.update();
@ -1344,34 +1355,20 @@ class PlayState extends MusicBeatSubState
trace('Song difficulty could not be loaded.');
}
// Switch the character we are playing as by manipulating currentPlayerId.
// TODO: How to choose which one to use for story mode?
var playableChars:Array<String> = currentChart.getPlayableChars();
if (playableChars.length == 0)
{
trace('WARNING: No playable characters found for this song.');
}
else if (playableChars.indexOf(currentPlayerId) == -1)
{
currentPlayerId = playableChars[0];
}
//
var currentCharData:SongPlayableChar = currentChart.getPlayableChar(currentPlayerId);
var currentCharacterData:SongCharacterData = currentChart.characters; // Switch the character we are playing as by manipulating currentPlayerId.
//
// GIRLFRIEND
//
var girlfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharData.girlfriend);
var girlfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharacterData.girlfriend);
if (girlfriend != null)
{
girlfriend.characterType = CharacterType.GF;
}
else if (currentCharData.girlfriend != '')
else if (currentCharacterData.girlfriend != '')
{
trace('WARNING: Could not load girlfriend character with ID ${currentCharData.girlfriend}, skipping...');
trace('WARNING: Could not load girlfriend character with ID ${currentCharacterData.girlfriend}, skipping...');
}
else
{
@ -1381,7 +1378,7 @@ class PlayState extends MusicBeatSubState
//
// DAD
//
var dad:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharData.opponent);
var dad:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharacterData.opponent);
if (dad != null)
{
@ -1400,7 +1397,7 @@ class PlayState extends MusicBeatSubState
//
// BOYFRIEND
//
var boyfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentPlayerId);
var boyfriend:BaseCharacter = CharacterDataParser.fetchCharacter(currentCharacterData.player);
if (boyfriend != null)
{
@ -1549,7 +1546,7 @@ class PlayState extends MusicBeatSubState
if (!overrideMusic)
{
vocals = currentChart.buildVocals(currentPlayerId);
vocals = currentChart.buildVocals();
if (vocals.members.length == 0)
{
@ -1893,6 +1890,7 @@ class PlayState extends MusicBeatSubState
{
// Grant the player health.
health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed;
songScore += Std.int(Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed);
}
// TODO: Potential penalty for dropping a hold note?
@ -2021,19 +2019,16 @@ class PlayState extends MusicBeatSubState
// Calling event.cancelEvent() skips all the other logic! Neat!
if (event.eventCanceled) return;
if (!note.isHoldNote)
{
Highscore.tallies.combo++;
Highscore.tallies.totalNotesHit++;
Highscore.tallies.combo++;
Highscore.tallies.totalNotesHit++;
if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo;
if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo;
popUpScore(note, input);
}
popUpScore(note, input);
playerStrumline.hitNote(note);
if (note.holdNoteSprite != null)
if (note.isHoldNote && note.holdNoteSprite != null)
{
playerStrumline.playNoteHoldCover(note.holdNoteSprite);
}

View File

@ -172,9 +172,13 @@ enum abstract BackdropType(String) from String to String
class MusicData
{
public var asset:String;
public var looped:Bool;
public var fadeTime:Float;
@:optional
@:default(false)
public var looped:Bool;
public function new(asset:String, looped:Bool, fadeTime:Float = 0.0)
{
this.asset = asset;

View File

@ -6,6 +6,7 @@ import funkin.play.cutscene.dialogue.ScriptedConversation;
/**
* Contains utilities for loading and parsing conversation data.
* TODO: Refactor to use the json2object + BaseRegistry system that actually validates things for you.
*/
class ConversationDataParser
{

View File

@ -11,7 +11,7 @@ import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongRegistry;
import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongData.SongPlayableChar;
import funkin.data.song.SongData.SongCharacterData;
import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.SongData.SongTimeFormat;
import funkin.data.IRegistryEntry;
@ -92,6 +92,15 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
_metadata = _data == null ? [] : [_data];
variations.clear();
variations.push(Constants.DEFAULT_VARIATION);
if (_data != null && _data.playData != null)
{
for (vari in _data.playData.songVariations)
variations.push(vari);
}
for (meta in fetchVariationMetadata(id))
_metadata.push(meta);
@ -101,15 +110,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
return;
}
variations.clear();
variations.push('default');
if (_data != null && _data.playData != null)
{
for (vari in _data.playData.songVariations)
variations.push(vari);
populateFromMetadata();
}
populateDifficulties();
}
@:allow(funkin.play.song.Song)
@ -128,7 +129,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
result.difficultyIds.clear();
result.populateFromMetadata();
result.populateDifficulties();
for (variation => chartData in charts)
result.applyChartData(chartData, variation);
@ -144,10 +145,10 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
}
/**
* Populate the song data from the provided metadata,
* including data from individual difficulties. Does not load chart data.
* Populate the difficulty data from the provided metadata.
* Does not load chart data (that is triggered later when we want to play the song).
*/
function populateFromMetadata():Void
function populateDifficulties():Void
{
if (_metadata == null || _metadata.length == 0) return;
@ -176,18 +177,11 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
difficulty.generatedBy = metadata.generatedBy;
difficulty.stage = metadata.playData.stage;
// difficulty.noteSkin = metadata.playData.noteSkin;
difficulty.noteStyle = metadata.playData.noteSkin;
difficulties.set(diffId, difficulty);
difficulty.chars = new Map<String, SongPlayableChar>();
if (metadata.playData.playableChars == null) continue;
for (charId in metadata.playData.playableChars.keys())
{
var char:Null<SongPlayableChar> = metadata.playData.playableChars.get(charId);
if (char == null) continue;
difficulty.chars.set(charId, char);
}
difficulty.characters = metadata.playData.characters;
}
}
}
@ -321,7 +315,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry<SongMeta
trace('Fetching song metadata for $id');
var version:Null<thx.semver.Version> = SongRegistry.instance.fetchEntryMetadataVersion(id);
if (version == null) return null;
return SongRegistry.instance.parseEntryMetadataWithMigration(id, '', version);
return SongRegistry.instance.parseEntryMetadataWithMigration(id, Constants.DEFAULT_VARIATION, version);
}
function fetchVariationMetadata(id:String):Array<SongMetadata>
@ -365,19 +359,20 @@ class SongDifficulty
*/
public var events:Array<SongEventData>;
public var songName:String = SongValidator.DEFAULT_SONGNAME;
public var songArtist:String = SongValidator.DEFAULT_ARTIST;
public var timeFormat:SongTimeFormat = SongValidator.DEFAULT_TIMEFORMAT;
public var divisions:Null<Int> = SongValidator.DEFAULT_DIVISIONS;
public var looped:Bool = SongValidator.DEFAULT_LOOPED;
public var songName:String = Constants.DEFAULT_SONGNAME;
public var songArtist:String = Constants.DEFAULT_ARTIST;
public var timeFormat:SongTimeFormat = Constants.DEFAULT_TIMEFORMAT;
public var divisions:Null<Int> = null;
public var looped:Bool = false;
public var generatedBy:String = SongRegistry.DEFAULT_GENERATEDBY;
public var timeChanges:Array<SongTimeChange> = [];
public var stage:String = SongValidator.DEFAULT_STAGE;
public var chars:Map<String, SongPlayableChar> = null;
public var stage:String = Constants.DEFAULT_STAGE;
public var noteStyle:String = Constants.DEFAULT_NOTE_STYLE;
public var characters:SongCharacterData = null;
public var scrollSpeed:Float = SongValidator.DEFAULT_SCROLLSPEED;
public var scrollSpeed:Float = Constants.DEFAULT_SCROLLSPEED;
public function new(song:Song, diffId:String, variation:String)
{
@ -401,28 +396,24 @@ class SongDifficulty
return timeChanges[0].bpm;
}
public function getPlayableChar(id:String):Null<SongPlayableChar>
{
if (id == null || id == '') return null;
return chars.get(id);
}
public function getPlayableChars():Array<String>
{
return chars.keys().array();
}
public function getEvents():Array<SongEventData>
{
return cast events;
}
public inline function cacheInst(?currentPlayerId:String = null):Void
public function cacheInst(instrumental = ''):Void
{
var currentPlayer:Null<SongPlayableChar> = getPlayableChar(currentPlayerId);
if (currentPlayer != null)
if (characters != null)
{
FlxG.sound.cache(Paths.inst(this.song.id, currentPlayer.inst));
if (instrumental != '' && characters.altInstrumentals.contains(instrumental))
{
FlxG.sound.cache(Paths.inst(this.song.id, instrumental));
}
else
{
// Fallback to default instrumental.
FlxG.sound.cache(Paths.inst(this.song.id, characters.instrumental));
}
}
else
{
@ -440,9 +431,9 @@ class SongDifficulty
* Cache the vocals for a given character.
* @param id The character we are about to play.
*/
public inline function cacheVocals(?id:String = 'bf'):Void
public inline function cacheVocals():Void
{
for (voice in buildVoiceList(id))
for (voice in buildVoiceList())
{
FlxG.sound.cache(voice);
}
@ -454,22 +445,15 @@ class SongDifficulty
*
* @param id The character we are about to play.
*/
public function buildVoiceList(?id:String = 'bf'):Array<String>
public function buildVoiceList():Array<String>
{
var playableCharData:SongPlayableChar = getPlayableChar(id);
if (playableCharData == null)
{
trace('Could not find playable char $id for song ${this.song.id}');
return [];
}
var suffix:String = (variation != null && variation != '' && variation != 'default') ? '-$variation' : '';
// Automatically resolve voices by removing suffixes.
// For example, if `Voices-bf-car.ogg` does not exist, check for `Voices-bf.ogg`.
var playerId:String = id;
var voicePlayer:String = Paths.voices(this.song.id, '-$id$suffix');
var playerId:String = characters.player;
var voicePlayer:String = Paths.voices(this.song.id, '-$playerId$suffix');
while (voicePlayer != null && !Assets.exists(voicePlayer))
{
// Remove the last suffix.
@ -479,7 +463,7 @@ class SongDifficulty
voicePlayer = playerId == '' ? null : Paths.voices(this.song.id, '-${playerId}$suffix');
}
var opponentId:String = playableCharData.opponent;
var opponentId:String = characters.opponent;
var voiceOpponent:String = Paths.voices(this.song.id, '-${opponentId}$suffix');
while (voiceOpponent != null && !Assets.exists(voiceOpponent))
{
@ -505,11 +489,11 @@ class SongDifficulty
* @param charId The player ID.
* @return The generated vocal group.
*/
public function buildVocals(charId:String = 'bf'):VoicesGroup
public function buildVocals():VoicesGroup
{
var result:VoicesGroup = new VoicesGroup();
var voiceList:Array<String> = buildVoiceList(charId);
var voiceList:Array<String> = buildVoiceList();
if (voiceList.length == 0)
{

View File

@ -1,256 +0,0 @@
package funkin.play.song;
import funkin.play.song.formats.FNFLegacy;
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongData.SongPlayableChar;
import funkin.util.VersionUtil;
class SongMigrator
{
/**
* The current latest version string for the song data format.
* Handle breaking changes by incrementing this value
* and adding migration to the SongMigrator class.
*/
public static final CHART_VERSION:String = '2.0.0';
/**
* Version rule for which chart versions are compatible with the current version.
*/
public static final CHART_VERSION_RULE:String = '2.0.x';
/**
* Migrate song data from an older chart version to the current version.
* @param jsonData The song metadata to migrate.
* @param songId The ID of the song (only used for error reporting).
* @return The migrated song metadata, or null if the migration failed.
*/
public static function migrateSongMetadata(jsonData:Dynamic, songId:String):SongMetadata
{
if (jsonData.version != null)
{
if (VersionUtil.validateVersionStr(jsonData.version, CHART_VERSION_RULE))
{
trace('Song (${songId}) metadata version (${jsonData.version}) is valid and up-to-date.');
var songMetadata:SongMetadata = cast jsonData;
return songMetadata;
}
else
{
trace('Song (${songId}) metadata version (${jsonData.version}) is outdated.');
switch (jsonData.version)
{
case '1.0.0':
return migrateSongMetadataFromLegacy(jsonData);
default:
trace('Song (${songId}) has unknown metadata version (${jsonData.version}), assuming FNF Legacy.');
return migrateSongMetadataFromLegacy(jsonData);
}
}
}
else
{
trace('Song metadata version is missing.');
}
return null;
}
/**
* Migrate song chart data from an older chart version to the current version.
* @param jsonData The song chart data to migrate.
* @param songId The ID of the song (only used for error reporting).
* @return The migrated song chart data, or null if the migration failed.
*/
public static function migrateSongChartData(jsonData:Dynamic, songId:String):SongChartData
{
if (jsonData.version)
{
if (VersionUtil.validateVersionStr(jsonData.version, CHART_VERSION_RULE))
{
trace('Song (${songId}) chart version (${jsonData.version}) is valid and up-to-date.');
var songChartData:SongChartData = cast jsonData;
return songChartData;
}
else
{
trace('Song (${songId}) chart version (${jsonData.version}) is outdated.');
switch (jsonData.version)
{
// TODO: Add migration functions as cases here.
default:
// Unknown version.
trace('Song (${songId}) unknown chart version: ${jsonData.version}');
}
}
}
else
{
trace('Song chart version is missing.');
}
return null;
}
/**
* Migrate song metadata from FNF Legacy chart version to the current version.
* @param jsonData The song metadata to migrate.
* @param songId The ID of the song (only used for error reporting).
* @return The migrated song metadata, or null if the migration failed.
*/
public static function migrateSongMetadataFromLegacy(jsonData:Dynamic, difficulty:String = 'normal'):SongMetadata
{
trace('Migrating song metadata from FNF Legacy.');
var songData:FNFLegacy = cast jsonData;
var songMetadata:SongMetadata = new SongMetadata('Import', 'Kawai Sprite', 'default');
var hadError:Bool = false;
// Set generatedBy string for debugging.
songMetadata.generatedBy = 'Chart Editor Import (FNF Legacy)';
try
{
// Set the song's BPM.
songMetadata.timeChanges[0].bpm = songData.song.bpm;
}
catch (e)
{
trace("Couldn't parse BPM!");
hadError = true;
}
try
{
// Set the song's stage.
songMetadata.playData.stage = songData.song.stageDefault;
}
catch (e)
{
trace("Couldn't parse stage!");
hadError = true;
}
try
{
// Set's the song's name.
songMetadata.songName = songData.song.song;
}
catch (e)
{
trace("Couldn't parse song name!");
hadError = true;
}
songMetadata.playData.difficulties = [];
if (songData.song != null && songData.song.notes != null)
{
if (Std.isOfType(songData.song.notes, Array))
{
// One difficulty of notes.
songMetadata.playData.difficulties.push(difficulty);
}
else
{
// Multiple difficulties of notes.
var songNoteDataDynamic:haxe.DynamicAccess<Dynamic> = cast songData.song.notes;
for (difficultyKey in songNoteDataDynamic.keys())
{
songMetadata.playData.difficulties.push(difficultyKey);
}
}
}
else
{
trace("Couldn't parse difficulties!");
hadError = true;
}
songMetadata.playData.songVariations = [];
// Set the song's song variations.
songMetadata.playData.playableChars = [];
try
{
songMetadata.playData.playableChars.set(songData.song.player1, new SongPlayableChar('', songData.song.player2));
}
catch (e)
{
trace("Couldn't parse characters!");
hadError = true;
}
return songMetadata;
}
/**
* Migrate song chart data from FNF Legacy chart version to the current version.
* @param jsonData The song data to migrate.
* @param songId The ID of the song (only used for error reporting).
* @param difficulty The difficulty to migrate.
* @return The migrated song chart data, or null if the migration failed.
*/
public static function migrateSongChartDataFromLegacy(jsonData:Dynamic, difficulty:String = 'normal'):SongChartData
{
trace('Migrating song chart data from FNF Legacy.');
var songData:FNFLegacy = cast jsonData;
var songChartData:SongChartData = new SongChartData(["normal" => 1.0], [], ["normal" => []]);
var songEventsEmpty:Bool = songChartData.getEvents() == null || songChartData.getEvents().length == 0;
if (songEventsEmpty) songChartData.setEvents(migrateSongEventDataFromLegacy(songData.song.notes));
songChartData.setNotes(migrateSongNoteDataFromLegacy(songData.song.notes), difficulty);
songChartData.setScrollSpeed(songData.song.speed, difficulty);
return songChartData;
}
static function migrateSongNoteDataFromLegacy(sections:Array<LegacyNoteSection>):Array<SongNoteData>
{
var songNotes:Array<SongNoteData> = [];
for (section in sections)
{
// Skip empty sections.
if (section.sectionNotes.length == 0) continue;
for (note in section.sectionNotes)
{
songNotes.push(new SongNoteData(note.time, note.getData(section.mustHitSection), note.length, note.kind));
}
}
return songNotes;
}
static function migrateSongEventDataFromLegacy(sections:Array<LegacyNoteSection>):Array<SongEventData>
{
var songEvents:Array<SongEventData> = [];
var lastSectionWasMustHit:Null<Bool> = null;
for (section in sections)
{
// Skip empty sections.
if (section.sectionNotes.length == 0) continue;
if (section.mustHitSection != lastSectionWasMustHit)
{
lastSectionWasMustHit = section.mustHitSection;
var firstNote:LegacyNote = section.sectionNotes[0];
songEvents.push(new SongEventData(firstNote.time, 'FocusCamera', {char: section.mustHitSection ? 0 : 1}));
}
}
return songEvents;
}
}

View File

@ -3,14 +3,14 @@ package funkin.play.song;
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongMetadata;
import funkin.util.SerializerUtil;
import funkin.util.FileUtil;
import lime.utils.Bytes;
import openfl.events.Event;
import openfl.events.IOErrorEvent;
import openfl.net.FileReference;
/**
* Utilities for exporting a chart to a JSON file.
* Primarily used for the chart editor.
* TODO: Refactor and remove this.
*/
class SongSerializer
{
@ -20,7 +20,7 @@ class SongSerializer
*/
public static function importSongChartDataSync(path:String):SongChartData
{
var fileData = readFile(path);
var fileData = FileUtil.readStringFromPath(path);
if (fileData == null) return null;
@ -35,7 +35,7 @@ class SongSerializer
*/
public static function importSongMetadataSync(path:String):SongMetadata
{
var fileData = readFile(path);
var fileData = FileUtil.readStringFromPath(path);
if (fileData == null) return null;
@ -50,7 +50,7 @@ class SongSerializer
*/
public static function importSongChartDataAsync(callback:SongChartData->Void):Void
{
browseFileReference(function(fileReference:FileReference) {
FileUtil.browseFileReference(function(fileReference:FileReference) {
var data = fileReference.data.toString();
if (data == null) return;
@ -67,7 +67,7 @@ class SongSerializer
*/
public static function importSongMetadataAsync(callback:SongMetadata->Void):Void
{
browseFileReference(function(fileReference:FileReference) {
FileUtil.browseFileReference(function(fileReference:FileReference) {
var data = fileReference.data.toString();
if (data == null) return;
@ -77,126 +77,4 @@ class SongSerializer
if (songMetadata != null) callback(songMetadata);
});
}
/**
* Save a SongChartData object as a JSON file to an automatically generated path.
* Works great on HTML5 and desktop.
*/
public static function exportSongChartData(data:SongChartData, songId:String)
{
var path = '${songId}-chart.json';
exportSongChartDataAs(path, data);
}
/**
* Save a SongMetadata object as a JSON file to an automatically generated path.
* Works great on HTML5 and desktop.
*/
public static function exportSongMetadata(data:SongMetadata, songId:String)
{
var path = '${songId}-metadata.json';
exportSongMetadataAs(path, data);
}
/**
* Save a SongChartData object as a JSON file to a specified path.
* Works great on HTML5 and desktop.
*
* @param path The file path to save to.
*/
public static function exportSongChartDataAs(path:String, data:SongChartData)
{
var dataString = SerializerUtil.toJSON(data);
writeFileReference(path, dataString);
}
/**
* Save a SongMetadata object as a JSON file to a specified path.
* Works great on HTML5 and desktop.
*
* @param path The file path to save to.
*/
public static function exportSongMetadataAs(path:String, data:SongMetadata)
{
var dataString = SerializerUtil.toJSON(data);
writeFileReference(path, dataString);
}
/**
* Read the string contents of a file.
* Only works on desktop platforms.
* @param path The file path to read from.
*/
static function readFile(path:String):String
{
#if sys
var fileBytes:Bytes = sys.io.File.getBytes(path);
if (fileBytes == null) return null;
return fileBytes.toString();
#end
trace('ERROR: readFile not implemented for this platform');
return null;
}
/**
* Write string contents to a file.
* Only works on desktop platforms.
* @param path The file path to read from.
*/
static function writeFile(path:String, data:String):Void
{
#if sys
sys.io.File.saveContent(path, data);
return;
#end
trace('ERROR: writeFile not implemented for this platform');
return;
}
/**
* Browse for a file to read and execute a callback once we have a file reference.
* Works great on HTML5 or desktop.
*
* @param callback The function to call when the file is loaded.
*/
static function browseFileReference(callback:FileReference->Void)
{
var file = new FileReference();
file.addEventListener(Event.SELECT, function(e) {
var selectedFileRef:FileReference = e.target;
trace('Selected file: ' + selectedFileRef.name);
selectedFileRef.addEventListener(Event.COMPLETE, function(e) {
var loadedFileRef:FileReference = e.target;
trace('Loaded file: ' + loadedFileRef.name);
callback(loadedFileRef);
});
selectedFileRef.load();
});
file.browse();
}
/**
* Prompts the user to save a file to their computer.
*/
static function writeFileReference(path:String, data:String)
{
var file = new FileReference();
file.addEventListener(Event.COMPLETE, function(e:Event) {
trace('Successfully wrote file.');
});
file.addEventListener(Event.CANCEL, function(e:Event) {
trace('Cancelled writing file.');
});
file.addEventListener(IOErrorEvent.IO_ERROR, function(e:IOErrorEvent) {
trace('IO error writing file.');
});
file.save(data, path);
}
}

View File

@ -1,149 +0,0 @@
package funkin.play.song;
import funkin.data.song.SongRegistry;
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongData.SongPlayData;
import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.SongData.SongTimeFormat;
/**
* For SongMetadata and SongChartData objects,
* ensures mandatory fields are present and populates optional fields with default values.
*/
class SongValidator
{
public static final DEFAULT_SONGNAME:String = "Unknown";
public static final DEFAULT_ARTIST:String = "Unknown";
public static final DEFAULT_TIMEFORMAT:SongTimeFormat = SongTimeFormat.MILLISECONDS;
public static final DEFAULT_DIVISIONS:Null<Int> = null;
public static final DEFAULT_LOOPED:Bool = false;
public static final DEFAULT_STAGE:String = "mainStage";
public static final DEFAULT_SCROLLSPEED:Float = 1.0;
public static var DEFAULT_GENERATEDBY(get, never):String;
static function get_DEFAULT_GENERATEDBY():String
{
return '${Constants.TITLE} - ${Constants.VERSION}';
}
/**
* Validates the fields of a SongMetadata object (excluding the version field).
*
* @param input The SongMetadata object to validate.
* @param songId The ID of the song being validated. Only used for error messages.
* @return The validated SongMetadata object.
*/
public static function validateSongMetadata(input:SongMetadata, songId:String = 'unknown'):SongMetadata
{
if (input == null)
{
trace('[SONGDATA] Could not parse metadata for song ${songId}');
return null;
}
if (input.songName == null)
{
trace('[SONGDATA] Song ${songId} is missing a songName field. ');
input.songName = DEFAULT_SONGNAME;
}
if (input.artist == null)
{
trace('[SONGDATA] Song ${songId} is missing an artist field. ');
input.artist = DEFAULT_ARTIST;
}
if (input.timeFormat == null)
{
trace('[SONGDATA] Song ${songId} is missing a timeFormat field. ');
input.timeFormat = DEFAULT_TIMEFORMAT;
}
if (input.generatedBy == null)
{
input.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
}
input.timeChanges = validateTimeChanges(input.timeChanges, songId);
if (input.timeChanges == null)
{
trace('[SONGDATA] Song ${songId} is missing a timeChanges field. ');
return null;
}
input.playData = validatePlayData(input.playData, songId);
if (input.variation == null) input.variation = '';
return input;
}
/**
* Validates the fields of a SongPlayData object.
*
* @param input The SongPlayData object to validate.
* @param songId The ID of the song being validated. Only used for error messages.
* @return The validated SongPlayData object.
*/
public static function validatePlayData(input:SongPlayData, songId:String = 'unknown'):SongPlayData
{
if (input == null)
{
trace('[SONGDATA] Could not parse metadata.playData for song ${songId}');
return null;
}
return input;
}
/**
* Validates the fields of a TimeChange object.
*
* @param input The TimeChange object to validate.
* @param songId The ID of the song being validated. Only used for error messages.
* @return The validated TimeChange object.
*/
public static function validateTimeChange(input:SongTimeChange, songId:String = 'unknown'):SongTimeChange
{
if (input == null)
{
trace('[SONGDATA] Could not parse metadata.timeChange for song ${songId}');
return null;
}
return input;
}
/**
* Validates multiple TimeChange objects in an array.
*/
public static function validateTimeChanges(input:Array<SongTimeChange>, songId:String = 'unknown'):Array<SongTimeChange>
{
if (input == null)
{
trace('[SONGDATA] Could not parse metadata.timeChange for song ${songId}');
return null;
}
input = input.map((timeChange) -> validateTimeChange(timeChange, songId));
return input;
}
/**
* Validates the fields of a SongChartData object (excluding the version field).
*
* @param input The SongChartData object to validate.
* @param songId The ID of the song being validated. Only used for error messages.
* @return The validated SongChartData object.
*/
public static function validateSongChartData(input:SongChartData, songId:String = 'unknown'):SongChartData
{
if (input == null)
{
trace('[SONGDATA] Could not parse chart data for song ${songId}');
return null;
}
return input;
}
}

View File

@ -1,131 +0,0 @@
package funkin.play.song.formats;
typedef FNFLegacy =
{
var song:LegacySongData;
}
typedef LegacySongData =
{
var player1:String; // Boyfriend
var player2:String; // Opponent
var speed:Float;
var stageDefault:String;
var bpm:Float;
var notes:Array<LegacyNoteSection>;
var song:String; // Song name
};
typedef LegacyScrollSpeeds =
{
var easy:Float;
var normal:Float;
var hard:Float;
};
typedef LegacyNoteData =
{
/**
* The easy difficulty.
*/
var ?easy:Array<LegacyNoteSection>;
/**
* The normal difficulty.
*/
var ?normal:Array<LegacyNoteSection>;
/**
* The hard difficulty.
*/
var ?hard:Array<LegacyNoteSection>;
};
typedef LegacyNoteSection =
{
/**
* Whether the section is a must-hit section.
* If true, 0-3 are boyfriends notes, 4-7 are opponents notes.
* If false, 0-3 are opponents notes, 4-7 are boyfriends notes.
*/
var mustHitSection:Bool;
/**
* Array of note data:
* - Direction
* - Time (ms)
* - Sustain Duration (ms)
* - Note kind (true = "alt", or string)
*/
var sectionNotes:Array<LegacyNote>;
var typeOfSection:Int;
var lengthInSteps:Int;
}
/**
* Notes in the old format are stored as an Array<Dynamic>
*/
abstract LegacyNote(Array<Dynamic>)
{
public var time(get, set):Float;
function get_time():Float
{
return this[0];
}
function set_time(value:Float):Float
{
return this[0] = value;
}
public var data(get, set):Int;
function get_data():Int
{
return this[1];
}
function set_data(value:Int):Int
{
return this[1] = value;
}
public function getData(mustHitSection:Bool):Int
{
if (mustHitSection) return this[1];
return (this[1] + 4) % 8;
}
public var length(get, set):Float;
function get_length():Float
{
if (this.length < 3) return 0.0;
return this[2];
}
function set_length(value:Float):Float
{
return this[2] = value;
}
public var kind(get, set):String;
function get_kind():String
{
if (this.length < 4) return 'normal';
if (Std.isOfType(this[3], Bool)) return this[3] ? 'alt' : 'normal';
return this[3];
}
function set_kind(value:String):String
{
return this[3] = value;
}
}

View File

@ -649,16 +649,20 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass
}
boppers = [];
for (sprite in this.group)
if (group != null)
{
if (sprite != null)
for (sprite in this.group)
{
sprite.kill();
sprite.destroy();
remove(sprite);
if (sprite != null)
{
sprite.kill();
sprite.destroy();
remove(sprite);
}
}
group.clear();
}
group.clear();
if (debugIconGroup != null && debugIconGroup.group != null)
{
debugIconGroup.kill();

View File

@ -0,0 +1,23 @@
package funkin.shaderslmfao;
import flixel.addons.display.FlxRuntimeShader;
import funkin.Paths;
import openfl.utils.Assets;
import openfl.display.BitmapData;
class BlendModesShader extends FlxRuntimeShader
{
public var camera:BitmapData;
public function new()
{
super(Assets.getText(Paths.frag('blendModes')));
}
public function setCamera(camera:BitmapData):Void
{
this.camera = camera;
this.setBitmapData('camera', camera);
}
}

View File

@ -0,0 +1,25 @@
package funkin.shaderslmfao;
import flixel.addons.display.FlxRuntimeShader;
import funkin.Paths;
import openfl.utils.Assets;
/**
* Note... not actually gaussian!
*/
class GaussianBlurShader extends FlxRuntimeShader
{
public var amount:Float;
public function new(amount:Float = 1.0)
{
super(Assets.getText(Paths.frag("gaussianBlur")));
setAmount(amount);
}
public function setAmount(value:Float):Void
{
this.amount = value;
this.setFloat("amount", amount);
}
}

View File

@ -0,0 +1,22 @@
package funkin.shaderslmfao;
import flixel.addons.display.FlxRuntimeShader;
import funkin.Paths;
import openfl.utils.Assets;
class Grayscale extends FlxRuntimeShader
{
public var amount:Float = 1;
public function new(amount:Float = 1)
{
super(Assets.getText(Paths.frag("grayscale")));
setAmount(amount);
}
public function setAmount(value:Float):Void
{
amount = value;
this.setFloat("amount", amount);
}
}

View File

@ -0,0 +1,44 @@
package funkin.shaderslmfao;
import flixel.addons.display.FlxRuntimeShader;
import funkin.Paths;
import openfl.utils.Assets;
class HSVShader extends FlxRuntimeShader
{
public var hue(default, set):Float;
public var saturation(default, set):Float;
public var value(default, set):Float;
public function new()
{
super(Assets.getText(Paths.frag('hsv')));
hue = 1;
saturation = 1;
value = 1;
}
function set_hue(value:Float):Float
{
this.setFloat('hue', value);
this.hue = value;
return this.hue;
}
function set_saturation(value:Float):Float
{
this.setFloat('sat', value);
this.saturation = value;
return this.saturation;
}
function set_value(value:Float):Float
{
this.setFloat('val', value);
this.value = value;
return this.value;
}
}

View File

@ -17,6 +17,9 @@ import openfl.geom.Matrix;
import openfl.display.Sprite;
import openfl.display.Bitmap;
using Lambda;
using StringTools;
class StickerSubState extends MusicBeatSubState
{
public var grpStickers:FlxTypedGroup<StickerSprite>;
@ -26,10 +29,60 @@ class StickerSubState extends MusicBeatSubState
var nextState:NEXTSTATE = FREEPLAY;
// what "folders" to potentially load from (as of writing only "keys" exist)
var soundSelections:Array<String> = [];
// what "folder" was randomly selected
var soundSelection:String = "";
var sounds:Array<String> = [];
public function new(?oldStickers:Array<StickerSprite>, ?nextState:NEXTSTATE = FREEPLAY):Void
{
super();
// todo still
// make sure that ONLY plays mp3/ogg files
// if there's no mp3/ogg file, then it regenerates/reloads the random folder
var assetsInList = openfl.utils.Assets.list();
var soundFilterFunc = function(a:String) {
return a.startsWith('assets/shared/sounds/stickersounds/');
};
soundSelections = assetsInList.filter(soundFilterFunc);
soundSelections = soundSelections.map(function(a:String) {
return a.replace('assets/shared/sounds/stickersounds/', '').split('/')[0];
});
// cracked cleanup... yuchh...
for (i in soundSelections)
{
while (soundSelections.contains(i))
{
soundSelections.remove(i);
}
soundSelections.push(i);
}
trace(soundSelections);
soundSelection = FlxG.random.getObject(soundSelections);
var filterFunc = function(a:String) {
return a.startsWith('assets/shared/sounds/stickersounds/' + soundSelection + '/');
};
var assetsInList3 = openfl.utils.Assets.list();
sounds = assetsInList3.filter(filterFunc);
for (i in 0...sounds.length)
{
sounds[i] = sounds[i].replace('assets/shared/sounds/', '');
sounds[i] = sounds[i].substring(0, sounds[i].lastIndexOf('.'));
}
trace(sounds);
// trace(assetsInList);
this.nextState = nextState;
grpStickers = new FlxTypedGroup<StickerSprite>();
@ -66,6 +119,8 @@ class StickerSubState extends MusicBeatSubState
{
new FlxTimer().start(sticker.timing, _ -> {
sticker.visible = false;
var daSound:String = FlxG.random.getObject(sounds);
FlxG.sound.play(Paths.sound(daSound));
if (grpStickers == null || ind == grpStickers.members.length - 1)
{
@ -151,7 +206,11 @@ class StickerSubState extends MusicBeatSubState
sticker.timing = FlxMath.remapToRange(ind, 0, grpStickers.members.length, 0, 0.9);
new FlxTimer().start(sticker.timing, _ -> {
if (grpStickers == null) return;
sticker.visible = true;
var daSound:String = FlxG.random.getObject(sounds);
FlxG.sound.play(Paths.sound(daSound));
var frameTimer:Int = FlxG.random.int(0, 2);
@ -212,10 +271,10 @@ class StickerSubState extends MusicBeatSubState
{
super.update(elapsed);
if (FlxG.keys.justPressed.ANY)
{
regenStickers();
}
// if (FlxG.keys.justPressed.ANY)
// {
// regenStickers();
// }
}
var switchingState:Bool = false;

View File

@ -0,0 +1,273 @@
package funkin.ui.debug.charting;
import flixel.system.FlxAssets.FlxSoundAsset;
import flixel.system.FlxSound;
import flixel.system.FlxSound;
import funkin.audio.VoicesGroup;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.util.FileUtil;
import haxe.io.Bytes;
import haxe.io.Path;
import openfl.utils.Assets;
/**
* Functions for loading audio for the chart editor.
*/
@:nullSafety
@:allow(funkin.ui.debug.charting.ChartEditorState)
@:allow(funkin.ui.debug.charting.ChartEditorDialogHandler)
@:allow(funkin.ui.debug.charting.ChartEditorImportExportHandler)
class ChartEditorAudioHandler
{
/**
* Loads and stores byte data for a vocal track from an absolute file path
*
* @param path The absolute path to the audio file.
* @param charId The character this vocal track will be for.
* @param instId The instrumental this vocal track will be for.
* @return Success or failure.
*/
static function loadVocalsFromPath(state:ChartEditorState, path:Path, charId:String, instId:String = ''):Bool
{
#if sys
var fileBytes:Bytes = sys.io.File.getBytes(path.toString());
return loadVocalsFromBytes(state, fileBytes, charId, instId);
#else
trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
return false;
#end
}
/**
* Loads and stores byte data for a vocal track from an asset
*
* @param path The path to the asset. Use `Paths` to build this.
* @param charId The character this vocal track will be for.
* @param instId The instrumental this vocal track will be for.
* @return Success or failure.
*/
static function loadVocalsFromAsset(state:ChartEditorState, path:String, charId:String, instId:String = ''):Bool
{
var trackData:Null<Bytes> = Assets.getBytes(path);
if (trackData != null)
{
return loadVocalsFromBytes(state, trackData, charId, instId);
}
return false;
}
/**
* Loads and stores byte data for a vocal track
*
* @param bytes The audio byte data.
* @param charId The character this vocal track will be for.
* @param instId The instrumental this vocal track will be for.
*/
static function loadVocalsFromBytes(state:ChartEditorState, bytes:Bytes, charId:String, instId:String = ''):Bool
{
var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}';
state.audioVocalTrackData.set(trackId, bytes);
return true;
}
/**
* Loads and stores byte data for an instrumental track from an absolute file path
*
* @param path The absolute path to the audio file.
* @param instId The instrumental this vocal track will be for.
* @return Success or failure.
*/
static function loadInstFromPath(state:ChartEditorState, path:Path, instId:String = ''):Bool
{
#if sys
var fileBytes:Bytes = sys.io.File.getBytes(path.toString());
return loadInstFromBytes(state, fileBytes, instId);
#else
trace("[WARN] This platform can't load audio from a file path, you'll need to fetch the bytes some other way.");
return false;
#end
}
/**
* Loads and stores byte data for an instrumental track from an asset
*
* @param path The path to the asset. Use `Paths` to build this.
* @param instId The instrumental this vocal track will be for.
* @return Success or failure.
*/
static function loadInstFromAsset(state:ChartEditorState, path:String, instId:String = ''):Bool
{
var trackData:Null<Bytes> = Assets.getBytes(path);
if (trackData != null)
{
return loadInstFromBytes(state, trackData, instId);
}
return false;
}
/**
* Loads and stores byte data for a vocal track
*
* @param bytes The audio byte data.
* @param charId The character this vocal track will be for.
* @param instId The instrumental this vocal track will be for.
*/
static function loadInstFromBytes(state:ChartEditorState, bytes:Bytes, instId:String = ''):Bool
{
if (instId == '') instId = 'default';
state.audioInstTrackData.set(instId, bytes);
return true;
}
public static function switchToInstrumental(state:ChartEditorState, instId:String = '', playerId:String, opponentId:String):Bool
{
var result:Bool = playInstrumental(state, instId);
if (!result) return false;
stopExistingVocals(state);
result = playVocals(state, BF, playerId, instId);
if (!result) return false;
result = playVocals(state, DAD, opponentId, instId);
if (!result) return false;
return true;
}
/**
* Tell the Chart Editor to select a specific instrumental track, that is already loaded.
*/
static function playInstrumental(state:ChartEditorState, instId:String = ''):Bool
{
if (instId == '') instId = 'default';
var instTrackData:Null<Bytes> = state.audioInstTrackData.get(instId);
var instTrack:Null<FlxSound> = buildFlxSoundFromBytes(instTrackData);
if (instTrack == null) return false;
stopExistingInstrumental(state);
state.audioInstTrack = instTrack;
state.postLoadInstrumental();
return true;
}
static function stopExistingInstrumental(state:ChartEditorState):Void
{
if (state.audioInstTrack != null)
{
state.audioInstTrack.stop();
state.audioInstTrack.destroy();
state.audioInstTrack = null;
}
}
/**
* Tell the Chart Editor to select a specific vocal track, that is already loaded.
*/
static function playVocals(state:ChartEditorState, charType:CharacterType, charId:String, instId:String = ''):Bool
{
var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}';
var vocalTrackData:Null<Bytes> = state.audioVocalTrackData.get(trackId);
var vocalTrack:Null<FlxSound> = buildFlxSoundFromBytes(vocalTrackData);
if (state.audioVocalTrackGroup == null) state.audioVocalTrackGroup = new VoicesGroup();
if (vocalTrack != null)
{
switch (charType)
{
case BF:
state.audioVocalTrackGroup.addPlayerVoice(vocalTrack);
return true;
case DAD:
state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
return true;
case OTHER:
state.audioVocalTrackGroup.add(vocalTrack);
return true;
default:
// Do nothing.
}
}
return false;
}
static function stopExistingVocals(state:ChartEditorState):Void
{
if (state.audioVocalTrackGroup != null)
{
state.audioVocalTrackGroup.clear();
}
}
/**
* Play a sound effect.
* Automatically cleans up after itself and recycles previous FlxSound instances if available, for performance.
* @param path The path to the sound effect. Use `Paths` to build this.
*/
public static function playSound(path:String):Void
{
var snd:FlxSound = FlxG.sound.list.recycle(FlxSound) ?? new FlxSound();
var asset:Null<FlxSoundAsset> = FlxG.sound.cache(path);
if (asset == null)
{
trace('WARN: Failed to play sound $path, asset not found.');
return;
}
snd.loadEmbedded(asset);
snd.autoDestroy = true;
FlxG.sound.list.add(snd);
snd.play();
}
/**
* Convert byte data into a playable sound.
*
* @param input The byte data.
* @return The playable sound, or `null` if loading failed.
*/
public static function buildFlxSoundFromBytes(input:Null<Bytes>):Null<FlxSound>
{
if (input == null) return null;
var openflSound:openfl.media.Sound = new openfl.media.Sound();
openflSound.loadCompressedDataFromByteArray(openfl.utils.ByteArray.fromBytes(input), input.length);
var output:FlxSound = FlxG.sound.load(openflSound, 1.0, false);
return output;
}
static function makeZIPEntriesFromInstrumentals(state:ChartEditorState):Array<haxe.zip.Entry>
{
var zipEntries = [];
for (key in state.audioInstTrackData.keys())
{
if (key == 'default')
{
var data:Null<Bytes> = state.audioInstTrackData.get('default');
if (data == null) continue;
zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst.ogg', data));
}
else
{
var data:Null<Bytes> = state.audioInstTrackData.get(key);
if (data == null) continue;
zipEntries.push(FileUtil.makeZIPEntryFromBytes('Inst-${key}.ogg', data));
}
}
return zipEntries;
}
static function makeZIPEntriesFromVocals(state:ChartEditorState):Array<haxe.zip.Entry>
{
var zipEntries = [];
for (key in state.audioVocalTrackData.keys())
{
var data:Null<Bytes> = state.audioVocalTrackData.get(key);
if (data == null) continue;
zipEntries.push(FileUtil.makeZIPEntryFromBytes('Vocals-${key}.ogg', data));
}
return zipEntries;
}
}

View File

@ -1,5 +1,7 @@
package funkin.ui.debug.charting;
import haxe.ui.notifications.NotificationType;
import haxe.ui.notifications.NotificationManager;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
import funkin.data.song.SongDataUtils;
@ -64,7 +66,7 @@ class AddNotesCommand implements ChartEditorCommand
state.currentEventSelection = [];
}
state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
@ -78,7 +80,7 @@ class AddNotesCommand implements ChartEditorCommand
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
state.currentNoteSelection = [];
state.currentEventSelection = [];
state.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
@ -114,7 +116,7 @@ class RemoveNotesCommand implements ChartEditorCommand
state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, notes);
state.currentNoteSelection = [];
state.currentEventSelection = [];
state.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
@ -131,7 +133,7 @@ class RemoveNotesCommand implements ChartEditorCommand
}
state.currentNoteSelection = notes;
state.currentEventSelection = [];
state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
@ -252,7 +254,7 @@ class AddEventsCommand implements ChartEditorCommand
state.currentEventSelection = events;
}
state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
@ -296,7 +298,7 @@ class RemoveEventsCommand implements ChartEditorCommand
{
state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, events);
state.currentEventSelection = [];
state.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
@ -312,7 +314,7 @@ class RemoveEventsCommand implements ChartEditorCommand
state.currentSongChartEventData.push(event);
}
state.currentEventSelection = events;
state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
@ -352,7 +354,7 @@ class RemoveItemsCommand implements ChartEditorCommand
state.currentNoteSelection = [];
state.currentEventSelection = [];
state.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-01'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
@ -376,7 +378,7 @@ class RemoveItemsCommand implements ChartEditorCommand
state.currentNoteSelection = notes;
state.currentEventSelection = events;
state.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
ChartEditorAudioHandler.playSound(Paths.sound('funnyNoise/funnyNoise-08'));
state.saveDataDirty = true;
state.noteDisplayDirty = true;
@ -760,6 +762,22 @@ class PasteItemsCommand implements ChartEditorCommand
{
var currentClipboard:SongClipboardItems = SongDataUtils.readItemsFromClipboard();
if (currentClipboard.valid != true)
{
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Failed to Paste',
body: 'Could not parse clipboard contents.',
type: NotificationType.Error,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
#end
return;
}
trace(currentClipboard.notes);
addedNotes = SongDataUtils.offsetSongNoteData(currentClipboard.notes, Std.int(targetTimestamp));
addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp));
@ -773,6 +791,16 @@ class PasteItemsCommand implements ChartEditorCommand
state.notePreviewDirty = true;
state.sortChartData();
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Paste Successful',
body: 'Successfully pasted clipboard contents.',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
#end
}
public function undo(state:ChartEditorState):Void

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,129 @@
package funkin.ui.debug.charting;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.stage.StageData;
import funkin.play.stage.StageData.StageDataParser;
import funkin.play.character.CharacterData;
import haxe.ui.components.DropDown;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.play.character.CharacterData.CharacterDataParser;
/**
* This class contains functions for populating dropdowns based on game data.
* These get used by both dialogs and toolboxes so they're in their own class to prevent "reaching over."
*/
@:nullSafety
@:access(ChartEditorState)
class ChartEditorDropdowns
{
public static function populateDropdownWithCharacters(dropDown:DropDown, charType:CharacterType, startingCharId:String):DropDownEntry
{
dropDown.dataSource.clear();
// TODO: Filter based on charType.
var charIds:Array<String> = CharacterDataParser.listCharacterIds();
var returnValue:DropDownEntry = switch (charType)
{
case BF: {id: "bf", text: "Boyfriend"};
case DAD: {id: "dad", text: "Daddy Dearest"};
default: {
dropDown.dataSource.add({id: "none", text: ""});
{id: "none", text: "None"};
}
}
for (charId in charIds)
{
var character:Null<CharacterData> = CharacterDataParser.fetchCharacterData(charId);
if (character == null) continue;
var value = {id: charId, text: character.name};
if (startingCharId == charId) returnValue = value;
dropDown.dataSource.add(value);
}
dropDown.dataSource.sort('text', ASCENDING);
return returnValue;
}
public static function populateDropdownWithStages(dropDown:DropDown, startingStageId:String):DropDownEntry
{
dropDown.dataSource.clear();
var stageIds:Array<String> = StageDataParser.listStageIds();
var returnValue:DropDownEntry = {id: "mainStage", text: "Main Stage"};
for (stageId in stageIds)
{
var stage:Null<StageData> = StageDataParser.parseStageData(stageId);
if (stage == null) continue;
var value = {id: stageId, text: stage.name};
if (startingStageId == stageId) returnValue = value;
dropDown.dataSource.add(value);
}
dropDown.dataSource.sort('text', ASCENDING);
return returnValue;
}
public static function populateDropdownWithNoteStyles(dropDown:DropDown, startingStyleId:String):DropDownEntry
{
dropDown.dataSource.clear();
var noteStyleIds:Array<String> = NoteStyleRegistry.instance.listEntryIds();
var returnValue:DropDownEntry = {id: "funkin", text: "Funkin'"};
for (noteStyleId in noteStyleIds)
{
var noteStyle:Null<NoteStyle> = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
if (noteStyle == null) continue;
var value = {id: noteStyleId, text: noteStyle.getName()};
if (startingStyleId == noteStyleId) returnValue = value;
dropDown.dataSource.add(value);
}
dropDown.dataSource.sort('text', ASCENDING);
return returnValue;
}
public static function populateDropdownWithVariations(dropDown:DropDown, state:ChartEditorState, includeNone:Bool = true):DropDownEntry
{
dropDown.dataSource.clear();
var variationIds:Array<String> = state.availableVariations;
if (includeNone)
{
dropDown.dataSource.add({id: "none", text: ""});
}
var returnValue:DropDownEntry = includeNone ? ({id: "none", text: ""}) : ({id: "default", text: "Default"});
for (variationId in variationIds)
{
dropDown.dataSource.add({id: variationId, text: variationId.toTitleCase()});
}
dropDown.dataSource.sort('text', ASCENDING);
return returnValue;
}
}
typedef DropDownEntry =
{
id:String,
text:String
};

View File

@ -0,0 +1,211 @@
package funkin.ui.debug.charting;
import haxe.ui.notifications.NotificationType;
import funkin.util.DateUtil;
import haxe.io.Path;
import funkin.util.SerializerUtil;
import haxe.ui.notifications.NotificationManager;
import funkin.util.FileUtil;
import funkin.util.FileUtil;
import funkin.play.song.Song;
import funkin.data.song.SongData.SongChartData;
import funkin.data.song.SongData.SongMetadata;
import funkin.data.song.SongRegistry;
/**
* Contains functions for importing, loading, saving, and exporting charts.
*/
@:nullSafety
@:allow(funkin.ui.debug.charting.ChartEditorState)
class ChartEditorImportExportHandler
{
/**
* Fetch's a song's existing chart and audio and loads it, replacing the current song.
*/
public static function loadSongAsTemplate(state:ChartEditorState, songId:String):Void
{
var song:Null<Song> = SongRegistry.instance.fetchEntry(songId);
if (song == null) return;
// Load the song metadata.
var rawSongMetadata:Array<SongMetadata> = song.getRawMetadata();
var songMetadata:Map<String, SongMetadata> = [];
var songChartData:Map<String, SongChartData> = [];
for (metadata in rawSongMetadata)
{
if (metadata == null) continue;
var variation = (metadata.variation == null || metadata.variation == '') ? Constants.DEFAULT_VARIATION : metadata.variation;
// Clone to prevent modifying the original.
var metadataClone:SongMetadata = metadata.clone(variation);
if (metadataClone != null) songMetadata.set(variation, metadataClone);
var chartData:Null<SongChartData> = SongRegistry.instance.parseEntryChartData(songId, metadata.variation);
if (chartData != null) songChartData.set(variation, chartData);
}
loadSong(state, songMetadata, songChartData);
state.sortChartData();
state.clearVocals();
var variations:Array<String> = state.availableVariations;
for (variation in variations)
{
if (variation == Constants.DEFAULT_VARIATION)
{
ChartEditorAudioHandler.loadInstFromAsset(state, Paths.inst(songId));
}
else
{
ChartEditorAudioHandler.loadInstFromAsset(state, Paths.inst(songId, '-$variation'), variation);
}
}
for (difficultyId in song.listDifficulties())
{
var diff:Null<SongDifficulty> = song.getDifficulty(difficultyId);
if (diff == null) continue;
var instId:String = diff.variation == Constants.DEFAULT_VARIATION ? '' : diff.variation;
var voiceList:Array<String> = diff.buildVoiceList(); // SongDifficulty accounts for variation already.
if (voiceList.length == 2)
{
ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[0], diff.characters.player, instId);
ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[1], diff.characters.opponent, instId);
}
else if (voiceList.length == 1)
{
ChartEditorAudioHandler.loadVocalsFromAsset(state, voiceList[0], diff.characters.player, instId);
}
else
{
trace('[WARN] Strange quantity of voice paths for difficulty ${difficultyId}: ${voiceList.length}');
}
}
state.switchToCurrentInstrumental();
state.refreshMetadataToolbox();
#if !mac
NotificationManager.instance.addNotification(
{
title: 'Success',
body: 'Loaded song (${rawSongMetadata[0].songName})',
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
#end
}
/**
* Loads song metadata and chart data into the editor.
* @param newSongMetadata The song metadata to load.
* @param newSongChartData The song chart data to load.
*/
public static function loadSong(state:ChartEditorState, newSongMetadata:Map<String, SongMetadata>, newSongChartData:Map<String, SongChartData>):Void
{
state.songMetadata = newSongMetadata;
state.songChartData = newSongChartData;
Conductor.forceBPM(null); // Disable the forced BPM.
Conductor.mapTimeChanges(state.currentSongMetadata.timeChanges);
state.notePreviewDirty = true;
state.notePreviewViewportBoundsDirty = true;
state.difficultySelectDirty = true;
state.opponentPreviewDirty = true;
state.playerPreviewDirty = true;
// Remove instrumental and vocal tracks, they will be loaded next.
if (state.audioInstTrack != null)
{
state.audioInstTrack.stop();
state.audioInstTrack = null;
}
if (state.audioVocalTrackGroup != null)
{
state.audioVocalTrackGroup.stop();
state.audioVocalTrackGroup.clear();
}
}
/**
* @param force Whether to force the export without prompting the user for a file location.
*/
public static function exportAllSongData(state:ChartEditorState, force:Bool = false):Void
{
var tmp = false;
var zipEntries:Array<haxe.zip.Entry> = [];
for (variation in state.availableVariations)
{
var variationId:String = variation;
if (variation == '' || variation == 'default' || variation == 'normal')
{
variationId = '';
}
if (variationId == '')
{
var variationMetadata:Null<SongMetadata> = state.songMetadata.get(variation);
if (variationMetadata != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata.json', variationMetadata.serialize()));
var variationChart:Null<SongChartData> = state.songChartData.get(variation);
if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart.json', variationChart.serialize()));
}
else
{
var variationMetadata:Null<SongMetadata> = state.songMetadata.get(variation);
if (variationMetadata != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-metadata-$variationId.json',
SerializerUtil.toJSON(variationMetadata)));
var variationChart:Null<SongChartData> = state.songChartData.get(variation);
if (variationChart != null) zipEntries.push(FileUtil.makeZIPEntry('${state.currentSongId}-chart-$variationId.json',
SerializerUtil.toJSON(variationChart)));
}
}
if (state.audioInstTrackData != null) zipEntries.concat(ChartEditorAudioHandler.makeZIPEntriesFromInstrumentals(state));
if (state.audioVocalTrackData != null) zipEntries.concat(ChartEditorAudioHandler.makeZIPEntriesFromVocals(state));
trace('Exporting ${zipEntries.length} files to ZIP...');
if (force)
{
var targetPath:String = if (tmp)
{
Path.join([
FileUtil.getTempDir(),
'chart-editor-exit-${DateUtil.generateTimestamp()}.${Constants.EXT_CHART}'
]);
}
else
{
Path.join([
'./backups/',
'chart-editor-exit-${DateUtil.generateTimestamp()}.${Constants.EXT_CHART}'
]);
}
// We have to force write because the program will die before the save dialog is closed.
trace('Force exporting to $targetPath...');
FileUtil.saveFilesAsZIPToPath(zipEntries, targetPath);
return;
}
// Prompt and save.
var onSave:Array<String>->Void = function(paths:Array<String>) {
trace('Successfully exported files.');
};
var onCancel:Void->Void = function() {
trace('Export cancelled.');
};
FileUtil.saveMultipleFiles(zipEntries, onSave, onCancel, '${state.currentSongId}-chart.${Constants.EXT_CHART}');
}
}

View File

@ -19,7 +19,7 @@ class ChartEditorNoteSprite extends FlxSprite
/**
* The list of available note skin to validate against.
*/
public static final NOTE_STYLES:Array<String> = ['Normal', 'Pixel'];
public static final NOTE_STYLES:Array<String> = ['funkin', 'pixel'];
/**
* The ChartEditorState this note belongs to.
@ -54,20 +54,20 @@ class ChartEditorNoteSprite extends FlxSprite
// Initialize all the animations, not just the one we're going to use immediately,
// so that later we can reuse the sprite without having to initialize more animations during scrolling.
this.animation.addByPrefix('tapLeftNormal', 'purple instance');
this.animation.addByPrefix('tapDownNormal', 'blue instance');
this.animation.addByPrefix('tapUpNormal', 'green instance');
this.animation.addByPrefix('tapRightNormal', 'red instance');
this.animation.addByPrefix('tapLeftFunkin', 'purple instance');
this.animation.addByPrefix('tapDownFunkin', 'blue instance');
this.animation.addByPrefix('tapUpFunkin', 'green instance');
this.animation.addByPrefix('tapRightFunkin', 'red instance');
this.animation.addByPrefix('holdLeftNormal', 'LeftHoldPiece');
this.animation.addByPrefix('holdDownNormal', 'DownHoldPiece');
this.animation.addByPrefix('holdUpNormal', 'UpHoldPiece');
this.animation.addByPrefix('holdRightNormal', 'RightHoldPiece');
this.animation.addByPrefix('holdLeftFunkin', 'LeftHoldPiece');
this.animation.addByPrefix('holdDownFunkin', 'DownHoldPiece');
this.animation.addByPrefix('holdUpFunkin', 'UpHoldPiece');
this.animation.addByPrefix('holdRightFunkin', 'RightHoldPiece');
this.animation.addByPrefix('holdEndLeftNormal', 'LeftHoldEnd');
this.animation.addByPrefix('holdEndDownNormal', 'DownHoldEnd');
this.animation.addByPrefix('holdEndUpNormal', 'UpHoldEnd');
this.animation.addByPrefix('holdEndRightNormal', 'RightHoldEnd');
this.animation.addByPrefix('holdEndLeftFunkin', 'LeftHoldEnd');
this.animation.addByPrefix('holdEndDownFunkin', 'DownHoldEnd');
this.animation.addByPrefix('holdEndUpFunkin', 'UpHoldEnd');
this.animation.addByPrefix('holdEndRightFunkin', 'RightHoldEnd');
this.animation.addByPrefix('tapLeftPixel', 'pixel4');
this.animation.addByPrefix('tapDownPixel', 'pixel5');
@ -187,8 +187,8 @@ class ChartEditorNoteSprite extends FlxSprite
function get_noteStyle():String
{
// Fall back to 'Normal' if it's not a valid note style.
return if (NOTE_STYLES.contains(this.parentState.currentSongNoteSkin)) this.parentState.currentSongNoteSkin else 'Normal';
// Fall back to Funkin' if it's not a valid note style.
return if (NOTE_STYLES.contains(this.parentState.currentSongNoteStyle)) this.parentState.currentSongNoteStyle else 'funkin';
}
public function playNoteAnimation():Void
@ -199,7 +199,7 @@ class ChartEditorNoteSprite extends FlxSprite
var baseAnimationName:String = 'tap';
// Play the appropriate animation for the type, direction, and skin.
var animationName:String = '${baseAnimationName}${this.noteData.getDirectionName()}${this.noteStyle}';
var animationName:String = '${baseAnimationName}${this.noteData.getDirectionName()}${this.noteStyle.toTitleCase()}';
this.animation.play(animationName);
@ -213,7 +213,7 @@ class ChartEditorNoteSprite extends FlxSprite
this.updateHitbox();
// TODO: Make this an attribute of the note skin.
this.antialiasing = (this.parentState.currentSongNoteSkin != 'Pixel');
this.antialiasing = (this.parentState.currentSongNoteStyle != 'Pixel');
}
/**

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,10 @@
package funkin.ui.debug.charting;
import funkin.ui.haxeui.components.FunkinDropDown;
import funkin.play.stage.StageData.StageDataParser;
import funkin.play.stage.StageData;
import funkin.play.character.CharacterData;
import funkin.play.character.CharacterData.CharacterDataParser;
import haxe.ui.components.HorizontalSlider;
import haxe.ui.containers.TreeView;
import haxe.ui.containers.TreeViewNode;
@ -9,6 +14,7 @@ import funkin.data.event.SongEventData;
import funkin.data.song.SongData.SongTimeChange;
import funkin.play.song.SongSerializer;
import funkin.ui.haxeui.components.CharacterPlayer;
import funkin.util.FileUtil;
import haxe.ui.components.Button;
import haxe.ui.components.CheckBox;
import haxe.ui.components.DropDown;
@ -78,8 +84,6 @@ class ChartEditorToolboxHandler
onShowToolboxDifficulty(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
onShowToolboxMetadata(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT:
onShowToolboxCharacters(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
onShowToolboxPlayerPreview(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:
@ -117,8 +121,6 @@ class ChartEditorToolboxHandler
onHideToolboxDifficulty(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
onHideToolboxMetadata(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT:
onHideToolboxCharacters(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
onHideToolboxPlayerPreview(state, toolbox);
case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:
@ -167,8 +169,6 @@ class ChartEditorToolboxHandler
toolbox = buildToolboxDifficultyLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_METADATA_LAYOUT:
toolbox = buildToolboxMetadataLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT:
toolbox = buildToolboxCharactersLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT:
toolbox = buildToolboxPlayerPreviewLayout(state);
case ChartEditorState.CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT:
@ -445,14 +445,20 @@ class ChartEditorToolboxHandler
state.setUICheckboxSelected('menubarItemToggleToolboxDifficulty', false);
}
var difficultyToolboxAddVariation:Null<Button> = toolbox.findComponent('difficultyToolboxAddVariation', Button);
if (difficultyToolboxAddVariation == null)
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxAddVariation component.';
var difficultyToolboxAddDifficulty:Null<Button> = toolbox.findComponent('difficultyToolboxAddDifficulty', Button);
if (difficultyToolboxAddDifficulty == null)
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxAddDifficulty component.';
var difficultyToolboxSaveMetadata:Null<Button> = toolbox.findComponent('difficultyToolboxSaveMetadata', Button);
if (difficultyToolboxSaveMetadata == null)
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveMetadata component.';
var difficultyToolboxSaveChart:Null<Button> = toolbox.findComponent('difficultyToolboxSaveChart', Button);
if (difficultyToolboxSaveChart == null)
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveChart component.';
var difficultyToolboxSaveAll:Null<Button> = toolbox.findComponent('difficultyToolboxSaveAll', Button);
if (difficultyToolboxSaveAll == null) throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveAll component.';
// var difficultyToolboxSaveAll:Null<Button> = toolbox.findComponent('difficultyToolboxSaveAll', Button);
// if (difficultyToolboxSaveAll == null) throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxSaveAll component.';
var difficultyToolboxLoadMetadata:Null<Button> = toolbox.findComponent('difficultyToolboxLoadMetadata', Button);
if (difficultyToolboxLoadMetadata == null)
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxLoadMetadata component.';
@ -460,26 +466,32 @@ class ChartEditorToolboxHandler
if (difficultyToolboxLoadChart == null)
throw 'ChartEditorToolboxHandler.buildToolboxDifficultyLayout() - Could not find difficultyToolboxLoadChart component.';
difficultyToolboxSaveMetadata.onClick = function(event:UIEvent) {
SongSerializer.exportSongMetadata(state.currentSongMetadata, state.currentSongId);
difficultyToolboxAddVariation.onClick = function(_:UIEvent) {
ChartEditorDialogHandler.openAddVariationDialog(state, true);
};
difficultyToolboxSaveChart.onClick = function(event:UIEvent) {
SongSerializer.exportSongChartData(state.currentSongChartData, state.currentSongId);
difficultyToolboxAddDifficulty.onClick = function(_:UIEvent) {
ChartEditorDialogHandler.openAddDifficultyDialog(state, true);
};
difficultyToolboxSaveAll.onClick = function(event:UIEvent) {
state.exportAllSongData();
difficultyToolboxSaveMetadata.onClick = function(_:UIEvent) {
var vari:String = state.selectedVariation != Constants.DEFAULT_VARIATION ? '-${state.selectedVariation}' : '';
FileUtil.writeFileReference('${state.currentSongId}$vari-metadata.json', state.currentSongMetadata.serialize());
};
difficultyToolboxLoadMetadata.onClick = function(event:UIEvent) {
difficultyToolboxSaveChart.onClick = function(_:UIEvent) {
var vari:String = state.selectedVariation != Constants.DEFAULT_VARIATION ? '-${state.selectedVariation}' : '';
FileUtil.writeFileReference('${state.currentSongId}$vari-chart.json', state.currentSongChartData.serialize());
};
difficultyToolboxLoadMetadata.onClick = function(_:UIEvent) {
// Replace metadata for current variation.
SongSerializer.importSongMetadataAsync(function(songMetadata) {
state.currentSongMetadata = songMetadata;
});
};
difficultyToolboxLoadChart.onClick = function(event:UIEvent) {
difficultyToolboxLoadChart.onClick = function(_:UIEvent) {
// Replace chart data for current variation.
SongSerializer.importSongChartDataAsync(function(songChartData) {
state.currentSongChartData = songChartData;
@ -554,7 +566,7 @@ class ChartEditorToolboxHandler
};
inputSongArtist.value = state.currentSongMetadata.artist;
var inputStage:Null<DropDown> = toolbox.findComponent('inputStage', DropDown);
var inputStage:Null<FunkinDropDown> = toolbox.findComponent('inputStage', FunkinDropDown);
if (inputStage == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputStage component.';
inputStage.onChange = function(event:UIEvent) {
var valid:Bool = event.data != null && event.data.id != null;
@ -564,15 +576,48 @@ class ChartEditorToolboxHandler
state.currentSongMetadata.playData.stage = event.data.id;
}
};
inputStage.value = state.currentSongMetadata.playData.stage;
var startingValueStage = ChartEditorDropdowns.populateDropdownWithStages(inputStage, state.currentSongMetadata.playData.stage);
inputStage.value = startingValueStage;
var inputNoteSkin:Null<DropDown> = toolbox.findComponent('inputNoteSkin', DropDown);
if (inputNoteSkin == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputNoteSkin component.';
inputNoteSkin.onChange = function(event:UIEvent) {
if ((event?.data?.id ?? null) == null) return;
state.currentSongNoteSkin = event.data.id;
var inputNoteStyle:Null<FunkinDropDown> = toolbox.findComponent('inputNoteStyle', FunkinDropDown);
if (inputNoteStyle == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputNoteStyle component.';
inputNoteStyle.onChange = function(event:UIEvent) {
if (event.data?.id == null) return;
state.currentSongNoteStyle = event.data.id;
};
inputNoteSkin.value = state.currentSongNoteSkin;
inputNoteStyle.value = state.currentSongNoteStyle;
// By using this flag, we prevent the dropdown value from changing while it is being populated.
var inputCharacterPlayer:Null<FunkinDropDown> = toolbox.findComponent('inputCharacterPlayer', FunkinDropDown);
if (inputCharacterPlayer == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputCharacterPlayer component.';
inputCharacterPlayer.onChange = function(event:UIEvent) {
if (event.data?.id == null) return;
state.currentSongMetadata.playData.characters.player = event.data.id;
};
var startingValuePlayer = ChartEditorDropdowns.populateDropdownWithCharacters(inputCharacterPlayer, CharacterType.BF,
state.currentSongMetadata.playData.characters.player);
inputCharacterPlayer.value = startingValuePlayer;
var inputCharacterOpponent:Null<FunkinDropDown> = toolbox.findComponent('inputCharacterOpponent', FunkinDropDown);
if (inputCharacterOpponent == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputCharacterOpponent component.';
inputCharacterOpponent.onChange = function(event:UIEvent) {
if (event.data?.id == null) return;
state.currentSongMetadata.playData.characters.opponent = event.data.id;
};
var startingValueOpponent = ChartEditorDropdowns.populateDropdownWithCharacters(inputCharacterOpponent, CharacterType.DAD,
state.currentSongMetadata.playData.characters.opponent);
inputCharacterOpponent.value = startingValueOpponent;
var inputCharacterGirlfriend:Null<FunkinDropDown> = toolbox.findComponent('inputCharacterGirlfriend', FunkinDropDown);
if (inputCharacterGirlfriend == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputCharacterGirlfriend component.';
inputCharacterGirlfriend.onChange = function(event:UIEvent) {
if (event.data?.id == null) return;
state.currentSongMetadata.playData.characters.girlfriend = event.data.id == "none" ? "" : event.data.id;
};
var startingValueGirlfriend = ChartEditorDropdowns.populateDropdownWithCharacters(inputCharacterGirlfriend, CharacterType.GF,
state.currentSongMetadata.playData.characters.girlfriend);
inputCharacterGirlfriend.value = startingValueGirlfriend;
var inputBPM:Null<NumberStepper> = toolbox.findComponent('inputBPM', NumberStepper);
if (inputBPM == null) throw 'ChartEditorToolboxHandler.buildToolboxMetadataLayout() - Could not find inputBPM component.';
@ -630,32 +675,11 @@ class ChartEditorToolboxHandler
static function onShowToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void
{
state.refreshSongMetadataToolbox();
state.refreshMetadataToolbox();
}
static function onHideToolboxMetadata(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function buildToolboxCharactersLayout(state:ChartEditorState):Null<CollapsibleDialog>
{
var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_CHARACTERS_LAYOUT);
if (toolbox == null) return null;
// Starting position.
toolbox.x = 175;
toolbox.y = 300;
toolbox.onDialogClosed = function(event:DialogEvent) {
state.setUICheckboxSelected('menubarItemToggleToolboxCharacters', false);
}
return toolbox;
}
static function onShowToolboxCharacters(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function onHideToolboxCharacters(state:ChartEditorState, toolbox:CollapsibleDialog):Void {}
static function buildToolboxPlayerPreviewLayout(state:ChartEditorState):Null<CollapsibleDialog>
{
var toolbox:CollapsibleDialog = cast state.buildComponent(ChartEditorState.CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);

View File

@ -0,0 +1,74 @@
package funkin.ui.title;
import flixel.FlxSprite;
import funkin.shaderslmfao.BlendModesShader;
import openfl.display.BitmapData;
import flixel.FlxCamera;
import flixel.FlxG;
import flixel.graphics.frames.FlxFrame.FlxFrameAngle;
class FlxSpriteOverlay extends FlxSprite
{
var blendShader:BlendModesShader;
var dipshitBitmap:BitmapData;
var temp:FlxSprite;
public function new(x:Float, y:Float)
{
super(x, y);
temp = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, 0xFF000000);
blendShader = new BlendModesShader();
dipshitBitmap = new BitmapData(2180, 1720, true, 0xFFCC00CC);
}
override function drawComplex(camera:FlxCamera):Void
{
_frame.prepareMatrix(_matrix, FlxFrameAngle.ANGLE_0, checkFlipX(), checkFlipY());
_matrix.translate(-origin.x, -origin.y);
_matrix.scale(scale.x, scale.y);
if (bakedRotationAngle <= 0)
{
updateTrig();
if (angle != 0) _matrix.rotateWithTrig(_cosAngle, _sinAngle);
}
getScreenPosition(_point, camera).subtractPoint(offset);
_point.add(origin.x, origin.y);
_matrix.translate(_point.x, _point.y);
if (isPixelPerfectRender(camera))
{
_matrix.tx = Math.floor(_matrix.tx);
_matrix.ty = Math.floor(_matrix.ty);
}
var sprRect = getScreenBounds();
// dipshitBitmap.draw(camera.canvas, camera.canvas.transform.matrix);
// blendShader.setCamera(dipshitBitmap);
// FlxG.bitmapLog.add(dipshitBitmap);
camera.drawPixels(_frame, framePixels, _matrix, colorTransform, blend, antialiasing, shader);
}
function copyToFlash(rect):openfl.geom.Rectangle
{
var flashRect = new openfl.geom.Rectangle();
flashRect.x = rect.x;
flashRect.y = rect.y;
flashRect.width = rect.width;
flashRect.height = rect.height;
return flashRect;
}
override public function isSimpleRender(?camera:FlxCamera):Bool
{
if (FlxG.renderBlit)
{
return super.isSimpleRender(camera);
}
else
{
return false;
}
}
}

View File

@ -23,6 +23,7 @@ import openfl.events.MouseEvent;
import openfl.events.NetStatusEvent;
import openfl.media.Video;
import openfl.net.NetStream;
import openfl.display.BlendMode;
#if desktop
#end
@ -101,7 +102,7 @@ class TitleState extends MusicBeatState
var logoBl:FlxSprite;
var outlineShaderShit:TitleOutline;
var gfDance:FlxSprite;
var gfDance:FlxSpriteOverlay;
var danceLeft:Bool = false;
var titleText:FlxSprite;
var maskShader = new LeftMaskShader();
@ -124,13 +125,11 @@ class TitleState extends MusicBeatState
outlineShaderShit = new TitleOutline();
gfDance = new FlxSprite(FlxG.width * 0.4, FlxG.height * 0.07);
gfDance = new FlxSpriteOverlay(FlxG.width * 0.4, FlxG.height * 0.07);
gfDance.frames = Paths.getSparrowAtlas('gfDanceTitle');
gfDance.animation.addByIndices('danceLeft', 'gfDance', [30, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14], "", 24, false);
gfDance.animation.addByIndices('danceRight', 'gfDance', [15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29], "", 24, false);
add(gfDance);
// maskShader.swagSprX = gfDance.x;
// maskShader.swagMaskX = gfDance.x + 200;
// maskShader.frameUV = gfDance.frame.uv;
@ -142,6 +141,8 @@ class TitleState extends MusicBeatState
add(logoBl);
add(gfDance);
titleText = new FlxSprite(100, FlxG.height * 0.8);
titleText.frames = Paths.getSparrowAtlas('titleEnter');
titleText.animation.addByPrefix('idle', "Press Enter to Begin", 24);
@ -245,6 +246,8 @@ class TitleState extends MusicBeatState
override function update(elapsed:Float)
{
FlxG.bitmapLog.add(FlxG.camera.buffer);
#if HAS_PITCH
if (FlxG.keys.pressed.UP) FlxG.sound.music.pitch += 0.5 * elapsed;

View File

@ -2,6 +2,7 @@ package funkin.util;
import flixel.util.FlxColor;
import lime.app.Application;
import funkin.data.song.SongData.SongTimeFormat;
/**
* A store of unchanging, globally relevant values.
@ -25,13 +26,23 @@ class Constants
*/
public static var VERSION(get, never):String;
/**
* The generatedBy string embedded in the chart files made by this application.
*/
public static var GENERATED_BY(get, never):String;
static function get_GENERATED_BY():String
{
return '${Constants.TITLE} - ${Constants.VERSION}';
}
/**
* A suffix to add to the game version.
* Add a suffix to prototype builds and remove it for releases.
*/
public static final VERSION_SUFFIX:String = ' PROTOTYPE';
#if debug
#if (debug || FORCE_DEBUG_VERSION)
static function get_VERSION():String
{
return 'v${Application.current.meta.get('version')} (${GIT_BRANCH} : ${GIT_HASH})' + VERSION_SUFFIX;
@ -63,7 +74,7 @@ class Constants
*/
// ==============================
#if debug
#if (debug || FORCE_DEBUG_VERSION)
/**
* The current Git branch.
*/
@ -143,7 +154,32 @@ class Constants
/**
* The default BPM for charts, so things don't break if none is specified.
*/
public static final DEFAULT_BPM:Int = 100;
public static final DEFAULT_BPM:Float = 100.0;
/**
* The default name for songs.
*/
public static final DEFAULT_SONGNAME:String = "Unknown";
/**
* The default artist for songs.
*/
public static final DEFAULT_ARTIST:String = "Unknown";
/**
* The default note style for songs.
*/
public static final DEFAULT_NOTE_STYLE:String = "funkin";
/**
* The default timing format for songs.
*/
public static final DEFAULT_TIMEFORMAT:SongTimeFormat = SongTimeFormat.MILLISECONDS;
/**
* The default scroll speed for songs.
*/
public static final DEFAULT_SCROLLSPEED:Float = 1.0;
/**
* Default numerator for the time signature.
@ -291,16 +327,60 @@ class Constants
public static final HEALTH_MINE_PENALTY:Float = 15.0 / 100.0 * HEALTH_MAX; // 15.0%
/**
* 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.
* SCORE VALUES
*/
public static final GHOST_TAPPING:Bool = false;
// ==============================
/**
* The amount of score the player gains for every send they hold a hold note.
* A fraction of this value is granted every frame.
*/
public static final SCORE_HOLD_BONUS_PER_SECOND:Float = 250.0;
/**
* FILE EXTENSIONS
*/
// ==============================
/**
* The file extension used when exporting chart files.
*
* - "I made a new file format"
* - "Actually new or just a renamed ZIP?"
*/
public static final EXT_CHART = "fnfc";
/**
* The file extension used when loading audio files.
*/
public static final EXT_SOUND = #if web "mp3" #else "ogg" #end;
/**
* The file extension used when loading video files.
*/
public static final EXT_VIDEO = "mp4";
/**
* The file extension used when loading image files.
*/
public static final EXT_IMAGE = "png";
/**
* The file extension used when loading data files.
*/
public static final EXT_DATA = "json";
/**
* OTHER
*/
// ==============================
/**
* 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.
*/
public static final GHOST_TAPPING:Bool = false;
/**
* The separator between an asset library and the asset path.
*/

View File

@ -5,10 +5,9 @@ import lime.utils.Bytes;
import lime.ui.FileDialog;
import openfl.net.FileFilter;
import haxe.io.Path;
#if html5
import openfl.net.FileReference;
import openfl.events.Event;
#end
import openfl.events.IOErrorEvent;
/**
* Utilities for reading and writing files on various platforms.
@ -260,8 +259,7 @@ class FileUtil
/**
* Takes an array of file entries and prompts the user to save them as a ZIP file.
*/
public static function saveFilesAsZIP(resources:Array<Entry>, ?onSave:Array<String>->Void, ?onCancel:Void->Void, ?defaultPath:String,
force:Bool = false):Bool
public static function saveFilesAsZIP(resources:Array<Entry>, ?onSave:Array<String>->Void, ?onCancel:Void->Void, ?defaultPath:String, force:Bool = false):Bool
{
// Create a ZIP file.
var zipBytes:Bytes = createZIPFromEntries(resources);
@ -309,6 +307,7 @@ class FileUtil
#if sys
return sys.io.File.getContent(path);
#else
trace('ERROR: readStringFromPath not implemented for this platform');
return null;
#end
}
@ -329,6 +328,48 @@ class FileUtil
#end
}
/**
* Browse for a file to read and execute a callback once we have a file reference.
* Works great on HTML5 or desktop.
*
* @param callback The function to call when the file is loaded.
*/
public static function browseFileReference(callback:FileReference->Void)
{
var file = new FileReference();
file.addEventListener(Event.SELECT, function(e) {
var selectedFileRef:FileReference = e.target;
trace('Selected file: ' + selectedFileRef.name);
selectedFileRef.addEventListener(Event.COMPLETE, function(e) {
var loadedFileRef:FileReference = e.target;
trace('Loaded file: ' + loadedFileRef.name);
callback(loadedFileRef);
});
selectedFileRef.load();
});
file.browse();
}
/**
* Prompts the user to save a file to their computer.
*/
public static function writeFileReference(path:String, data:String)
{
var file = new FileReference();
file.addEventListener(Event.COMPLETE, function(e:Event) {
trace('Successfully wrote file.');
});
file.addEventListener(Event.CANCEL, function(e:Event) {
trace('Cancelled writing file.');
});
file.addEventListener(IOErrorEvent.IO_ERROR, function(e:IOErrorEvent) {
trace('IO error writing file.');
});
file.save(data, path);
}
/**
* Read JSON file contents directly from a given path.
* Only works on desktop.

View File

@ -13,6 +13,7 @@ typedef ScoreInput =
/**
* A class of functions dedicated to serializing and deserializing data.
* TODO: Rewrite/refactor this to use json2object.
*/
class SerializerUtil
{

View File

@ -1,6 +1,6 @@
package funkin.util.macro;
#if debug
#if (debug || FORCE_DEBUG_VERSION)
class GitCommit
{
/**