diff --git a/.github/actions/setup-haxeshit/action.yml b/.github/actions/setup-haxeshit/action.yml
index 9c1fae0b1..236d29944 100644
--- a/.github/actions/setup-haxeshit/action.yml
+++ b/.github/actions/setup-haxeshit/action.yml
@@ -3,21 +3,46 @@ description: "sets up haxe shit, using HMM!"
runs:
using: "composite"
steps:
- - uses: funkincrew/ci-haxe@v3
+ - name: Install Haxe lol
+ uses: funkincrew/ci-haxe@v3.1.0
with:
haxe-version: 4.3.3
- name: Config haxelib
run: |
- haxelib config
+ haxelib --never install haxelib 4.1.0 --global
+ haxelib --never deleterepo || true
+ haxelib --never newrepo
+ echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV"
+ haxelib --never git haxelib https://github.com/HaxeFoundation/haxelib.git master
shell: bash
- - name: Installing Haxe lol
+ - name: Gather debug info
+ run: |
+ cat << EOF >> "$GITHUB_STEP_SUMMARY"
+ ## haxe
+ - version: \`$(haxe -version)\`
+ - exe: \`$(which haxe)\`
+ ## haxelib
+ - version: \`$(haxelib version)\`
+ - exe: \`$(which haxelib)\`
+ - path: \`$HAXEPATH\`
+ ### local
+ - config: \`$(haxelib config)\`
+ - path: \`$(haxelib path haxelib || true)\`
+ ### global
+ - config: \`$(haxelib config --global)\`
+ - path: \`$(haxelib path haxelib --global || true)\`
+ ### system
+ - version: \`$(haxelib --system version)\`
+ - local: \`$(haxelib --system config)\`
+ - global: \`$(haxelib --system config --global)\`
+ EOF
+ shell: bash
+ - name: Install hmm
+ # hmm only supports global installs
run: |
- haxe -version
- haxelib git haxelib https://github.com/HaxeFoundation/haxelib.git master
- haxelib version
haxelib --global install hmm
shell: bash
- - name: dependency install cache
+ - name: Restore cached dependencies
id: cache-hmm
uses: actions/cache@v4
with:
diff --git a/.github/workflows/build-shit.yml b/.github/workflows/build-shit.yml
index 8ea3b16f3..49bab1ac1 100644
--- a/.github/workflows/build-shit.yml
+++ b/.github/workflows/build-shit.yml
@@ -8,71 +8,80 @@ jobs:
runs-on: [self-hosted, linux]
container: ubuntu:23.10
steps:
- - name: prepare container
+ - name: Install tools missing in container
run: |
apt update
- apt install sudo git curl unzip -y
- git config --global --add safe.directory $GITHUB_WORKSPACE
- - name: get token from gh app
+ apt install -y sudo git curl unzip
+ - name: Fix git config on posix runner
+ run: |
+ git config --global --add safe.directory ${{ github.workspace }}
+ - name: Get checkout token
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- - name: checkout repo
+ - name: Checkout repo
uses: funkincrew/ci-checkout@v6
with:
submodules: 'recursive'
token: ${{ steps.app_token.outputs.token }}
- - uses: ./.github/actions/setup-haxeshit
- - name: gather game dependencies
+ - name: Install Haxe, dependencies
+ uses: ./.github/actions/setup-haxeshit
+ - name: Install native dependencies
run: |
- sudo apt-get install -y libx11-dev xorg-dev libgl-dev libxi-dev libxext-dev libasound2-dev libxinerama-dev libxrandr-dev libgl1-mesa-dev
- - name: build game
+ apt install -y \
+ libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \
+ libgl-dev libgl1-mesa-dev \
+ libasound2-dev
+ - name: Build game
run: |
haxelib run lime build html5 -release --times -DGITHUB_BUILD
- ls
- - uses: ./.github/actions/upload-itch
+ - name: Upload build artifacts
+ uses: ./.github/actions/upload-itch
with:
butler-key: ${{ secrets.BUTLER_API_KEY}}
build-dir: export/release/html5/bin
target: html5
create-nightly-win:
- runs-on: windows-latest
+ runs-on: [self-hosted, windows]
+ defaults:
+ run:
+ shell: bash
steps:
- - name: get token from gh app
+ - name: Get checkout token
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- - name: checkout repo
+ - name: Checkout repo
uses: funkincrew/ci-checkout@v6
with:
submodules: 'recursive'
token: ${{ steps.app_token.outputs.token }}
- - uses: ./.github/actions/setup-haxeshit
- - name: Make HXCPP cache dir
+ - name: Install Haxe, dependencies
+ uses: ./.github/actions/setup-haxeshit
+ - name: Setup build cache
run: |
- mkdir -p ${{ runner.temp }}\hxcpp_cache
+ mkdir -p ${{ runner.temp }}/hxcpp_cache
- name: Restore build cache
id: cache-build-win
uses: actions/cache@v4
with:
path: |
- .haxelib
export
- ${{ runner.temp }}\hxcpp_cache
- key: ${{ runner.os }}-build-win-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
- - name: build game
+ ${{ runner.temp }}/hxcpp_cache
+ key: ${{ runner.os }}-build-win-${{ github.ref_name }}
+ - name: Build game
run: |
- haxelib run lime build windows -release -DNO_REDIRECT_ASSETS_FOLDER -DGITHUB_BUILD
- dir
+ haxelib run lime build windows -v -release -DGITHUB_BUILD
env:
HXCPP_COMPILE_CACHE: "${{ runner.temp }}\\hxcpp_cache"
- - uses: ./.github/actions/upload-itch
+ - name: Upload build artifacts
+ uses: ./.github/actions/upload-itch
with:
butler-key: ${{ secrets.BUTLER_API_KEY }}
build-dir: export/release/windows/bin
@@ -80,78 +89,42 @@ jobs:
create-nightly-mac:
runs-on: [self-hosted, macos]
steps:
- - name: prepare container
+ - name: Fix git config on posix runner
run: |
- git config --global --add safe.directory $GITHUB_WORKSPACE
- - name: get token from gh app
+ git config --global --add safe.directory ${{ github.workspace }}
+ - name: Get checkout token
uses: actions/create-github-app-token@v1
id: app_token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ github.repository_owner }}
- - name: checkout repo
+ - name: Checkout repo
uses: funkincrew/ci-checkout@v6
with:
submodules: 'recursive'
token: ${{ steps.app_token.outputs.token }}
- - uses: ./.github/actions/setup-haxeshit
- - name: Make HXCPP cache dir
+ - name: Install Haxe, dependencies
+ uses: ./.github/actions/setup-haxeshit
+ - name: Setup build cache
run: |
mkdir -p ${{ runner.temp }}/hxcpp_cache
- - name: restore build cache
+ - name: Restore build cache
id: cache-build-win
uses: actions/cache@v4
with:
path: |
- .haxelib
export
${{ runner.temp }}/hxcpp_cache
- key: ${{ runner.os }}-build-mac-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
+ key: ${{ runner.os }}-build-mac-${{ github.ref_name }}
- name: Build game
run: |
haxelib run lime build macos -release --times -DGITHUB_BUILD
- ls
env:
HXCPP_COMPILE_CACHE: "${{ runner.temp }}/hxcpp_cache"
- - uses: ./.github/actions/upload-itch
+ - name: Upload build artifacts
+ uses: ./.github/actions/upload-itch
with:
butler-key: ${{ secrets.BUTLER_API_KEY}}
build-dir: export/release/macos/bin
target: macos
-
-# test-unit-win:
-# needs: create-nightly-win
-# runs-on: windows-latest
-# steps:
-# - name: get token from gh app
-# uses: actions/create-github-app-token@v1
-# id: app_token
-# with:
-# app-id: ${{ vars.APP_ID }}
-# private-key: ${{ secrets.APP_PEM }}
-# owner: ${{ github.repository_owner }}
-# - name: checkout repo
-# uses: funkincrew/ci-checkout@v6
-# with:
-# submodules: 'recursive'
-# token: ${{ steps.app_token.outputs.token }}
-# - name: Make HXCPP cache dir
-# run: |
-# mkdir -p ${{ runner.temp }}\hxcpp_cache
-# - name: Restore build cache
-# id: cache-build-win
-# uses: actions/cache@v4
-# with:
-# path: |
-# .haxelib
-# export
-# ${{ runner.temp }}\hxcpp_cache
-# key: ${{ runner.os }}-test-win-${{ github.ref_name }}-${{ hashFiles('**/hmm.json') }}
-# - uses: ./.github/actions/setup-haxeshit
-# - name: Run unit tests
-# run: |
-# cd ./tests/unit/
-# ./start-win-native.bat
-# env:
-# HXCPP_COMPILE_CACHE: "${{ runner.temp }}\\hxcpp_cache"
diff --git a/.gitignore b/.gitignore
index b2fe731ea..b6a415fe9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,10 @@
.DS_STORE
.haxelib/
+.vs/
APIStuff.hx
dump/
export/
RECOVER_*.fla
shitAudio/
+.build_time
+.swp
diff --git a/.gitmodules b/.gitmodules
index 17c3cc026..452c0089b 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,10 +1,6 @@
[submodule "assets"]
path = assets
- url = https://github.com/FunkinCrew/Funkin-history-rewrite-assets
- branch = master
- update = merge
+ url = https://github.com/FunkinCrew/Funkin-Assets-secret
[submodule "art"]
path = art
- url = https://github.com/FunkinCrew/Funkin-history-rewrite-art
- branch = master
- update = merge
+ url = https://github.com/FunkinCrew/Funkin-Art-secret
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 000000000..2e132c06f
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,11 @@
+# Ignore artifacts
+export
+
+# Ignore all JSONS in the images folder (including FlxAnimate JSONs)
+assets/preload/images
+assets/shared/images
+assets/weekend1/images
+
+# Don't ignore data files
+# TODO: These don't work.
+!assets/preload/data/
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 2468e883a..84af3a3fd 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -71,6 +71,7 @@
"haxe.displayPort": "auto",
"haxe.enableCompilationServer": false,
+ "haxe.enableServerView": true,
"haxe.displayServer": {
"arguments": ["-v"]
},
@@ -84,7 +85,7 @@
},
"projectManager.git.baseFolders": ["./"],
- "haxecheckstyle.sourceFolders": ["src", "Source"],
+ "haxecheckstyle.sourceFolders": ["src", "source"],
"haxecheckstyle.externalSourceRoots": [],
"haxecheckstyle.configurationFile": "checkstyle.json",
"haxecheckstyle.codeSimilarityBufferSize": 100,
@@ -93,7 +94,7 @@
{
"label": "Windows / Debug",
"target": "windows",
- "args": ["-debug"]
+ "args": ["-debug", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug",
@@ -103,7 +104,7 @@
{
"label": "Windows / Debug (FlxAnimate Test)",
"target": "windows",
- "args": ["-debug", "-DANIMATE"]
+ "args": ["-debug", "-DANIMATE", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (FlxAnimate Test)",
@@ -113,7 +114,7 @@
{
"label": "Windows / Debug (Straight to Freeplay)",
"target": "windows",
- "args": ["-debug", "-DFREEPLAY"]
+ "args": ["-debug", "-DFREEPLAY", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Straight to Freeplay)",
@@ -123,7 +124,11 @@
{
"label": "Windows / Debug (Straight to Play - Bopeebo Normal)",
"target": "windows",
- "args": ["-debug", "-DSONG=bopeebo -DDIFFICULTY=normal"]
+ "args": [
+ "-debug",
+ "-DSONG=bopeebo -DDIFFICULTY=normal",
+ "-DFORCE_DEBUG_VERSION"
+ ]
},
{
"label": "HashLink / Debug (Straight to Play - Bopeebo Normal)",
@@ -133,7 +138,7 @@
{
"label": "Windows / Debug (Conversation Test)",
"target": "windows",
- "args": ["-debug", "-DDIALOGUE"]
+ "args": ["-debug", "-DDIALOGUE", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Conversation Test)",
@@ -143,7 +148,7 @@
{
"label": "Windows / Debug (Straight to Chart Editor)",
"target": "windows",
- "args": ["-debug", "-DCHARTING"]
+ "args": ["-debug", "-DCHARTING", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Straight to Chart Editor)",
@@ -153,7 +158,7 @@
{
"label": "Windows / Debug (Straight to Animation Editor)",
"target": "windows",
- "args": ["-debug", "-DANIMDEBUG"]
+ "args": ["-debug", "-DANIMDEBUG", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Straight to Animation Editor)",
@@ -163,7 +168,7 @@
{
"label": "Windows / Debug (Latency Test)",
"target": "windows",
- "args": ["-debug", "-DLATENCY"]
+ "args": ["-debug", "-DLATENCY", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HashLink / Debug (Latency Test)",
@@ -173,7 +178,17 @@
{
"label": "Windows / Debug (Waveform Test)",
"target": "windows",
- "args": ["-debug", "-DWAVEFORM"]
+ "args": ["-debug", "-DWAVEFORM", "-DFORCE_DEBUG_VERSION"]
+ },
+ {
+ "label": "Windows / Release",
+ "target": "windows",
+ "args": ["-release"]
+ },
+ {
+ "label": "Windows / Release (GitHub Actions)",
+ "target": "windows",
+ "args": ["-release", "-DGITHUB_BUILD"]
},
{
"label": "HashLink / Debug (Waveform Test)",
@@ -183,12 +198,12 @@
{
"label": "HTML5 / Debug",
"target": "html5",
- "args": ["-debug"]
+ "args": ["-debug", "-DFORCE_DEBUG_VERSION"]
},
{
"label": "HTML5 / Debug (Watch)",
"target": "html5",
- "args": ["-debug", "-watch"]
+ "args": ["-debug", "-watch", "-DFORCE_DEBUG_VERSION"]
}
],
"cmake.configureOnOpen": false,
diff --git a/Project.xml b/Project.xml
index c58153575..ffc8382a4 100644
--- a/Project.xml
+++ b/Project.xml
@@ -22,8 +22,8 @@
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
@@ -108,7 +111,7 @@
-
+
@@ -194,7 +197,7 @@
-
+
-
+
diff --git a/assets b/assets
index e76470d66..526743915 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit e76470d66794bf3796f7ace84f2121b698e37a30
+Subproject commit 52674391511577300cdb8c08df293ea72099aa82
diff --git a/checkstyle.json b/checkstyle.json
index 445f82b42..5300d94ad 100644
--- a/checkstyle.json
+++ b/checkstyle.json
@@ -23,10 +23,10 @@
},
{
"props": {
- "allowSingleArgParens": false,
- "allowReturn": false,
- "allowFunction": false,
- "allowCurlyBody": false
+ "allowSingleArgParens": true,
+ "allowReturn": true,
+ "allowFunction": true,
+ "allowCurlyBody": true
},
"type": "ArrowFunction"
},
@@ -79,8 +79,8 @@
{
"props": {
"ignoreExtern": true,
- "format": "^[A-Z][A-Z0-9]*(_[A-Z0-9_]+)*$",
- "tokens": []
+ "format": "^[a-z][A-Z][A-Z0-9]*(_[A-Z0-9_]+)*$",
+ "tokens": ["INLINE", "NOTINLINE"]
},
"type": "ConstantName"
},
@@ -178,13 +178,7 @@
"fieldType": "BOTH",
"requireReturn": true,
"ignoreOverride": true,
- "tokens": [
- "ABSTRACT_DEF",
- "CLASS_DEF",
- "ENUM_DEF",
- "INTERFACE_DEF",
- "TYPEDEF_DEF"
- ],
+ "tokens": ["ABSTRACT_DEF", "CLASS_DEF", "ENUM_DEF", "INTERFACE_DEF"],
"modifier": "PUBLIC",
"excludeNames": ["new", "toString"]
},
@@ -198,10 +192,6 @@
},
"type": "FileLength"
},
- {
- "props": {},
- "type": "Final"
- },
{
"props": {
"option": "upperCase"
@@ -375,7 +365,7 @@
},
{
"props": {
- "max": 1
+ "max": 4
},
"type": "NestedIfDepth"
},
@@ -387,7 +377,7 @@
},
{
"props": {
- "option": "nullDefault"
+ "option": "questionMark"
},
"type": "NullableParameter"
},
@@ -396,7 +386,6 @@
"oldFunctionTypePolicy": "none",
"unaryOpPolicy": "none",
"intervalOpPolicy": "none",
-
"newFunctionTypePolicy": "around",
"ternaryOpPolicy": "around",
"boolOpPolicy": "around",
@@ -589,19 +578,7 @@
},
{
"props": {
- "ignoreEnumAbstractValues": true
- },
- "type": "Type"
- },
- {
- "props": {
- "tokens": [
- "ABSTRACT_DEF",
- "CLASS_DEF",
- "ENUM_DEF",
- "INTERFACE_DEF",
- "TYPEDEF_DEF"
- ]
+ "tokens": ["CLASS_DEF", "ENUM_DEF", "INTERFACE_DEF", "TYPEDEF_DEF"]
},
"type": "TypeDocComment"
},
@@ -630,12 +607,6 @@
},
"type": "UnusedLocalVar"
},
- {
- "props": {
- "severity": "IGNORE"
- },
- "type": "VariableInitialisation"
- },
{
"props": {
"typeHintPolicy": "enforce_all",
diff --git a/hmm.json b/hmm.json
index dbb9d646e..6da12d535 100644
--- a/hmm.json
+++ b/hmm.json
@@ -11,7 +11,7 @@
"name": "flixel",
"type": "git",
"dir": null,
- "ref": "4d054bd10b05bb1309a0ba3427ffa5378e0b4b99",
+ "ref": "5823c46b4e7410372d58b99f8d5c52fc18b3cb3d",
"url": "https://github.com/FunkinCrew/flixel"
},
{
@@ -54,14 +54,14 @@
"name": "haxeui-core",
"type": "git",
"dir": null,
- "ref": "8a7846b",
+ "ref": "0212d8fdfcafeb5f0d5a41e1ddba8ff21d0e183b",
"url": "https://github.com/haxeui/haxeui-core"
},
{
"name": "haxeui-flixel",
"type": "git",
"dir": null,
- "ref": "e9f880522e27134b29df4067f82df7d7e5237b70",
+ "ref": "63a906a6148958dbfde8c7b48d90b0693767fd95",
"url": "https://github.com/haxeui/haxeui-flixel"
},
{
@@ -73,8 +73,8 @@
"name": "hxCodec",
"type": "git",
"dir": null,
- "ref": "c8c47e706ad82a423783006ed901b6d93c89a421",
- "url": "https://github.com/polybiusproxy/hxCodec"
+ "ref": "387e1665d6feb5762358134f168e6ebfe46acec8",
+ "url": "https://github.com/FunkinCrew/hxCodec"
},
{
"name": "hxcpp",
@@ -146,7 +146,7 @@
"name": "polymod",
"type": "git",
"dir": null,
- "ref": "d5a3b8995f64d20b95f844454e8c3b38c3d3a9fa",
+ "ref": "8553b800965f225bb14c7ab8f04bfa9cdec362ac",
"url": "https://github.com/larsiusprime/polymod"
},
{
diff --git a/hxformat.json b/hxformat.json
index cacbbf810..88a528e34 100644
--- a/hxformat.json
+++ b/hxformat.json
@@ -1,31 +1,30 @@
-{
- "disableFormatting": false,
-
- "indentation": {
- "character": " ",
- "tabWidth": 2,
- "indentCaseLabels": true
- },
-
- "lineEnds": {
- "anonFunctionCurly": {
- "emptyCurly": "break",
- "leftCurly": "after",
- "rightCurly": "both"
- },
- "leftCurly": "both",
- "rightCurly": "both"
- },
-
- "sameLine": {
- "ifBody": "same",
- "ifElse": "next",
- "doWhile": "next",
- "tryBody": "next",
- "tryCatch": "next"
- },
-
- "whitespace": {
- "switchPolicy": "around"
- }
-}
+{
+ "disableFormatting": false,
+
+ "indentation": {
+ "character": " ",
+ "tabWidth": 2,
+ "indentCaseLabels": true
+ },
+ "lineEnds": {
+ "anonFunctionCurly": {
+ "emptyCurly": "break",
+ "leftCurly": "after",
+ "rightCurly": "both"
+ },
+ "leftCurly": "both",
+ "rightCurly": "both"
+ },
+
+ "sameLine": {
+ "ifBody": "same",
+ "ifElse": "next",
+ "doWhile": "next",
+ "tryBody": "next",
+ "tryCatch": "next"
+ },
+
+ "whitespace": {
+ "switchPolicy": "around"
+ }
+}
diff --git a/source/Main.hx b/source/Main.hx
index a40fda29d..f4c5d9eb2 100644
--- a/source/Main.hx
+++ b/source/Main.hx
@@ -11,9 +11,11 @@ import openfl.display.Sprite;
import openfl.events.Event;
import openfl.Lib;
import openfl.media.Video;
-import funkin.util.CLIUtil;
import openfl.net.NetStream;
+/**
+ * The main class which initializes HaxeFlixel and starts the game in its initial state.
+ */
class Main extends Sprite
{
var gameWidth:Int = 1280; // Width of the game in pixels (might be less / more in actual pixels depending on your zoom).
@@ -76,35 +78,45 @@ class Main extends Sprite
var netStream:NetStream;
var overlay:Sprite;
+ /**
+ * A frame counter displayed at the top left.
+ */
public static var fpsCounter:FPS;
+
+ /**
+ * A RAM counter displayed at the top left.
+ */
public static var memoryCounter:MemoryCounter;
function setupGame():Void
{
- /**
- * The `zoom` argument of FlxGame was removed in the dev branch of Flixel,
- * since it was considered confusing and unintuitive.
- * If you want to change how the game scales when you resize the window,
- * you can use `FlxG.scaleMode`.
- * -Eric
- */
-
initHaxeUI();
+ // addChild gets called by the user settings code.
fpsCounter = new FPS(10, 3, 0xFFFFFF);
- // addChild(fpsCounter); // Handled by Preferences.init
+
#if !html5
+ // addChild gets called by the user settings code.
+ // TODO: disabled on HTML5 (todo: find another method that works?)
memoryCounter = new MemoryCounter(10, 13, 0xFFFFFF);
- // addChild(memoryCounter);
#end
// George recommends binding the save before FlxGame is created.
Save.load();
+ var game:FlxGame = new FlxGame(gameWidth, gameHeight, initialState, framerate, framerate, skipSplash, startFullscreen);
- addChild(new FlxGame(gameWidth, gameHeight, initialState, framerate, framerate, skipSplash, startFullscreen));
+ // FlxG.game._customSoundTray wants just the class, it calls new from
+ // create() in there, which gets called when it's added to stage
+ // which is why it needs to be added before addChild(game) here
+ @:privateAccess
+ game._customSoundTray = funkin.ui.options.FunkinSoundTray;
+
+ addChild(game);
#if hxcpp_debug_server
trace('hxcpp_debug_server is enabled! You can now connect to the game with a debugger.');
+ #else
+ trace('hxcpp_debug_server is disabled! This build does not support debugging.');
#end
}
diff --git a/source/Postbuild.hx b/source/Postbuild.hx
index d48b670a4..405d0400b 100644
--- a/source/Postbuild.hx
+++ b/source/Postbuild.hx
@@ -1,11 +1,40 @@
package source; // Yeah, I know...
+import sys.FileSystem;
+import sys.io.File;
+
+/**
+ * A script which executes after the game is built.
+ */
class Postbuild
{
- static function main()
- {
- trace('Postbuild');
+ static inline final BUILD_TIME_FILE:String = '.build_time';
- // TODO: Maybe put a 'Build took X seconds' message here?
+ static function main():Void
+ {
+ printBuildTime();
+ }
+
+ static function printBuildTime():Void
+ {
+ // get buildEnd before fs operations since they are blocking
+ var end:Float = Sys.time();
+ if (FileSystem.exists(BUILD_TIME_FILE))
+ {
+ var fi:sys.io.FileInput = File.read(BUILD_TIME_FILE);
+ var start:Float = fi.readDouble();
+ fi.close();
+
+ sys.FileSystem.deleteFile(BUILD_TIME_FILE);
+
+ var buildTime:Float = roundToTwoDecimals(end - start);
+
+ trace('Build took: ${buildTime} seconds');
+ }
+ }
+
+ static function roundToTwoDecimals(value:Float):Float
+ {
+ return Math.round(value * 100) / 100;
}
}
diff --git a/source/Prebuild.hx b/source/Prebuild.hx
index 63782fc56..6856c414c 100644
--- a/source/Prebuild.hx
+++ b/source/Prebuild.hx
@@ -1,9 +1,25 @@
package source; // Yeah, I know...
+import sys.io.File;
+
+/**
+ * A script which executes before the game is built.
+ */
class Prebuild
{
- static function main()
+ static inline final BUILD_TIME_FILE:String = '.build_time';
+
+ static function main():Void
{
- trace('Prebuild');
+ saveBuildTime();
+ trace('Building...');
+ }
+
+ static function saveBuildTime():Void
+ {
+ var fo:sys.io.FileOutput = File.write(BUILD_TIME_FILE);
+ var now:Float = Sys.time();
+ fo.writeDouble(now);
+ fo.close();
}
}
diff --git a/source/flixel/addons/transition/FlxTransitionableSubState.hx b/source/flixel/addons/transition/FlxTransitionableSubState.hx
deleted file mode 100644
index ab416adbc..000000000
--- a/source/flixel/addons/transition/FlxTransitionableSubState.hx
+++ /dev/null
@@ -1,234 +0,0 @@
-package flixel.addons.transition;
-
-import flixel.FlxSubState;
-import flixel.addons.transition.FlxTransitionableState;
-
-/**
- * A `FlxSubState` which can perform visual transitions
- *
- * Usage:
- *
- * First, extend `FlxTransitionableSubState` as ie, `FooState`.
- *
- * Method 1:
- *
- * ```haxe
- * var in:TransitionData = new TransitionData(...); // add your data where "..." is
- * var out:TransitionData = new TransitionData(...);
- *
- * FlxG.switchState(() -> new FooState(in,out));
- * ```
- *
- * Method 2:
- *
- * ```haxe
- * FlxTransitionableSubState.defaultTransIn = new TransitionData(...);
- * FlxTransitionableSubState.defaultTransOut = new TransitionData(...);
- *
- * FlxG.switchState(() -> new FooState());
- * ```
- */
-class FlxTransitionableSubState extends FlxSubState
-{
- // global default transitions for ALL states, used if transIn/transOut are null
- public static var defaultTransIn(get, set):TransitionData;
-
- static function get_defaultTransIn():TransitionData
- {
- return FlxTransitionableState.defaultTransIn;
- }
-
- static function set_defaultTransIn(value:TransitionData):TransitionData
- {
- return FlxTransitionableState.defaultTransIn = value;
- }
-
- public static var defaultTransOut(get, set):TransitionData;
-
- static function get_defaultTransOut():TransitionData
- {
- return FlxTransitionableState.defaultTransOut;
- }
-
- static function set_defaultTransOut(value:TransitionData):TransitionData
- {
- return FlxTransitionableState.defaultTransOut = value;
- }
-
- public static var skipNextTransIn(get, set):Bool;
-
- static function get_skipNextTransIn():Bool
- {
- return FlxTransitionableState.skipNextTransIn;
- }
-
- static function set_skipNextTransIn(value:Bool):Bool
- {
- return FlxTransitionableState.skipNextTransIn = value;
- }
-
- public static var skipNextTransOut(get, set):Bool;
-
- static function get_skipNextTransOut():Bool
- {
- return FlxTransitionableState.skipNextTransOut;
- }
-
- static function set_skipNextTransOut(value:Bool):Bool
- {
- return FlxTransitionableState.skipNextTransOut = value;
- }
-
- // beginning & ending transitions for THIS state:
- public var transIn:TransitionData;
- public var transOut:TransitionData;
-
- public var hasTransIn(get, never):Bool;
- public var hasTransOut(get, never):Bool;
-
- /**
- * Create a state with the ability to do visual transitions
- * @param TransIn Plays when the state begins
- * @param TransOut Plays when the state ends
- */
- public function new(?TransIn:TransitionData, ?TransOut:TransitionData)
- {
- transIn = TransIn;
- transOut = TransOut;
-
- if (transIn == null && defaultTransIn != null)
- {
- transIn = defaultTransIn;
- }
- if (transOut == null && defaultTransOut != null)
- {
- transOut = defaultTransOut;
- }
- super();
- }
-
- override function destroy():Void
- {
- super.destroy();
- transIn = null;
- transOut = null;
- _onExit = null;
- }
-
- override function create():Void
- {
- super.create();
- transitionIn();
- }
-
- override function startOutro(onOutroComplete:() -> Void)
- {
- if (!hasTransOut) onOutroComplete();
- else if (!_exiting)
- {
- // play the exit transition, and when it's done call FlxG.switchState
- _exiting = true;
- transitionOut(onOutroComplete);
-
- if (skipNextTransOut)
- {
- skipNextTransOut = false;
- finishTransOut();
- }
- }
- }
-
- /**
- * Starts the in-transition. Can be called manually at any time.
- */
- public function transitionIn():Void
- {
- if (transIn != null && transIn.type != NONE)
- {
- if (skipNextTransIn)
- {
- skipNextTransIn = false;
- if (finishTransIn != null)
- {
- finishTransIn();
- }
- return;
- }
-
- var _trans = createTransition(transIn);
-
- _trans.setStatus(FULL);
- openSubState(_trans);
-
- _trans.finishCallback = finishTransIn;
- _trans.start(OUT);
- }
- }
-
- /**
- * Starts the out-transition. Can be called manually at any time.
- */
- public function transitionOut(?OnExit:Void->Void):Void
- {
- _onExit = OnExit;
- if (hasTransOut)
- {
- var _trans = createTransition(transOut);
-
- _trans.setStatus(EMPTY);
- openSubState(_trans);
-
- _trans.finishCallback = finishTransOut;
- _trans.start(IN);
- }
- else
- {
- _onExit();
- }
- }
-
- var transOutFinished:Bool = false;
-
- var _exiting:Bool = false;
- var _onExit:Void->Void;
-
- function get_hasTransIn():Bool
- {
- return transIn != null && transIn.type != NONE;
- }
-
- function get_hasTransOut():Bool
- {
- return transOut != null && transOut.type != NONE;
- }
-
- function createTransition(data:TransitionData):Transition
- {
- return switch (data.type)
- {
- case TILES: new Transition(data);
- case FADE: new Transition(data);
- default: null;
- }
- }
-
- function finishTransIn()
- {
- closeSubState();
- }
-
- function finishTransOut()
- {
- transOutFinished = true;
-
- if (!_exiting)
- {
- closeSubState();
- }
-
- if (_onExit != null)
- {
- _onExit();
- }
- }
-}
diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx
index 4b1b89dfa..aba154814 100644
--- a/source/funkin/Conductor.hx
+++ b/source/funkin/Conductor.hx
@@ -3,7 +3,6 @@ package funkin;
import funkin.util.Constants;
import flixel.util.FlxSignal;
import flixel.math.FlxMath;
-import funkin.play.song.Song.SongDifficulty;
import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.SongDataUtils;
import funkin.save.Save;
@@ -142,6 +141,9 @@ class Conductor
return beatLengthMs / timeSignatureNumerator;
}
+ /**
+ * The numerator for the current time signature (the `3` in `3/4`).
+ */
public var timeSignatureNumerator(get, never):Int;
function get_timeSignatureNumerator():Int
@@ -151,6 +153,9 @@ class Conductor
return currentTimeChange.timeSignatureNum;
}
+ /**
+ * The denominator for the current time signature (the `4` in `3/4`).
+ */
public var timeSignatureDenominator(get, never):Int;
function get_timeSignatureDenominator():Int
@@ -284,7 +289,7 @@ class Conductor
* WARNING: Avoid this for things like setting the BPM of the title screen music,
* you should have a metadata file for it instead.
*/
- public function forceBPM(?bpm:Float = null)
+ public function forceBPM(?bpm:Float):Void
{
if (bpm != null)
{
@@ -292,7 +297,7 @@ class Conductor
}
else
{
- // trace('[CONDUCTOR] Resetting BPM to default');
+ trace('[CONDUCTOR] Resetting BPM to default');
}
this.bpmOverride = bpm;
@@ -310,15 +315,15 @@ class Conductor
{
if (songPos == null)
{
- // Take into account instrumental and file format song offsets.
- songPos = (FlxG.sound.music != null) ? (FlxG.sound.music.time + instrumentalOffset + formatOffset + audioVisualOffset) : 0.0;
+ songPos = (FlxG.sound.music != null) ? FlxG.sound.music.time : 0.0;
}
- else
- songPos += applyOffsets ? instrumentalOffset + formatOffset + audioVisualOffset : 0;
- var oldMeasure = this.currentMeasure;
- var oldBeat = this.currentBeat;
- var oldStep = this.currentStep;
+ // Take into account instrumental and file format song offsets.
+ songPos += applyOffsets ? (instrumentalOffset + formatOffset + audioVisualOffset) : 0;
+
+ var oldMeasure:Float = this.currentMeasure;
+ var oldBeat:Float = this.currentBeat;
+ var oldStep:Float = this.currentStep;
// Set the song position we are at (for purposes of calculating note positions, etc).
this.songPosition = songPos;
@@ -409,39 +414,43 @@ class Conductor
return this.songPosition;
}
- public function mapTimeChanges(songTimeChanges:Array)
+ /**
+ * Apply the `SongTimeChange` data from the song metadata to this Conductor.
+ * @param songTimeChanges The SongTimeChanges.
+ */
+ public function mapTimeChanges(songTimeChanges:Array):Void
{
timeChanges = [];
// Sort in place just in case it's out of order.
SongDataUtils.sortTimeChanges(songTimeChanges);
- for (currentTimeChange in songTimeChanges)
+ for (songTimeChange in songTimeChanges)
{
// TODO: Maybe handle this different?
// Do we care about BPM at negative timestamps?
// Without any custom handling, `currentStepTime` becomes non-zero at `songPosition = 0`.
- if (currentTimeChange.timeStamp < 0.0) currentTimeChange.timeStamp = 0.0;
+ if (songTimeChange.timeStamp < 0.0) songTimeChange.timeStamp = 0.0;
- if (currentTimeChange.timeStamp <= 0.0)
+ if (songTimeChange.timeStamp <= 0.0)
{
- currentTimeChange.beatTime = 0.0;
+ songTimeChange.beatTime = 0.0;
}
else
{
// Calculate the beat time of this timestamp.
- currentTimeChange.beatTime = 0.0;
+ songTimeChange.beatTime = 0.0;
- if (currentTimeChange.timeStamp > 0.0 && timeChanges.length > 0)
+ if (songTimeChange.timeStamp > 0.0 && timeChanges.length > 0)
{
var prevTimeChange:SongTimeChange = timeChanges[timeChanges.length - 1];
- currentTimeChange.beatTime = FlxMath.roundDecimal(prevTimeChange.beatTime
- + ((currentTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC),
+ songTimeChange.beatTime = FlxMath.roundDecimal(prevTimeChange.beatTime
+ + ((songTimeChange.timeStamp - prevTimeChange.timeStamp) * prevTimeChange.bpm / Constants.SECS_PER_MIN / Constants.MS_PER_SEC),
4);
}
}
- timeChanges.push(currentTimeChange);
+ timeChanges.push(songTimeChange);
}
if (timeChanges.length > 0)
@@ -455,6 +464,8 @@ class Conductor
/**
* Given a time in milliseconds, return a time in steps.
+ * @param ms The time in milliseconds.
+ * @return The time in steps.
*/
public function getTimeInSteps(ms:Float):Float
{
@@ -484,7 +495,7 @@ class Conductor
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
var resultFractionalStep:Float = (ms - lastTimeChange.timeStamp) / lastStepLengthMs;
- resultStep += resultFractionalStep; // Math.floor();
+ resultStep += resultFractionalStep;
return resultStep;
}
@@ -492,6 +503,8 @@ class Conductor
/**
* Given a time in steps and fractional steps, return a time in milliseconds.
+ * @param stepTime The time in steps.
+ * @return The time in milliseconds.
*/
public function getStepTimeInMs(stepTime:Float):Float
{
@@ -528,6 +541,8 @@ class Conductor
/**
* Given a time in beats and fractional beats, return a time in milliseconds.
+ * @param beatTime The time in beats.
+ * @return The time in milliseconds.
*/
public function getBeatTimeInMs(beatTime:Float):Float
{
@@ -563,7 +578,8 @@ class Conductor
}
/**
- * @param conductorToUse defaults to Conductor.instance if null
+ * Adds Conductor fields to the Flixel debugger variable display.
+ * @param conductorToUse The conductor to use. Defaults to `Conductor.instance`.
*/
public static function watchQuick(?conductorToUse:Conductor):Void
{
diff --git a/source/funkin/Highscore.hx b/source/funkin/Highscore.hx
index 24b65832b..996e2367e 100644
--- a/source/funkin/Highscore.hx
+++ b/source/funkin/Highscore.hx
@@ -5,7 +5,42 @@ package funkin;
*/
class Highscore
{
+ /**
+ * Keeps track of notes hit for the current song
+ * and how accurate you were with each note (bad, missed, shit, etc.)
+ */
public static var tallies:Tallies = new Tallies();
+
+ /**
+ * Keeps track of notes hit for the current WEEK / level
+ * for use with storymode, or likely any other "playlist" esque option
+ */
+ public static var talliesLevel:Tallies = new Tallies();
+
+ /**
+ * Produces a new Tallies object which represents the sum of two existing Tallies
+ * @param newTally The first tally
+ * @param baseTally The second tally
+ * @return The combined tally
+ */
+ public static function combineTallies(newTally:Tallies, baseTally:Tallies):Tallies
+ {
+ var combinedTally:Tallies = new Tallies();
+ combinedTally.missed = newTally.missed + baseTally.missed;
+ combinedTally.shit = newTally.shit + baseTally.shit;
+ combinedTally.bad = newTally.bad + baseTally.bad;
+ combinedTally.good = newTally.good + baseTally.good;
+ combinedTally.sick = newTally.sick + baseTally.sick;
+ combinedTally.totalNotes = newTally.totalNotes + baseTally.totalNotes;
+ combinedTally.totalNotesHit = newTally.totalNotesHit + baseTally.totalNotesHit;
+
+ // Current combo = use most recent.
+ combinedTally.combo = newTally.combo;
+ // Max combo = use maximum value.
+ combinedTally.maxCombo = Std.int(Math.max(newTally.maxCombo, baseTally.maxCombo));
+
+ return combinedTally;
+ }
}
@:forward
@@ -29,6 +64,9 @@ abstract Tallies(RawTallies)
}
}
+/**
+ * A structure object containing the data for highscore tallies.
+ */
typedef RawTallies =
{
var combo:Int;
@@ -51,7 +89,7 @@ typedef RawTallies =
var totalNotesHit:Int;
/**
- * How many notes PASSED BY AND/OR HIT!!!
+ * How many notes in the current chart
*/
var totalNotes:Int;
}
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index 0ad3c19b8..6ea77ec18 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -12,7 +12,6 @@ import flixel.math.FlxRect;
import flixel.FlxSprite;
import flixel.system.debug.log.LogStyle;
import flixel.util.FlxColor;
-import funkin.ui.options.PreferencesMenu;
import funkin.util.macro.MacroUtil;
import funkin.util.WindowUtil;
import funkin.play.PlayStatePlaylist;
@@ -24,13 +23,15 @@ import funkin.data.stage.StageRegistry;
import funkin.data.dialogue.ConversationRegistry;
import funkin.data.dialogue.DialogueBoxRegistry;
import funkin.data.dialogue.SpeakerRegistry;
+import funkin.data.freeplay.AlbumRegistry;
import funkin.data.song.SongRegistry;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.modding.module.ModuleHandler;
import funkin.ui.title.TitleState;
import funkin.util.CLIUtil;
import funkin.util.CLIUtil.CLIParams;
-import funkin.ui.transition.LoadingState;
+import funkin.util.TimerUtil;
+import funkin.util.TrackerUtil;
#if discord_rpc
import Discord.DiscordClient;
#end
@@ -66,7 +67,7 @@ class InitState extends FlxState
/**
* Setup a bunch of important Flixel stuff.
*/
- function setupShit()
+ function setupShit():Void
{
//
// GAME SETUP
@@ -88,70 +89,7 @@ class InitState extends FlxState
// Set the game to a lower frame rate while it is in the background.
FlxG.game.focusLostFramerate = 30;
- //
- // FLIXEL DEBUG SETUP
- //
- #if (debug || FORCE_DEBUG_VERSION)
- // Disable using ~ to open the console (we use that for the Editor menu)
- FlxG.debugger.toggleKeys = [F2];
-
- // Adds an additional Close Debugger button.
- // This big obnoxious white button is for MOBILE, so that you can press it
- // easily with your finger when debug bullshit pops up during testing lol!
- FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function() {
- FlxG.debugger.visible = false;
- });
-
- // Adds a red button to the debugger.
- // This pauses the game AND the music! This ensures the Conductor stops.
- FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFFCC2233), function() {
- if (FlxG.vcr.paused)
- {
- FlxG.vcr.resume();
-
- for (snd in FlxG.sound.list)
- {
- snd.resume();
- }
-
- FlxG.sound.music.resume();
- }
- else
- {
- FlxG.vcr.pause();
-
- for (snd in FlxG.sound.list)
- {
- snd.pause();
- }
-
- FlxG.sound.music.pause();
- }
- });
-
- // Adds a blue button to the debugger.
- // This skips forward in the song.
- FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFF2222CC), function() {
- FlxG.game.debugger.vcr.onStep();
-
- for (snd in FlxG.sound.list)
- {
- snd.pause();
- snd.time += FlxG.elapsed * 1000;
- }
-
- FlxG.sound.music.pause();
- FlxG.sound.music.time += FlxG.elapsed * 1000;
- });
- #end
-
- // Make errors and warnings less annoying.
- #if FORCE_DEBUG_VERSION
- LogStyle.ERROR.openConsole = false;
- LogStyle.ERROR.errorSound = null;
- LogStyle.WARNING.openConsole = false;
- LogStyle.WARNING.errorSound = null;
- #end
+ setupFlixelDebug();
//
// FLIXEL TRANSITIONS
@@ -202,8 +140,12 @@ class InitState extends FlxState
//
// Plugins provide a useful interface for globally active Flixel objects,
// that receive update events regardless of the current state.
- // TODO: Move Module behavior to a Flixel plugin.
+ // TODO: Move scripted Module behavior to a Flixel plugin.
+ #if debug
+ funkin.util.plugins.MemoryGCPlugin.initialize();
+ #end
funkin.util.plugins.EvacuateDebugPlugin.initialize();
+ funkin.util.plugins.ForceCrashPlugin.initialize();
funkin.util.plugins.ReloadAssetsDebugPlugin.initialize();
funkin.util.plugins.ScreenshotPlugin.initialize();
funkin.util.plugins.VolumePlugin.initialize();
@@ -216,7 +158,7 @@ class InitState extends FlxState
// NOTE: Registries must be imported and not referenced with fully qualified names,
// to ensure build macros work properly.
trace('Parsing game data...');
- var perfStart = haxe.Timer.stamp();
+ var perfStart:Float = TimerUtil.start();
SongEventRegistry.loadEventCache(); // SongEventRegistry is structured differently so it's not a BaseRegistry.
SongRegistry.instance.loadEntries();
LevelRegistry.instance.loadEntries();
@@ -224,18 +166,18 @@ class InitState extends FlxState
ConversationRegistry.instance.loadEntries();
DialogueBoxRegistry.instance.loadEntries();
SpeakerRegistry.instance.loadEntries();
+ AlbumRegistry.instance.loadEntries();
StageRegistry.instance.loadEntries();
- // TODO: CharacterDataParser doesn't use json2object, so it's way slower than the other parsers.
- CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
+ // TODO: CharacterDataParser doesn't use json2object, so it's way slower than the other parsers and more prone to syntax errors.
+ // Move it to use a BaseRegistry.
+ CharacterDataParser.loadCharacterCache();
ModuleHandler.buildModuleCallbacks();
ModuleHandler.loadModuleCache();
ModuleHandler.callOnCreate();
- var perfEnd = haxe.Timer.stamp();
-
- trace('Parsing game data took ${Math.floor((perfEnd - perfStart) * 1000)}ms.');
+ trace('Parsing game data took: ${TimerUtil.ms(perfStart)}');
}
/**
@@ -247,25 +189,35 @@ class InitState extends FlxState
*/
function startGame():Void
{
- #if SONG // -DSONG=bopeebo
+ #if SONG
+ // -DSONG=bopeebo
startSong(defineSong(), defineDifficulty());
- #elseif LEVEL // -DLEVEL=week1 -DDIFFICULTY=hard
+ #elseif LEVEL
+ // -DLEVEL=week1 -DDIFFICULTY=hard
startLevel(defineLevel(), defineDifficulty());
- #elseif FREEPLAY // -DFREEPLAY
+ #elseif FREEPLAY
+ // -DFREEPLAY
FlxG.switchState(() -> new funkin.ui.freeplay.FreeplayState());
- #elseif DIALOGUE // -DDIALOGUE
+ #elseif DIALOGUE
+ // -DDIALOGUE
FlxG.switchState(() -> new funkin.ui.debug.dialogue.ConversationDebugState());
- #elseif ANIMATE // -DANIMATE
+ #elseif ANIMATE
+ // -DANIMATE
FlxG.switchState(() -> new funkin.ui.debug.anim.FlxAnimateTest());
- #elseif WAVEFORM // -DWAVEFORM
+ #elseif WAVEFORM
+ // -DWAVEFORM
FlxG.switchState(() -> new funkin.ui.debug.WaveformTestState());
- #elseif CHARTING // -DCHARTING
+ #elseif CHARTING
+ // -DCHARTING
FlxG.switchState(() -> new funkin.ui.debug.charting.ChartEditorState());
- #elseif STAGEBUILD // -DSTAGEBUILD
+ #elseif STAGEBUILD
+ // -DSTAGEBUILD
FlxG.switchState(() -> new funkin.ui.debug.stage.StageBuilderState());
- #elseif ANIMDEBUG // -DANIMDEBUG
+ #elseif ANIMDEBUG
+ // -DANIMDEBUG
FlxG.switchState(() -> new funkin.ui.debug.anim.DebugBoundingState());
- #elseif LATENCY // -DLATENCY
+ #elseif LATENCY
+ // -DLATENCY
FlxG.switchState(() -> new funkin.LatencyState());
#else
startGameNormally();
@@ -346,6 +298,80 @@ class InitState extends FlxState
});
}
+ function setupFlixelDebug():Void
+ {
+ //
+ // FLIXEL DEBUG SETUP
+ //
+ #if (debug || FORCE_DEBUG_VERSION)
+ // Make errors and warnings less annoying.
+ // Forcing this always since I have never been happy to have the debugger to pop up
+ LogStyle.ERROR.openConsole = false;
+ LogStyle.ERROR.errorSound = null;
+ LogStyle.WARNING.openConsole = false;
+ LogStyle.WARNING.errorSound = null;
+
+ // Disable using ~ to open the console (we use that for the Editor menu)
+ FlxG.debugger.toggleKeys = [F2];
+ TrackerUtil.initTrackers();
+ // Adds an additional Close Debugger button.
+ // This big obnoxious white button is for MOBILE, so that you can press it
+ // easily with your finger when debug bullshit pops up during testing lol!
+ FlxG.debugger.addButton(LEFT, new BitmapData(200, 200), function() {
+ FlxG.debugger.visible = false;
+
+ // Make errors and warnings less annoying.
+ // Forcing this always since I have never been happy to have the debugger to pop up
+ LogStyle.ERROR.openConsole = false;
+ LogStyle.ERROR.errorSound = null;
+ LogStyle.WARNING.openConsole = false;
+ LogStyle.WARNING.errorSound = null;
+ });
+
+ // Adds a red button to the debugger.
+ // This pauses the game AND the music! This ensures the Conductor stops.
+ FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFFCC2233), function() {
+ if (FlxG.vcr.paused)
+ {
+ FlxG.vcr.resume();
+
+ for (snd in FlxG.sound.list)
+ {
+ snd.resume();
+ }
+
+ FlxG.sound.music.resume();
+ }
+ else
+ {
+ FlxG.vcr.pause();
+
+ for (snd in FlxG.sound.list)
+ {
+ snd.pause();
+ }
+
+ FlxG.sound.music.pause();
+ }
+ });
+
+ // Adds a blue button to the debugger.
+ // This skips forward in the song.
+ FlxG.debugger.addButton(CENTER, new BitmapData(20, 20, true, 0xFF2222CC), function() {
+ FlxG.game.debugger.vcr.onStep();
+
+ for (snd in FlxG.sound.list)
+ {
+ snd.pause();
+ snd.time += FlxG.elapsed * 1000;
+ }
+
+ FlxG.sound.music.pause();
+ FlxG.sound.music.time += FlxG.elapsed * 1000;
+ });
+ #end
+ }
+
function defineSong():String
{
return MacroUtil.getDefine('SONG');
diff --git a/source/funkin/Paths.hx b/source/funkin/Paths.hx
index e0212e573..d2c4833f2 100644
--- a/source/funkin/Paths.hx
+++ b/source/funkin/Paths.hx
@@ -11,130 +11,144 @@ class Paths
{
static var currentLevel:String;
- static public function setCurrentLevel(name:String)
+ public static function setCurrentLevel(name:String):Void
{
currentLevel = name.toLowerCase();
}
- static function getPath(file:String, type:AssetType, library:Null)
+ public static function stripLibrary(path:String):String
+ {
+ var parts:Array = path.split(':');
+ if (parts.length < 2) return path;
+ return parts[1];
+ }
+
+ public static function getLibrary(path:String):String
+ {
+ var parts:Array = path.split(':');
+ if (parts.length < 2) return 'preload';
+ return parts[0];
+ }
+
+ static function getPath(file:String, type:AssetType, library:Null):String
{
if (library != null) return getLibraryPath(file, library);
if (currentLevel != null)
{
- var levelPath = getLibraryPathForce(file, currentLevel);
+ var levelPath:String = getLibraryPathForce(file, currentLevel);
if (OpenFlAssets.exists(levelPath, type)) return levelPath;
}
- var levelPath = getLibraryPathForce(file, "shared");
+ var levelPath:String = getLibraryPathForce(file, 'shared');
if (OpenFlAssets.exists(levelPath, type)) return levelPath;
return getPreloadPath(file);
}
- static public function getLibraryPath(file:String, library = "preload")
+ public static function getLibraryPath(file:String, library = 'preload'):String
{
- return if (library == "preload" || library == "default") getPreloadPath(file); else getLibraryPathForce(file, library);
+ return if (library == 'preload' || library == 'default') getPreloadPath(file); else getLibraryPathForce(file, library);
}
- inline static function getLibraryPathForce(file:String, library:String)
+ static inline function getLibraryPathForce(file:String, library:String):String
{
return '$library:assets/$library/$file';
}
- inline static function getPreloadPath(file:String)
+ static inline function getPreloadPath(file:String):String
{
return 'assets/$file';
}
- inline static public function file(file:String, type:AssetType = TEXT, ?library:String)
+ public static function file(file:String, type:AssetType = TEXT, ?library:String):String
{
return getPath(file, type, library);
}
- public static inline function animateAtlas(path:String, ?library:String)
+ public static function animateAtlas(path:String, ?library:String):String
{
return getLibraryPath('images/$path', library);
}
- inline static public function txt(key:String, ?library:String)
+ public static function txt(key:String, ?library:String):String
{
return getPath('data/$key.txt', TEXT, library);
}
- inline static public function frag(key:String, ?library:String)
+ public static function frag(key:String, ?library:String):String
{
return getPath('shaders/$key.frag', TEXT, library);
}
- inline static public function vert(key:String, ?library:String)
+ public static function vert(key:String, ?library:String):String
{
return getPath('shaders/$key.vert', TEXT, library);
}
- inline static public function xml(key:String, ?library:String)
+ public static function xml(key:String, ?library:String):String
{
return getPath('data/$key.xml', TEXT, library);
}
- inline static public function json(key:String, ?library:String)
+ public static function json(key:String, ?library:String):String
{
return getPath('data/$key.json', TEXT, library);
}
- static public function sound(key:String, ?library:String)
+ public static function sound(key:String, ?library:String):String
{
return getPath('sounds/$key.${Constants.EXT_SOUND}', SOUND, library);
}
- inline static public function soundRandom(key:String, min:Int, max:Int, ?library:String)
+ public static function soundRandom(key:String, min:Int, max:Int, ?library:String):String
{
return sound(key + FlxG.random.int(min, max), library);
}
- inline static public function music(key:String, ?library:String)
+ public static function music(key:String, ?library:String):String
{
return getPath('music/$key.${Constants.EXT_SOUND}', MUSIC, library);
}
- inline static public function videos(key:String, ?library:String)
+ public static function videos(key:String, ?library:String):String
{
return getPath('videos/$key.${Constants.EXT_VIDEO}', BINARY, library);
}
- inline static public function voices(song:String, ?suffix:String = '')
+ public static function voices(song:String, ?suffix:String = ''):String
{
- if (suffix == null) suffix = ""; // no suffix, for a sorta backwards compatibility with older-ish voice files
+ if (suffix == null) suffix = ''; // no suffix, for a sorta backwards compatibility with older-ish voice files
return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.${Constants.EXT_SOUND}';
}
- inline static public function inst(song:String, ?suffix:String = '')
+ public static function inst(song:String, ?suffix:String = ''):String
{
return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.${Constants.EXT_SOUND}';
}
- inline static public function image(key:String, ?library:String)
+ public static function image(key:String, ?library:String):String
{
return getPath('images/$key.png', IMAGE, library);
}
- inline static public function font(key:String)
+ public static function font(key:String):String
{
return 'assets/fonts/$key';
}
- inline static public function ui(key:String, ?library:String)
+ public static function ui(key:String, ?library:String):String
{
return xml('ui/$key', library);
}
- static public function getSparrowAtlas(key:String, ?library:String)
+ public static function getSparrowAtlas(key:String, ?library:String):FlxAtlasFrames
{
return FlxAtlasFrames.fromSparrow(image(key, library), file('images/$key.xml', library));
}
- inline static public function getPackerAtlas(key:String, ?library:String)
+ public static function getPackerAtlas(key:String, ?library:String):FlxAtlasFrames
{
return FlxAtlasFrames.fromSpriteSheetPacker(image(key, library), file('images/$key.txt', library));
}
diff --git a/source/funkin/PlayerSettings.hx b/source/funkin/PlayerSettings.hx
index 2e3e0e425..b4c871c29 100644
--- a/source/funkin/PlayerSettings.hx
+++ b/source/funkin/PlayerSettings.hx
@@ -13,6 +13,7 @@ import flixel.util.FlxSignal;
*/
class PlayerSettings
{
+ // TODO: Finish implementation of second player.
public static var numPlayers(default, null) = 0;
public static var numAvatars(default, null) = 0;
public static var player1(default, null):PlayerSettings;
@@ -21,12 +22,21 @@ class PlayerSettings
public static var onAvatarAdd(default, null) = new FlxTypedSignalVoid>();
public static var onAvatarRemove(default, null) = new FlxTypedSignalVoid>();
+ /**
+ * The player number associated with this settings object.
+ */
public var id(default, null):Int;
+ /**
+ * The controls handler for this player.
+ */
public var controls(default, null):Controls;
/**
* Return the PlayerSettings for the given player number, or `null` if that player isn't active.
+ *
+ * @param id The player number this represents.
+ * @return The PlayerSettings for the given player number, or `null` if that player isn't active.
*/
public static function get(id:Int):Null
{
@@ -38,6 +48,9 @@ class PlayerSettings
};
}
+ /**
+ * Initialize the PlayerSettings singletons for each player.
+ */
public static function init():Void
{
if (player1 == null)
@@ -56,22 +69,30 @@ class PlayerSettings
}
}
- public static function reset()
+ /**
+ * Forcibly destroy the PlayerSettings singletons for each player.
+ */
+ public static function reset():Void
{
player1 = null;
player2 = null;
numPlayers = 0;
}
- static function onGamepadAdded(gamepad:FlxGamepad)
+ /**
+ * Callback invoked when a gamepad is added.
+ * @param gamepad The gamepad that was added.
+ */
+ static function onGamepadAdded(gamepad:FlxGamepad):Void
{
+ // TODO: Make this detect and handle multiple players
player1.addGamepad(gamepad);
}
/**
* @param id The player number this represents. This was refactored to START AT `1`.
*/
- private function new(id:Int)
+ function new(id:Int)
{
trace('loading player settings for id: $id');
@@ -83,11 +104,11 @@ class PlayerSettings
function addKeyboard():Void
{
- var useDefault = true;
- if (Save.get().hasControls(id, Keys))
+ var useDefault:Bool = true;
+ if (Save.instance.hasControls(id, Keys))
{
- var keyControlData = Save.get().getControls(id, Keys);
- trace("keyControlData: " + haxe.Json.stringify(keyControlData));
+ var keyControlData = Save.instance.getControls(id, Keys);
+ trace('Loading keyboard control scheme from user save');
useDefault = false;
controls.fromSaveData(keyControlData, Keys);
}
@@ -98,7 +119,7 @@ class PlayerSettings
if (useDefault)
{
- trace("Loading default keyboard control scheme");
+ trace('Loading default keyboard control scheme');
controls.setKeyboardScheme(Solo);
}
@@ -109,13 +130,13 @@ class PlayerSettings
* Called after an FlxGamepad has been detected.
* @param gamepad The gamepad that was detected.
*/
- function addGamepad(gamepad:FlxGamepad)
+ function addGamepad(gamepad:FlxGamepad):Void
{
var useDefault = true;
- if (Save.get().hasControls(id, Gamepad(gamepad.id)))
+ if (Save.instance.hasControls(id, Gamepad(gamepad.id)))
{
- var padControlData = Save.get().getControls(id, Gamepad(gamepad.id));
- trace("padControlData: " + haxe.Json.stringify(padControlData));
+ var padControlData = Save.instance.getControls(id, Gamepad(gamepad.id));
+ trace('Loading gamepad control scheme from user save');
useDefault = false;
controls.addGamepadWithSaveData(gamepad.id, padControlData);
}
@@ -126,7 +147,7 @@ class PlayerSettings
if (useDefault)
{
- trace("Loading gamepad control scheme");
+ trace('Loading default gamepad control scheme');
controls.addDefaultGamepad(gamepad.id);
}
PreciseInputManager.instance.initializeButtons(controls, gamepad);
@@ -135,13 +156,13 @@ class PlayerSettings
/**
* Save this player's controls to the game's persistent save.
*/
- public function saveControls()
+ public function saveControls():Void
{
var keyData = controls.createSaveData(Keys);
if (keyData != null)
{
- trace("saving key data: " + haxe.Json.stringify(keyData));
- Save.get().setControls(id, Keys, keyData);
+ trace('Saving keyboard control scheme to user save');
+ Save.instance.setControls(id, Keys, keyData);
}
if (controls.gamepadsAdded.length > 0)
@@ -149,8 +170,8 @@ class PlayerSettings
var padData = controls.createSaveData(Gamepad(controls.gamepadsAdded[0]));
if (padData != null)
{
- trace("saving pad data: " + haxe.Json.stringify(padData));
- Save.get().setControls(id, Gamepad(controls.gamepadsAdded[0]), padData);
+ trace('Saving gamepad control scheme to user save');
+ Save.instance.setControls(id, Gamepad(controls.gamepadsAdded[0]), padData);
}
}
}
diff --git a/source/funkin/Preferences.hx b/source/funkin/Preferences.hx
index 039a4c285..d8066c581 100644
--- a/source/funkin/Preferences.hx
+++ b/source/funkin/Preferences.hx
@@ -15,12 +15,12 @@ class Preferences
static function get_naughtyness():Bool
{
- return Save.get().options.naughtyness;
+ return Save.instance.options.naughtyness;
}
static function set_naughtyness(value:Bool):Bool
{
- var save = Save.get();
+ var save:Save = Save.instance;
save.options.naughtyness = value;
save.flush();
return value;
@@ -34,12 +34,12 @@ class Preferences
static function get_downscroll():Bool
{
- return Save.get().options.downscroll;
+ return Save.instance.options.downscroll;
}
static function set_downscroll(value:Bool):Bool
{
- var save = Save.get();
+ var save:Save = Save.instance;
save.options.downscroll = value;
save.flush();
return value;
@@ -53,12 +53,12 @@ class Preferences
static function get_flashingLights():Bool
{
- return Save.get().options.flashingLights;
+ return Save.instance.options.flashingLights;
}
static function set_flashingLights(value:Bool):Bool
{
- var save = Save.get();
+ var save:Save = Save.instance;
save.options.flashingLights = value;
save.flush();
return value;
@@ -72,12 +72,12 @@ class Preferences
static function get_zoomCamera():Bool
{
- return Save.get().options.zoomCamera;
+ return Save.instance.options.zoomCamera;
}
static function set_zoomCamera(value:Bool):Bool
{
- var save = Save.get();
+ var save:Save = Save.instance;
save.options.zoomCamera = value;
save.flush();
return value;
@@ -91,17 +91,17 @@ class Preferences
static function get_debugDisplay():Bool
{
- return Save.get().options.debugDisplay;
+ return Save.instance.options.debugDisplay;
}
static function set_debugDisplay(value:Bool):Bool
{
- if (value != Save.get().options.debugDisplay)
+ if (value != Save.instance.options.debugDisplay)
{
toggleDebugDisplay(value);
}
- var save = Save.get();
+ var save = Save.instance;
save.options.debugDisplay = value;
save.flush();
return value;
@@ -115,22 +115,27 @@ class Preferences
static function get_autoPause():Bool
{
- return Save.get().options.autoPause;
+ return Save.instance.options.autoPause;
}
static function set_autoPause(value:Bool):Bool
{
- if (value != Save.get().options.autoPause) FlxG.autoPause = value;
+ if (value != Save.instance.options.autoPause) FlxG.autoPause = value;
- var save = Save.get();
+ var save:Save = Save.instance;
save.options.autoPause = value;
save.flush();
return value;
}
+ /**
+ * Loads the user's preferences from the save data and apply them.
+ */
public static function init():Void
{
+ // Apply the autoPause setting (enables automatic pausing on focus lost).
FlxG.autoPause = Preferences.autoPause;
+ // Apply the debugDisplay setting (enables the FPS and RAM display).
toggleDebugDisplay(Preferences.debugDisplay);
}
diff --git a/source/funkin/Preloader.hx b/source/funkin/Preloader.hx
index 2a73d8199..157015ba6 100644
--- a/source/funkin/Preloader.hx
+++ b/source/funkin/Preloader.hx
@@ -3,7 +3,6 @@ package funkin;
import flash.Lib;
import flash.display.Bitmap;
import flash.display.BitmapData;
-import flash.display.BlendMode;
import flash.display.Sprite;
import flixel.system.FlxBasePreloader;
import openfl.display.Sprite;
@@ -12,7 +11,8 @@ import openfl.text.TextField;
import openfl.text.TextFormat;
import flixel.system.FlxAssets;
-@:bitmap("art/preloaderArt.png") class LogoImage extends BitmapData {}
+@:bitmap('art/preloaderArt.png')
+class LogoImage extends BitmapData {}
class Preloader extends FlxBasePreloader
{
diff --git a/source/funkin/api/newgrounds/NGUtil.hx b/source/funkin/api/newgrounds/NGUtil.hx
index c8289fc46..e9902a798 100644
--- a/source/funkin/api/newgrounds/NGUtil.hx
+++ b/source/funkin/api/newgrounds/NGUtil.hx
@@ -86,10 +86,10 @@ class NGUtil
#end
var onSessionFail:Error->Void = null;
- if (sessionId == null && Save.get().ngSessionId != null)
+ if (sessionId == null && Save.instance.ngSessionId != null)
{
trace("using stored session id");
- sessionId = Save.get().ngSessionId;
+ sessionId = Save.instance.ngSessionId;
onSessionFail = function(error) savedSessionFailed = true;
}
#end
@@ -159,8 +159,8 @@ class NGUtil
static function onNGLogin():Void
{
trace('logged in! user:${NG.core.user.name}');
- Save.get().ngSessionId = NG.core.sessionId;
- Save.get().flush();
+ Save.instance.ngSessionId = NG.core.sessionId;
+ Save.instance.flush();
// Load medals then call onNGMedalFetch()
NG.core.requestMedals(onNGMedalFetch);
@@ -174,8 +174,8 @@ class NGUtil
{
NG.core.logOut();
- Save.get().ngSessionId = null;
- Save.get().flush();
+ Save.instance.ngSessionId = null;
+ Save.instance.flush();
}
// --- MEDALS
diff --git a/source/funkin/api/newgrounds/NGio.hx b/source/funkin/api/newgrounds/NGio.hx
index e505bdedf..c1f8ad3ba 100644
--- a/source/funkin/api/newgrounds/NGio.hx
+++ b/source/funkin/api/newgrounds/NGio.hx
@@ -86,10 +86,10 @@ class NGio
#end
var onSessionFail:Error->Void = null;
- if (sessionId == null && Save.get().ngSessionId != null)
+ if (sessionId == null && Save.instance.ngSessionId != null)
{
trace("using stored session id");
- sessionId = Save.get().ngSessionId;
+ sessionId = Save.instance.ngSessionId;
onSessionFail = function(error) savedSessionFailed = true;
}
#end
@@ -159,8 +159,8 @@ class NGio
static function onNGLogin():Void
{
trace('logged in! user:${NG.core.user.name}');
- Save.get().ngSessionId = NG.core.sessionId;
- Save.get().flush();
+ Save.instance.ngSessionId = NG.core.sessionId;
+ Save.instance.flush();
// Load medals then call onNGMedalFetch()
NG.core.requestMedals(onNGMedalFetch);
@@ -174,8 +174,8 @@ class NGio
{
NG.core.logOut();
- Save.get().ngSessionId = null;
- Save.get().flush();
+ Save.instance.ngSessionId = null;
+ Save.instance.flush();
}
// --- MEDALS
diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx
index e7ce68d08..c64240909 100644
--- a/source/funkin/audio/FunkinSound.hx
+++ b/source/funkin/audio/FunkinSound.hx
@@ -1,13 +1,12 @@
package funkin.audio;
-#if flash11
-import flash.media.Sound;
-import flash.utils.ByteArray;
-#end
import flixel.sound.FlxSound;
import flixel.group.FlxGroup.FlxTypedGroup;
+import flixel.util.FlxSignal.FlxTypedSignal;
import flixel.system.FlxAssets.FlxSoundAsset;
import funkin.util.tools.ICloneable;
+import funkin.data.song.SongData.SongMusicData;
+import funkin.data.song.SongRegistry;
import funkin.audio.waveform.WaveformData;
import funkin.audio.waveform.WaveformDataParser;
import flixel.math.FlxMath;
@@ -23,9 +22,31 @@ import openfl.utils.AssetType;
@:nullSafety
class FunkinSound extends FlxSound implements ICloneable
{
- static final MAX_VOLUME:Float = 2.0;
+ static final MAX_VOLUME:Float = 1.0;
- static var cache(default, null):FlxTypedGroup = new FlxTypedGroup();
+ /**
+ * An FlxSignal which is dispatched when the volume changes.
+ */
+ public static var onVolumeChanged(get, never):FlxTypedSignalVoid>;
+
+ static var _onVolumeChanged:NullVoid>> = null;
+
+ static function get_onVolumeChanged():FlxTypedSignalVoid>
+ {
+ if (_onVolumeChanged == null)
+ {
+ _onVolumeChanged = new FlxTypedSignalVoid>();
+ FlxG.sound.volumeHandler = function(volume:Float) {
+ _onVolumeChanged.dispatch(volume);
+ }
+ }
+ return _onVolumeChanged;
+ }
+
+ /**
+ * Using `FunkinSound.load` will override a dead instance from here rather than creating a new one, if possible!
+ */
+ static var pool(default, null):FlxTypedGroup = new FlxTypedGroup();
public var muted(default, set):Bool = false;
@@ -40,7 +61,6 @@ class FunkinSound extends FlxSound implements ICloneable
override function set_volume(value:Float):Float
{
// Uncap the volume.
- fixMaxVolume();
_volume = FlxMath.bound(value, 0.0, MAX_VOLUME);
updateTransform();
return _volume;
@@ -88,6 +108,11 @@ class FunkinSound extends FlxSound implements ICloneable
*/
var _label:String = "unknown";
+ /**
+ * Whether we received a focus lost event.
+ */
+ var _lostFocus:Bool = false;
+
public function new()
{
super();
@@ -126,17 +151,6 @@ class FunkinSound extends FlxSound implements ICloneable
return this;
}
- function fixMaxVolume():Void
- {
- #if lime_openal
- // This code is pretty fragile, it reaches through 5 layers of private access.
- @:privateAccess
- var handle = this?._channel?.__source?.__backend?.handle;
- if (handle == null) return;
- lime.media.openal.AL.sourcef(handle, lime.media.openal.AL.MAX_GAIN, MAX_VOLUME);
- #end
- }
-
public override function play(forceRestart:Bool = false, startTime:Float = 0, ?endTime:Float):FunkinSound
{
if (!exists) return this;
@@ -176,8 +190,18 @@ class FunkinSound extends FlxSound implements ICloneable
public override function pause():FunkinSound
{
- super.pause();
- this._shouldPlay = false;
+ if (_shouldPlay)
+ {
+ // This sound will eventually play, but is still at a negative timestamp.
+ // Manually set the paused flag to ensure proper focus/unfocus behavior.
+ _shouldPlay = false;
+ _paused = true;
+ active = false;
+ }
+ else
+ {
+ super.pause();
+ }
return this;
}
@@ -186,10 +210,18 @@ class FunkinSound extends FlxSound implements ICloneable
*/
override function onFocus():Void
{
- if (!_alreadyPaused && this._shouldPlay)
+ // Flixel can sometimes toss spurious `onFocus` events, e.g. if the Flixel debugger is toggled
+ // on and off. We only want to resume the sound if we actually lost focus, and if we weren't
+ // already paused before we lost focus.
+ if (_lostFocus && !_alreadyPaused)
{
resume();
}
+ else
+ {
+ trace('Not resuming audio on focus!');
+ }
+ _lostFocus = false;
}
/**
@@ -197,6 +229,8 @@ class FunkinSound extends FlxSound implements ICloneable
*/
override function onFocusLost():Void
{
+ trace('Focus lost, pausing audio!');
+ _lostFocus = true;
_alreadyPaused = _paused;
pause();
}
@@ -205,7 +239,10 @@ class FunkinSound extends FlxSound implements ICloneable
{
if (this._time < 0)
{
- this._shouldPlay = true;
+ // Sound with negative timestamp, restart the timer.
+ _shouldPlay = true;
+ _paused = false;
+ active = true;
}
else
{
@@ -246,24 +283,58 @@ class FunkinSound extends FlxSound implements ICloneable
}
/**
- * Creates a new `FunkinSound` object.
+ * Creates a new `FunkinSound` object and loads it as the current music track.
*
- * @param embeddedSound The embedded sound resource you want to play. To stream, use the optional URL parameter instead.
- * @param volume How loud to play it (0 to 1).
- * @param looped Whether to loop this sound.
- * @param group The group to add this sound to.
- * @param autoDestroy Whether to destroy this sound when it finishes playing.
+ * @param key The key of the music you want to play. Music should be at `music//.ogg`.
+ * @param overrideExisting Whether to override music if it is already playing.
+ * @param mapTimeChanges Whether to check for `SongMusicData` to update the Conductor with.
+ * Data should be at `music//-metadata.json`.
+ */
+ public static function playMusic(key:String, overrideExisting:Bool = false, mapTimeChanges:Bool = true):Void
+ {
+ if (!overrideExisting && FlxG.sound.music?.playing) return;
+
+ if (mapTimeChanges)
+ {
+ var songMusicData:Null = SongRegistry.instance.parseMusicData(key);
+ // Will fall back and return null if the metadata doesn't exist or can't be parsed.
+ if (songMusicData != null)
+ {
+ Conductor.instance.mapTimeChanges(songMusicData.timeChanges);
+ }
+ else
+ {
+ FlxG.log.warn('Tried and failed to find music metadata for $key');
+ }
+ }
+
+ FlxG.sound.music = FunkinSound.load(Paths.music('$key/$key'));
+
+ // Prevent repeat update() and onFocus() calls.
+ FlxG.sound.list.remove(FlxG.sound.music);
+ }
+
+ /**
+ * Creates a new `FunkinSound` object synchronously.
+ *
+ * @param embeddedSound The embedded sound resource you want to play. To stream, use the optional URL parameter instead.
+ * @param volume How loud to play it (0 to 1).
+ * @param looped Whether to loop this sound.
+ * @param group The group to add this sound to.
+ * @param autoDestroy Whether to destroy this sound when it finishes playing.
* Leave this value set to `false` if you want to re-use this `FunkinSound` instance.
- * @param autoPlay Whether to play the sound immediately or wait for a `play()` call.
- * @param onComplete Called when the sound finished playing.
- * @param onLoad Called when the sound finished loading. Called immediately for succesfully loaded embedded sounds.
- * @return A `FunkinSound` object.
+ * @param autoPlay Whether to play the sound immediately or wait for a `play()` call.
+ * @param onComplete Called when the sound finished playing.
+ * @param onLoad Called when the sound finished loading. Called immediately for succesfully loaded embedded sounds.
+ * @return A `FunkinSound` object.
*/
public static function load(embeddedSound:FlxSoundAsset, volume:Float = 1.0, looped:Bool = false, autoDestroy:Bool = false, autoPlay:Bool = false,
?onComplete:Void->Void, ?onLoad:Void->Void):FunkinSound
{
- var sound:FunkinSound = cache.recycle(construct);
+ var sound:FunkinSound = pool.recycle(construct);
+ // Load the sound.
+ // Sets `exists = true` as a side effect.
sound.loadEmbedded(embeddedSound, looped, autoDestroy, onComplete);
if (embeddedSound is String)
@@ -276,9 +347,11 @@ class FunkinSound extends FlxSound implements ICloneable
sound.persist = true;
if (autoPlay) sound.play();
- // Call OnlLoad() because the sound already loaded
+ // Call onLoad() because the sound already loaded
if (onLoad != null && sound._sound != null) onLoad();
+ FlxG.sound.list.remove(FlxG.sound.music);
+
return sound;
}
@@ -286,7 +359,7 @@ class FunkinSound extends FlxSound implements ICloneable
{
var sound:FunkinSound = new FunkinSound();
- cache.add(sound);
+ pool.add(sound);
FlxG.sound.list.add(sound);
return sound;
diff --git a/source/funkin/audio/SoundGroup.hx b/source/funkin/audio/SoundGroup.hx
index 0e81a0901..a26537c2a 100644
--- a/source/funkin/audio/SoundGroup.hx
+++ b/source/funkin/audio/SoundGroup.hx
@@ -80,8 +80,9 @@ class SoundGroup extends FlxTypedGroup
// We have to play, then pause the sound to set the time,
// else the sound will restart immediately when played.
- result.play(true, 0.0);
- result.pause();
+ // TODO: Past me experienced that issue but present me didn't? Investigate.
+ // result.play(true, 0.0);
+ // result.pause();
result.time = this.time;
result.onComplete = function() {
diff --git a/source/funkin/audio/visualize/SpectogramSprite.hx b/source/funkin/audio/visualize/SpectogramSprite.hx
index b4e024a4c..636c0726a 100644
--- a/source/funkin/audio/visualize/SpectogramSprite.hx
+++ b/source/funkin/audio/visualize/SpectogramSprite.hx
@@ -4,13 +4,11 @@ import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
-import flixel.math.FlxVector;
import flixel.sound.FlxSound;
import flixel.util.FlxColor;
import funkin.audio.visualize.PolygonSpectogram.VISTYPE;
import funkin.audio.visualize.VisShit.CurAudioInfo;
import funkin.audio.visualize.dsp.FFT;
-import haxe.Timer;
import lime.system.ThreadPool;
import lime.utils.Int16Array;
diff --git a/source/funkin/audio/visualize/VisShit.hx b/source/funkin/audio/visualize/VisShit.hx
index 5bfb8c7c5..204ced1e1 100644
--- a/source/funkin/audio/visualize/VisShit.hx
+++ b/source/funkin/audio/visualize/VisShit.hx
@@ -3,7 +3,6 @@ package funkin.audio.visualize;
import flixel.math.FlxMath;
import flixel.sound.FlxSound;
import funkin.audio.visualize.dsp.FFT;
-import haxe.Timer;
import lime.system.ThreadPool;
import lime.utils.Int16Array;
import funkin.util.MathUtil;
diff --git a/source/funkin/audio/waveform/WaveformData.hx b/source/funkin/audio/waveform/WaveformData.hx
index b82d141e7..1f649b472 100644
--- a/source/funkin/audio/waveform/WaveformData.hx
+++ b/source/funkin/audio/waveform/WaveformData.hx
@@ -187,6 +187,8 @@ class WaveformData
*/
public function merge(that:WaveformData):WaveformData
{
+ if (that == null) return this.clone();
+
var result = this.clone([]);
for (channelIndex in 0...this.channels)
diff --git a/source/funkin/audio/waveform/WaveformDataParser.hx b/source/funkin/audio/waveform/WaveformDataParser.hx
index 54a142f6a..5aa54d744 100644
--- a/source/funkin/audio/waveform/WaveformDataParser.hx
+++ b/source/funkin/audio/waveform/WaveformDataParser.hx
@@ -1,5 +1,7 @@
package funkin.audio.waveform;
+import funkin.util.TimerUtil;
+
class WaveformDataParser
{
static final INT16_MAX:Int = 32767;
@@ -71,7 +73,7 @@ class WaveformDataParser
var outputData:Array = [];
- var perfStart = haxe.Timer.stamp();
+ var perfStart:Float = TimerUtil.start();
for (pointIndex in 0...outputPointCount)
{
@@ -108,8 +110,7 @@ class WaveformDataParser
var outputDataLength:Int = Std.int(outputData.length / channels / 2);
var result = new WaveformData(null, channels, sampleRate, samplesPerPoint, bitsPerSample, outputPointCount, outputData);
- var perfEnd = haxe.Timer.stamp();
- trace('[WAVEFORM] Interpreted audio buffer in ${perfEnd - perfStart} seconds.');
+ trace('[WAVEFORM] Interpreted audio buffer in ${TimerUtil.seconds(perfStart)}.');
return result;
}
diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx
index 0ccbe2f18..ad028fa94 100644
--- a/source/funkin/data/BaseRegistry.hx
+++ b/source/funkin/data/BaseRegistry.hx
@@ -1,6 +1,5 @@
package funkin.data;
-import openfl.Assets;
import funkin.util.assets.DataAssets;
import funkin.util.VersionUtil;
import haxe.Constraints.Constructible;
@@ -19,12 +18,23 @@ typedef EntryConstructorFunction = String->Void;
@:generic
abstract class BaseRegistry & Constructible), J>
{
+ /**
+ * The ID of the registry. Used when logging.
+ */
public final registryId:String;
final dataFilePath:String;
+ /**
+ * A map of entry IDs to entries.
+ */
final entries:Map;
+ /**
+ * A map of entry IDs to scripted class names.
+ */
+ final scriptedEntryIds:Map;
+
/**
* The version rule to use when loading entries.
* If the entry's version does not match this rule, migration is needed.
@@ -37,17 +47,18 @@ abstract class BaseRegistry & Constructible();
+ this.scriptedEntryIds = [];
}
/**
- * TODO: Create a `loadEntriesAsync()` function.
+ * TODO: Create a `loadEntriesAsync(onProgress, onComplete)` function.
*/
public function loadEntries():Void
{
@@ -61,12 +72,22 @@ abstract class BaseRegistry & Constructible = null;
+ try
+ {
+ entry = createScriptedEntry(entryCls);
+ }
+ catch (e)
+ {
+ log('Failed to create scripted entry (${entryCls})');
+ continue;
+ }
if (entry != null)
{
log('Successfully created scripted entry (${entryCls} = ${entry.id})');
entries.set(entry.id, entry);
+ scriptedEntryIds.set(entry.id, entryCls);
}
else
{
@@ -93,7 +114,7 @@ abstract class BaseRegistry & Constructible & Constructible & Constructible
{
var entryStr:String = loadEntryFile(id).contents;
@@ -176,6 +232,8 @@ abstract class BaseRegistry & Constructible;
@@ -185,6 +243,7 @@ abstract class BaseRegistry & Constructible;
@@ -193,9 +252,17 @@ abstract class BaseRegistry & Constructible
{
+ if (version == null)
+ {
+ throw '[${registryId}] Entry ${id} could not be JSON-parsed or does not have a parseable version.';
+ }
+
// If a version rule is not specified, do not check against it.
if (versionRule == null || VersionUtil.validateVersion(version, versionRule))
{
@@ -206,12 +273,17 @@ abstract class BaseRegistry & Constructible & Constructible
{
+ // We enforce that T is Constructible to ensure this is valid.
return new T(id);
}
@@ -240,10 +313,15 @@ abstract class BaseRegistry & Constructible, currentTime:Float):Void
+ {
+ for (event in events)
+ {
+ // Deactivate future events.
+ if (event.time > currentTime)
+ {
+ event.activated = false;
+ }
+
+ // Skip past events.
+ if (event.time < currentTime)
+ {
+ event.activated = true;
+ }
+ }
+ }
+
/**
* Reset activation of all the provided events.
*/
diff --git a/source/funkin/data/freeplay/AlbumData.hx b/source/funkin/data/freeplay/AlbumData.hx
new file mode 100644
index 000000000..265a01fce
--- /dev/null
+++ b/source/funkin/data/freeplay/AlbumData.hx
@@ -0,0 +1,36 @@
+package funkin.data.freeplay;
+
+/**
+ * A type definition for the data for an album of songs.
+ * It includes things like what graphics to display in Freeplay.
+ * @see https://lib.haxe.org/p/json2object/
+ */
+typedef AlbumData =
+{
+ /**
+ * Semantic version for album data.
+ */
+ public var version:String;
+
+ /**
+ * Readable name of the album.
+ */
+ public var name:String;
+
+ /**
+ * Readable name of the artist(s) of the album.
+ */
+ public var artists:Array;
+
+ /**
+ * Asset key for the album art.
+ * The album art will be displayed in Freeplay.
+ */
+ public var albumArtAsset:String;
+
+ /**
+ * Asset key for the album title.
+ * The album title will be displayed below the album art in Freeplay.
+ */
+ public var albumTitleAsset:String;
+}
diff --git a/source/funkin/data/freeplay/AlbumRegistry.hx b/source/funkin/data/freeplay/AlbumRegistry.hx
new file mode 100644
index 000000000..78fba451b
--- /dev/null
+++ b/source/funkin/data/freeplay/AlbumRegistry.hx
@@ -0,0 +1,84 @@
+package funkin.data.freeplay;
+
+import funkin.ui.freeplay.Album;
+import funkin.data.freeplay.AlbumData;
+import funkin.ui.freeplay.ScriptedAlbum;
+
+class AlbumRegistry extends BaseRegistry
+{
+ /**
+ * The current version string for the album data format.
+ * Handle breaking changes by incrementing this value
+ * and adding migration to the `migrateAlbumData()` function.
+ */
+ public static final ALBUM_DATA_VERSION:thx.semver.Version = '1.0.0';
+
+ public static final ALBUM_DATA_VERSION_RULE:thx.semver.VersionRule = '1.0.x';
+
+ public static final instance:AlbumRegistry = new AlbumRegistry();
+
+ public function new()
+ {
+ super('ALBUM', 'ui/freeplay/albums', ALBUM_DATA_VERSION_RULE);
+ }
+
+ /**
+ * Read, parse, and validate the JSON data and produce the corresponding data object.
+ * @param id The ID of the entry to load.
+ * @return The parsed data object.
+ */
+ public function parseEntryData(id:String):Null
+ {
+ // JsonParser does not take type parameters,
+ // otherwise this function would be in BaseRegistry.
+ var parser:json2object.JsonParser = new json2object.JsonParser();
+ parser.ignoreUnknownVariables = false;
+
+ switch (loadEntryFile(id))
+ {
+ case {fileName: fileName, contents: contents}:
+ parser.fromJson(contents, fileName);
+ default:
+ return null;
+ }
+
+ if (parser.errors.length > 0)
+ {
+ printErrors(parser.errors, id);
+ return null;
+ }
+ return parser.value;
+ }
+
+ /**
+ * Parse and validate the JSON data and produce the corresponding data object.
+ *
+ * NOTE: Must be implemented on the implementation class.
+ * @param contents The JSON as a string.
+ * @param fileName An optional file name for error reporting.
+ * @return The parsed data object.
+ */
+ public function parseEntryDataRaw(contents:String, ?fileName:String):Null
+ {
+ var parser:json2object.JsonParser = new json2object.JsonParser();
+ parser.ignoreUnknownVariables = false;
+ parser.fromJson(contents, fileName);
+
+ if (parser.errors.length > 0)
+ {
+ printErrors(parser.errors, fileName);
+ return null;
+ }
+ return parser.value;
+ }
+
+ function createScriptedEntry(clsName:String):Album
+ {
+ return ScriptedAlbum.init(clsName, 'unknown');
+ }
+
+ function getScriptedClassNames():Array
+ {
+ return ScriptedAlbum.listScriptClasses();
+ }
+}
diff --git a/source/funkin/data/level/LevelData.hx b/source/funkin/data/level/LevelData.hx
index 843389cae..f5e58ae16 100644
--- a/source/funkin/data/level/LevelData.hx
+++ b/source/funkin/data/level/LevelData.hx
@@ -17,7 +17,7 @@ typedef LevelData =
var version:String;
/**
- * The title of the week, as seen in the top corner.
+ * The title of the level, as seen in the top corner.
*/
var name:String;
@@ -27,21 +27,35 @@ typedef LevelData =
@:jcustomparse(funkin.data.DataParse.stringNotEmpty)
var titleAsset:String;
+ /**
+ * The props to display over the colored background.
+ * In the base game this is usually Boyfriend and the opponent.
+ */
@:default([])
var props:Array;
- @:default(["bopeebo"])
+
+ /**
+ * The list of song IDs included in this level.
+ */
+ @:default(['bopeebo'])
var songs:Array;
- @:default("#F9CF51")
+
+ /**
+ * The background for the level behind the props.
+ */
+ @:default('#F9CF51')
@:optional
var background:String;
}
+/**
+ * Data for a single prop for a story mode level.
+ */
typedef LevelPropData =
{
/**
* The image to use for the prop. May optionally be a sprite sheet.
*/
- // @:jcustomparse(funkin.data.DataParse.stringNotEmpty)
var assetPath:String;
/**
diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index bba5f899f..26380947a 100644
--- a/source/funkin/data/song/SongData.hx
+++ b/source/funkin/data/song/SongData.hx
@@ -106,11 +106,14 @@ class SongMetadata implements ICloneable
/**
* Serialize this SongMetadata into a JSON string.
+ * @param pretty Whether the JSON should be big ol string (false),
+ * or formatted with tabs (true)
* @return The JSON string.
*/
public function serialize(pretty:Bool = true):String
{
- var writer = new json2object.JsonWriter();
+ var ignoreNullOptionals = true;
+ var writer = new json2object.JsonWriter(ignoreNullOptionals);
// 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.
@@ -597,7 +600,8 @@ class SongChartData implements ICloneable
*/
public function serialize(pretty:Bool = true):String
{
- var writer = new json2object.JsonWriter();
+ var ignoreNullOptionals = true;
+ var writer = new json2object.JsonWriter(ignoreNullOptionals);
return writer.write(this, pretty ? ' ' : null);
}
@@ -648,7 +652,7 @@ class SongEventDataRaw implements ICloneable
* Custom events can be added by scripts with the `ScriptedSongEvent` class.
*/
@:alias("e")
- public var event:String;
+ public var eventKind:String;
/**
* The data for the event.
@@ -668,10 +672,10 @@ class SongEventDataRaw implements ICloneable
@:jignored
public var activated:Bool = false;
- public function new(time:Float, event:String, value:Dynamic = null)
+ public function new(time:Float, eventKind:String, value:Dynamic = null)
{
this.time = time;
- this.event = event;
+ this.eventKind = eventKind;
this.value = value;
}
@@ -687,22 +691,22 @@ class SongEventDataRaw implements ICloneable
public function clone():SongEventDataRaw
{
- return new SongEventDataRaw(this.time, this.event, this.value);
+ return new SongEventDataRaw(this.time, this.eventKind, this.value);
}
}
/**
* Wrap SongEventData in an abstract so we can overload operators.
*/
-@:forward(time, event, value, activated, getStepTime, clone)
+@:forward(time, eventKind, value, activated, getStepTime, clone)
abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
{
- public function new(time:Float, event:String, value:Dynamic = null)
+ public function new(time:Float, eventKind:String, value:Dynamic = null)
{
- this = new SongEventDataRaw(time, event, value);
+ this = new SongEventDataRaw(time, eventKind, value);
}
- public inline function valueAsStruct(?defaultKey:String = "key"):Dynamic
+ public function valueAsStruct(?defaultKey:String = "key"):Dynamic
{
if (this.value == null) return {};
if (Std.isOfType(this.value, Array))
@@ -726,12 +730,12 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
public inline function getHandler():Null
{
- return SongEventRegistry.getEvent(this.event);
+ return SongEventRegistry.getEvent(this.eventKind);
}
public inline function getSchema():Null
{
- return SongEventRegistry.getEventSchema(this.event);
+ return SongEventRegistry.getEventSchema(this.eventKind);
}
public inline function getDynamic(key:String):Null
@@ -784,7 +788,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
var eventHandler = getHandler();
var eventSchema = getSchema();
- if (eventSchema == null) return 'Unknown Event: ${this.event}';
+ if (eventSchema == null) return 'Unknown Event: ${this.eventKind}';
var result = '${eventHandler.getTitle()}';
@@ -809,19 +813,19 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
public function clone():SongEventData
{
- return new SongEventData(this.time, this.event, this.value);
+ return new SongEventData(this.time, this.eventKind, this.value);
}
@:op(A == B)
public function op_equals(other:SongEventData):Bool
{
- return this.time == other.time && this.event == other.event && this.value == other.value;
+ return this.time == other.time && this.eventKind == other.eventKind && this.value == other.value;
}
@:op(A != B)
public function op_notEquals(other:SongEventData):Bool
{
- return this.time != other.time || this.event != other.event || this.value != other.value;
+ return this.time != other.time || this.eventKind != other.eventKind || this.value != other.value;
}
@:op(A > B)
@@ -853,7 +857,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
*/
public function toString():String
{
- return 'SongEventData(${this.time}ms, ${this.event}: ${this.value})';
+ return 'SongEventData(${this.time}ms, ${this.eventKind}: ${this.value})';
}
}
@@ -1022,6 +1026,12 @@ class SongNoteDataRaw implements ICloneable
{
return new SongNoteDataRaw(this.time, this.data, this.length, this.kind);
}
+
+ public function toString():String
+ {
+ return 'SongNoteData(${this.time}ms, ' + (this.length > 0 ? '[${this.length}ms hold]' : '') + ' ${this.data}'
+ + (this.kind != '' ? ' [kind: ${this.kind}])' : ')');
+ }
}
/**
diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx
index 7f3b01eb4..c93c5379a 100644
--- a/source/funkin/data/song/SongDataUtils.hx
+++ b/source/funkin/data/song/SongDataUtils.hx
@@ -47,7 +47,7 @@ class SongDataUtils
public static function offsetSongEventData(events:Array, offset:Float):Array
{
return events.map(function(event:SongEventData):SongEventData {
- return new SongEventData(event.time + offset, event.event, event.value);
+ return new SongEventData(event.time + offset, event.eventKind, event.value);
});
}
diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx
index dad287e82..e7f74569c 100644
--- a/source/funkin/data/song/SongRegistry.hx
+++ b/source/funkin/data/song/SongRegistry.hx
@@ -68,6 +68,7 @@ class SongRegistry extends BaseRegistry
{
log('Successfully created scripted entry (${entryCls} = ${entry.id})');
entries.set(entry.id, entry);
+ scriptedEntryIds.set(entry.id, entryCls);
}
else
{
@@ -441,6 +442,13 @@ class SongRegistry extends BaseRegistry
return {fileName: entryFilePath, contents: rawJson};
}
+ function hasMusicDataFile(id:String, ?variation:String):Bool
+ {
+ variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
+ var entryFilePath:String = Paths.file('music/$id/$id-metadata${variation == Constants.DEFAULT_VARIATION ? '' : '-$variation'}.json');
+ return openfl.Assets.exists(entryFilePath);
+ }
+
function loadEntryChartFile(id:String, ?variation:String):Null
{
variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
diff --git a/source/funkin/data/stage/StageData.hx b/source/funkin/data/stage/StageData.hx
index cb914007f..22b883c75 100644
--- a/source/funkin/data/stage/StageData.hx
+++ b/source/funkin/data/stage/StageData.hx
@@ -32,18 +32,21 @@ class StageData
bf:
{
zIndex: 0,
+ scale: 1,
position: [0, 0],
cameraOffsets: [-100, -100]
},
dad:
{
zIndex: 0,
+ scale: 1,
position: [0, 0],
cameraOffsets: [100, -100]
},
gf:
{
zIndex: 0,
+ scale: 1,
position: [0, 0],
cameraOffsets: [0, 0]
}
@@ -114,6 +117,7 @@ typedef StageDataProp =
@:jcustomparse(funkin.data.DataParse.eitherFloatOrFloats)
@:jcustomwrite(funkin.data.DataWrite.eitherFloatOrFloats)
@:optional
+ @:default(Left(1.0))
var scale:haxe.ds.Either>;
/**
@@ -190,6 +194,13 @@ typedef StageDataCharacter =
@:default([0, 0])
var position:Array;
+ /**
+ * The scale to render the character at.
+ */
+ @:optional
+ @:default(1)
+ var scale:Float;
+
/**
* The camera offsets to apply when focusing on the character on this stage.
* @default [-100, -100] for BF, [100, -100] for DAD/OPPONENT, [0, 0] for GF
diff --git a/source/funkin/graphics/FunkinSprite.hx b/source/funkin/graphics/FunkinSprite.hx
index 487aaac34..ffbd63fab 100644
--- a/source/funkin/graphics/FunkinSprite.hx
+++ b/source/funkin/graphics/FunkinSprite.hx
@@ -3,12 +3,30 @@ package funkin.graphics;
import flixel.FlxSprite;
import flixel.util.FlxColor;
import flixel.graphics.FlxGraphic;
+import flixel.tweens.FlxTween;
+import openfl.display3D.textures.TextureBase;
+import funkin.graphics.framebuffer.FixedBitmapData;
+import openfl.display.BitmapData;
/**
* An FlxSprite with additional functionality.
+ * - A more efficient method for creating solid color sprites.
+ * - TODO: Better cache handling for textures.
*/
class FunkinSprite extends FlxSprite
{
+ /**
+ * An internal list of all the textures cached with `cacheTexture`.
+ * This excludes any temporary textures like those from `FlxText` or `makeSolidColor`.
+ */
+ static var currentCachedTextures:Map = [];
+
+ /**
+ * An internal list of textures that were cached in the previous state.
+ * We don't know whether we want to keep them cached or not.
+ */
+ static var previousCachedTextures:Map = [];
+
/**
* @param x Starting X position
* @param y Starting Y position
@@ -18,26 +36,225 @@ class FunkinSprite extends FlxSprite
super(x, y);
}
+ /**
+ * Create a new FunkinSprite with a static texture.
+ * @param x The starting X position.
+ * @param y The starting Y position.
+ * @param key The key of the texture to load.
+ * @return The new FunkinSprite.
+ */
+ public static function create(x:Float = 0.0, y:Float = 0.0, key:String):FunkinSprite
+ {
+ var sprite:FunkinSprite = new FunkinSprite(x, y);
+ sprite.loadTexture(key);
+ return sprite;
+ }
+
+ /**
+ * Create a new FunkinSprite with a Sparrow atlas animated texture.
+ * @param x The starting X position.
+ * @param y The starting Y position.
+ * @param key The key of the texture to load.
+ * @return The new FunkinSprite.
+ */
+ public static function createSparrow(x:Float = 0.0, y:Float = 0.0, key:String):FunkinSprite
+ {
+ var sprite:FunkinSprite = new FunkinSprite(x, y);
+ sprite.loadSparrow(key);
+ return sprite;
+ }
+
+ /**
+ * Create a new FunkinSprite with a Packer atlas animated texture.
+ * @param x The starting X position.
+ * @param y The starting Y position.
+ * @param key The key of the texture to load.
+ * @return The new FunkinSprite.
+ */
+ public static function createPacker(x:Float = 0.0, y:Float = 0.0, key:String):FunkinSprite
+ {
+ var sprite:FunkinSprite = new FunkinSprite(x, y);
+ sprite.loadPacker(key);
+ return sprite;
+ }
+
+ /**
+ * Load a static image as the sprite's texture.
+ * @param key The key of the texture to load.
+ * @return This sprite, for chaining.
+ */
+ public function loadTexture(key:String):FunkinSprite
+ {
+ var graphicKey:String = Paths.image(key);
+ if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
+
+ loadGraphic(graphicKey);
+
+ return this;
+ }
+
+ /**
+ * Apply an OpenFL `BitmapData` to this sprite.
+ * @param input The OpenFL `BitmapData` to apply
+ * @return This sprite, for chaining
+ */
+ public function loadBitmapData(input:BitmapData):FunkinSprite
+ {
+ loadGraphic(input);
+
+ return this;
+ }
+
+ /**
+ * Apply an OpenFL `TextureBase` to this sprite.
+ * @param input The OpenFL `TextureBase` to apply
+ * @return This sprite, for chaining
+ */
+ public function loadTextureBase(input:TextureBase):FunkinSprite
+ {
+ var inputBitmap:FixedBitmapData = FixedBitmapData.fromTexture(input);
+
+ return loadBitmapData(inputBitmap);
+ }
+
+ /**
+ * Load an animated texture (Sparrow atlas spritesheet) as the sprite's texture.
+ * @param key The key of the texture to load.
+ * @return This sprite, for chaining.
+ */
+ public function loadSparrow(key:String):FunkinSprite
+ {
+ var graphicKey:String = Paths.image(key);
+ if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
+
+ this.frames = Paths.getSparrowAtlas(key);
+
+ return this;
+ }
+
+ /**
+ * Load an animated texture (Packer atlas spritesheet) as the sprite's texture.
+ * @param key The key of the texture to load.
+ * @return This sprite, for chaining.
+ */
+ public function loadPacker(key:String):FunkinSprite
+ {
+ var graphicKey:String = Paths.image(key);
+ if (!isTextureCached(graphicKey)) FlxG.log.warn('Texture not cached, may experience stuttering! $graphicKey');
+
+ this.frames = Paths.getPackerAtlas(key);
+
+ return this;
+ }
+
+ /**
+ * Determine whether the texture with the given key is cached.
+ * @param key The key of the texture to check.
+ * @return Whether the texture is cached.
+ */
+ public static function isTextureCached(key:String):Bool
+ {
+ return FlxG.bitmap.get(key) != null;
+ }
+
+ /**
+ * Ensure the texture with the given key is cached.
+ * @param key The key of the texture to cache.
+ */
+ public static function cacheTexture(key:String):Void
+ {
+ // We don't want to cache the same texture twice.
+ if (currentCachedTextures.exists(key)) return;
+
+ if (previousCachedTextures.exists(key))
+ {
+ // Move the graphic from the previous cache to the current cache.
+ var graphic = previousCachedTextures.get(key);
+ previousCachedTextures.remove(key);
+ currentCachedTextures.set(key, graphic);
+ return;
+ }
+
+ // Else, texture is currently uncached.
+ var graphic:FlxGraphic = FlxGraphic.fromAssetKey(key, false, null, true);
+ if (graphic == null)
+ {
+ FlxG.log.warn('Failed to cache graphic: $key');
+ }
+ else
+ {
+ trace('Successfully cached graphic: $key');
+ graphic.persist = true;
+ currentCachedTextures.set(key, graphic);
+ }
+ }
+
+ public static function cacheSparrow(key:String):Void
+ {
+ cacheTexture(Paths.image(key));
+ }
+
+ public static function cachePacker(key:String):Void
+ {
+ cacheTexture(Paths.image(key));
+ }
+
+ /**
+ * Call this, then `cacheTexture` to keep the textures we still need, then `purgeCache` to remove the textures that we won't be using anymore.
+ */
+ public static function preparePurgeCache():Void
+ {
+ previousCachedTextures = currentCachedTextures;
+ currentCachedTextures = [];
+ }
+
+ public static function purgeCache():Void
+ {
+ // Everything that is in previousCachedTextures but not in currentCachedTextures should be destroyed.
+ for (graphicKey in previousCachedTextures.keys())
+ {
+ var graphic = previousCachedTextures.get(graphicKey);
+ FlxG.bitmap.remove(graphic);
+ graphic.destroy();
+ previousCachedTextures.remove(graphicKey);
+ }
+ }
+
+ static function isGraphicCached(graphic:FlxGraphic):Bool
+ {
+ if (graphic == null) return false;
+ var result = FlxG.bitmap.get(graphic.key);
+ if (result == null) return false;
+ if (result != graphic)
+ {
+ FlxG.log.warn('Cached graphic does not match original: ${graphic.key}');
+ return false;
+ }
+ return true;
+ }
+
/**
* Acts similarly to `makeGraphic`, but with improved memory usage,
- * at the expense of not being able to paint onto the sprite.
+ * at the expense of not being able to paint onto the resulting sprite.
*
* @param width The target width of the sprite.
* @param height The target height of the sprite.
* @param color The color to fill the sprite with.
+ * @return This sprite, for chaining.
*/
public function makeSolidColor(width:Int, height:Int, color:FlxColor = FlxColor.WHITE):FunkinSprite
{
+ // Create a tiny solid color graphic and scale it up to the desired size.
var graphic:FlxGraphic = FlxG.bitmap.create(2, 2, color, false, 'solid#${color.toHexString(true, false)}');
frames = graphic.imageFrame;
- scale.set(width / 2, height / 2);
+ scale.set(width / 2.0, height / 2.0);
updateHitbox();
return this;
}
/**
- * Ensure scale is applied when cloning a sprite.
+ * Ensure scale is applied when cloning a sprite.R
* The default `clone()` method acts kinda weird TBH.
* @return A clone of this sprite.
*/
@@ -50,4 +267,13 @@ class FunkinSprite extends FlxSprite
return result;
}
+
+ public override function destroy():Void
+ {
+ frames = null;
+ // Cancel all tweens so they don't continue to run on a destroyed sprite.
+ // This prevents crashes.
+ FlxTween.cancelTweensOf(this);
+ super.destroy();
+ }
}
diff --git a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx
index ae7a5708c..c5a3a3771 100644
--- a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx
+++ b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx
@@ -3,7 +3,9 @@ package funkin.graphics.adobeanimate;
import flixel.util.FlxSignal.FlxTypedSignal;
import flxanimate.FlxAnimate;
import flxanimate.FlxAnimate.Settings;
-import flixel.math.FlxPoint;
+import flxanimate.frames.FlxAnimateFrames;
+import openfl.display.BitmapData;
+import openfl.utils.Assets;
/**
* A sprite which provides convenience functions for rendering a texture atlas with animations.
@@ -18,7 +20,7 @@ class FlxAtlasSprite extends FlxAnimate
// ?OnComplete:Void -> Void,
ShowPivot: #if debug false #else false #end,
Antialiasing: true,
- ScrollFactor: new FlxPoint(1, 1),
+ ScrollFactor: null,
// Offset: new FlxPoint(0, 0), // This is just FlxSprite.offset
};
@@ -31,7 +33,7 @@ class FlxAtlasSprite extends FlxAnimate
var canPlayOtherAnims:Bool = true;
- public function new(x:Float, y:Float, path:String, ?settings:Settings)
+ public function new(x:Float, y:Float, ?path:String, ?settings:Settings)
{
if (settings == null) settings = SETTINGS;
@@ -55,8 +57,9 @@ class FlxAtlasSprite extends FlxAnimate
*/
public function listAnimations():Array
{
- // return this.anim.getFrameLabels();
- return [""];
+ if (this.anim == null) return [];
+ return this.anim.getFrameLabels();
+ // return [""];
}
/**
@@ -81,9 +84,13 @@ class FlxAtlasSprite extends FlxAnimate
* @param id A string ID of the animation to play.
* @param restart Whether to restart the animation if it is already playing.
* @param ignoreOther Whether to ignore all other animation inputs, until this one is done playing
+ * @param loop Whether to loop the animation
+ * NOTE: `loop` and `ignoreOther` are not compatible with each other!
*/
- public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false):Void
+ public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false, ?loop:Bool = false):Void
{
+ if (loop == null) loop = false;
+
// Skip if not allowed to play animations.
if ((!canPlayOtherAnims && !ignoreOther)) return;
@@ -110,15 +117,21 @@ class FlxAtlasSprite extends FlxAnimate
return;
}
- // Stop the current animation if it is playing.
- // This includes removing existing frame callbacks.
- if (this.currentAnimation != null) this.stopAnimation();
+ anim.callback = function(_, frame:Int) {
+ var offset = loop ? 0 : -1;
- // Add a callback to ensure `onAnimationFinish` is dispatched.
- addFrameCallback(getNextFrameLabel(id), function() {
- trace('Animation finished: ' + id);
- onAnimationFinish.dispatch(id);
- });
+ if (frame == (anim.getFrameLabel(id).duration + offset) + anim.getFrameLabel(id).index)
+ {
+ if (loop)
+ {
+ playAnimation(id, true, false, true);
+ }
+ else
+ {
+ onAnimationFinish.dispatch(id);
+ }
+ }
+ };
// Prevent other animations from playing if `ignoreOther` is true.
if (ignoreOther) canPlayOtherAnims = false;
@@ -128,6 +141,11 @@ class FlxAtlasSprite extends FlxAnimate
this.currentAnimation = id;
}
+ override public function update(elapsed:Float)
+ {
+ super.update(elapsed);
+ }
+
/**
* Stops the current animation.
*/
@@ -146,22 +164,22 @@ class FlxAtlasSprite extends FlxAnimate
frameLabel.add(callback);
}
- inline function goToFrameLabel(label:String):Void
+ function goToFrameLabel(label:String):Void
{
this.anim.goToFrameLabel(label);
}
- inline function getNextFrameLabel(label:String):String
+ function getNextFrameLabel(label:String):String
{
return listAnimations()[(getLabelIndex(label) + 1) % listAnimations().length];
}
- inline function getLabelIndex(label:String):Int
+ function getLabelIndex(label:String):Int
{
return listAnimations().indexOf(label);
}
- inline function goToFrameIndex(index:Int):Void
+ function goToFrameIndex(index:Int):Void
{
this.anim.curFrame = index;
}
@@ -169,7 +187,7 @@ class FlxAtlasSprite extends FlxAnimate
public function cleanupAnimation(_:String):Void
{
canPlayOtherAnims = true;
- this.currentAnimation = null;
- this.anim.stop();
+ // this.currentAnimation = null;
+ this.anim.pause();
}
}
diff --git a/source/funkin/graphics/framebuffer/FixedBitmapData.hx b/source/funkin/graphics/framebuffer/FixedBitmapData.hx
index 00b39ce1c..4ffcbb867 100644
--- a/source/funkin/graphics/framebuffer/FixedBitmapData.hx
+++ b/source/funkin/graphics/framebuffer/FixedBitmapData.hx
@@ -32,11 +32,11 @@ class FixedBitmapData extends BitmapData
public static function fromTexture(texture:TextureBase):FixedBitmapData
{
if (texture == null) return null;
- final bitmapData = new FixedBitmapData(texture.__width, texture.__height, true, 0);
- bitmapData.readable = false;
+ final bitmapData:FixedBitmapData = new FixedBitmapData(texture.__width, texture.__height, true, 0);
+ // bitmapData.readable = false;
bitmapData.__texture = texture;
bitmapData.__textureContext = texture.__textureContext;
- bitmapData.image = null;
+ // bitmapData.image = null;
return bitmapData;
}
}
diff --git a/source/funkin/graphics/video/FlxVideo.hx b/source/funkin/graphics/video/FlxVideo.hx
index e95d7caa6..a0fab9c09 100644
--- a/source/funkin/graphics/video/FlxVideo.hx
+++ b/source/funkin/graphics/video/FlxVideo.hx
@@ -1,45 +1,113 @@
package funkin.graphics.video;
-import flixel.FlxBasic;
-import flixel.FlxSprite;
+import flixel.util.FlxColor;
+import flixel.util.FlxSignal.FlxTypedSignal;
+import funkin.audio.FunkinSound;
+import openfl.display3D.textures.TextureBase;
import openfl.events.NetStatusEvent;
+import openfl.media.SoundTransform;
import openfl.media.Video;
import openfl.net.NetConnection;
import openfl.net.NetStream;
/**
* Plays a video via a NetStream. Only works on HTML5.
- * This does NOT replace hxCodec, nor does hxCodec replace this. hxCodec only works on desktop and does not work on HTML5!
+ * This does NOT replace hxCodec, nor does hxCodec replace this.
+ * hxCodec only works on desktop and does not work on HTML5!
*/
-class FlxVideo extends FlxBasic
+class FlxVideo extends FunkinSprite
{
var video:Video;
var netStream:NetStream;
-
- public var finishCallback:Void->Void;
+ var videoPath:String;
/**
- * Doesn't actually interact with Flixel shit, only just a pleasant to use class
+ * A callback to execute when the video finishes.
*/
+ public var finishCallback:Void->Void;
+
public function new(videoPath:String)
{
super();
+ this.videoPath = videoPath;
+
+ makeGraphic(2, 2, FlxColor.TRANSPARENT);
+
video = new Video();
video.x = 0;
video.y = 0;
+ video.alpha = 0;
- FlxG.addChildBelowMouse(video);
+ FlxG.game.addChild(video);
- var netConnection = new NetConnection();
+ var netConnection:NetConnection = new NetConnection();
netConnection.connect(null);
netStream = new NetStream(netConnection);
- netStream.client = {onMetaData: client_onMetaData};
- netConnection.addEventListener(NetStatusEvent.NET_STATUS, netConnection_onNetStatus);
+ netStream.client = {onMetaData: onClientMetaData};
+ netConnection.addEventListener(NetStatusEvent.NET_STATUS, onNetConnectionNetStatus);
netStream.play(videoPath);
}
+ /**
+ * Tell the FlxVideo to pause playback.
+ */
+ public function pauseVideo():Void
+ {
+ if (netStream != null)
+ {
+ netStream.pause();
+ }
+ }
+
+ /**
+ * Tell the FlxVideo to resume if it is paused.
+ */
+ public function resumeVideo():Void
+ {
+ // Resume playing the video.
+ if (netStream != null)
+ {
+ netStream.resume();
+ }
+ }
+
+ var videoAvailable:Bool = false;
+ var frameTimer:Float;
+
+ static final FRAME_RATE:Float = 60;
+
+ public override function update(elapsed:Float):Void
+ {
+ super.update(elapsed);
+
+ if (frameTimer >= (1 / FRAME_RATE))
+ {
+ frameTimer = 0;
+ // TODO: We just draw the video buffer to the sprite 60 times a second.
+ // Can we copy the video buffer instead somehow?
+ pixels.draw(video);
+ }
+
+ if (videoAvailable) frameTimer += elapsed;
+ }
+
+ /**
+ * Tell the FlxVideo to seek to the beginning.
+ */
+ public function restartVideo():Void
+ {
+ // Seek to the beginning of the video.
+ if (netStream != null)
+ {
+ netStream.seek(0);
+ }
+ }
+
+ /**
+ * Tell the FlxVideo to end.
+ */
public function finishVideo():Void
{
netStream.dispose();
@@ -48,15 +116,48 @@ class FlxVideo extends FlxBasic
if (finishCallback != null) finishCallback();
}
- public function client_onMetaData(metaData:Dynamic)
+ public override function destroy():Void
+ {
+ if (netStream != null)
+ {
+ netStream.dispose();
+
+ if (FlxG.game.contains(video)) FlxG.game.removeChild(video);
+ }
+
+ super.destroy();
+ }
+
+ /**
+ * Callback executed when the video stream loads.
+ * @param metaData The metadata of the video
+ */
+ public function onClientMetaData(metaData:Dynamic):Void
{
video.attachNetStream(netStream);
- video.width = FlxG.width;
- video.height = FlxG.height;
+ onVideoReady();
}
- function netConnection_onNetStatus(event:NetStatusEvent):Void
+ function onVideoReady():Void
+ {
+ video.width = FlxG.width;
+ video.height = FlxG.height;
+
+ videoAvailable = true;
+
+ FunkinSound.onVolumeChanged.add(onVolumeChanged);
+ onVolumeChanged(FlxG.sound.muted ? 0 : FlxG.sound.volume);
+
+ makeGraphic(Std.int(video.width), Std.int(video.height), FlxColor.TRANSPARENT);
+ }
+
+ function onVolumeChanged(volume:Float):Void
+ {
+ netStream.soundTransform = new SoundTransform(volume);
+ }
+
+ function onNetConnectionNetStatus(event:NetStatusEvent):Void
{
if (event.info.code == 'NetStream.Play.Complete') finishVideo();
}
diff --git a/source/funkin/import.hx b/source/funkin/import.hx
index 02055d4ed..250de99cb 100644
--- a/source/funkin/import.hx
+++ b/source/funkin/import.hx
@@ -13,7 +13,6 @@ using Lambda;
using StringTools;
using funkin.util.tools.ArraySortTools;
using funkin.util.tools.ArrayTools;
-using funkin.util.tools.DynamicTools;
using funkin.util.tools.FloatTools;
using funkin.util.tools.Int64Tools;
using funkin.util.tools.IntTools;
diff --git a/source/funkin/input/Controls.hx b/source/funkin/input/Controls.hx
index c4760cf5f..0857678d0 100644
--- a/source/funkin/input/Controls.hx
+++ b/source/funkin/input/Controls.hx
@@ -58,12 +58,11 @@ class Controls extends FlxActionSet
var _back = new FlxActionDigital(Action.BACK);
var _pause = new FlxActionDigital(Action.PAUSE);
var _reset = new FlxActionDigital(Action.RESET);
+ var _screenshot = new FlxActionDigital(Action.SCREENSHOT);
var _cutscene_advance = new FlxActionDigital(Action.CUTSCENE_ADVANCE);
- var _cutscene_skip = new FlxActionDigital(Action.CUTSCENE_SKIP);
var _debug_menu = new FlxActionDigital(Action.DEBUG_MENU);
var _debug_chart = new FlxActionDigital(Action.DEBUG_CHART);
var _debug_stage = new FlxActionDigital(Action.DEBUG_STAGE);
- var _screenshot = new FlxActionDigital(Action.SCREENSHOT);
var _volume_up = new FlxActionDigital(Action.VOLUME_UP);
var _volume_down = new FlxActionDigital(Action.VOLUME_DOWN);
var _volume_mute = new FlxActionDigital(Action.VOLUME_MUTE);
@@ -208,16 +207,21 @@ class Controls extends FlxActionSet
inline function get_PAUSE()
return _pause.check();
+ public var RESET(get, never):Bool;
+
+ inline function get_RESET()
+ return _reset.check();
+
+ public var SCREENSHOT(get, never):Bool;
+
+ inline function get_SCREENSHOT()
+ return _screenshot.check();
+
public var CUTSCENE_ADVANCE(get, never):Bool;
inline function get_CUTSCENE_ADVANCE()
return _cutscene_advance.check();
- public var CUTSCENE_SKIP(get, never):Bool;
-
- inline function get_CUTSCENE_SKIP()
- return _cutscene_skip.check();
-
public var DEBUG_MENU(get, never):Bool;
inline function get_DEBUG_MENU()
@@ -233,11 +237,6 @@ class Controls extends FlxActionSet
inline function get_DEBUG_STAGE()
return _debug_stage.check();
- public var SCREENSHOT(get, never):Bool;
-
- inline function get_SCREENSHOT()
- return _screenshot.check();
-
public var VOLUME_UP(get, never):Bool;
inline function get_VOLUME_UP()
@@ -253,11 +252,6 @@ class Controls extends FlxActionSet
inline function get_VOLUME_MUTE()
return _volume_mute.check();
- public var RESET(get, never):Bool;
-
- inline function get_RESET()
- return _reset.check();
-
public function new(name, scheme:KeyboardScheme = null)
{
super(name);
@@ -289,16 +283,15 @@ class Controls extends FlxActionSet
add(_accept);
add(_back);
add(_pause);
+ add(_reset);
+ add(_screenshot);
add(_cutscene_advance);
- add(_cutscene_skip);
add(_debug_menu);
add(_debug_chart);
add(_debug_stage);
- add(_screenshot);
add(_volume_up);
add(_volume_down);
add(_volume_mute);
- add(_reset);
for (action in digitalActions)
byName[action.name] = action;
@@ -383,12 +376,11 @@ class Controls extends FlxActionSet
case BACK: _back;
case PAUSE: _pause;
case RESET: _reset;
+ case SCREENSHOT: _screenshot;
case CUTSCENE_ADVANCE: _cutscene_advance;
- case CUTSCENE_SKIP: _cutscene_skip;
case DEBUG_MENU: _debug_menu;
case DEBUG_CHART: _debug_chart;
case DEBUG_STAGE: _debug_stage;
- case SCREENSHOT: _screenshot;
case VOLUME_UP: _volume_up;
case VOLUME_DOWN: _volume_down;
case VOLUME_MUTE: _volume_mute;
@@ -449,26 +441,24 @@ class Controls extends FlxActionSet
func(_back, JUST_PRESSED);
case PAUSE:
func(_pause, JUST_PRESSED);
+ case RESET:
+ func(_reset, JUST_PRESSED);
+ case SCREENSHOT:
+ func(_screenshot, JUST_PRESSED);
case CUTSCENE_ADVANCE:
func(_cutscene_advance, JUST_PRESSED);
- case CUTSCENE_SKIP:
- func(_cutscene_skip, PRESSED);
case DEBUG_MENU:
func(_debug_menu, JUST_PRESSED);
case DEBUG_CHART:
func(_debug_chart, JUST_PRESSED);
case DEBUG_STAGE:
func(_debug_stage, JUST_PRESSED);
- case SCREENSHOT:
- func(_screenshot, JUST_PRESSED);
case VOLUME_UP:
func(_volume_up, JUST_PRESSED);
case VOLUME_DOWN:
func(_volume_down, JUST_PRESSED);
case VOLUME_MUTE:
func(_volume_mute, JUST_PRESSED);
- case RESET:
- func(_reset, JUST_PRESSED);
}
}
@@ -654,13 +644,12 @@ class Controls extends FlxActionSet
bindKeys(Control.ACCEPT, getDefaultKeybinds(scheme, Control.ACCEPT));
bindKeys(Control.BACK, getDefaultKeybinds(scheme, Control.BACK));
bindKeys(Control.PAUSE, getDefaultKeybinds(scheme, Control.PAUSE));
+ bindKeys(Control.RESET, getDefaultKeybinds(scheme, Control.RESET));
+ bindKeys(Control.SCREENSHOT, getDefaultKeybinds(scheme, Control.SCREENSHOT));
bindKeys(Control.CUTSCENE_ADVANCE, getDefaultKeybinds(scheme, Control.CUTSCENE_ADVANCE));
- bindKeys(Control.CUTSCENE_SKIP, getDefaultKeybinds(scheme, Control.CUTSCENE_SKIP));
bindKeys(Control.DEBUG_MENU, getDefaultKeybinds(scheme, Control.DEBUG_MENU));
bindKeys(Control.DEBUG_CHART, getDefaultKeybinds(scheme, Control.DEBUG_CHART));
bindKeys(Control.DEBUG_STAGE, getDefaultKeybinds(scheme, Control.DEBUG_STAGE));
- bindKeys(Control.RESET, getDefaultKeybinds(scheme, Control.RESET));
- bindKeys(Control.SCREENSHOT, getDefaultKeybinds(scheme, Control.SCREENSHOT));
bindKeys(Control.VOLUME_UP, getDefaultKeybinds(scheme, Control.VOLUME_UP));
bindKeys(Control.VOLUME_DOWN, getDefaultKeybinds(scheme, Control.VOLUME_DOWN));
bindKeys(Control.VOLUME_MUTE, getDefaultKeybinds(scheme, Control.VOLUME_MUTE));
@@ -683,16 +672,15 @@ class Controls extends FlxActionSet
case Control.ACCEPT: return [Z, SPACE, ENTER];
case Control.BACK: return [X, BACKSPACE, ESCAPE];
case Control.PAUSE: return [P, ENTER, ESCAPE];
+ case Control.RESET: return [R];
+ case Control.SCREENSHOT: return [F3]; // TODO: Change this back to PrintScreen
case Control.CUTSCENE_ADVANCE: return [Z, ENTER];
- case Control.CUTSCENE_SKIP: return [P, ESCAPE];
case Control.DEBUG_MENU: return [GRAVEACCENT];
case Control.DEBUG_CHART: return [];
case Control.DEBUG_STAGE: return [];
- case Control.SCREENSHOT: return [F3]; // TODO: Change this back to PrintScreen
case Control.VOLUME_UP: return [PLUS, NUMPADPLUS];
case Control.VOLUME_DOWN: return [MINUS, NUMPADMINUS];
case Control.VOLUME_MUTE: return [ZERO, NUMPADZERO];
- case Control.RESET: return [R];
}
case Duo(true):
switch (control) {
@@ -707,16 +695,15 @@ class Controls extends FlxActionSet
case Control.ACCEPT: return [G, Z];
case Control.BACK: return [H, X];
case Control.PAUSE: return [ONE];
+ case Control.RESET: return [R];
+ case Control.SCREENSHOT: return [PRINTSCREEN];
case Control.CUTSCENE_ADVANCE: return [G, Z];
- case Control.CUTSCENE_SKIP: return [ONE];
case Control.DEBUG_MENU: return [GRAVEACCENT];
case Control.DEBUG_CHART: return [];
case Control.DEBUG_STAGE: return [];
- case Control.SCREENSHOT: return [PRINTSCREEN];
case Control.VOLUME_UP: return [PLUS];
case Control.VOLUME_DOWN: return [MINUS];
case Control.VOLUME_MUTE: return [ZERO];
- case Control.RESET: return [R];
}
case Duo(false):
switch (control) {
@@ -731,16 +718,15 @@ class Controls extends FlxActionSet
case Control.ACCEPT: return [ENTER];
case Control.BACK: return [ESCAPE];
case Control.PAUSE: return [ONE];
+ case Control.RESET: return [R];
+ case Control.SCREENSHOT: return [PRINTSCREEN];
case Control.CUTSCENE_ADVANCE: return [ENTER];
- case Control.CUTSCENE_SKIP: return [ONE];
case Control.DEBUG_MENU: return [GRAVEACCENT];
case Control.DEBUG_CHART: return [];
case Control.DEBUG_STAGE: return [];
- case Control.SCREENSHOT: return [PRINTSCREEN];
case Control.VOLUME_UP: return [NUMPADPLUS];
case Control.VOLUME_DOWN: return [NUMPADMINUS];
case Control.VOLUME_MUTE: return [NUMPADZERO];
- case Control.RESET: return [R];
}
default:
// Fallthrough.
@@ -843,15 +829,14 @@ class Controls extends FlxActionSet
Control.NOTE_LEFT => getDefaultGamepadBinds(Control.NOTE_LEFT),
Control.NOTE_RIGHT => getDefaultGamepadBinds(Control.NOTE_RIGHT),
Control.PAUSE => getDefaultGamepadBinds(Control.PAUSE),
+ Control.RESET => getDefaultGamepadBinds(Control.RESET),
// Control.SCREENSHOT => [],
// Control.VOLUME_UP => [RIGHT_SHOULDER],
// Control.VOLUME_DOWN => [LEFT_SHOULDER],
// Control.VOLUME_MUTE => [RIGHT_TRIGGER],
Control.CUTSCENE_ADVANCE => getDefaultGamepadBinds(Control.CUTSCENE_ADVANCE),
- Control.CUTSCENE_SKIP => getDefaultGamepadBinds(Control.CUTSCENE_SKIP),
// Control.DEBUG_MENU
// Control.DEBUG_CHART
- Control.RESET => getDefaultGamepadBinds(Control.RESET)
]);
}
@@ -868,15 +853,14 @@ class Controls extends FlxActionSet
case Control.NOTE_LEFT: return [DPAD_LEFT, X, LEFT_STICK_DIGITAL_LEFT, RIGHT_STICK_DIGITAL_LEFT];
case Control.NOTE_RIGHT: return [DPAD_RIGHT, B, LEFT_STICK_DIGITAL_RIGHT, RIGHT_STICK_DIGITAL_RIGHT];
case Control.PAUSE: return [START];
+ case Control.RESET: return [RIGHT_SHOULDER];
case Control.SCREENSHOT: return [];
case Control.VOLUME_UP: return [];
case Control.VOLUME_DOWN: return [];
case Control.VOLUME_MUTE: return [];
case Control.CUTSCENE_ADVANCE: return [A];
- case Control.CUTSCENE_SKIP: return [START];
case Control.DEBUG_MENU: return [];
case Control.DEBUG_CHART: return [];
- case Control.RESET: return [RIGHT_SHOULDER];
default:
// Fallthrough.
}
@@ -1228,14 +1212,13 @@ enum Control
UI_RIGHT;
UI_DOWN;
RESET;
+ SCREENSHOT;
ACCEPT;
BACK;
PAUSE;
// CUTSCENE
CUTSCENE_ADVANCE;
- CUTSCENE_SKIP;
// SCREENSHOT
- SCREENSHOT;
// VOLUME
VOLUME_UP;
VOLUME_DOWN;
@@ -1279,15 +1262,14 @@ abstract Action(String) to String from String
var BACK = "back";
var PAUSE = "pause";
var RESET = "reset";
+ // SCREENSHOT
+ var SCREENSHOT = "screenshot";
// CUTSCENE
var CUTSCENE_ADVANCE = "cutscene_advance";
- var CUTSCENE_SKIP = "cutscene_skip";
// VOLUME
var VOLUME_UP = "volume_up";
var VOLUME_DOWN = "volume_down";
var VOLUME_MUTE = "volume_mute";
- // SCREENSHOT
- var SCREENSHOT = "screenshot";
// DEBUG
var DEBUG_MENU = "debug_menu";
var DEBUG_CHART = "debug_chart";
diff --git a/source/funkin/modding/IScriptedClass.hx b/source/funkin/modding/IScriptedClass.hx
index b009aea41..5f2ff2b9e 100644
--- a/source/funkin/modding/IScriptedClass.hx
+++ b/source/funkin/modding/IScriptedClass.hx
@@ -56,7 +56,20 @@ interface IStateStageProp extends IScriptedClass
*/
interface INoteScriptedClass extends IScriptedClass
{
- public function onNoteHit(event:NoteScriptEvent):Void;
+ /**
+ * Called when a note enters the field of view and approaches the strumline.
+ */
+ public function onNoteIncoming(event:NoteScriptEvent):Void;
+
+ /**
+ * Called when EITHER player hits a note.
+ * Query the note attached to the event to determine if it was hit by the player or CPU.
+ */
+ public function onNoteHit(event:HitNoteScriptEvent):Void;
+
+ /**
+ * Called when EITHER player (usually the player) misses a note.
+ */
public function onNoteMiss(event:NoteScriptEvent):Void;
}
@@ -73,7 +86,7 @@ interface INoteScriptedClass extends IScriptedClass
/**
* Defines a set of callbacks available to scripted classes that involve the lifecycle of the Play State.
*/
-interface IPlayStateScriptedClass extends IScriptedClass
+interface IPlayStateScriptedClass extends INoteScriptedClass
{
/**
* Called when the game is paused.
@@ -113,17 +126,6 @@ interface IPlayStateScriptedClass extends IScriptedClass
*/
public function onSongRetry(event:ScriptEvent):Void;
- /**
- * Called when EITHER player hits a note.
- * Query the note attached to the event to determine if it was hit by the player or CPU.
- */
- public function onNoteHit(event:NoteScriptEvent):Void;
-
- /**
- * Called when EITHER player (usually the player) misses a note.
- */
- public function onNoteMiss(event:NoteScriptEvent):Void;
-
/**
* Called when the player presses a key when no note is on the strumline.
*/
diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx
index f1e82aee9..a88476d4d 100644
--- a/source/funkin/modding/PolymodHandler.hx
+++ b/source/funkin/modding/PolymodHandler.hx
@@ -1,24 +1,26 @@
package funkin.modding;
-import funkin.util.macro.ClassMacro;
-import funkin.modding.module.ModuleHandler;
-import funkin.data.song.SongData;
-import funkin.data.stage.StageData;
-import polymod.Polymod;
-import polymod.backends.PolymodAssets.PolymodAssetType;
-import polymod.format.ParseRules.TextFileFormat;
-import funkin.data.event.SongEventRegistry;
-import funkin.data.stage.StageRegistry;
-import funkin.util.FileUtil;
-import funkin.data.level.LevelRegistry;
-import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.dialogue.ConversationRegistry;
import funkin.data.dialogue.DialogueBoxRegistry;
import funkin.data.dialogue.SpeakerRegistry;
+import funkin.data.event.SongEventRegistry;
+import funkin.data.level.LevelRegistry;
+import funkin.data.notestyle.NoteStyleRegistry;
+import funkin.data.song.SongRegistry;
+import funkin.data.stage.StageRegistry;
+import funkin.data.freeplay.AlbumRegistry;
+import funkin.modding.module.ModuleHandler;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.save.Save;
-import funkin.data.song.SongRegistry;
+import funkin.util.FileUtil;
+import funkin.util.macro.ClassMacro;
+import polymod.backends.PolymodAssets.PolymodAssetType;
+import polymod.format.ParseRules.TextFileFormat;
+import polymod.Polymod;
+/**
+ * A class for interacting with Polymod, the atomic modding framework for Haxe.
+ */
class PolymodHandler
{
/**
@@ -27,16 +29,33 @@ class PolymodHandler
* Bug fixes increment the patch version, new features increment the minor version.
* Changes that break old mods increment the major version.
*/
- static final API_VERSION:String = "0.1.0";
+ static final API_VERSION:String = '0.1.0';
/**
* Where relative to the executable that mods are located.
*/
- static final MOD_FOLDER:String = #if (REDIRECT_ASSETS_FOLDER && macos) "../../../../../../../example_mods" #elseif REDIRECT_ASSETS_FOLDER "../../../../example_mods" #else "mods" #end;
+ static final MOD_FOLDER:String =
+ #if (REDIRECT_ASSETS_FOLDER && macos)
+ '../../../../../../../example_mods'
+ #elseif REDIRECT_ASSETS_FOLDER
+ '../../../../example_mods'
+ #else
+ 'mods'
+ #end;
- static final CORE_FOLDER:Null = #if (REDIRECT_ASSETS_FOLDER && macos) "../../../../../../../assets" #elseif REDIRECT_ASSETS_FOLDER "../../../../assets" #else null #end;
+ static final CORE_FOLDER:Null =
+ #if (REDIRECT_ASSETS_FOLDER && macos)
+ '../../../../../../../assets'
+ #elseif REDIRECT_ASSETS_FOLDER
+ '../../../../assets'
+ #else
+ null
+ #end;
- public static function createModRoot()
+ /**
+ * If the mods folder doesn't exist, create it.
+ */
+ public static function createModRoot():Void
{
FileUtil.createDirIfNotExists(MOD_FOLDER);
}
@@ -44,40 +63,44 @@ class PolymodHandler
/**
* Loads the game with ALL mods enabled with Polymod.
*/
- public static function loadAllMods()
+ public static function loadAllMods():Void
{
// Create the mod root if it doesn't exist.
createModRoot();
- trace("Initializing Polymod (using all mods)...");
+ trace('Initializing Polymod (using all mods)...');
loadModsById(getAllModIds());
}
/**
* Loads the game with configured mods enabled with Polymod.
*/
- public static function loadEnabledMods()
+ public static function loadEnabledMods():Void
{
// Create the mod root if it doesn't exist.
createModRoot();
- trace("Initializing Polymod (using configured mods)...");
- loadModsById(Save.get().enabledModIds);
+ trace('Initializing Polymod (using configured mods)...');
+ loadModsById(Save.instance.enabledModIds);
}
/**
* Loads the game without any mods enabled with Polymod.
*/
- public static function loadNoMods()
+ public static function loadNoMods():Void
{
// Create the mod root if it doesn't exist.
createModRoot();
// We still need to configure the debug print calls etc.
- trace("Initializing Polymod (using no mods)...");
+ trace('Initializing Polymod (using no mods)...');
loadModsById([]);
}
- public static function loadModsById(ids:Array)
+ /**
+ * Load all the mods with the given ids.
+ * @param ids The ORDERED list of mod ids to load.
+ */
+ public static function loadModsById(ids:Array):Void
{
if (ids.length == 0)
{
@@ -90,7 +113,7 @@ class PolymodHandler
buildImports();
- var loadedModList = polymod.Polymod.init(
+ var loadedModList:Array = polymod.Polymod.init(
{
// Root directory for all mods.
modRoot: MOD_FOLDER,
@@ -142,30 +165,40 @@ class PolymodHandler
}
#if debug
- var fileList = Polymod.listModFiles(PolymodAssetType.IMAGE);
+ var fileList:Array = Polymod.listModFiles(PolymodAssetType.IMAGE);
trace('Installed mods have replaced ${fileList.length} images.');
for (item in fileList)
+ {
trace(' * $item');
+ }
fileList = Polymod.listModFiles(PolymodAssetType.TEXT);
trace('Installed mods have added/replaced ${fileList.length} text files.');
for (item in fileList)
+ {
trace(' * $item');
+ }
fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_MUSIC);
trace('Installed mods have replaced ${fileList.length} music files.');
for (item in fileList)
+ {
trace(' * $item');
+ }
fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_SOUND);
trace('Installed mods have replaced ${fileList.length} sound files.');
for (item in fileList)
+ {
trace(' * $item');
+ }
fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_GENERIC);
trace('Installed mods have replaced ${fileList.length} generic audio files.');
for (item in fileList)
+ {
trace(' * $item');
+ }
#end
}
@@ -183,21 +216,21 @@ class PolymodHandler
for (cls in ClassMacro.listClassesInPackage('polymod'))
{
if (cls == null) continue;
- var className = Type.getClassName(cls);
+ var className:String = Type.getClassName(cls);
Polymod.blacklistImport(className);
}
}
static function buildParseRules():polymod.format.ParseRules
{
- var output = polymod.format.ParseRules.getDefault();
+ var output:polymod.format.ParseRules = polymod.format.ParseRules.getDefault();
// Ensure TXT files have merge support.
- output.addType("txt", TextFileFormat.LINES);
+ output.addType('txt', TextFileFormat.LINES);
// Ensure script files have merge support.
- output.addType("hscript", TextFileFormat.PLAINTEXT);
- output.addType("hxs", TextFileFormat.PLAINTEXT);
- output.addType("hxc", TextFileFormat.PLAINTEXT);
- output.addType("hx", TextFileFormat.PLAINTEXT);
+ output.addType('hscript', TextFileFormat.PLAINTEXT);
+ output.addType('hxs', TextFileFormat.PLAINTEXT);
+ output.addType('hxc', TextFileFormat.PLAINTEXT);
+ output.addType('hx', TextFileFormat.PLAINTEXT);
// You can specify the format of a specific file, with file extension.
// output.addFile("data/introText.txt", TextFileFormat.LINES)
@@ -208,17 +241,21 @@ class PolymodHandler
{
return {
assetLibraryPaths: [
- "default" => "preload", "shared" => "shared", "songs" => "songs", "tutorial" => "tutorial", "week1" => "week1", "week2" => "week2",
- "week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1",
+ 'default' => 'preload', 'shared' => 'shared', 'songs' => 'songs', 'tutorial' => 'tutorial', 'week1' => 'week1', 'week2' => 'week2',
+ 'week3' => 'week3', 'week4' => 'week4', 'week5' => 'week5', 'week6' => 'week6', 'week7' => 'week7', 'weekend1' => 'weekend1',
],
coreAssetRedirect: CORE_FOLDER,
}
}
+ /**
+ * Retrieve a list of metadata for ALL installed mods, including disabled mods.
+ * @return An array of mod metadata
+ */
public static function getAllMods():Array
{
trace('Scanning the mods folder...');
- var modMetadata = Polymod.scan(
+ var modMetadata:Array = Polymod.scan(
{
modRoot: MOD_FOLDER,
apiVersionRule: API_VERSION,
@@ -228,17 +265,25 @@ class PolymodHandler
return modMetadata;
}
+ /**
+ * Retrieve a list of ALL mod IDs, including disabled mods.
+ * @return An array of mod IDs
+ */
public static function getAllModIds():Array
{
- var modIds = [for (i in getAllMods()) i.id];
+ var modIds:Array = [for (i in getAllMods()) i.id];
return modIds;
}
+ /**
+ * Retrieve a list of metadata for all enabled mods.
+ * @return An array of mod metadata
+ */
public static function getEnabledMods():Array
{
- var modIds = Save.get().enabledModIds;
- var modMetadata = getAllMods();
- var enabledMods = [];
+ var modIds:Array = Save.instance.enabledModIds;
+ var modMetadata:Array = getAllMods();
+ var enabledMods:Array = [];
for (item in modMetadata)
{
if (modIds.indexOf(item.id) != -1)
@@ -249,7 +294,11 @@ class PolymodHandler
return enabledMods;
}
- public static function forceReloadAssets()
+ /**
+ * Clear and reload from disk all data assets.
+ * Useful for "hot reloading" for fast iteration!
+ */
+ public static function forceReloadAssets():Void
{
// Forcibly clear scripts so that scripts can be edited.
ModuleHandler.clearModuleCache();
@@ -276,6 +325,7 @@ class PolymodHandler
ConversationRegistry.instance.loadEntries();
DialogueBoxRegistry.instance.loadEntries();
SpeakerRegistry.instance.loadEntries();
+ AlbumRegistry.instance.loadEntries();
StageRegistry.instance.loadEntries();
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
ModuleHandler.loadModuleCache();
diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx
index 18f934aee..0d424a281 100644
--- a/source/funkin/modding/events/ScriptEvent.hx
+++ b/source/funkin/modding/events/ScriptEvent.hx
@@ -106,12 +106,19 @@ class NoteScriptEvent extends ScriptEvent
*/
public var playSound(default, default):Bool;
- public function new(type:ScriptEventType, note:NoteSprite, comboCount:Int = 0, cancelable:Bool = false):Void
+ /**
+ * The health gained or lost from this note.
+ * This affects both hits and misses. Remember that max health is 2.00.
+ */
+ public var healthChange:Float;
+
+ public function new(type:ScriptEventType, note:NoteSprite, healthChange:Float, comboCount:Int = 0, cancelable:Bool = false):Void
{
super(type, cancelable);
this.note = note;
this.comboCount = comboCount;
this.playSound = true;
+ this.healthChange = healthChange;
}
public override function toString():String
@@ -120,6 +127,31 @@ class NoteScriptEvent extends ScriptEvent
}
}
+class HitNoteScriptEvent extends NoteScriptEvent
+{
+ /**
+ * The judgement the player received for hitting the note.
+ */
+ public var judgement:String;
+
+ /**
+ * The score the player received for hitting the note.
+ */
+ public var score:Int;
+
+ public function new(note:NoteSprite, healthChange:Float, score:Int, judgement:String, comboCount:Int = 0):Void
+ {
+ super(NOTE_HIT, note, healthChange, comboCount, true);
+ this.score = score;
+ this.judgement = judgement;
+ }
+
+ public override function toString():String
+ {
+ return 'HitNoteScriptEvent(note=' + note + ', comboCount=' + comboCount + ', judgement=' + judgement + ', score=' + score + ')';
+ }
+}
+
/**
* An event that is fired when you press a key with no note present.
*/
@@ -182,17 +214,17 @@ class SongEventScriptEvent extends ScriptEvent
* The note associated with this event.
* You cannot replace it, but you can edit it.
*/
- public var event(default, null):funkin.data.song.SongData.SongEventData;
+ public var eventData(default, null):funkin.data.song.SongData.SongEventData;
- public function new(event:funkin.data.song.SongData.SongEventData):Void
+ public function new(eventData:funkin.data.song.SongData.SongEventData):Void
{
super(SONG_EVENT, true);
- this.event = event;
+ this.eventData = eventData;
}
public override function toString():String
{
- return 'SongEventScriptEvent(event=' + event + ')';
+ return 'SongEventScriptEvent(event=' + eventData + ')';
}
}
diff --git a/source/funkin/modding/events/ScriptEventDispatcher.hx b/source/funkin/modding/events/ScriptEventDispatcher.hx
index f5d797ea4..fd58d0fad 100644
--- a/source/funkin/modding/events/ScriptEventDispatcher.hx
+++ b/source/funkin/modding/events/ScriptEventDispatcher.hx
@@ -71,17 +71,29 @@ class ScriptEventDispatcher
}
}
- if (Std.isOfType(target, IPlayStateScriptedClass))
+ if (Std.isOfType(target, INoteScriptedClass))
{
- var t:IPlayStateScriptedClass = cast(target, IPlayStateScriptedClass);
+ var t:INoteScriptedClass = cast(target, INoteScriptedClass);
switch (event.type)
{
+ case NOTE_INCOMING:
+ t.onNoteIncoming(cast event);
+ return;
case NOTE_HIT:
t.onNoteHit(cast event);
return;
case NOTE_MISS:
t.onNoteMiss(cast event);
return;
+ default: // Continue;
+ }
+ }
+
+ if (Std.isOfType(target, IPlayStateScriptedClass))
+ {
+ var t:IPlayStateScriptedClass = cast(target, IPlayStateScriptedClass);
+ switch (event.type)
+ {
case NOTE_GHOST_MISS:
t.onNoteGhostMiss(cast event);
return;
diff --git a/source/funkin/modding/events/ScriptEventType.hx b/source/funkin/modding/events/ScriptEventType.hx
index e06b5ad24..eeeb8ef29 100644
--- a/source/funkin/modding/events/ScriptEventType.hx
+++ b/source/funkin/modding/events/ScriptEventType.hx
@@ -63,6 +63,13 @@ enum abstract ScriptEventType(String) from String to String
*/
var SONG_STEP_HIT = 'STEP_HIT';
+ /**
+ * Called when a note comes on screen and starts approaching the strumline.
+ *
+ * This event is not cancelable.
+ */
+ var NOTE_INCOMING = 'NOTE_INCOMING';
+
/**
* Called when a character hits a note.
* Important information such as judgement/timing, note data, player/opponent, etc. are all provided.
diff --git a/source/funkin/modding/module/Module.hx b/source/funkin/modding/module/Module.hx
index f50c9936a..be9b7146b 100644
--- a/source/funkin/modding/module/Module.hx
+++ b/source/funkin/modding/module/Module.hx
@@ -83,7 +83,9 @@ class Module implements IPlayStateScriptedClass implements IStateChangingScripte
public function onGameOver(event:ScriptEvent) {}
- public function onNoteHit(event:NoteScriptEvent) {}
+ public function onNoteIncoming(event:NoteScriptEvent) {}
+
+ public function onNoteHit(event:HitNoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent) {}
diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx
index 5b7ce9fc2..747565100 100644
--- a/source/funkin/play/Countdown.hx
+++ b/source/funkin/play/Countdown.hx
@@ -3,6 +3,7 @@ package funkin.play;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.FlxSprite;
+import funkin.graphics.FunkinSprite;
import funkin.modding.events.ScriptEventDispatcher;
import funkin.modding.module.ModuleHandler;
import funkin.modding.events.ScriptEvent;
@@ -214,7 +215,7 @@ class Countdown
if (spritePath == null) return;
- var countdownSprite:FlxSprite = new FlxSprite(0, 0).loadGraphic(Paths.image(spritePath));
+ var countdownSprite:FunkinSprite = FunkinSprite.create(spritePath);
countdownSprite.scrollFactor.set(0, 0);
if (isPixelStyle) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE));
diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx
index 74b39417e..ec3282164 100644
--- a/source/funkin/play/GameOverSubState.hx
+++ b/source/funkin/play/GameOverSubState.hx
@@ -4,16 +4,19 @@ import flixel.FlxG;
import flixel.FlxObject;
import flixel.FlxSprite;
import flixel.sound.FlxSound;
-import funkin.ui.story.StoryMenuState;
+import funkin.audio.FunkinSound;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
import funkin.graphics.FunkinSprite;
-import funkin.ui.MusicBeatSubState;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEventDispatcher;
-import funkin.play.PlayState;
-import funkin.ui.freeplay.FreeplayState;
import funkin.play.character.BaseCharacter;
+import funkin.play.PlayState;
+import funkin.util.MathUtil;
+import funkin.ui.freeplay.FreeplayState;
+import funkin.ui.MusicBeatSubState;
+import funkin.ui.story.StoryMenuState;
+import openfl.utils.Assets;
/**
* A substate which renders over the PlayState when the player dies.
@@ -63,7 +66,7 @@ class GameOverSubState extends MusicBeatSubState
/**
* The music playing in the background of the state.
*/
- var gameOverMusic:FlxSound = new FlxSound();
+ var gameOverMusic:Null = null;
/**
* Whether the player has confirmed and prepared to restart the level.
@@ -71,10 +74,18 @@ class GameOverSubState extends MusicBeatSubState
*/
var isEnding:Bool = false;
+ /**
+ * Whether the death music is on its first loop.
+ */
+ var isStarting:Bool = true;
+
var isChartingMode:Bool = false;
var transparent:Bool;
+ final CAMERA_ZOOM_DURATION:Float = 0.5;
+ var targetCameraZoom:Float = 1.0;
+
public function new(params:GameOverParams)
{
super();
@@ -86,14 +97,14 @@ class GameOverSubState extends MusicBeatSubState
/**
* Reset the game over configuration to the default.
*/
- public static function reset()
+ public static function reset():Void
{
- animationSuffix = "";
- musicSuffix = "";
- blueBallSuffix = "";
+ animationSuffix = '';
+ musicSuffix = '';
+ blueBallSuffix = '';
}
- override public function create()
+ override public function create():Void
{
if (instance != null)
{
@@ -108,6 +119,8 @@ class GameOverSubState extends MusicBeatSubState
// Set up the visuals
//
+ var playState = PlayState.instance;
+
// Add a black background to the screen.
var bg = new FunkinSprite().makeSolidColor(FlxG.width * 2, FlxG.height * 2, FlxColor.BLACK);
// We make this transparent so that we can see the stage underneath during debugging,
@@ -119,55 +132,73 @@ class GameOverSubState extends MusicBeatSubState
// Pluck Boyfriend from the PlayState and place him (in the same position) in the GameOverSubState.
// We can then play the character's `firstDeath` animation.
- boyfriend = PlayState.instance.currentStage.getBoyfriend(true);
- boyfriend.isDead = true;
- add(boyfriend);
- boyfriend.resetCharacter();
+ if (PlayState.instance.isMinimalMode) {}
+ else
+ {
+ boyfriend = PlayState.instance.currentStage.getBoyfriend(true);
+ boyfriend.isDead = true;
+ add(boyfriend);
+ boyfriend.resetCharacter();
- // Assign a camera follow point to the boyfriend's position.
- cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
- cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x;
- cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y;
- var offsets:Array = boyfriend.getDeathCameraOffsets();
- cameraFollowPoint.x += offsets[0];
- cameraFollowPoint.y += offsets[1];
- add(cameraFollowPoint);
+ // Assign a camera follow point to the boyfriend's position.
+ cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
+ cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x;
+ cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y;
+ var offsets:Array = boyfriend.getDeathCameraOffsets();
+ cameraFollowPoint.x += offsets[0];
+ cameraFollowPoint.y += offsets[1];
+ add(cameraFollowPoint);
- FlxG.camera.target = null;
- FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.01);
+ FlxG.camera.target = null;
+ FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.01);
+ targetCameraZoom = PlayState?.instance?.currentStage?.camZoom * boyfriend.getDeathCameraZoom();
+ }
//
// Set up the audio
//
- // Prepare the game over music.
- FlxG.sound.list.add(gameOverMusic);
- gameOverMusic.stop();
-
// The conductor now represents the BPM of the game over music.
Conductor.instance.update(0);
}
+ public function resetCameraZoom():Void
+ {
+ // Apply camera zoom level from stage data.
+ FlxG.camera.zoom = PlayState?.instance?.currentStage?.camZoom ?? 1.0;
+ }
+
var hasStartedAnimation:Bool = false;
- override function update(elapsed:Float)
+ override function update(elapsed:Float):Void
{
if (!hasStartedAnimation)
{
hasStartedAnimation = true;
- if (boyfriend.hasAnimation('fakeoutDeath') && FlxG.random.bool((1 / 4096) * 100))
+ if (PlayState.instance.isMinimalMode)
{
- boyfriend.playAnimation('fakeoutDeath', true, false);
- }
- else
- {
- boyfriend.playAnimation('firstDeath', true, false); // ignoreOther is set to FALSE since you WANT to be able to mash and confirm game over!
// Play the "blue balled" sound. May play a variant if one has been assigned.
playBlueBalledSFX();
}
+ else
+ {
+ if (boyfriend.hasAnimation('fakeoutDeath') && FlxG.random.bool((1 / 4096) * 100))
+ {
+ boyfriend.playAnimation('fakeoutDeath', true, false);
+ }
+ else
+ {
+ boyfriend.playAnimation('firstDeath', true, false); // ignoreOther is set to FALSE since you WANT to be able to mash and confirm game over!
+ // Play the "blue balled" sound. May play a variant if one has been assigned.
+ playBlueBalledSFX();
+ }
+ }
}
+ // Smoothly lerp the camera
+ FlxG.camera.zoom = MathUtil.smoothLerp(FlxG.camera.zoom, targetCameraZoom, elapsed, CAMERA_ZOOM_DURATION);
+
//
// Handle user inputs.
//
@@ -216,7 +247,7 @@ class GameOverSubState extends MusicBeatSubState
}
}
- if (gameOverMusic.playing)
+ if (gameOverMusic != null && gameOverMusic.playing)
{
// Match the conductor to the music.
// This enables the stepHit and beatHit events.
@@ -224,27 +255,34 @@ class GameOverSubState extends MusicBeatSubState
}
else
{
- // Music hasn't started yet.
- switch (PlayStatePlaylist.campaignId)
+ if (PlayState.instance.isMinimalMode)
{
- // TODO: Make the behavior for playing Jeff's voicelines generic or un-hardcoded.
- // This will simplify the class and make it easier for mods to add death quotes.
- case "week7":
- if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished() && !playingJeffQuote)
- {
- playingJeffQuote = true;
- playJeffQuote();
- // Start music at lower volume
- startDeathMusic(0.2, false);
- boyfriend.playAnimation('deathLoop' + animationSuffix);
- }
- default:
- // Start music at normal volume once the initial death animation finishes.
- if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished())
- {
- startDeathMusic(1.0, false);
- boyfriend.playAnimation('deathLoop' + animationSuffix);
- }
+ // startDeathMusic(1.0, false);
+ }
+ else
+ {
+ // Music hasn't started yet.
+ switch (PlayStatePlaylist.campaignId)
+ {
+ // TODO: Make the behavior for playing Jeff's voicelines generic or un-hardcoded.
+ // This will simplify the class and make it easier for mods to add death quotes.
+ case 'week7':
+ if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished() && !playingJeffQuote)
+ {
+ playingJeffQuote = true;
+ playJeffQuote();
+ // Start music at lower volume
+ startDeathMusic(0.2, false);
+ boyfriend.playAnimation('deathLoop' + animationSuffix);
+ }
+ default:
+ // Start music at normal volume once the initial death animation finishes.
+ if (boyfriend.getCurrentAnimation().startsWith('firstDeath') && boyfriend.isAnimationFinished())
+ {
+ startDeathMusic(1.0, false);
+ boyfriend.playAnimation('deathLoop' + animationSuffix);
+ }
+ }
}
}
@@ -262,7 +300,11 @@ class GameOverSubState extends MusicBeatSubState
isEnding = true;
startDeathMusic(1.0, true); // isEnding changes this function's behavior.
- boyfriend.playAnimation('deathConfirm' + animationSuffix, true);
+ if (PlayState.instance.isMinimalMode) {}
+ else
+ {
+ boyfriend.playAnimation('deathConfirm' + animationSuffix, true);
+ }
// After the animation finishes...
new FlxTimer().start(0.7, function(tmr:FlxTimer) {
@@ -272,10 +314,17 @@ class GameOverSubState extends MusicBeatSubState
FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true);
PlayState.instance.needsReset = true;
- // Readd Boyfriend to the stage.
- boyfriend.isDead = false;
- remove(boyfriend);
- PlayState.instance.currentStage.addCharacter(boyfriend, BF);
+ if (PlayState.instance.isMinimalMode) {}
+ else
+ {
+ // Readd Boyfriend to the stage.
+ boyfriend.isDead = false;
+ remove(boyfriend);
+ PlayState.instance.currentStage.addCharacter(boyfriend, BF);
+ }
+
+ // Snap reset the camera which may have changed because of the player character data.
+ resetCameraZoom();
// Close the substate.
close();
@@ -291,24 +340,71 @@ class GameOverSubState extends MusicBeatSubState
ScriptEventDispatcher.callEvent(boyfriend, event);
}
+ /**
+ * Rather than hardcoding stuff, we look for the presence of a music file
+ * with the given suffix, and strip it down until we find one that's valid.
+ */
+ function resolveMusicPath(suffix:String, starting:Bool = false, ending:Bool = false):Null
+ {
+ var basePath = 'gameplay/gameover/gameOver';
+ if (starting) basePath += 'Start';
+ else if (ending) basePath += 'End';
+
+ var musicPath = Paths.music(basePath + suffix);
+ while (!Assets.exists(musicPath) && suffix.length > 0)
+ {
+ suffix = suffix.split('-').slice(0, -1).join('-');
+ musicPath = Paths.music(basePath + suffix);
+ }
+ if (!Assets.exists(musicPath)) return null;
+ trace('Resolved music path: ' + musicPath);
+ return musicPath;
+ }
+
/**
* Starts the death music at the appropriate volume.
* @param startingVolume
*/
- function startDeathMusic(?startingVolume:Float = 1, force:Bool = false):Void
+ public function startDeathMusic(startingVolume:Float = 1, force:Bool = false):Void
{
- var musicPath = Paths.music('gameplay/gameover/gameOver' + musicSuffix);
- if (isEnding)
+ var musicPath = resolveMusicPath(musicSuffix, isStarting, isEnding);
+ var onComplete = null;
+ if (isStarting)
{
- musicPath = Paths.music('gameplay/gameover/gameOverEnd' + musicSuffix);
+ if (musicPath == null)
+ {
+ isStarting = false;
+ musicPath = resolveMusicPath(musicSuffix, isStarting, isEnding);
+ }
+ else
+ {
+ onComplete = function() {
+ isStarting = false;
+ // We need to force to ensure that the non-starting music plays.
+ startDeathMusic(1.0, true);
+ };
+ }
}
- if (!gameOverMusic.playing || force)
+
+ if (musicPath == null)
{
- gameOverMusic.loadEmbedded(musicPath);
+ trace('Could not find game over music!');
+ return;
+ }
+ else if (gameOverMusic == null || !gameOverMusic.playing || force)
+ {
+ if (gameOverMusic != null) gameOverMusic.stop();
+ gameOverMusic = FunkinSound.load(musicPath);
gameOverMusic.volume = startingVolume;
- gameOverMusic.looped = !isEnding;
+ gameOverMusic.looped = !(isEnding || isStarting);
+ gameOverMusic.onComplete = onComplete;
gameOverMusic.play();
}
+ else
+ {
+ @:privateAccess
+ trace('Music already playing! ${gameOverMusic?._label}');
+ }
}
static var blueballed:Bool = false;
@@ -320,7 +416,14 @@ class GameOverSubState extends MusicBeatSubState
public static function playBlueBalledSFX()
{
blueballed = true;
- FlxG.sound.play(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix));
+ if (Assets.exists(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix)))
+ {
+ FlxG.sound.play(Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix));
+ }
+ else
+ {
+ FlxG.log.error('Missing blue ball sound effect: ' + Paths.sound('gameplay/gameover/fnf_loss_sfx' + blueBallSuffix));
+ }
}
var playingJeffQuote:Bool = false;
@@ -344,6 +447,14 @@ class GameOverSubState extends MusicBeatSubState
});
}
+ public override function destroy()
+ {
+ super.destroy();
+ if (gameOverMusic != null) gameOverMusic.stop();
+ gameOverMusic = null;
+ instance = null;
+ }
+
public override function toString():String
{
return "GameOverSubState";
diff --git a/source/funkin/play/GitarooPause.hx b/source/funkin/play/GitarooPause.hx
index edeb4229c..eae56a9c3 100644
--- a/source/funkin/play/GitarooPause.hx
+++ b/source/funkin/play/GitarooPause.hx
@@ -3,6 +3,7 @@ package funkin.play;
import flixel.FlxSprite;
import flixel.graphics.frames.FlxAtlasFrames;
import funkin.play.PlayState;
+import funkin.graphics.FunkinSprite;
import funkin.ui.MusicBeatState;
import flixel.addons.transition.FlxTransitionableState;
import funkin.ui.mainmenu.MainMenuState;
@@ -27,25 +28,22 @@ class GitarooPause extends MusicBeatState
{
if (FlxG.sound.music != null) FlxG.sound.music.stop();
- var bg:FlxSprite = new FlxSprite().loadGraphic(Paths.image('pauseAlt/pauseBG'));
+ var bg:FunkinSprite = FunkinSprite.create('pauseAlt/pauseBG');
add(bg);
- var bf:FlxSprite = new FlxSprite(0, 30);
- bf.frames = Paths.getSparrowAtlas('pauseAlt/bfLol');
+ var bf:FunkinSprite = FunkinSprite.createSparrow(0, 30, 'pauseAlt/bfLol');
bf.animation.addByPrefix('lol', "funnyThing", 13);
bf.animation.play('lol');
add(bf);
bf.screenCenter(X);
- replayButton = new FlxSprite(FlxG.width * 0.28, FlxG.height * 0.7);
- replayButton.frames = Paths.getSparrowAtlas('pauseAlt/pauseUI');
+ replayButton = FunkinSprite.createSparrow(FlxG.width * 0.28, FlxG.height * 0.7, 'pauseAlt/pauseUI');
replayButton.animation.addByPrefix('selected', 'bluereplay', 0, false);
replayButton.animation.appendByPrefix('selected', 'yellowreplay');
replayButton.animation.play('selected');
add(replayButton);
- cancelButton = new FlxSprite(FlxG.width * 0.58, replayButton.y);
- cancelButton.frames = Paths.getSparrowAtlas('pauseAlt/pauseUI');
+ cancelButton = FunkinSprite.createSparrow(FlxG.width * 0.58, replayButton.y, 'pauseAlt/pauseUI');
cancelButton.animation.addByPrefix('selected', 'bluecancel', 0, false);
cancelButton.animation.appendByPrefix('selected', 'cancelyellow');
cancelButton.animation.play('selected');
diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx
index ff56dcb8f..5acfcfa63 100644
--- a/source/funkin/play/PauseSubState.hx
+++ b/source/funkin/play/PauseSubState.hx
@@ -1,316 +1,738 @@
package funkin.play;
-import funkin.play.PlayStatePlaylist;
-import flixel.FlxSprite;
import flixel.addons.transition.FlxTransitionableState;
+import flixel.FlxG;
+import flixel.FlxSprite;
import flixel.group.FlxGroup.FlxTypedGroup;
-import funkin.ui.MusicBeatSubState;
-import funkin.ui.debug.latency.LatencyState;
+import flixel.group.FlxSpriteGroup;
+import flixel.math.FlxMath;
import flixel.sound.FlxSound;
import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
-import funkin.play.PlayState;
+import flixel.util.FlxTimer;
+import funkin.audio.FunkinSound;
import funkin.data.song.SongRegistry;
-import funkin.ui.Alphabet;
+import funkin.graphics.FunkinSprite;
+import funkin.play.cutscene.VideoCutscene;
+import funkin.play.PlayState;
+import funkin.ui.AtlasText;
+import funkin.ui.debug.latency.LatencyState;
+import funkin.ui.MusicBeatSubState;
+import funkin.ui.transition.StickerSubState;
+/**
+ * Parameters for initializing the PauseSubState.
+ */
+typedef PauseSubStateParams =
+{
+ /**
+ * Which mode to start in. Dictates what entries are displayed.
+ */
+ ?mode:PauseMode,
+};
+
+/**
+ * The menu displayed when the Play State is paused.
+ */
class PauseSubState extends MusicBeatSubState
{
- var grpMenuShit:FlxTypedGroup;
+ // ===============
+ // Constants
+ // ===============
- // todo: maybe make these enums or somethin?
- final pauseOptionsBase:Array = [
- 'Resume',
- 'Restart Song',
- 'Change Difficulty',
- 'Toggle Practice Mode',
- 'Adjust Input Offsets',
- 'Exit to Menu'
+ /**
+ * Pause menu entries for when the game is paused during a song.
+ */
+ static final PAUSE_MENU_ENTRIES_STANDARD:Array = [
+ {text: 'Resume', callback: resume},
+ {text: 'Restart Song', callback: restartPlayState},
+ {text: 'Change Difficulty', callback: switchMode.bind(_, Difficulty)},
+ {text: 'Enable Practice Mode', callback: enablePracticeMode, filter: () -> !(PlayState.instance?.isPracticeMode ?? false)},
+ {text: 'Exit to Menu', callback: quitToMenu},
];
- final pauseOptionsCharting:Array = ['Resume', 'Restart Song', 'Adjust Input Offsets', 'Exit to Chart Editor'];
- final pauseOptionsDifficultyBase:Array = ['BACK'];
+ /**
+ * Pause menu entries for when the game is paused in the Chart Editor preview.
+ */
+ static final PAUSE_MENU_ENTRIES_CHARTING:Array = [
+ {text: 'Resume', callback: resume},
+ {text: 'Restart Song', callback: restartPlayState},
+ {text: 'Return to Chart Editor', callback: quitToChartEditor},
+ ];
- var pauseOptionsDifficulty:Array = []; // AUTO-POPULATED
+ /**
+ * Pause menu entries for when the user selects "Change Difficulty".
+ */
+ static final PAUSE_MENU_ENTRIES_DIFFICULTY:Array = [
+ {text: 'Back', callback: switchMode.bind(_, Standard)}
+ // Other entries are added dynamically.
+ ];
- var menuItems:Array = [];
- var curSelected:Int = 0;
+ /**
+ * Pause menu entries for when the game is paused during a video cutscene.
+ */
+ static final PAUSE_MENU_ENTRIES_VIDEO_CUTSCENE:Array = [
+ {text: 'Resume', callback: resume},
+ {text: 'Restart Cutscene', callback: restartVideoCutscene},
+ {text: 'Skip Cutscene', callback: skipVideoCutscene},
+ {text: 'Exit to Menu', callback: quitToMenu},
+ ];
- var pauseMusic:FlxSound;
+ /**
+ * Pause menu entries for when the game is paused during a conversation.
+ */
+ static final PAUSE_MENU_ENTRIES_CONVERSATION:Array = [
+ {text: 'Resume', callback: resume},
+ {text: 'Restart Dialogue', callback: restartConversation},
+ {text: 'Skip Dialogue', callback: skipConversation},
+ {text: 'Exit to Menu', callback: quitToMenu},
+ ];
- var practiceText:FlxText;
+ /**
+ * Duration for the music to fade in when the pause menu is opened.
+ */
+ static final MUSIC_FADE_IN_TIME:Float = 5;
- public var exitingToMenu:Bool = false;
+ /**
+ * The final volume for the music when the pause menu is opened.
+ */
+ static final MUSIC_FINAL_VOLUME:Float = 0.75;
- var bg:FlxSprite;
- var metaDataGrp:FlxTypedGroup;
+ /**
+ * Defines which pause music to use.
+ */
+ public static var musicSuffix:String = '';
- var isChartingMode:Bool;
+ /**
+ * Reset the pause configuration to the default.
+ */
+ public static function reset():Void
+ {
+ musicSuffix = '';
+ }
- public function new(isChartingMode:Bool = false)
+ // ===============
+ // Status Variables
+ // ===============
+
+ /**
+ * Disallow input until transitions are complete!
+ * This prevents the pause menu from immediately closing when opened, among other things.
+ */
+ public var allowInput:Bool = false;
+
+ /**
+ * The entries currently displayed in the pause menu.
+ */
+ var currentMenuEntries:Array;
+
+ /**
+ * The index of `currentMenuEntries` that is currently selected.
+ */
+ var currentEntry:Int = 0;
+
+ /**
+ * The mode that the pause menu is currently in.
+ */
+ var currentMode:PauseMode;
+
+ // ===============
+ // Graphics Variables
+ // ===============
+
+ /**
+ * The semi-transparent black background that appears when the game is paused.
+ */
+ var background:FunkinSprite;
+
+ /**
+ * The metadata displayed in the top right.
+ */
+ var metadata:FlxTypedSpriteGroup;
+
+ /**
+ * A text object that displays the current practice mode status.
+ */
+ var metadataPractice:FlxText;
+
+ /**
+ * A text object that displays the current death count.
+ */
+ var metadataDeaths:FlxText;
+
+ /**
+ * The actual text objects for the menu entries.
+ */
+ var menuEntryText:FlxTypedSpriteGroup;
+
+ // ===============
+ // Audio Variables
+ // ===============
+ var pauseMusic:FunkinSound;
+
+ // ===============
+ // Constructor
+ // ===============
+
+ public function new(?params:PauseSubStateParams)
{
super();
-
- this.isChartingMode = isChartingMode;
-
- menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase;
- var difficultiesInVariation = PlayState.instance.currentSong.listDifficulties(PlayState.instance.currentChart.variation);
- trace('DIFFICULTIES: ${difficultiesInVariation}');
-
- pauseOptionsDifficulty = difficultiesInVariation.map(function(item:String):String {
- return item.toUpperCase();
- }).concat(pauseOptionsDifficultyBase);
-
- if (PlayStatePlaylist.campaignId == 'week6')
- {
- pauseMusic = new FlxSound().loadEmbedded(Paths.music('breakfast-pixel'), true, true);
- }
- else
- {
- pauseMusic = new FlxSound().loadEmbedded(Paths.music('breakfast'), true, true);
- }
- pauseMusic.volume = 0;
- pauseMusic.play(false, FlxG.random.int(0, Std.int(pauseMusic.length / 2)));
-
- FlxG.sound.list.add(pauseMusic);
-
- bg = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.BLACK);
- bg.alpha = 0;
- bg.scrollFactor.set();
- add(bg);
-
- metaDataGrp = new FlxTypedGroup();
- add(metaDataGrp);
-
- var levelInfo:FlxText = new FlxText(20, 15, 0, '', 32);
- if (PlayState.instance.currentChart != null)
- {
- levelInfo.text += '${PlayState.instance.currentChart.songName} - ${PlayState.instance.currentChart.songArtist}';
- }
- levelInfo.scrollFactor.set();
- levelInfo.setFormat(Paths.font('vcr.ttf'), 32);
- levelInfo.updateHitbox();
- metaDataGrp.add(levelInfo);
-
- var levelDifficulty:FlxText = new FlxText(20, 15 + 32, 0, '', 32);
- levelDifficulty.text += PlayState.instance.currentDifficulty.toTitleCase();
- levelDifficulty.scrollFactor.set();
- levelDifficulty.setFormat(Paths.font('vcr.ttf'), 32);
- levelDifficulty.updateHitbox();
- metaDataGrp.add(levelDifficulty);
-
- var deathCounter:FlxText = new FlxText(20, 15 + 64, 0, '', 32);
- deathCounter.text = 'Blue balled: ${PlayState.instance.deathCounter}';
- FlxG.watch.addQuick('totalNotesHit', Highscore.tallies.totalNotesHit);
- FlxG.watch.addQuick('totalNotes', Highscore.tallies.totalNotes);
- deathCounter.scrollFactor.set();
- deathCounter.setFormat(Paths.font('vcr.ttf'), 32);
- deathCounter.updateHitbox();
- metaDataGrp.add(deathCounter);
-
- practiceText = new FlxText(20, 15 + 64 + 32, 0, 'PRACTICE MODE', 32);
- practiceText.scrollFactor.set();
- practiceText.setFormat(Paths.font('vcr.ttf'), 32);
- practiceText.updateHitbox();
- practiceText.x = FlxG.width - (practiceText.width + 20);
- practiceText.visible = PlayState.instance.isPracticeMode;
- metaDataGrp.add(practiceText);
-
- levelDifficulty.alpha = 0;
- levelInfo.alpha = 0;
- deathCounter.alpha = 0;
-
- levelInfo.x = FlxG.width - (levelInfo.width + 20);
- levelDifficulty.x = FlxG.width - (levelDifficulty.width + 20);
- deathCounter.x = FlxG.width - (deathCounter.width + 20);
-
- FlxTween.tween(bg, {alpha: 0.6}, 0.4, {ease: FlxEase.quartInOut});
- FlxTween.tween(levelInfo, {alpha: 1, y: 20}, 0.4, {ease: FlxEase.quartInOut, startDelay: 0.3});
- FlxTween.tween(levelDifficulty, {alpha: 1, y: levelDifficulty.y + 5}, 0.4, {ease: FlxEase.quartInOut, startDelay: 0.5});
- FlxTween.tween(deathCounter, {alpha: 1, y: deathCounter.y + 5}, 0.4, {ease: FlxEase.quartInOut, startDelay: 0.7});
-
- grpMenuShit = new FlxTypedGroup();
- add(grpMenuShit);
-
- regenMenu();
-
- // cameras = [FlxG.cameras.list[FlxG.cameras.list.length - 1]];
+ this.currentMode = params?.mode ?? Standard;
}
- function regenMenu():Void
+ // ===============
+ // Lifecycle Functions
+ // ===============
+
+ /**
+ * Called when the state is first loaded.
+ */
+ public override function create():Void
{
- while (grpMenuShit.members.length > 0)
- {
- grpMenuShit.remove(grpMenuShit.members[0], true);
- }
+ super.create();
- for (i in 0...menuItems.length)
- {
- var songText:Alphabet = new Alphabet(0, (70 * i) + 30, menuItems[i], true, false);
- songText.isMenuItem = true;
- songText.targetY = i;
- grpMenuShit.add(songText);
- }
+ startPauseMusic();
- curSelected = 0;
- changeSelection();
+ buildBackground();
+
+ buildMetadata();
+
+ regenerateMenu();
+
+ transitionIn();
}
- override function update(elapsed:Float):Void
+ /**
+ * Called every frame.
+ * @param elapsed The time elapsed since the last frame, in seconds.
+ */
+ public override function update(elapsed:Float):Void
{
- if (pauseMusic.volume < 0.5) pauseMusic.volume += 0.01 * elapsed;
-
super.update(elapsed);
handleInputs();
}
+ /**
+ * Called when the state is closed.
+ */
+ public override function destroy():Void
+ {
+ super.destroy();
+ pauseMusic.stop();
+ }
+
+ // ===============
+ // Initialization Functions
+ // ===============
+
+ /**
+ * Play the pause music.
+ */
+ function startPauseMusic():Void
+ {
+ var pauseMusicPath:String = Paths.music('breakfast$musicSuffix');
+ pauseMusic = FunkinSound.load(pauseMusicPath, true, true);
+
+ if (pauseMusic == null)
+ {
+ FlxG.log.warn('Could not play pause music: ${pauseMusicPath} does not exist!');
+ }
+
+ // Start playing at a random point in the song.
+ pauseMusic.play(false, FlxG.random.int(0, Std.int(pauseMusic.length / 2)));
+ pauseMusic.fadeIn(MUSIC_FADE_IN_TIME, 0, MUSIC_FINAL_VOLUME);
+ }
+
+ /**
+ * Render the semi-transparent black background.
+ */
+ function buildBackground():Void
+ {
+ // Using state.bgColor causes bugs!
+ background = new FunkinSprite(0, 0);
+ background.makeSolidColor(FlxG.width, FlxG.height, FlxColor.BLACK);
+ background.alpha = 0.0;
+ background.scrollFactor.set(0, 0);
+ background.updateHitbox();
+ add(background);
+ }
+
+ /**
+ * Render the metadata in the top right.
+ */
+ function buildMetadata():Void
+ {
+ metadata = new FlxTypedSpriteGroup();
+ metadata.scrollFactor.set(0, 0);
+ add(metadata);
+
+ var metadataSong:FlxText = new FlxText(20, 15, FlxG.width - 40, 'Song Name - Artist');
+ metadataSong.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
+ if (PlayState.instance?.currentChart != null)
+ {
+ metadataSong.text = '${PlayState.instance.currentChart.songName} - ${PlayState.instance.currentChart.songArtist}';
+ }
+ metadataSong.scrollFactor.set(0, 0);
+ metadata.add(metadataSong);
+
+ var metadataDifficulty:FlxText = new FlxText(20, 15 + 32, FlxG.width - 40, 'Difficulty: ');
+ metadataDifficulty.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
+ if (PlayState.instance?.currentDifficulty != null)
+ {
+ metadataDifficulty.text += PlayState.instance.currentDifficulty.toTitleCase();
+ }
+ metadataDifficulty.scrollFactor.set(0, 0);
+ metadata.add(metadataDifficulty);
+
+ metadataDeaths = new FlxText(20, 15 + 64, FlxG.width - 40, '${PlayState.instance?.deathCounter} Blue Balls');
+ metadataDeaths.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
+ metadataDeaths.scrollFactor.set(0, 0);
+ metadata.add(metadataDeaths);
+
+ metadataPractice = new FlxText(20, 15 + 96, FlxG.width - 40, 'PRACTICE MODE');
+ metadataPractice.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
+ metadataPractice.visible = PlayState.instance?.isPracticeMode ?? false;
+ metadataPractice.scrollFactor.set(0, 0);
+ metadata.add(metadataPractice);
+
+ updateMetadataText();
+ }
+
+ /**
+ * Perform additional animations to transition the pause menu in when it is first displayed.
+ */
+ function transitionIn():Void
+ {
+ FlxTween.tween(background, {alpha: 0.6}, 0.8, {ease: FlxEase.quartOut});
+
+ // Animate each element a little bit downwards.
+ var delay:Float = 0.1;
+ for (child in metadata.members)
+ {
+ FlxTween.tween(child, {alpha: 1, y: child.y + 5}, 1.8, {ease: FlxEase.quartOut, startDelay: delay});
+ delay += 0.1;
+ }
+
+ new FlxTimer().start(0.2, (_) -> {
+ allowInput = true;
+ });
+ }
+
+ // ===============
+ // Input Handling
+ // ===============
+
+ /**
+ * Process user inputs every frame.
+ */
function handleInputs():Void
{
- var upP = controls.UI_UP_P;
- var downP = controls.UI_DOWN_P;
- var accepted = controls.ACCEPT;
+ if (!allowInput) return;
+
+ if (controls.UI_UP_P)
+ {
+ changeSelection(-1);
+ }
+ if (controls.UI_DOWN_P)
+ {
+ changeSelection(1);
+ }
+
+ if (controls.ACCEPT)
+ {
+ currentMenuEntries[currentEntry].callback(this);
+ }
+ else if (controls.PAUSE)
+ {
+ resume(this);
+ }
#if (debug || FORCE_DEBUG_VERSION)
// to pause the game and get screenshots easy, press H on pause menu!
if (FlxG.keys.justPressed.H)
{
- bg.visible = !bg.visible;
- grpMenuShit.visible = !grpMenuShit.visible;
- metaDataGrp.visible = !metaDataGrp.visible;
+ var visible = !metadata.visible;
+
+ metadata.visible = visible;
+ menuEntryText.visible = visible;
+ this.bgColor = visible ? 0x99000000 : 0x00000000; // 60% or fully transparent black
}
#end
-
- if (!exitingToMenu)
- {
- if (upP)
- {
- changeSelection(-1);
- }
- if (downP)
- {
- changeSelection(1);
- }
-
- var androidPause:Bool = false;
-
- #if android
- androidPause = FlxG.android.justPressed.BACK;
- #end
-
- if (androidPause) close();
-
- if (accepted)
- {
- var daSelected:String = menuItems[curSelected];
-
- switch (daSelected)
- {
- case 'Resume':
- close();
-
- case 'Change Difficulty':
- menuItems = pauseOptionsDifficulty;
- regenMenu();
-
- case 'Toggle Practice Mode':
- PlayState.instance.isPracticeMode = true;
- practiceText.visible = PlayState.instance.isPracticeMode;
-
- case 'Restart Song':
- PlayState.instance.needsReset = true;
- close();
-
- case 'Exit to Menu':
- exitingToMenu = true;
- PlayState.instance.deathCounter = 0;
-
- for (item in grpMenuShit.members)
- {
- item.targetY = -3;
- item.alpha = 0.6;
- }
-
- FlxTransitionableState.skipNextTransIn = true;
- FlxTransitionableState.skipNextTransOut = true;
-
- if (PlayStatePlaylist.isStoryMode)
- {
- PlayStatePlaylist.reset();
- openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.story.StoryMenuState(sticker)));
- }
- else
- {
- openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.freeplay.FreeplayState(null, sticker)));
- }
- case 'Adjust Input Offsets':
- openSubState(new LatencyState());
- case 'Exit to Chart Editor':
- this.close();
- if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position!
- PlayState.instance.close(); // This only works because PlayState is a substate!
-
- case 'BACK':
- menuItems = this.isChartingMode ? pauseOptionsCharting : pauseOptionsBase;
- regenMenu();
-
- default:
- if (pauseOptionsDifficulty.contains(daSelected))
- {
- PlayState.instance.currentSong = SongRegistry.instance.fetchEntry(PlayState.instance.currentSong.id.toLowerCase());
-
- // Reset campaign score when changing difficulty
- // So if you switch difficulty on the last song of a week you get a really low overall score.
- PlayStatePlaylist.campaignScore = 0;
- PlayStatePlaylist.campaignDifficulty = daSelected.toLowerCase();
- PlayState.instance.currentDifficulty = PlayStatePlaylist.campaignDifficulty;
-
- PlayState.instance.needsReset = true;
-
- close();
- }
- else
- {
- trace('[WARN] Unhandled pause menu option: ${daSelected}');
- }
- }
- }
-
- if (FlxG.keys.justPressed.J)
- {
- // for reference later!
- // PlayerSettings.player1.controls.replaceBinding(Control.LEFT, Keys, FlxKey.J, null);
- }
- }
- }
-
- override function destroy():Void
- {
- pauseMusic.destroy();
-
- super.destroy();
}
+ /**
+ * Move the current selection up or down.
+ * @param change The amount to change the selection by, with sign indicating direction.
+ */
function changeSelection(change:Int = 0):Void
{
FlxG.sound.play(Paths.sound('scrollMenu'), 0.4);
- curSelected += change;
+ currentEntry += change;
- if (curSelected < 0) curSelected = menuItems.length - 1;
- if (curSelected >= menuItems.length) curSelected = 0;
+ if (currentEntry < 0) currentEntry = currentMenuEntries.length - 1;
+ if (currentEntry >= currentMenuEntries.length) currentEntry = 0;
- for (index => item in grpMenuShit.members)
+ for (entryIndex in 0...currentMenuEntries.length)
{
- item.targetY = index - curSelected;
+ var isCurrent:Bool = entryIndex == currentEntry;
- item.alpha = 0.6;
+ var entry:PauseMenuEntry = currentMenuEntries[entryIndex];
+ var text:AtlasText = entry.sprite;
- if (item.targetY == 0)
- {
- item.alpha = 1;
- }
+ // Set the transparency.
+ text.alpha = isCurrent ? 1.0 : 0.6;
+
+ // Set the position.
+ var targetX = FlxMath.remapToRange((entryIndex - currentEntry), 0, 1, 0, 1.3) * 20 + 90;
+ var targetY = FlxMath.remapToRange((entryIndex - currentEntry), 0, 1, 0, 1.3) * 120 + (FlxG.height * 0.48);
+ FlxTween.globalManager.cancelTweensOf(text);
+ FlxTween.tween(text, {x: targetX, y: targetY}, 0.33, {ease: FlxEase.quartOut});
}
}
+
+ // ===============
+ // Menu Functions
+ // ===============
+
+ /**
+ * Clear the current menu entries and regenerate them based on the current mode.
+ * @param targetMode Optionally specify a mode to switch to before regenerating the menu.
+ */
+ function regenerateMenu(?targetMode:PauseMode):Void
+ {
+ // If targetMode is null, keep the current mode.
+ if (targetMode == null) targetMode = this.currentMode;
+
+ var previousMode:PauseMode = this.currentMode;
+ this.currentMode = targetMode;
+
+ resetSelection();
+ chooseMenuEntries();
+ clearAndAddMenuEntries();
+ updateMetadataText();
+ changeSelection();
+ }
+
+ /**
+ * Reset the current selection to the first entry.
+ */
+ function resetSelection():Void
+ {
+ this.currentEntry = 0;
+ }
+
+ /**
+ * Select which menu entries to display based on the current mode.
+ */
+ function chooseMenuEntries():Void
+ {
+ // Choose the correct menu entries.
+ // NOTE: We clone the arrays to prevent modifications to the arrays from affecting the original.
+ switch (this.currentMode)
+ {
+ case PauseMode.Standard:
+ currentMenuEntries = PAUSE_MENU_ENTRIES_STANDARD.clone();
+ case PauseMode.Charting:
+ currentMenuEntries = PAUSE_MENU_ENTRIES_CHARTING.clone();
+ case PauseMode.Difficulty:
+ // Prepend the difficulties.
+ var entries:Array = [];
+ if (PlayState.instance.currentChart != null)
+ {
+ var difficultiesInVariation = PlayState.instance.currentSong.listDifficulties(PlayState.instance.currentChart.variation);
+ trace('DIFFICULTIES: ${difficultiesInVariation}');
+ for (difficulty in difficultiesInVariation)
+ {
+ entries.push({text: difficulty.toTitleCase(), callback: (state) -> changeDifficulty(state, difficulty)});
+ }
+ }
+
+ // Add the back button.
+ currentMenuEntries = entries.concat(PAUSE_MENU_ENTRIES_DIFFICULTY.clone());
+ case PauseMode.Conversation:
+ currentMenuEntries = PAUSE_MENU_ENTRIES_CONVERSATION.clone();
+ case PauseMode.Cutscene:
+ currentMenuEntries = PAUSE_MENU_ENTRIES_VIDEO_CUTSCENE.clone();
+ }
+ }
+
+ /**
+ * Clear the `menuEntryText` group and render the current menu entries to it.
+ * We first create the `menuEntryText` group if it doesn't already exist.
+ */
+ function clearAndAddMenuEntries():Void
+ {
+ if (menuEntryText == null)
+ {
+ menuEntryText = new FlxTypedSpriteGroup();
+ menuEntryText.scrollFactor.set(0, 0);
+ add(menuEntryText);
+ }
+ menuEntryText.clear();
+
+ // Render out the entries depending on the mode.
+ var entryIndex:Int = 0;
+ var toRemove = [];
+ for (entry in currentMenuEntries)
+ {
+ if (entry == null || (entry.filter != null && !entry.filter()))
+ {
+ // Remove entries that should be hidden.
+ toRemove.push(entry);
+ }
+ else
+ {
+ // Handle visible entries.
+ var yPos:Float = 70 * entryIndex + 30;
+ var text:AtlasText = new AtlasText(0, yPos, entry.text, AtlasFont.BOLD);
+ text.scrollFactor.set(0, 0);
+ text.alpha = 0;
+ menuEntryText.add(text);
+
+ entry.sprite = text;
+
+ entryIndex++;
+ }
+ }
+ for (entry in toRemove)
+ {
+ currentMenuEntries.remove(entry);
+ }
+ }
+
+ // ===============
+ // Metadata Functions
+ // ===============
+
+ /**
+ * Update the values for the metadata text in the top right.
+ */
+ function updateMetadataText():Void
+ {
+ metadataPractice.visible = PlayState.instance?.isPracticeMode ?? false;
+
+ switch (this.currentMode)
+ {
+ case Standard | Difficulty:
+ metadataDeaths.text = '${PlayState.instance?.deathCounter} Blue Balls';
+ case Charting:
+ metadataDeaths.text = 'Chart Editor Preview';
+ case Conversation:
+ metadataDeaths.text = 'Dialogue Paused';
+ case Cutscene:
+ metadataDeaths.text = 'Video Paused';
+ }
+ }
+
+ // ===============
+ // Menu Callbacks
+ // ===============
+
+ /**
+ * Close the pause menu and resume the game.
+ * @param state The current PauseSubState.
+ */
+ static function resume(state:PauseSubState):Void
+ {
+ // Resume a paused video if it exists.
+ VideoCutscene.resumeVideo();
+
+ state.close();
+ }
+
+ /**
+ * Switch the pause menu to the indicated mode.
+ * Create a callback from this using `.bind(_, targetMode)`.
+ * @param state The current PauseSubState.
+ * @param targetMode The mode to switch to.
+ */
+ static function switchMode(state:PauseSubState, targetMode:PauseMode):Void
+ {
+ state.regenerateMenu(targetMode);
+ }
+
+ /**
+ * Switch the game's difficulty to the indicated difficulty, then resume the game.
+ * @param state The current PauseSubState.
+ * @param difficulty The difficulty to switch to.
+ */
+ static function changeDifficulty(state:PauseSubState, difficulty:String):Void
+ {
+ PlayState.instance.currentSong = SongRegistry.instance.fetchEntry(PlayState.instance.currentSong.id.toLowerCase());
+
+ // Reset campaign score when changing difficulty
+ // So if you switch difficulty on the last song of a week you get a really low overall score.
+ PlayStatePlaylist.campaignScore = 0;
+ PlayStatePlaylist.campaignDifficulty = difficulty;
+ PlayState.instance.currentDifficulty = PlayStatePlaylist.campaignDifficulty;
+
+ PlayState.instance.needsReset = true;
+
+ state.close();
+ }
+
+ /**
+ * Restart the current level, then resume the game.
+ * @param state The current PauseSubState.
+ */
+ static function restartPlayState(state:PauseSubState):Void
+ {
+ PlayState.instance.needsReset = true;
+ state.close();
+ }
+
+ /**
+ * Force the game into practice mode, then update the pause menu.
+ * @param state The current PauseSubState.
+ */
+ static function enablePracticeMode(state:PauseSubState):Void
+ {
+ if (PlayState.instance == null) return;
+
+ PlayState.instance.isPracticeMode = true;
+ state.regenerateMenu();
+ }
+
+ /**
+ * Restart the paused video cutscene, then resume the game.
+ * @param state The current PauseSubState.
+ */
+ static function restartVideoCutscene(state:PauseSubState):Void
+ {
+ VideoCutscene.restartVideo();
+ state.close();
+ }
+
+ /**
+ * Skip the paused video cutscene, then resume the game.
+ * @param state The current PauseSubState.
+ */
+ static function skipVideoCutscene(state:PauseSubState):Void
+ {
+ VideoCutscene.finishVideo();
+ state.close();
+ }
+
+ /**
+ * Restart the paused conversation, then resume the game.
+ * @param state The current PauseSubState.
+ */
+ static function restartConversation(state:PauseSubState):Void
+ {
+ if (PlayState.instance?.currentConversation == null) return;
+
+ PlayState.instance.currentConversation.resetConversation();
+ state.close();
+ }
+
+ /**
+ * Skip the paused conversation, then resume the game.
+ * @param state The current PauseSubState.
+ */
+ static function skipConversation(state:PauseSubState):Void
+ {
+ if (PlayState.instance?.currentConversation == null) return;
+
+ PlayState.instance.currentConversation.skipConversation();
+ state.close();
+ }
+
+ /**
+ * Quit the game and return to the main menu.
+ * @param state The current PauseSubState.
+ */
+ static function quitToMenu(state:PauseSubState):Void
+ {
+ state.allowInput = false;
+
+ PlayState.instance.deathCounter = 0;
+
+ FlxTransitionableState.skipNextTransIn = true;
+ FlxTransitionableState.skipNextTransOut = true;
+
+ if (PlayStatePlaylist.isStoryMode)
+ {
+ PlayStatePlaylist.reset();
+ state.openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.story.StoryMenuState(sticker)));
+ }
+ else
+ {
+ state.openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.freeplay.FreeplayState(null, sticker)));
+ }
+ }
+
+ /**
+ * Quit the game and return to the chart editor.
+ * @param state The current PauseSubState.
+ */
+ static function quitToChartEditor(state:PauseSubState):Void
+ {
+ state.close();
+ if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position!
+ PlayState.instance.close(); // This only works because PlayState is a substate!
+ }
}
+
+/**
+ * Which set of options the pause menu should display.
+ */
+enum PauseMode
+{
+ /**
+ * The menu displayed when the player pauses the game during a song.
+ */
+ Standard;
+
+ /**
+ * The menu displayed when the player pauses the game during a song while in charting mode.
+ */
+ Charting;
+
+ /**
+ * The menu displayed when the player moves to change the game's difficulty.
+ */
+ Difficulty;
+
+ /**
+ * The menu displayed when the player pauses the game during a conversation.
+ */
+ Conversation;
+
+ /**
+ * The menu displayed when the player pauses the game during a video cutscene.
+ */
+ Cutscene;
+}
+
+/**
+ * Represents a single entry in the pause menu.
+ */
+typedef PauseMenuEntry =
+{
+ /**
+ * The text to display for this entry.
+ * TODO: Implement localization.
+ */
+ var text:String;
+
+ /**
+ * The callback to execute when the user selects this entry.
+ */
+ var callback:PauseSubState->Void;
+
+ /**
+ * If this returns true, the entry will be displayed. If it returns false, the entry will be hidden.
+ */
+ var ?filter:Void->Bool;
+
+ // Instance-specific properties
+
+ /**
+ * The text object currently displaying this entry.
+ */
+ var ?sprite:AtlasText;
+};
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index 1dbba5b54..12b290afd 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -1,21 +1,20 @@
package funkin.play;
-import flixel.addons.display.FlxPieDial;
+import funkin.audio.FunkinSound;
import flixel.addons.display.FlxPieDial;
import flixel.addons.transition.FlxTransitionableState;
-import flixel.addons.transition.FlxTransitionableState;
-import flixel.addons.transition.FlxTransitionableSubState;
-import flixel.addons.transition.FlxTransitionableSubState;
import flixel.addons.transition.Transition;
import flixel.addons.transition.Transition;
import flixel.FlxCamera;
import flixel.FlxObject;
-import flixel.FlxSprite;
import flixel.FlxState;
+import funkin.graphics.FunkinSprite;
import flixel.FlxSubState;
+import funkin.graphics.FunkinSprite;
import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
+import funkin.graphics.FunkinSprite;
import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import flixel.tweens.FlxTween;
@@ -95,7 +94,7 @@ typedef PlayStateParams =
?targetDifficulty:String,
/**
* The variation to play on.
- * @default `Constants.DEFAULT_VARIATION` .
+ * @default `Constants.DEFAULT_VARIATION`
*/
?targetVariation:String,
/**
@@ -109,6 +108,11 @@ typedef PlayStateParams =
* @default `false`
*/
?practiceMode:Bool,
+ /**
+ * Whether the song should start in Bot Play Mode.
+ * @default `false`
+ */
+ ?botPlayMode:Bool,
/**
* Whether the song should be in minimal mode.
* @default `false`
@@ -116,8 +120,14 @@ typedef PlayStateParams =
?minimalMode:Bool,
/**
* If specified, the game will jump to the specified timestamp after the countdown ends.
+ * @default `0.0`
*/
?startTimestamp:Float,
+ /**
+ * If specified, the game will play the song with the given speed.
+ * @default `1.0` for 100% speed.
+ */
+ ?playbackRate:Float,
/**
* If specified, the game will not load the instrumental or vocal tracks,
* and must be loaded externally.
@@ -208,15 +218,32 @@ class PlayState extends MusicBeatSubState
*/
public var startTimestamp:Float = 0.0;
+ /**
+ * Play back the song at this speed.
+ * @default `1.0` for normal speed.
+ */
+ public var playbackRate:Float = 1.0;
+
/**
* An empty FlxObject contained in the scene.
* The current gameplay camera will always follow this object. Tween its position to move the camera smoothly.
*
* It needs to be an object in the scene for the camera to be configured to follow it.
- * We optionally make this an FlxSprite so we can draw a debug graphic with it.
+ * We optionally make this a sprite so we can draw a debug graphic with it.
*/
public var cameraFollowPoint:FlxObject;
+ /**
+ * An FlxTween that tweens the camera to the follow point.
+ * Only used when tweening the camera manually, rather than tweening via follow.
+ */
+ public var cameraFollowTween:FlxTween;
+
+ /**
+ * An FlxTween that zooms the camera to the desired amount.
+ */
+ public var cameraZoomTween:FlxTween;
+
/**
* The camera follow point from the last stage.
* Used to persist the position of the `cameraFollowPosition` between levels.
@@ -224,14 +251,23 @@ class PlayState extends MusicBeatSubState
public var previousCameraFollowPoint:FlxPoint = null;
/**
- * The current camera zoom level.
- *
- * The camera zoom is increased every beat, and lerped back to this value every frame, creating a smooth 'zoom-in' effect.
- * Defaults to 1.05 but may be larger or smaller depending on the current stage,
- * and may be changed by the `ZoomCamera` song event.
+ * The current camera zoom level without any modifiers applied.
+ */
+ public var currentCameraZoom:Float = FlxCamera.defaultZoom * 1.05;
+
+ /**
+ * currentCameraZoom is increased every beat, and lerped back to this value every frame, creating a smooth 'zoom-in' effect.
+ * Defaults to 1.05, but may be larger or smaller depending on the current stage.
+ * Tweened via the `ZoomCamera` song event in direct mode.
*/
public var defaultCameraZoom:Float = FlxCamera.defaultZoom * 1.05;
+ /**
+ * Camera zoom applied on top of currentCameraZoom.
+ * Tweened via the `ZoomCamera` song event in additive mode.
+ */
+ public var additiveCameraZoom:Float = 0;
+
/**
* The current HUD camera zoom level.
*
@@ -268,6 +304,18 @@ class PlayState extends MusicBeatSubState
*/
public var isPracticeMode:Bool = false;
+ /**
+ * Whether the game is currently in Bot Play Mode.
+ * If true, player will not lose gain or lose score from notes.
+ */
+ public var isBotPlayMode:Bool = false;
+
+ /**
+ * Whether the player has dropped below zero health,
+ * and we are just waiting for an animation to play out before transitioning.
+ */
+ public var isPlayerDying:Bool = false;
+
/**
* In Minimal Mode, the stage and characters are not loaded and a standard background is used.
*/
@@ -314,6 +362,11 @@ class PlayState extends MusicBeatSubState
**/
var inputReleaseQueue:Array = [];
+ /**
+ * If we just unpaused the game, we shouldn't be able to pause again for one frame.
+ */
+ var justUnpaused:Bool = false;
+
/**
* PRIVATE INSTANCE VARIABLES
* Private instance variables should be used for information that must be reset or dereferenced
@@ -360,10 +413,15 @@ class PlayState extends MusicBeatSubState
var startingSong:Bool = false;
/**
- * False if `FlxG.sound.music`
+ * Track if we currently have the music paused for a Pause substate, so we can unpause it when we return.
*/
var musicPausedBySubState:Bool = false;
+ /**
+ * Track any camera tweens we've paused for a Pause substate, so we can unpause them when we return.
+ */
+ var cameraTweensPausedBySubState:List = new List();
+
/**
* False until `create()` has completed.
*/
@@ -400,7 +458,7 @@ class PlayState extends MusicBeatSubState
* The background image used for the health bar.
* Emma says the image is slightly skewed so I'm leaving it as an image instead of a `createGraphic`.
*/
- public var healthBarBG:FlxSprite;
+ public var healthBarBG:FunkinSprite;
/**
* The health icon representing the player.
@@ -442,11 +500,6 @@ class PlayState extends MusicBeatSubState
*/
var comboPopUps:PopUpStuff;
- /**
- * The circular sprite that appears while the user is holding down the Skip Cutscene button.
- */
- var skipTimer:FlxPieDial;
-
/**
* PROPERTIES
*/
@@ -474,7 +527,7 @@ class PlayState extends MusicBeatSubState
if (!Std.isOfType(this.subState, PauseSubState)) return false;
var pauseSubState:PauseSubState = cast this.subState;
- return pauseSubState.exitingToMenu;
+ return !pauseSubState.allowInput;
}
/**
@@ -546,8 +599,10 @@ class PlayState extends MusicBeatSubState
if (params.targetDifficulty != null) currentDifficulty = params.targetDifficulty;
if (params.targetVariation != null) currentVariation = params.targetVariation;
isPracticeMode = params.practiceMode ?? false;
+ isBotPlayMode = params.botPlayMode ?? false;
isMinimalMode = params.minimalMode ?? false;
startTimestamp = params.startTimestamp ?? 0.0;
+ playbackRate = params.playbackRate ?? 1.0;
overrideMusic = params.overrideMusic ?? false;
previousCameraFollowPoint = params.cameraFollowPoint;
@@ -568,12 +623,15 @@ class PlayState extends MusicBeatSubState
if (!assertChartExists()) return;
+ // TODO: Add something to toggle this on!
if (false)
{
// Displays the camera follow point as a sprite for debug purposes.
- cameraFollowPoint = new FlxSprite(0, 0).makeGraphic(8, 8, 0xFF00FF00);
+ var cameraFollowPoint = new FunkinSprite(0, 0);
+ cameraFollowPoint.makeSolidColor(8, 8, 0xFF00FF00);
cameraFollowPoint.visible = false;
cameraFollowPoint.zIndex = 1000000;
+ this.cameraFollowPoint = cameraFollowPoint;
}
else
{
@@ -626,16 +684,9 @@ class PlayState extends MusicBeatSubState
// Initialize the judgements and combo meter.
comboPopUps = new PopUpStuff();
- comboPopUps.cameras = [camHUD];
+ comboPopUps.zIndex = 900;
add(comboPopUps);
-
- // The little dial that shows up when you hold the Skip Cutscene key.
- skipTimer = new FlxPieDial(16, 16, 32, FlxColor.WHITE, 36, CIRCLE, true, 24);
- skipTimer.amount = 0;
- skipTimer.zIndex = 1000;
- add(skipTimer);
- // Renders only in video cutscene mode.
- skipTimer.cameras = [camCutscene];
+ comboPopUps.cameras = [camHUD];
#if discord_rpc
// Initialize Discord Rich Presence.
@@ -752,6 +803,8 @@ class PlayState extends MusicBeatSubState
public override function update(elapsed:Float):Void
{
+ // TOTAL: 9.42% CPU Time when profiled in VS 2019.
+
if (criticalFailure) return;
super.update(elapsed);
@@ -772,12 +825,13 @@ class PlayState extends MusicBeatSubState
persistentDraw = true;
startingSong = true;
+ isPlayerDying = false;
inputSpitter = [];
// Reset music properly.
-
- FlxG.sound.music.time = Math.max(0, startTimestamp - Conductor.instance.instrumentalOffset);
+ FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset;
+ FlxG.sound.music.pitch = playbackRate;
FlxG.sound.music.pause();
if (!overrideMusic)
@@ -853,7 +907,7 @@ class PlayState extends MusicBeatSubState
#end
// Attempt to pause the game.
- if ((controls.PAUSE || androidPause) && isInCountdown && mayPauseGame)
+ if ((controls.PAUSE || androidPause) && isInCountdown && mayPauseGame && !justUnpaused)
{
var event = new PauseScriptEvent(FlxG.random.bool(1 / 1000));
@@ -888,12 +942,12 @@ class PlayState extends MusicBeatSubState
boyfriendPos = currentStage.getBoyfriend().getScreenPosition();
}
- var pauseSubState:FlxSubState = new PauseSubState(isChartingMode);
+ var pauseSubState:FlxSubState = new PauseSubState({mode: isChartingMode ? Charting : Standard});
- FlxTransitionableSubState.skipNextTransIn = true;
- FlxTransitionableSubState.skipNextTransOut = true;
- openSubState(pauseSubState);
+ FlxTransitionableState.skipNextTransIn = true;
+ FlxTransitionableState.skipNextTransOut = true;
pauseSubState.camera = camHUD;
+ openSubState(pauseSubState);
// boyfriendPos.put(); // TODO: Why is this here?
}
@@ -910,14 +964,17 @@ class PlayState extends MusicBeatSubState
// Lerp the camera zoom towards the target level.
if (subState == null)
{
- FlxG.camera.zoom = FlxMath.lerp(defaultCameraZoom, FlxG.camera.zoom, 0.95);
+ currentCameraZoom = FlxMath.lerp(defaultCameraZoom, currentCameraZoom, 0.95);
+ FlxG.camera.zoom = currentCameraZoom + additiveCameraZoom;
+
camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, 0.95);
}
- if (currentStage != null)
+ if (currentStage != null && currentStage.getBoyfriend() != null)
{
FlxG.watch.addQuick('bfAnim', currentStage.getBoyfriend().getCurrentAnimation());
}
+ FlxG.watch.addQuick('health', health);
// TODO: Add a song event for Handle GF dance speed.
@@ -939,7 +996,7 @@ class PlayState extends MusicBeatSubState
}
#end
- if (health <= Constants.HEALTH_MIN && !isPracticeMode)
+ if (health <= Constants.HEALTH_MIN && !isPracticeMode && !isPlayerDying)
{
vocals.pause();
FlxG.sound.music.pause();
@@ -965,24 +1022,61 @@ class PlayState extends MusicBeatSubState
}
#end
- var gameOverSubState = new GameOverSubState(
- {
- isChartingMode: isChartingMode,
- transparent: persistentDraw
+ isPlayerDying = true;
+
+ var deathPreTransitionDelay = currentStage?.getBoyfriend()?.getDeathPreTransitionDelay() ?? 0.0;
+ if (deathPreTransitionDelay > 0)
+ {
+ new FlxTimer().start(deathPreTransitionDelay, function(_) {
+ moveToGameOver();
});
- FlxTransitionableSubState.skipNextTransIn = true;
- FlxTransitionableSubState.skipNextTransOut = true;
- openSubState(gameOverSubState);
+ }
+ else
+ {
+ // Transition immediately.
+ moveToGameOver();
+ }
#if discord_rpc
// Game Over doesn't get his own variable because it's only used here
DiscordClient.changePresence('Game Over - ' + detailsText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC);
#end
}
+ else if (isPlayerDying)
+ {
+ // Wait up.
+ }
}
+ processSongEvents();
+
+ // Handle keybinds.
+ processInputQueue();
+ if (!isInCutscene && !disableKeys) debugKeyShit();
+ if (isInCutscene && !disableKeys) handleCutsceneKeys(elapsed);
+
+ // Moving notes into position is now done by Strumline.update().
+ if (!isInCutscene) processNotes(elapsed);
+
+ justUnpaused = false;
+ }
+
+ function moveToGameOver():Void
+ {
+ var gameOverSubState = new GameOverSubState(
+ {
+ isChartingMode: isChartingMode,
+ transparent: persistentDraw
+ });
+ FlxTransitionableState.skipNextTransIn = true;
+ FlxTransitionableState.skipNextTransOut = true;
+ openSubState(gameOverSubState);
+ }
+
+ function processSongEvents():Void
+ {
// Query and activate song events.
- // TODO: Check that these work even when songPosition is less than 0.
+ // TODO: Check that these work appropriately even when songPosition is less than 0, to play events during countdown.
if (songEvents != null && songEvents.length > 0)
{
var songEventsToActivate:Array = SongEventRegistry.queryEvents(songEvents, Conductor.instance.songPosition);
@@ -992,8 +1086,9 @@ class PlayState extends MusicBeatSubState
trace('Found ${songEventsToActivate.length} event(s) to activate.');
for (event in songEventsToActivate)
{
- // If an event is trying to play, but it's over 5 seconds old, skip it.
- if (event.time - Conductor.instance.songPosition < -5000)
+ // If an event is trying to play, but it's over 1 second old, skip it.
+ var eventAge:Float = Conductor.instance.songPosition - event.time;
+ if (eventAge > 1000)
{
event.activated = true;
continue;
@@ -1009,14 +1104,6 @@ class PlayState extends MusicBeatSubState
}
}
}
-
- // Handle keybinds.
- processInputQueue();
- if (!isInCutscene && !disableKeys) debugKeyShit();
- if (isInCutscene && !disableKeys) handleCutsceneKeys(elapsed);
-
- // Moving notes into position is now done by Strumline.update().
- processNotes(elapsed);
}
public override function dispatchEvent(event:ScriptEvent):Void
@@ -1057,11 +1144,30 @@ class PlayState extends MusicBeatSubState
// Pause the music.
if (FlxG.sound.music != null)
{
- musicPausedBySubState = FlxG.sound.music.playing;
- FlxG.sound.music.pause();
+ if (FlxG.sound.music.playing)
+ {
+ FlxG.sound.music.pause();
+ musicPausedBySubState = true;
+ }
+
+ // Pause vocals.
+ // Not tracking that we've done this via a bool because vocal re-syncing involves pausing the vocals anyway.
if (vocals != null) vocals.pause();
}
+ // Pause camera tweening, and keep track of which tweens we pause.
+ if (cameraFollowTween != null && cameraFollowTween.active)
+ {
+ cameraFollowTween.active = false;
+ cameraTweensPausedBySubState.add(cameraFollowTween);
+ }
+
+ if (cameraZoomTween != null && cameraZoomTween.active)
+ {
+ cameraZoomTween.active = false;
+ cameraTweensPausedBySubState.add(cameraZoomTween);
+ }
+
// Pause the countdown.
Countdown.pauseCountdown();
}
@@ -1083,12 +1189,26 @@ class PlayState extends MusicBeatSubState
if (event.eventCanceled) return;
- // Resume
+ // Resume music if we paused it.
if (musicPausedBySubState)
{
- FlxG.sound.music.play(FlxG.sound.music.time);
+ FlxG.sound.music.play();
+ musicPausedBySubState = false;
}
+ // Resume camera tweens if we paused any.
+ for (camTween in cameraTweensPausedBySubState)
+ {
+ camTween.active = true;
+ }
+ cameraTweensPausedBySubState.clear();
+
+ if (currentConversation != null)
+ {
+ currentConversation.resumeMusic();
+ }
+
+ // Re-sync vocals.
if (FlxG.sound.music != null && !startingSong && !isInCutscene) resyncVocals();
// Resume the countdown.
@@ -1105,6 +1225,8 @@ class PlayState extends MusicBeatSubState
DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC);
}
#end
+
+ justUnpaused = true;
}
else if (Std.isOfType(subState, Transition))
{
@@ -1234,7 +1356,7 @@ class PlayState extends MusicBeatSubState
if (FlxG.camera.zoom < (1.35 * defaultCameraZoom) && cameraZoomRate > 0 && Conductor.instance.currentBeat % cameraZoomRate == 0)
{
// Zoom camera in (1.5%)
- FlxG.camera.zoom += cameraZoomIntensity * defaultCameraZoom;
+ currentCameraZoom += cameraZoomIntensity * defaultCameraZoom;
// Hud zooms double (3%)
camHUD.zoom += hudCameraZoomIntensity * defaultHUDCameraZoom;
}
@@ -1265,6 +1387,7 @@ class PlayState extends MusicBeatSubState
{
var animShit:ComboMilestone = new ComboMilestone(-100, 300, Highscore.tallies.combo);
animShit.scrollFactor.set(0.6, 0.6);
+ animShit.zIndex = 1100;
animShit.cameras = [camHUD];
add(animShit);
@@ -1286,12 +1409,6 @@ class PlayState extends MusicBeatSubState
public override function destroy():Void
{
- if (currentConversation != null)
- {
- remove(currentConversation);
- currentConversation.kill();
- }
-
performCleanup();
super.destroy();
@@ -1348,21 +1465,24 @@ class PlayState extends MusicBeatSubState
function initHealthBar():Void
{
var healthBarYPos:Float = Preferences.downscroll ? FlxG.height * 0.1 : FlxG.height * 0.9;
- healthBarBG = new FlxSprite(0, healthBarYPos).loadGraphic(Paths.image('healthBar'));
+ healthBarBG = FunkinSprite.create(0, healthBarYPos, 'healthBar');
healthBarBG.screenCenter(X);
healthBarBG.scrollFactor.set(0, 0);
+ healthBarBG.zIndex = 800;
add(healthBarBG);
healthBar = new FlxBar(healthBarBG.x + 4, healthBarBG.y + 4, RIGHT_TO_LEFT, Std.int(healthBarBG.width - 8), Std.int(healthBarBG.height - 8), this,
'healthLerp', 0, 2);
healthBar.scrollFactor.set();
healthBar.createFilledBar(Constants.COLOR_HEALTH_BAR_RED, Constants.COLOR_HEALTH_BAR_GREEN);
+ healthBar.zIndex = 801;
add(healthBar);
// The score text below the health bar.
scoreText = new FlxText(healthBarBG.x + healthBarBG.width - 190, healthBarBG.y + 30, 0, '', 20);
scoreText.setFormat(Paths.font('vcr.ttf'), 16, FlxColor.WHITE, RIGHT, FlxTextBorderStyle.OUTLINE, FlxColor.BLACK);
scoreText.scrollFactor.set();
+ scoreText.zIndex = 802;
add(scoreText);
// Move the health bar to the HUD camera.
@@ -1382,7 +1502,7 @@ class PlayState extends MusicBeatSubState
function initMinimalMode():Void
{
// Create the green background.
- var menuBG = new FlxSprite().loadGraphic(Paths.image('menuDesat'));
+ var menuBG = FunkinSprite.create('menuDesat');
menuBG.color = 0xFF4CAF50;
menuBG.setGraphicSize(Std.int(menuBG.width * 1.1));
menuBG.updateHitbox();
@@ -1408,8 +1528,7 @@ class PlayState extends MusicBeatSubState
var event:ScriptEvent = new ScriptEvent(CREATE, false);
ScriptEventDispatcher.callEvent(currentStage, event);
- // Apply camera zoom level from stage data.
- defaultCameraZoom = currentStage.camZoom;
+ resetCameraZoom();
// Add the stage to the scene.
this.add(currentStage);
@@ -1425,6 +1544,18 @@ class PlayState extends MusicBeatSubState
}
}
+ public function resetCameraZoom():Void
+ {
+ if (PlayState.instance.isMinimalMode) return;
+ // Apply camera zoom level from stage data.
+ defaultCameraZoom = currentStage.camZoom;
+ currentCameraZoom = defaultCameraZoom;
+ FlxG.camera.zoom = currentCameraZoom;
+
+ // Reset additive zoom.
+ additiveCameraZoom = 0;
+ }
+
/**
* Generates the character sprites and adds them to the stage.
*/
@@ -1463,16 +1594,17 @@ class PlayState extends MusicBeatSubState
if (dad != null)
{
dad.characterType = CharacterType.DAD;
- }
- //
- // OPPONENT HEALTH ICON
- //
- iconP2 = new HealthIcon('dad', 1);
- iconP2.y = healthBar.y - (iconP2.height / 2);
- dad.initHealthIcon(true); // Apply the character ID here
- add(iconP2);
- iconP2.cameras = [camHUD];
+ //
+ // OPPONENT HEALTH ICON
+ //
+ iconP2 = new HealthIcon('dad', 1);
+ iconP2.y = healthBar.y - (iconP2.height / 2);
+ dad.initHealthIcon(true); // Apply the character ID here
+ iconP2.zIndex = 850;
+ add(iconP2);
+ iconP2.cameras = [camHUD];
+ }
//
// BOYFRIEND
@@ -1482,16 +1614,17 @@ class PlayState extends MusicBeatSubState
if (boyfriend != null)
{
boyfriend.characterType = CharacterType.BF;
- }
- //
- // PLAYER HEALTH ICON
- //
- iconP1 = new HealthIcon('bf', 0);
- iconP1.y = healthBar.y - (iconP1.height / 2);
- boyfriend.initHealthIcon(false); // Apply the character ID here
- add(iconP1);
- iconP1.cameras = [camHUD];
+ //
+ // PLAYER HEALTH ICON
+ //
+ iconP1 = new HealthIcon('bf', 0);
+ iconP1.y = healthBar.y - (iconP1.height / 2);
+ boyfriend.initHealthIcon(false); // Apply the character ID here
+ iconP1.zIndex = 850;
+ add(iconP1);
+ iconP1.cameras = [camHUD];
+ }
//
// ADD CHARACTERS TO SCENE
@@ -1548,8 +1681,10 @@ class PlayState extends MusicBeatSubState
var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
- playerStrumline = new Strumline(noteStyle, true);
+ playerStrumline = new Strumline(noteStyle, !isBotPlayMode);
+ playerStrumline.onNoteIncoming.add(onStrumlineNoteIncoming);
opponentStrumline = new Strumline(noteStyle, false);
+ opponentStrumline.onNoteIncoming.add(onStrumlineNoteIncoming);
add(playerStrumline);
add(opponentStrumline);
@@ -1557,13 +1692,13 @@ class PlayState extends MusicBeatSubState
playerStrumline.x = FlxG.width / 2 + Constants.STRUMLINE_X_OFFSET; // Classic style
// playerStrumline.x = FlxG.width - playerStrumline.width - Constants.STRUMLINE_X_OFFSET; // Centered style
playerStrumline.y = Preferences.downscroll ? FlxG.height - playerStrumline.height - Constants.STRUMLINE_Y_OFFSET : Constants.STRUMLINE_Y_OFFSET;
- playerStrumline.zIndex = 200;
+ playerStrumline.zIndex = 1001;
playerStrumline.cameras = [camHUD];
// Position the opponent strumline on the left half of the screen
opponentStrumline.x = Constants.STRUMLINE_X_OFFSET;
opponentStrumline.y = Preferences.downscroll ? FlxG.height - opponentStrumline.height - Constants.STRUMLINE_Y_OFFSET : Constants.STRUMLINE_Y_OFFSET;
- opponentStrumline.zIndex = 100;
+ opponentStrumline.zIndex = 1000;
opponentStrumline.cameras = [camHUD];
if (!PlayStatePlaylist.isStoryMode)
@@ -1666,7 +1801,6 @@ class PlayState extends MusicBeatSubState
if (strumTime < startTime) continue; // Skip notes that are before the start time.
var noteData:Int = songNote.getDirection();
-
var playerNote:Bool = true;
if (noteData > 3) playerNote = false;
@@ -1675,6 +1809,8 @@ class PlayState extends MusicBeatSubState
{
case 0:
playerNoteData.push(songNote);
+ // increment totalNotes for total possible notes able to be hit by the player
+ Highscore.tallies.totalNotes++;
case 1:
opponentNoteData.push(songNote);
}
@@ -1684,6 +1820,13 @@ class PlayState extends MusicBeatSubState
opponentStrumline.applyNoteData(opponentNoteData);
}
+ function onStrumlineNoteIncoming(noteSprite:NoteSprite):Void
+ {
+ var event:NoteScriptEvent = new NoteScriptEvent(NOTE_INCOMING, noteSprite, 0, false);
+
+ dispatchEvent(event);
+ }
+
/**
* Prepares to start the countdown.
* Ends any running cutscenes, creates the strumlines, and starts the countdown.
@@ -1710,6 +1853,7 @@ class PlayState extends MusicBeatSubState
currentConversation = ConversationRegistry.instance.fetchEntry(conversationId);
if (currentConversation == null) return;
+ if (!currentConversation.alive) currentConversation.revive();
currentConversation.completeCallback = onConversationComplete;
currentConversation.cameras = [camCutscene];
@@ -1727,8 +1871,13 @@ class PlayState extends MusicBeatSubState
function onConversationComplete():Void
{
isInCutscene = false;
- remove(currentConversation);
- currentConversation = null;
+
+ if (currentConversation != null)
+ {
+ currentConversation.kill();
+ remove(currentConversation);
+ currentConversation = null;
+ }
if (startingSong && !isInCountdown)
{
@@ -1750,16 +1899,21 @@ class PlayState extends MusicBeatSubState
currentChart.playInst(1.0, false);
}
- FlxG.sound.music.onComplete = endSong;
+ FlxG.sound.music.onComplete = endSong.bind(false);
// A negative instrumental offset means the song skips the first few milliseconds of the track.
// This just gets added into the startTimestamp behavior so we don't need to do anything extra.
- FlxG.sound.music.time = startTimestamp - Conductor.instance.instrumentalOffset;
+ FlxG.sound.music.play(true, startTimestamp - Conductor.instance.instrumentalOffset);
+ FlxG.sound.music.pitch = playbackRate;
+
+ // I am going insane.
+ FlxG.sound.music.volume = 1.0;
+
+ FlxG.sound.music.fadeTween?.cancel();
trace('Playing vocals...');
add(vocals);
-
- FlxG.sound.music.play(FlxG.sound.music.time);
vocals.play();
+ vocals.pitch = playbackRate;
resyncVocals();
#if discord_rpc
@@ -1798,7 +1952,14 @@ class PlayState extends MusicBeatSubState
function updateScoreText():Void
{
// TODO: Add functionality for modules to update the score text.
- scoreText.text = 'Score:' + songScore;
+ if (isBotPlayMode)
+ {
+ scoreText.text = 'Bot Play Enabled';
+ }
+ else
+ {
+ scoreText.text = 'Score:' + songScore;
+ }
}
/**
@@ -1806,7 +1967,14 @@ class PlayState extends MusicBeatSubState
*/
function updateHealthBar():Void
{
- healthLerp = FlxMath.lerp(healthLerp, health, 0.15);
+ if (isBotPlayMode)
+ {
+ healthLerp = Constants.HEALTH_MAX;
+ }
+ else
+ {
+ healthLerp = FlxMath.lerp(healthLerp, health, 0.15);
+ }
}
/**
@@ -1850,13 +2018,16 @@ class PlayState extends MusicBeatSubState
if (Conductor.instance.songPosition > hitWindowEnd)
{
- if (note.hasMissed) continue;
+ if (note.hasMissed || note.hasBeenHit) continue;
note.tooEarly = false;
note.mayHit = false;
note.hasMissed = true;
- if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true;
+ if (note.holdNoteSprite != null)
+ {
+ note.holdNoteSprite.missedNote = true;
+ }
}
else if (Conductor.instance.songPosition > hitWindowCenter)
{
@@ -1864,7 +2035,7 @@ class PlayState extends MusicBeatSubState
// Call an event to allow canceling the note hit.
// NOTE: This is what handles the character animations!
- var event:NoteScriptEvent = new NoteScriptEvent(NOTE_HIT, note, 0, true);
+ var event:NoteScriptEvent = new HitNoteScriptEvent(note, 0.0, 0, 'perfect', 0);
dispatchEvent(event);
// Calling event.cancelEvent() skips all the other logic! Neat!
@@ -1943,10 +2114,38 @@ class PlayState extends MusicBeatSubState
if (Conductor.instance.songPosition > hitWindowEnd)
{
+ if (note.hasMissed || note.hasBeenHit) continue;
note.tooEarly = false;
note.mayHit = false;
note.hasMissed = true;
- if (note.holdNoteSprite != null) note.holdNoteSprite.missedNote = true;
+ if (note.holdNoteSprite != null)
+ {
+ note.holdNoteSprite.missedNote = true;
+ }
+ }
+ else if (isBotPlayMode && Conductor.instance.songPosition > hitWindowCenter)
+ {
+ if (note.hasBeenHit) continue;
+
+ // We call onHitNote to play the proper animations,
+ // but not goodNoteHit! This means zero score and zero notes hit for the results screen!
+
+ // Call an event to allow canceling the note hit.
+ // NOTE: This is what handles the character animations!
+ var event:NoteScriptEvent = new HitNoteScriptEvent(note, 0.0, 0, 'perfect', 0);
+ dispatchEvent(event);
+
+ // Calling event.cancelEvent() skips all the other logic! Neat!
+ if (event.eventCanceled) continue;
+
+ // Command the bot to hit the note on time.
+ // NOTE: This is what handles the strumline and cleaning up the note itself!
+ playerStrumline.hitNote(note);
+
+ if (note.holdNoteSprite != null)
+ {
+ playerStrumline.playNoteHoldCover(note.holdNoteSprite);
+ }
}
else if (Conductor.instance.songPosition > hitWindowStart)
{
@@ -1969,7 +2168,7 @@ class PlayState extends MusicBeatSubState
{
// Call an event to allow canceling the note miss.
// NOTE: This is what handles the character animations!
- var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, 0, true);
+ var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, -Constants.HEALTH_MISS_PENALTY, 0, true);
dispatchEvent(event);
// Calling event.cancelEvent() skips all the other logic! Neat!
@@ -1978,7 +2177,7 @@ class PlayState extends MusicBeatSubState
// Judge the miss.
// NOTE: This is what handles the scoring.
trace('Missed note! ${note.noteData}');
- onNoteMiss(note);
+ onNoteMiss(note, event.playSound, event.healthChange);
note.handledMiss = true;
}
@@ -1991,7 +2190,7 @@ class PlayState extends MusicBeatSubState
if (holdNote == null || !holdNote.alive) continue;
// While the hold note is being hit, and there is length on the hold note...
- if (holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0)
+ if (!isBotPlayMode && holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0)
{
// Grant the player health.
health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed;
@@ -2030,6 +2229,7 @@ class PlayState extends MusicBeatSubState
}
}
+ // Respawns notes that were b
playerStrumline.handleSkippedNotes();
opponentStrumline.handleSkippedNotes();
}
@@ -2123,13 +2323,41 @@ class PlayState extends MusicBeatSubState
function goodNoteHit(note:NoteSprite, input:PreciseInputEvent):Void
{
- var event:NoteScriptEvent = new NoteScriptEvent(NOTE_HIT, note, Highscore.tallies.combo + 1, true);
+ // Calculate the input latency (do this as late as possible).
+ // trace('Compare: ${PreciseInputManager.getCurrentTimestamp()} - ${input.timestamp}');
+ var inputLatencyNs:Int64 = PreciseInputManager.getCurrentTimestamp() - input.timestamp;
+ var inputLatencyMs:Float = inputLatencyNs.toFloat() / Constants.NS_PER_MS;
+ // trace('Input: ${daNote.noteData.getDirectionName()} pressed ${inputLatencyMs}ms ago!');
+
+ // Get the offset and compensate for input latency.
+ // Round inward (trim remainder) for consistency.
+ var noteDiff:Int = Std.int(Conductor.instance.songPosition - note.noteData.time - inputLatencyMs);
+
+ var score = Scoring.scoreNote(noteDiff, PBOT1);
+ var daRating = Scoring.judgeNote(noteDiff, PBOT1);
+
+ var healthChange = 0.0;
+ switch (daRating)
+ {
+ case 'sick':
+ healthChange = Constants.HEALTH_SICK_BONUS;
+ case 'good':
+ healthChange = Constants.HEALTH_GOOD_BONUS;
+ case 'bad':
+ healthChange = Constants.HEALTH_BAD_BONUS;
+ case 'shit':
+ healthChange = Constants.HEALTH_SHIT_BONUS;
+ }
+
+ // Send the note hit event.
+ var event:HitNoteScriptEvent = new HitNoteScriptEvent(note, healthChange, score, daRating, Highscore.tallies.combo + 1);
dispatchEvent(event);
// Calling event.cancelEvent() skips all the other logic! Neat!
if (event.eventCanceled) return;
- popUpScore(note, input);
+ // Display the combo meter and add the calculation to the score.
+ popUpScore(note, event.score, event.judgement, event.healthChange);
if (note.isHoldNote && note.holdNoteSprite != null)
{
@@ -2143,15 +2371,11 @@ class PlayState extends MusicBeatSubState
* Called when a note leaves the screen and is considered missed by the player.
* @param note
*/
- function onNoteMiss(note:NoteSprite):Void
+ function onNoteMiss(note:NoteSprite, playSound:Bool = false, healthChange:Float):Void
{
- // a MISS is when you let a note scroll past you!!
- var event:NoteScriptEvent = new NoteScriptEvent(NOTE_MISS, note, Highscore.tallies.combo, true);
- dispatchEvent(event);
- // Calling event.cancelEvent() skips all the other logic! Neat!
- if (event.eventCanceled) return;
+ // If we are here, we already CALLED the onNoteMiss script hook!
- health -= Constants.HEALTH_MISS_PENALTY;
+ health += healthChange;
songScore -= 10;
if (!isPracticeMode)
@@ -2201,7 +2425,7 @@ class PlayState extends MusicBeatSubState
Highscore.tallies.combo = comboPopUps.displayCombo(0);
}
- if (event.playSound)
+ if (playSound)
{
vocals.playerVolume = 0;
FlxG.sound.play(Paths.soundRandom('missnote', 1, 3), FlxG.random.float(0.1, 0.2));
@@ -2274,11 +2498,6 @@ class PlayState extends MusicBeatSubState
if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible;
#end
- // Eject button
- if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
-
- if (FlxG.keys.justPressed.F5) debug_refreshModules();
-
// Open the stage editor overlaying the current state.
if (controls.DEBUG_STAGE)
{
@@ -2301,7 +2520,7 @@ class PlayState extends MusicBeatSubState
#if (debug || FORCE_DEBUG_VERSION)
// 1: End the song immediately.
- if (FlxG.keys.justPressed.ONE) endSong();
+ if (FlxG.keys.justPressed.ONE) endSong(true);
// 2: Gain 10% health.
if (FlxG.keys.justPressed.TWO) health += 0.1 * Constants.HEALTH_MAX;
@@ -2328,23 +2547,10 @@ class PlayState extends MusicBeatSubState
/**
* Handles health, score, and rating popups when a note is hit.
*/
- function popUpScore(daNote:NoteSprite, input:PreciseInputEvent):Void
+ function popUpScore(daNote:NoteSprite, score:Int, daRating:String, healthChange:Float):Void
{
vocals.playerVolume = 1;
- // Calculate the input latency (do this as late as possible).
- // trace('Compare: ${PreciseInputManager.getCurrentTimestamp()} - ${input.timestamp}');
- var inputLatencyNs:Int64 = PreciseInputManager.getCurrentTimestamp() - input.timestamp;
- var inputLatencyMs:Float = inputLatencyNs.toFloat() / Constants.NS_PER_MS;
- // trace('Input: ${daNote.noteData.getDirectionName()} pressed ${inputLatencyMs}ms ago!');
-
- // Get the offset and compensate for input latency.
- // Round inward (trim remainder) for consistency.
- var noteDiff:Int = Std.int(Conductor.instance.songPosition - daNote.noteData.time - inputLatencyMs);
-
- var score = Scoring.scoreNote(noteDiff, PBOT1);
- var daRating = Scoring.judgeNote(noteDiff, PBOT1);
-
if (daRating == 'miss')
{
// If daRating is 'miss', that means we made a mistake and should not continue.
@@ -2359,22 +2565,20 @@ class PlayState extends MusicBeatSubState
{
case 'sick':
Highscore.tallies.sick += 1;
- health += Constants.HEALTH_SICK_BONUS;
isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK;
case 'good':
Highscore.tallies.good += 1;
- health += Constants.HEALTH_GOOD_BONUS;
isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK;
case 'bad':
Highscore.tallies.bad += 1;
- health += Constants.HEALTH_BAD_BONUS;
isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK;
case 'shit':
Highscore.tallies.shit += 1;
- health += Constants.HEALTH_SHIT_BONUS;
isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK;
}
+ health += healthChange;
+
if (isComboBreak)
{
// Break the combo, but don't increment tallies.misses.
@@ -2444,67 +2648,71 @@ class PlayState extends MusicBeatSubState
*/
function handleCutsceneKeys(elapsed:Float):Void
{
+ if (isGamePaused) return;
+
if (currentConversation != null)
{
- if (controls.CUTSCENE_ADVANCE) currentConversation?.advanceConversation();
-
- if (controls.CUTSCENE_SKIP)
+ // Pause/unpause may conflict with advancing the conversation!
+ if (controls.CUTSCENE_ADVANCE && !justUnpaused)
{
- currentConversation?.trySkipConversation(elapsed);
+ currentConversation.advanceConversation();
}
- else
+ else if (controls.PAUSE && !justUnpaused)
{
- currentConversation?.trySkipConversation(-1);
+ currentConversation.pauseMusic();
+
+ var pauseSubState:FlxSubState = new PauseSubState({mode: Conversation});
+
+ persistentUpdate = false;
+ FlxTransitionableState.skipNextTransIn = true;
+ FlxTransitionableState.skipNextTransOut = true;
+ pauseSubState.camera = camCutscene;
+ openSubState(pauseSubState);
}
}
else if (VideoCutscene.isPlaying())
{
// This is a video cutscene.
+ if (controls.PAUSE && !justUnpaused)
+ {
+ VideoCutscene.pauseVideo();
- if (controls.CUTSCENE_SKIP)
- {
- trySkipVideoCutscene(elapsed);
- }
- else
- {
- trySkipVideoCutscene(-1);
+ var pauseSubState:FlxSubState = new PauseSubState({mode: Cutscene});
+
+ persistentUpdate = false;
+ FlxTransitionableState.skipNextTransIn = true;
+ FlxTransitionableState.skipNextTransOut = true;
+ pauseSubState.camera = camCutscene;
+ openSubState(pauseSubState);
}
}
}
/**
- * Handle logic for the skip timer.
- * If the skip button is being held, pass the amount of time elapsed since last game update.
- * If the skip button has been released, pass a negative number.
+ * Handle logic for actually skipping a video cutscene after it has been held.
*/
- function trySkipVideoCutscene(elapsed:Float):Void
+ function skipVideoCutscene():Void
{
- if (skipTimer == null || skipTimer.animation == null) return;
-
- if (elapsed < 0)
- {
- skipHeldTimer = 0.0;
- }
- else
- {
- skipHeldTimer += elapsed;
- }
-
- skipTimer.visible = skipHeldTimer >= 0.05;
- skipTimer.amount = Math.min(skipHeldTimer / 1.5, 1.0);
-
- if (skipHeldTimer >= 1.5)
- {
- VideoCutscene.finishVideo();
- }
+ VideoCutscene.finishVideo();
}
/**
* End the song. Handle saving high scores and transitioning to the results screen.
+ *
+ * Broadcasts an `onSongEnd` event, which can be cancelled to prevent the song from ending (for a cutscene or something).
+ * Remember to call `endSong` again when the song should actually end!
+ * @param rightGoddamnNow If true, don't play the fancy animation where you zoom onto Girlfriend. Used after a cutscene.
*/
- function endSong():Void
+ public function endSong(rightGoddamnNow:Bool = false):Void
{
- dispatchEvent(new ScriptEvent(SONG_END));
+ FlxG.sound.music.volume = 0;
+ vocals.volume = 0;
+ mayPauseGame = false;
+
+ // Check if any events want to prevent the song from ending.
+ var event = new ScriptEvent(SONG_END, true);
+ dispatchEvent(event);
+ if (event.eventCanceled) return;
#if sys
// spitter for ravy, teehee!!
@@ -2514,9 +2722,7 @@ class PlayState extends MusicBeatSubState
#end
deathCounter = 0;
- mayPauseGame = false;
- FlxG.sound.music.volume = 0;
- vocals.volume = 0;
+
if (currentSong != null && currentSong.validScore)
{
// crackhead double thingie, sets whether was new highscore, AND saves the song!
@@ -2538,9 +2744,12 @@ class PlayState extends MusicBeatSubState
accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
};
- if (Save.get().isSongHighScore(currentSong.id, currentDifficulty, data))
+ // adds current song data into the tallies for the level (story levels)
+ Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel);
+
+ if (Save.instance.isSongHighScore(currentSong.id, currentDifficulty, data))
{
- Save.get().setSongScore(currentSong.id, currentDifficulty, data);
+ Save.instance.setSongScore(currentSong.id, currentDifficulty, data);
#if newgrounds
NGio.postScore(score, currentSong.id);
#end
@@ -2557,7 +2766,7 @@ class PlayState extends MusicBeatSubState
if (targetSongId == null)
{
- FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
+ FunkinSound.playMusic('freakyMenu');
// transIn = FlxTransitionableState.defaultTransIn;
// transOut = FlxTransitionableState.defaultTransOut;
@@ -2588,9 +2797,9 @@ class PlayState extends MusicBeatSubState
accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
};
- if (Save.get().isLevelHighScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data))
+ if (Save.instance.isLevelHighScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data))
{
- Save.get().setLevelScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data);
+ Save.instance.setLevelScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data);
#if newgrounds
NGio.postScore(score, 'Level ${PlayStatePlaylist.campaignId}');
#end
@@ -2603,7 +2812,14 @@ class PlayState extends MusicBeatSubState
}
else
{
- moveToResultsScreen();
+ if (rightGoddamnNow)
+ {
+ moveToResultsScreen();
+ }
+ else
+ {
+ zoomIntoResultsScreen();
+ }
}
}
else
@@ -2621,10 +2837,10 @@ class PlayState extends MusicBeatSubState
// TODO: Softcode this cutscene.
if (currentSong.id == 'eggnog')
{
- var blackShit:FlxSprite = new FlxSprite(-FlxG.width * FlxG.camera.zoom,
- -FlxG.height * FlxG.camera.zoom).makeGraphic(FlxG.width * 3, FlxG.height * 3, FlxColor.BLACK);
- blackShit.scrollFactor.set();
- add(blackShit);
+ var blackBG:FunkinSprite = new FunkinSprite(-FlxG.width * FlxG.camera.zoom, -FlxG.height * FlxG.camera.zoom);
+ blackBG.makeSolidColor(FlxG.width * 3, FlxG.height * 3, FlxColor.BLACK);
+ blackBG.scrollFactor.set();
+ add(blackBG);
camHUD.visible = false;
isInCutscene = true;
@@ -2661,7 +2877,14 @@ class PlayState extends MusicBeatSubState
}
else
{
- moveToResultsScreen();
+ if (rightGoddamnNow)
+ {
+ moveToResultsScreen();
+ }
+ else
+ {
+ zoomIntoResultsScreen();
+ }
}
}
}
@@ -2678,19 +2901,23 @@ class PlayState extends MusicBeatSubState
*/
function performCleanup():Void
{
+ // If the camera is being tweened, stop it.
+ cancelAllCameraTweens();
+
+ if (currentConversation != null)
+ {
+ remove(currentConversation);
+ currentConversation.kill();
+ }
+
if (currentChart != null)
{
// TODO: Uncache the song.
}
- if (!overrideMusic)
- {
- // Stop the music.
- FlxG.sound.music.pause();
- if (vocals != null) vocals.stop();
- }
- else
+ if (overrideMusic)
{
+ // Stop the music. Do NOT destroy it, something still references it!
FlxG.sound.music.pause();
if (vocals != null)
{
@@ -2698,6 +2925,16 @@ class PlayState extends MusicBeatSubState
remove(vocals);
}
}
+ else
+ {
+ // Stop and destroy the music.
+ FlxG.sound.music.pause();
+ if (vocals != null)
+ {
+ vocals.destroy();
+ remove(vocals);
+ }
+ }
// Remove reference to stage and remove sprites from it to save memory.
if (currentStage != null)
@@ -2709,21 +2946,25 @@ class PlayState extends MusicBeatSubState
}
GameOverSubState.reset();
+ PauseSubState.reset();
// Clear the static reference to this state.
instance = null;
}
/**
- * Play the camera zoom animation and move to the results screen.
+ * Play the camera zoom animation and then move to the results screen once it's done.
*/
- function moveToResultsScreen():Void
+ function zoomIntoResultsScreen():Void
{
trace('WENT TO RESULTS SCREEN!');
// Stop camera zooming on beat.
cameraZoomRate = 0;
+ // Cancel camera tweening if it's active.
+ cancelAllCameraTweens();
+
// If the opponent is GF, zoom in on the opponent.
// Else, if there is no GF, zoom in on BF.
// Else, zoom in on GF.
@@ -2771,22 +3012,33 @@ class PlayState extends MusicBeatSubState
{
ease: FlxEase.expoIn,
onComplete: function(_) {
- persistentUpdate = false;
- vocals.stop();
- camHUD.alpha = 1;
- var res:ResultState = new ResultState(
- {
- storyMode: PlayStatePlaylist.isStoryMode,
- title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
- tallies: Highscore.tallies,
- });
- res.camera = camHUD;
- openSubState(res);
+ moveToResultsScreen();
}
});
});
}
+ /**
+ * Move to the results screen right goddamn now.
+ */
+ function moveToResultsScreen():Void
+ {
+ persistentUpdate = false;
+ vocals.stop();
+ camHUD.alpha = 1;
+
+ var talliesToUse:Tallies = PlayStatePlaylist.isStoryMode ? Highscore.talliesLevel : Highscore.tallies;
+
+ var res:ResultState = new ResultState(
+ {
+ storyMode: PlayStatePlaylist.isStoryMode,
+ title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
+ tallies: talliesToUse,
+ });
+ res.camera = camHUD;
+ openSubState(res);
+ }
+
/**
* Pauses music and vocals easily.
*/
@@ -2799,15 +3051,119 @@ class PlayState extends MusicBeatSubState
/**
* Resets the camera's zoom level and focus point.
*/
- public function resetCamera():Void
+ public function resetCamera(?resetZoom:Bool = true, ?cancelTweens:Bool = true):Void
{
+ // Cancel camera tweens if any are active.
+ if (cancelTweens)
+ {
+ cancelAllCameraTweens();
+ }
+
FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.04);
FlxG.camera.targetOffset.set();
- FlxG.camera.zoom = defaultCameraZoom;
+
+ if (resetZoom)
+ {
+ resetCameraZoom();
+ }
+
// Snap the camera to the follow point immediately.
FlxG.camera.focusOn(cameraFollowPoint.getPosition());
}
+ /**
+ * Disables camera following and tweens the camera to the follow point manually.
+ */
+ public function tweenCameraToFollowPoint(?duration:Float, ?ease:NullFloat>):Void
+ {
+ // Cancel the current tween if it's active.
+ cancelCameraFollowTween();
+
+ if (duration == 0)
+ {
+ // Instant movement. Just reset the camera to force it to the follow point.
+ resetCamera(false, false);
+ }
+ else
+ {
+ // Disable camera following for the duration of the tween.
+ FlxG.camera.target = null;
+
+ // Follow tween! Caching it so we can cancel/pause it later if needed.
+ var followPos:FlxPoint = cameraFollowPoint.getPosition() - FlxPoint.weak(FlxG.camera.width * 0.5, FlxG.camera.height * 0.5);
+ cameraFollowTween = FlxTween.tween(FlxG.camera.scroll, {x: followPos.x, y: followPos.y}, duration,
+ {
+ ease: ease,
+ onComplete: function(_) {
+ resetCamera(false, false); // Re-enable camera following when the tween is complete.
+ }
+ });
+ }
+ }
+
+ public function cancelCameraFollowTween()
+ {
+ if (cameraFollowTween != null)
+ {
+ cameraFollowTween.cancel();
+ }
+ }
+
+ /**
+ * Tweens the camera zoom to the desired amount.
+ */
+ public function tweenCameraZoom(?zoom:Float, ?duration:Float, ?directMode:Bool, ?ease:NullFloat>):Void
+ {
+ // Cancel the current tween if it's active.
+ cancelCameraZoomTween();
+
+ var targetZoom = zoom * FlxCamera.defaultZoom;
+
+ if (directMode) // Direct mode: Tween defaultCameraZoom for basic "smooth" zooms.
+ {
+ if (duration == 0)
+ {
+ // Instant zoom. No tween needed.
+ defaultCameraZoom = targetZoom;
+ }
+ else
+ {
+ // Zoom tween! Caching it so we can cancel/pause it later if needed.
+ cameraZoomTween = FlxTween.tween(this, {defaultCameraZoom: targetZoom}, duration, {ease: ease});
+ }
+ }
+ else // Additive mode: Tween additiveCameraZoom for ease-based zooms.
+ {
+ if (duration == 0)
+ {
+ // Instant zoom. No tween needed.
+ additiveCameraZoom = targetZoom;
+ }
+ else
+ {
+ // Zoom tween! Caching it so we can cancel/pause it later if needed.
+ cameraZoomTween = FlxTween.tween(this, {additiveCameraZoom: targetZoom}, duration, {ease: ease});
+ }
+ }
+ }
+
+ public function cancelCameraZoomTween()
+ {
+ if (cameraZoomTween != null)
+ {
+ cameraZoomTween.cancel();
+ }
+ }
+
+ /**
+ * Cancel all active camera tweens simultaneously.
+ */
+ public function cancelAllCameraTweens()
+ {
+ cancelCameraFollowTween();
+ cancelCameraZoomTween();
+ }
+
#if (debug || FORCE_DEBUG_VERSION)
/**
* Jumps forward or backward a number of sections in the song.
@@ -2816,14 +3172,18 @@ class PlayState extends MusicBeatSubState
*/
function changeSection(sections:Int):Void
{
- FlxG.sound.music.pause();
+ // FlxG.sound.music.pause();
- var targetTimeSteps:Float = Conductor.instance.currentStepTime + (Conductor.instance.timeSignatureNumerator * Constants.STEPS_PER_BEAT * sections);
+ var targetTimeSteps:Float = Conductor.instance.currentStepTime + (Conductor.instance.stepsPerMeasure * sections);
var targetTimeMs:Float = Conductor.instance.getStepTimeInMs(targetTimeSteps);
+ // Don't go back in time to before the song started.
+ targetTimeMs = Math.max(0, targetTimeMs);
+
FlxG.sound.music.time = targetTimeMs;
handleSkippedNotes();
+ SongEventRegistry.handleSkippedEvents(songEvents, Conductor.instance.songPosition);
// regenNoteData(FlxG.sound.music.time);
Conductor.instance.update(FlxG.sound.music.time);
diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx
index 9ffeefcfd..a78d61583 100644
--- a/source/funkin/play/ResultState.hx
+++ b/source/funkin/play/ResultState.hx
@@ -2,26 +2,25 @@ package funkin.play;
import funkin.ui.story.StoryMenuState;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
-import flixel.FlxBasic;
import flixel.FlxSprite;
-import flixel.graphics.frames.FlxAtlasFrames;
+import funkin.graphics.FunkinSprite;
import flixel.graphics.frames.FlxBitmapFont;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.math.FlxPoint;
import funkin.ui.MusicBeatSubState;
import flixel.math.FlxRect;
import flixel.text.FlxBitmapText;
-import flixel.text.FlxText;
import flixel.tweens.FlxEase;
import funkin.ui.freeplay.FreeplayState;
import flixel.tweens.FlxTween;
-import flixel.util.FlxColor;
import flixel.util.FlxGradient;
import flixel.util.FlxTimer;
import funkin.graphics.shaders.LeftMaskShader;
import funkin.play.components.TallyCounter;
-import flxanimate.FlxAnimate.Settings;
+/**
+ * The state for the results screen after a song or week is finished.
+ */
class ResultState extends MusicBeatSubState
{
final params:ResultsStateParams;
@@ -30,8 +29,8 @@ class ResultState extends MusicBeatSubState
var songName:FlxBitmapText;
var difficulty:FlxSprite;
- var maskShaderSongName = new LeftMaskShader();
- var maskShaderDifficulty = new LeftMaskShader();
+ var maskShaderSongName:LeftMaskShader = new LeftMaskShader();
+ var maskShaderDifficulty:LeftMaskShader = new LeftMaskShader();
public function new(params:ResultsStateParams)
{
@@ -49,22 +48,22 @@ class ResultState extends MusicBeatSubState
else
resultsVariation = NORMAL;
- var loops = resultsVariation != SHIT;
+ var loops:Bool = resultsVariation != SHIT;
FlxG.sound.playMusic(Paths.music("results" + resultsVariation), 1, loops);
// TEMP-ish, just used to sorta "cache" the 3000x3000 image!
- var cacheBullShit = new FlxSprite().loadGraphic(Paths.image("resultScreen/soundSystem"));
+ var cacheBullShit:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/soundSystem"));
add(cacheBullShit);
- var dumb = new FlxSprite().loadGraphic(Paths.image("resultScreen/scorePopin"));
+ var dumb:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/scorePopin"));
add(dumb);
var bg:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFECC5C, 0xFFFDC05C], 90);
bg.scrollFactor.set();
add(bg);
- var bgFlash:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFffeb69, 0xFFffe66a], 90);
+ var bgFlash:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFFEB69, 0xFFFFE66A], 90);
bgFlash.scrollFactor.set();
bgFlash.visible = false;
add(bgFlash);
@@ -96,8 +95,7 @@ class ResultState extends MusicBeatSubState
bfSHIT.anim.play(); // unpauses this anim, since it's on PlayOnce!
};
- var gf:FlxSprite = new FlxSprite(500, 300);
- gf.frames = Paths.getSparrowAtlas('resultScreen/resultGirlfriendGOOD');
+ var gf:FlxSprite = FunkinSprite.createSparrow(500, 300, 'resultScreen/resultGirlfriendGOOD');
gf.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false);
gf.visible = false;
gf.animation.finishCallback = _ -> {
@@ -105,8 +103,7 @@ class ResultState extends MusicBeatSubState
};
add(gf);
- var boyfriend:FlxSprite = new FlxSprite(640, -200);
- boyfriend.frames = Paths.getSparrowAtlas('resultScreen/resultBoyfriendGOOD');
+ var boyfriend:FlxSprite = FunkinSprite.createSparrow(640, -200, 'resultScreen/resultBoyfriendGOOD');
boyfriend.animation.addByPrefix("fall", "Boyfriend Good", 24, false);
boyfriend.visible = false;
boyfriend.animation.finishCallback = function(_) {
@@ -115,8 +112,7 @@ class ResultState extends MusicBeatSubState
add(boyfriend);
- var soundSystem:FlxSprite = new FlxSprite(-15, -180);
- soundSystem.frames = Paths.getSparrowAtlas("resultScreen/soundSystem");
+ var soundSystem:FlxSprite = FunkinSprite.createSparrow(-15, -180, 'resultScreen/soundSystem');
soundSystem.animation.addByPrefix("idle", "sound system", 24, false);
soundSystem.visible = false;
new FlxTimer().start(0.4, _ -> {
@@ -162,20 +158,17 @@ class ResultState extends MusicBeatSubState
FlxTween.tween(blackTopBar, {y: 0}, 0.4, {ease: FlxEase.quartOut, startDelay: 0.5});
add(blackTopBar);
- var resultsAnim:FlxSprite = new FlxSprite(-200, -10);
- resultsAnim.frames = Paths.getSparrowAtlas("resultScreen/results");
+ var resultsAnim:FunkinSprite = FunkinSprite.createSparrow(-200, -10, "resultScreen/results");
resultsAnim.animation.addByPrefix("result", "results", 24, false);
resultsAnim.animation.play("result");
add(resultsAnim);
- var ratingsPopin:FlxSprite = new FlxSprite(-150, 120);
- ratingsPopin.frames = Paths.getSparrowAtlas("resultScreen/ratingsPopin");
+ var ratingsPopin:FunkinSprite = FunkinSprite.createSparrow(-150, 120, "resultScreen/ratingsPopin");
ratingsPopin.animation.addByPrefix("idle", "Categories", 24, false);
ratingsPopin.visible = false;
add(ratingsPopin);
- var scorePopin:FlxSprite = new FlxSprite(-180, 520);
- scorePopin.frames = Paths.getSparrowAtlas("resultScreen/scorePopin");
+ var scorePopin:FunkinSprite = FunkinSprite.createSparrow(-180, 520, "resultScreen/scorePopin");
scorePopin.animation.addByPrefix("score", "tally score", 24, false);
scorePopin.visible = false;
add(scorePopin);
@@ -193,6 +186,10 @@ class ResultState extends MusicBeatSubState
var ratingGrp:FlxTypedGroup = new FlxTypedGroup();
add(ratingGrp);
+ /**
+ * NOTE: We display how many notes were HIT, not how many notes there were in total.
+ *
+ */
var totalHit:TallyCounter = new TallyCounter(375, hStuf * 3, params.tallies.totalNotesHit);
ratingGrp.add(totalHit);
@@ -207,7 +204,7 @@ class ResultState extends MusicBeatSubState
var tallyGood:TallyCounter = new TallyCounter(210, (hStuf * 6) + extraYOffset, params.tallies.good, 0xFF89C9E5);
ratingGrp.add(tallyGood);
- var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, params.tallies.bad, 0xffE6CF8A);
+ var tallyBad:TallyCounter = new TallyCounter(190, (hStuf * 7) + extraYOffset, params.tallies.bad, 0xFFE6CF8A);
ratingGrp.add(tallyBad);
var tallyShit:TallyCounter = new TallyCounter(220, (hStuf * 8) + extraYOffset, params.tallies.shit, 0xFFE68C8A);
@@ -279,13 +276,13 @@ class ResultState extends MusicBeatSubState
super.create();
}
- function timerThenSongName()
+ function timerThenSongName():Void
{
movingSongStuff = false;
difficulty.x = 555;
- var diffYTween = 122;
+ var diffYTween:Float = 122;
difficulty.y = -difficulty.height;
FlxTween.tween(difficulty, {y: diffYTween}, 0.5, {ease: FlxEase.quartOut, startDelay: 0.8});
@@ -315,7 +312,7 @@ class ResultState extends MusicBeatSubState
// maskShaderSongName.frameUV = songName.frame.uv;
}
- override function update(elapsed:Float)
+ override function update(elapsed:Float):Void
{
// maskShaderSongName.swagSprX = songName.x;
maskShaderDifficulty.swagSprX = difficulty.x;
@@ -350,6 +347,10 @@ class ResultState extends MusicBeatSubState
if (controls.PAUSE)
{
+ FlxTween.tween(FlxG.sound.music, {volume: 0}, 0.8);
+ FlxTween.tween(FlxG.sound.music, {pitch: 3}, 0.1, {onComplete: _ -> {
+ FlxTween.tween(FlxG.sound.music, {pitch: 0.5}, 0.4);
+ }});
if (params.storyMode)
{
openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new StoryMenuState(sticker)));
diff --git a/source/funkin/play/character/AnimateAtlasCharacter.hx b/source/funkin/play/character/AnimateAtlasCharacter.hx
index f9dc18119..9e7aa98bf 100644
--- a/source/funkin/play/character/AnimateAtlasCharacter.hx
+++ b/source/funkin/play/character/AnimateAtlasCharacter.hx
@@ -46,7 +46,8 @@ class AnimateAtlasCharacter extends BaseCharacter
var _skipTransformChildren:Bool = false;
var animations:Map = new Map();
- var currentAnimation:String;
+ var currentAnimName:Null = null;
+ var animFinished:Bool = false;
public function new(id:String)
{
@@ -77,6 +78,7 @@ class AnimateAtlasCharacter extends BaseCharacter
var atlasSprite:FlxAtlasSprite = loadAtlasSprite();
setSprite(atlasSprite);
+
loadAnimations();
super.onCreate(event);
@@ -86,10 +88,36 @@ class AnimateAtlasCharacter extends BaseCharacter
{
if ((!canPlayOtherAnims && !ignoreOther)) return;
- currentAnimation = name;
- var prefix:String = getAnimationData(name).prefix;
- if (prefix == null) prefix = name;
- this.mainSprite.playAnimation(prefix, restart, ignoreOther);
+ var correctName = correctAnimationName(name);
+ if (correctName == null)
+ {
+ trace('Could not find Atlas animation: ' + name);
+ return;
+ }
+
+ var animData = getAnimationData(correctName);
+ currentAnimName = correctName;
+ var prefix:String = animData.prefix;
+ if (prefix == null) prefix = correctName;
+ var loop:Bool = animData.looped;
+
+ this.mainSprite.playAnimation(prefix, restart, ignoreOther, loop);
+
+ animFinished = false;
+ }
+
+ public override function hasAnimation(name:String):Bool
+ {
+ return getAnimationData(name) != null;
+ }
+
+ /**
+ * Returns true if the animation has finished playing.
+ * Never true if animation is configured to loop.
+ */
+ public override function isAnimationFinished():Bool
+ {
+ return animFinished;
}
function loadAtlasSprite():FlxAtlasSprite
@@ -114,7 +142,13 @@ class AnimateAtlasCharacter extends BaseCharacter
}
else
{
+ // Make the game hold on the last frame.
this.mainSprite.cleanupAnimation(prefix);
+ // currentAnimName = null;
+ animFinished = true;
+
+ // Fallback to idle!
+ // playAnimation('idle', true, false);
}
}
@@ -140,19 +174,30 @@ class AnimateAtlasCharacter extends BaseCharacter
function loadAnimations():Void
{
- trace('[ATLASCHAR] Loading ${_data.animations.length} animations for ${characterId}');
+ trace('[ATLASCHAR] Attempting to load ${_data.animations.length} animations for ${characterId}');
var animData:Array = cast _data.animations;
for (anim in animData)
{
+ // Validate the animation before adding.
+ var prefix = anim.prefix;
+ if (!this.mainSprite.hasAnimation(prefix))
+ {
+ FlxG.log.warn('[ATLASCHAR] Animation ${prefix} not found in Animate Atlas ${_data.assetPath}');
+ continue;
+ }
animations.set(anim.name, anim);
+ trace('[ATLASCHAR] - Successfully loaded animation ${anim.name} to ${characterId}');
}
+
+ trace('[ATLASCHAR] Loaded ${animations.size()} animations for ${characterId}');
}
public override function getCurrentAnimation():String
{
- return this.mainSprite.getCurrentAnimation();
+ // return this.mainSprite.getCurrentAnimation();
+ return currentAnimName;
}
function getAnimationData(name:String = null):AnimateAtlasAnimation
@@ -191,7 +236,11 @@ class AnimateAtlasCharacter extends BaseCharacter
_skipTransformChildren = true;
super.kill();
_skipTransformChildren = false;
- this.mainSprite.kill();
+ if (this.mainSprite != null)
+ {
+ this.mainSprite.kill();
+ this.mainSprite = null;
+ }
}
/**
diff --git a/source/funkin/play/character/BaseCharacter.hx b/source/funkin/play/character/BaseCharacter.hx
index 390864148..2796f8123 100644
--- a/source/funkin/play/character/BaseCharacter.hx
+++ b/source/funkin/play/character/BaseCharacter.hx
@@ -60,7 +60,7 @@ class BaseCharacter extends Bopper
@:allow(funkin.ui.debug.anim.DebugBoundingState)
final _data:CharacterData;
- final singTimeSec:Float;
+ final singTimeSteps:Float;
/**
* The offset between the corner of the sprite and the origin of the sprite (at the character's feet).
@@ -180,7 +180,7 @@ class BaseCharacter extends Bopper
{
this.characterName = _data.name;
this.name = _data.name;
- this.singTimeSec = _data.singTime;
+ this.singTimeSteps = _data.singTime;
this.globalOffsets = _data.offsets;
this.flipX = _data.flipX;
}
@@ -193,6 +193,21 @@ class BaseCharacter extends Bopper
return _data.death?.cameraOffsets ?? [0.0, 0.0];
}
+ public function getBaseScale():Float
+ {
+ return _data.scale;
+ }
+
+ public function getDeathCameraZoom():Float
+ {
+ return _data.death?.cameraZoom ?? 1.0;
+ }
+
+ public function getDeathPreTransitionDelay():Float
+ {
+ return _data.death?.preTransitionDelay ?? 0.0;
+ }
+
/**
* Gets the value of flipX from the character data.
* `!getFlipX()` is the direction Boyfriend should face.
@@ -250,8 +265,8 @@ class BaseCharacter extends Bopper
}
/**
- * Set the sprite scale to the appropriate value.
- * @param scale
+ * Set the character's sprite scale to the appropriate value.
+ * @param scale The desired scale.
*/
public function setScale(scale:Null):Void
{
@@ -367,9 +382,9 @@ class BaseCharacter extends Bopper
// This lets you add frames to the end of the sing animation to ease back into the idle!
holdTimer += event.elapsed;
- var singTimeSec:Float = singTimeSec * (Conductor.instance.beatLengthMs * 0.001); // x beats, to ms.
+ var singTimeSec:Float = singTimeSteps * (Conductor.instance.stepLengthMs / Constants.MS_PER_SEC); // x beats, to ms.
- if (getCurrentAnimation().endsWith('miss')) singTimeSec *= 2; // makes it feel more awkward when you miss
+ if (getCurrentAnimation().endsWith('miss')) singTimeSec *= 2; // makes it feel more awkward when you miss???
// Without this check here, the player character would only play the `sing` animation
// for one beat, as opposed to holding it as long as the player is holding the button.
@@ -378,7 +393,7 @@ class BaseCharacter extends Bopper
FlxG.watch.addQuick('singTimeSec-${characterId}', singTimeSec);
if (holdTimer > singTimeSec && shouldStopSinging)
{
- // trace('holdTimer reached ${holdTimer}sec (> ${singTimeSec}), stopping sing animation');
+ trace('holdTimer reached ${holdTimer}sec (> ${singTimeSec}), stopping sing animation');
holdTimer = 0;
dance(true);
}
@@ -475,7 +490,7 @@ class BaseCharacter extends Bopper
* Every time a note is hit, check if the note is from the same strumline.
* If it is, then play the sing animation.
*/
- public override function onNoteHit(event:NoteScriptEvent)
+ public override function onNoteHit(event:HitNoteScriptEvent)
{
super.onNoteHit(event);
diff --git a/source/funkin/play/character/CharacterData.hx b/source/funkin/play/character/CharacterData.hx
index 69e3ca48e..7d3d6cfb9 100644
--- a/source/funkin/play/character/CharacterData.hx
+++ b/source/funkin/play/character/CharacterData.hx
@@ -305,9 +305,15 @@ class CharacterDataParser
icon = "darnell";
case "senpai-angry":
icon = "senpai";
+ case "tankman" | "tankman-atlas":
+ icon = "tankmen";
}
- return Paths.image("freeplay/icons/" + icon + "pixel");
+ var path = Paths.image("freeplay/icons/" + icon + "pixel");
+ if (Assets.exists(path)) return path;
+
+ // TODO: Hardcode some additional behavior or a fallback.
+ return null;
}
/**
@@ -372,12 +378,12 @@ class CharacterDataParser
}
/**
- * The default time the character should sing for, in beats.
+ * The default time the character should sing for, in steps.
* Values that are too low will cause the character to stop singing between notes.
- * Originally, this value was set to 1, but it was changed to 2 because that became
- * too low after some other code changes.
+ * Values that are too high will cause the character to hold their singing pose for too long after they're done.
+ * @default `8 steps`
*/
- static final DEFAULT_SINGTIME:Float = 2.0;
+ static final DEFAULT_SINGTIME:Float = 8.0;
static final DEFAULT_DANCEEVERY:Int = 1;
static final DEFAULT_FLIPX:Bool = false;
@@ -738,4 +744,17 @@ typedef DeathData =
* @default [0, 0]
*/
var ?cameraOffsets:Array;
+
+ /**
+ * The amount to zoom the camera by while focusing on this character as they die.
+ * Value is a multiplier of the default camera zoom for the stage.
+ * @default 1.0
+ */
+ var ?cameraZoom:Float;
+
+ /**
+ * Impose a delay between when the character reaches `0` health and when the death animation plays.
+ * @default 0.0
+ */
+ var ?preTransitionDelay:Float;
}
diff --git a/source/funkin/play/components/HealthIcon.hx b/source/funkin/play/components/HealthIcon.hx
index 420a4fdc4..419c5b3ea 100644
--- a/source/funkin/play/components/HealthIcon.hx
+++ b/source/funkin/play/components/HealthIcon.hx
@@ -6,6 +6,7 @@ import flixel.math.FlxMath;
import flixel.math.FlxPoint;
import funkin.play.character.CharacterData.CharacterDataParser;
import openfl.utils.Assets;
+import funkin.graphics.FunkinSprite;
import funkin.util.MathUtil;
/**
@@ -26,7 +27,7 @@ import funkin.util.MathUtil;
* @author MasterEric
*/
@:nullSafety
-class HealthIcon extends FlxSprite
+class HealthIcon extends FunkinSprite
{
/**
* The character this icon is representing.
@@ -408,7 +409,7 @@ class HealthIcon extends FlxSprite
if (!isLegacyStyle)
{
- frames = Paths.getSparrowAtlas('icons/icon-$charId');
+ loadSparrow('icons/icon-$charId');
loadAnimationNew();
}
diff --git a/source/funkin/play/components/PopUpStuff.hx b/source/funkin/play/components/PopUpStuff.hx
index 9553856a9..39fc192a0 100644
--- a/source/funkin/play/components/PopUpStuff.hx
+++ b/source/funkin/play/components/PopUpStuff.hx
@@ -3,7 +3,10 @@ package funkin.play.components;
import flixel.FlxSprite;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.tweens.FlxTween;
+import flixel.util.FlxDirection;
+import funkin.graphics.FunkinSprite;
import funkin.play.PlayState;
+import funkin.util.TimerUtil;
class PopUpStuff extends FlxTypedGroup
{
@@ -14,26 +17,20 @@ class PopUpStuff extends FlxTypedGroup
public function displayRating(daRating:String)
{
+ var perfStart:Float = TimerUtil.start();
+
if (daRating == null) daRating = "good";
- var rating:FlxSprite = new FlxSprite(0, 0);
- rating.scrollFactor.set(0.2, 0.2);
-
- rating.zIndex = 1000;
var ratingPath:String = daRating;
if (PlayState.instance.currentStageId.startsWith('school')) ratingPath = "weeb/pixelUI/" + ratingPath + "-pixel";
- rating.loadGraphic(Paths.image(ratingPath));
- rating.x = FlxG.width * 0.50;
- rating.x -= FlxG.camera.scroll.x * 0.2;
- // make sure rating is visible lol!
- // if (rating.x < FlxG.camera.scroll.x)
- // rating.x = FlxG.camera.scroll.x;
- // else if (rating.x > FlxG.camera.scroll.x + FlxG.camera.width - rating.width)
- // rating.x = FlxG.camera.scroll.x + FlxG.camera.width - rating.width;
+ var rating:FunkinSprite = FunkinSprite.create(0, 0, ratingPath);
+ rating.scrollFactor.set(0.2, 0.2);
- // FlxG.camera.scroll.y +
+ rating.zIndex = 1000;
+ rating.x = FlxG.width * 0.50;
+ // rating.x -= FlxG.camera.scroll.x * 0.2;
rating.y = FlxG.camera.height * 0.4 - 60;
rating.acceleration.y = 550;
rating.velocity.y -= FlxG.random.int(140, 175);
@@ -61,10 +58,14 @@ class PopUpStuff extends FlxTypedGroup
},
startDelay: Conductor.instance.beatLengthMs * 0.001
});
+
+ trace('displayRating took: ${TimerUtil.seconds(perfStart)}');
}
public function displayCombo(?combo:Int = 0):Int
{
+ var perfStart:Float = TimerUtil.start();
+
if (combo == null) combo = 0;
var pixelShitPart1:String = "";
@@ -75,16 +76,10 @@ class PopUpStuff extends FlxTypedGroup
pixelShitPart1 = 'weeb/pixelUI/';
pixelShitPart2 = '-pixel';
}
- var comboSpr:FlxSprite = new FlxSprite().loadGraphic(Paths.image(pixelShitPart1 + 'combo' + pixelShitPart2));
+ var comboSpr:FunkinSprite = FunkinSprite.create(pixelShitPart1 + 'combo' + pixelShitPart2);
comboSpr.y = FlxG.camera.height * 0.4 + 80;
comboSpr.x = FlxG.width * 0.50;
- comboSpr.x -= FlxG.camera.scroll.x * 0.2;
- // make sure combo is visible lol!
- // 194 fits 4 combo digits
- // if (comboSpr.x < FlxG.camera.scroll.x + 194)
- // comboSpr.x = FlxG.camera.scroll.x + 194;
- // else if (comboSpr.x > FlxG.camera.scroll.x + FlxG.camera.width - comboSpr.width)
- // comboSpr.x = FlxG.camera.scroll.x + FlxG.camera.width - comboSpr.width;
+ // comboSpr.x -= FlxG.camera.scroll.x * 0.2;
comboSpr.acceleration.y = 600;
comboSpr.velocity.y -= 150;
@@ -129,8 +124,7 @@ class PopUpStuff extends FlxTypedGroup
var daLoop:Int = 1;
for (i in seperatedScore)
{
- var numScore:FlxSprite = new FlxSprite().loadGraphic(Paths.image(pixelShitPart1 + 'num' + Std.int(i) + pixelShitPart2));
- numScore.y = comboSpr.y;
+ var numScore:FunkinSprite = FunkinSprite.create(0, comboSpr.y, pixelShitPart1 + 'num' + Std.int(i) + pixelShitPart2);
if (PlayState.instance.currentStageId.startsWith('school'))
{
@@ -163,6 +157,8 @@ class PopUpStuff extends FlxTypedGroup
daLoop++;
}
+ trace('displayCombo took: ${TimerUtil.seconds(perfStart)}');
+
return combo;
}
}
diff --git a/source/funkin/play/cutscene/VideoCutscene.hx b/source/funkin/play/cutscene/VideoCutscene.hx
index 934919b65..ff56e0919 100644
--- a/source/funkin/play/cutscene/VideoCutscene.hx
+++ b/source/funkin/play/cutscene/VideoCutscene.hx
@@ -19,13 +19,22 @@ import hxcodec.flixel.FlxVideoSprite;
class VideoCutscene
{
static var blackScreen:FlxSprite;
+ static var cutsceneType:CutsceneType;
+
+ #if html5
+ static var vid:FlxVideo;
+ #end
+ #if hxCodec
+ static var vid:FlxVideoSprite;
+ #end
/**
* Play a video cutscene.
* TODO: Currently this is hardcoded to start the countdown after the video is done.
* @param path The path to the video file. Use Paths.file(path) to get the correct path.
+ * @param cutseneType The type of cutscene to play, determines what the game does after. Defaults to `CutsceneType.STARTING`.
*/
- public static function play(filePath:String):Void
+ public static function play(filePath:String, ?cutsceneType:CutsceneType = STARTING):Void
{
if (PlayState.instance == null) return;
@@ -36,6 +45,8 @@ class VideoCutscene
return;
}
+ var rawFilePath = Paths.stripLibrary(filePath);
+
// Trigger the cutscene. Don't play the song in the background.
PlayState.instance.isInCutscene = true;
PlayState.instance.camHUD.visible = false;
@@ -47,12 +58,14 @@ class VideoCutscene
blackScreen.cameras = [PlayState.instance.camCutscene];
PlayState.instance.add(blackScreen);
- #if html5
- playVideoHTML5(filePath);
- #end
+ VideoCutscene.cutsceneType = cutsceneType;
- #if hxCodec
- playVideoNative(filePath);
+ #if html5
+ playVideoHTML5(rawFilePath);
+ #elseif hxCodec
+ playVideoNative(rawFilePath);
+ #else
+ throw "No video support for this platform!";
#end
}
@@ -66,8 +79,6 @@ class VideoCutscene
}
#if html5
- static var vid:FlxVideo;
-
static function playVideoHTML5(filePath:String):Void
{
// Video displays OVER the FlxState.
@@ -92,8 +103,6 @@ class VideoCutscene
#end
#if hxCodec
- static var vid:FlxVideoSprite;
-
static function playVideoNative(filePath:String):Void
{
// Video displays OVER the FlxState.
@@ -103,6 +112,7 @@ class VideoCutscene
{
vid.zIndex = 0;
vid.bitmap.onEndReached.add(finishVideo.bind(0.5));
+ vid.autoPause = false;
vid.cameras = [PlayState.instance.camCutscene];
@@ -110,6 +120,15 @@ class VideoCutscene
PlayState.instance.refresh();
vid.play(filePath, false);
+
+ // Resize videos bigger or smaller than the screen.
+ vid.bitmap.onTextureSetup.add(() -> {
+ vid.setGraphicSize(FlxG.width, FlxG.height);
+ vid.updateHitbox();
+ vid.x = 0;
+ vid.y = 0;
+ // vid.scale.set(0.5, 0.5);
+ });
}
else
{
@@ -118,10 +137,74 @@ class VideoCutscene
}
#end
+ public static function restartVideo(resume:Bool = true):Void
+ {
+ #if html5
+ if (vid != null)
+ {
+ vid.restartVideo();
+ }
+ #end
+
+ #if hxCodec
+ if (vid != null)
+ {
+ // Seek to the start of the video.
+ vid.bitmap.time = 0;
+ if (resume)
+ {
+ // Resume the video if it was paused.
+ vid.resume();
+ }
+ }
+ #end
+ }
+
+ public static function pauseVideo():Void
+ {
+ #if html5
+ if (vid != null)
+ {
+ vid.pauseVideo();
+ }
+ #end
+
+ #if hxCodec
+ if (vid != null)
+ {
+ vid.pause();
+ }
+ #end
+ }
+
+ public static function resumeVideo():Void
+ {
+ #if html5
+ if (vid != null)
+ {
+ vid.resumeVideo();
+ }
+ #end
+
+ #if hxCodec
+ if (vid != null)
+ {
+ vid.resume();
+ }
+ #end
+ }
+
+ /**
+ * Finish the active video cutscene. Done when the video is finished or when the player skips the cutscene.
+ * @param transitionTime The duration of the transition to the next state. Defaults to 0.5 seconds (this time is always used when cancelling the video).
+ * @param finishCutscene The callback to call when the transition is finished.
+ */
public static function finishVideo(?transitionTime:Float = 0.5):Void
{
trace('ALERT: Finish video cutscene called!');
+ var cutsceneType:CutsceneType = VideoCutscene.cutsceneType;
+
#if html5
if (vid != null)
{
@@ -157,8 +240,32 @@ class VideoCutscene
{
ease: FlxEase.quadInOut,
onComplete: function(twn:FlxTween) {
- PlayState.instance.startCountdown();
+ onCutsceneFinish(cutsceneType);
}
});
}
+
+ /**
+ * The default callback used when a cutscene is finished.
+ * You can specify your own callback when calling `VideoCutscene#play()`.
+ */
+ static function onCutsceneFinish(cutsceneType:CutsceneType):Void
+ {
+ switch (cutsceneType)
+ {
+ case CutsceneType.STARTING:
+ PlayState.instance.startCountdown();
+ case CutsceneType.ENDING:
+ PlayState.instance.endSong(true); // true = right goddamn now
+ case CutsceneType.MIDSONG:
+ throw "Not implemented!";
+ }
+ }
+}
+
+enum CutsceneType
+{
+ STARTING; // The default cutscene type. Starts the countdown after the video is done.
+ MIDSONG; // TODO: Implement this!
+ ENDING; // Ends the song after the video is done.
}
diff --git a/source/funkin/play/cutscene/dialogue/Conversation.hx b/source/funkin/play/cutscene/dialogue/Conversation.hx
index dc3fd8c8a..f865f3b7b 100644
--- a/source/funkin/play/cutscene/dialogue/Conversation.hx
+++ b/source/funkin/play/cutscene/dialogue/Conversation.hx
@@ -31,10 +31,6 @@ import funkin.data.dialogue.DialogueBoxRegistry;
*/
class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass implements IRegistryEntry
{
- static final CONVERSATION_SKIP_TIMER:Float = 1.5;
-
- var skipHeldTimer:Float = 0.0;
-
/**
* The ID of the conversation.
*/
@@ -105,8 +101,6 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
var currentDialogueBox:DialogueBox;
- var skipTimer:FlxPieDial;
-
public function new(id:String)
{
super();
@@ -124,8 +118,8 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
{
// Reset the progress in the dialogue.
currentDialogueEntry = 0;
+ currentDialogueLine = 0;
this.state = ConversationState.Start;
- this.alpha = 1.0;
// Start the dialogue.
dispatchEvent(new DialogueScriptEvent(DIALOGUE_START, this, false));
@@ -151,8 +145,31 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
music.play();
}
+ public function pauseMusic():Void
+ {
+ if (music != null)
+ {
+ music.pause();
+ }
+ }
+
+ public function resumeMusic():Void
+ {
+ if (music != null)
+ {
+ music.resume();
+ }
+ }
+
function setupBackdrop():Void
{
+ if (backdrop != null)
+ {
+ backdrop.destroy();
+ remove(backdrop);
+ backdrop = null;
+ }
+
backdrop = new FunkinSprite(0, 0);
if (_data.backdrop == null) return;
@@ -181,12 +198,6 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
refresh();
}
- function setupSkipTimer():Void
- {
- add(skipTimer = new FlxPieDial(16, 16, 32, FlxColor.WHITE, 36, CIRCLE, true, 24));
- skipTimer.amount = 0;
- }
-
public override function update(elapsed:Float):Void
{
super.update(elapsed);
@@ -199,9 +210,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
var nextSpeakerId:String = currentDialogueEntryData.speaker;
// Skip the next steps if the current speaker is already displayed.
- if (currentSpeaker != null && nextSpeakerId == currentSpeaker.id) return;
-
- var nextSpeaker:Speaker = SpeakerRegistry.instance.fetchEntry(nextSpeakerId);
+ if ((currentSpeaker != null && currentSpeaker.alive) && nextSpeakerId == currentSpeaker.id) return;
if (currentSpeaker != null)
{
@@ -210,6 +219,8 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
currentSpeaker = null;
}
+ var nextSpeaker:Speaker = SpeakerRegistry.instance.fetchEntry(nextSpeakerId);
+
if (nextSpeaker == null)
{
if (nextSpeakerId == null)
@@ -222,6 +233,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
}
return;
}
+ if (!nextSpeaker.alive) nextSpeaker.revive();
ScriptEventDispatcher.callEvent(nextSpeaker, new ScriptEvent(CREATE, true));
@@ -249,8 +261,8 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
{
var nextDialogueBoxId:String = currentDialogueEntryData?.box;
- // Skip the next steps if the current speaker is already displayed.
- if (currentDialogueBox != null && nextDialogueBoxId == currentDialogueBox.id) return;
+ // Skip the next steps if the current dialogue box is already displayed.
+ if ((currentDialogueBox != null && currentDialogueBox.alive) && nextDialogueBoxId == currentDialogueBox.id) return;
if (currentDialogueBox != null)
{
@@ -266,6 +278,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
trace('Dialogue box could not be retrieved.');
return;
}
+ if (!nextDialogueBox.alive) nextDialogueBox.revive();
ScriptEventDispatcher.callEvent(nextDialogueBox, new ScriptEvent(CREATE, true));
@@ -347,29 +360,28 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
currentDialogueEntry = 0;
this.state = ConversationState.Start;
- advanceConversation();
- }
-
- public function trySkipConversation(elapsed:Float):Void
- {
- if (skipTimer == null || skipTimer.animation == null) return;
-
- if (elapsed < 0)
+ if (outroTween != null)
{
- skipHeldTimer = 0.0;
- }
- else
- {
- skipHeldTimer += elapsed;
+ outroTween.cancel();
}
+ outroTween = null;
- skipTimer.visible = skipHeldTimer >= 0.05;
- skipTimer.amount = Math.min(skipHeldTimer / CONVERSATION_SKIP_TIMER, 1.0);
+ if (this.music != null) this.music.stop();
+ this.music = null;
- if (skipHeldTimer >= CONVERSATION_SKIP_TIMER)
- {
- skipConversation();
- }
+ if (currentSpeaker != null) currentSpeaker.kill();
+ remove(currentSpeaker);
+ currentSpeaker = null;
+
+ if (currentDialogueBox != null) currentDialogueBox.kill();
+ remove(currentDialogueBox);
+ currentDialogueBox = null;
+
+ if (backdrop != null) backdrop.destroy();
+ remove(backdrop);
+ backdrop = null;
+
+ startConversation();
}
/**
@@ -383,7 +395,7 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
dispatchEvent(new DialogueScriptEvent(DIALOGUE_SKIP, this, true));
}
- static var outroTween:FlxTween;
+ var outroTween:FlxTween;
public function startOutro():Void
{
@@ -411,7 +423,6 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
public function endOutro():Void
{
- outroTween = null;
ScriptEventDispatcher.callEvent(this, new ScriptEvent(DESTROY, false));
}
@@ -425,7 +436,6 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
// Fade in the music and backdrop.
setupMusic();
setupBackdrop();
- setupSkipTimer();
// Advance the conversation.
state = ConversationState.Opening;
@@ -547,19 +557,25 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
{
propagateEvent(event);
- if (outroTween != null) outroTween.cancel(); // Canc
+ if (outroTween != null)
+ {
+ outroTween.cancel();
+ }
outroTween = null;
- this.alpha = 0.0;
if (this.music != null) this.music.stop();
this.music = null;
- this.skipTimer = null;
if (currentSpeaker != null) currentSpeaker.kill();
+ remove(currentSpeaker);
currentSpeaker = null;
+
if (currentDialogueBox != null) currentDialogueBox.kill();
+ remove(currentDialogueBox);
currentDialogueBox = null;
- if (backdrop != null) backdrop.kill();
+
+ if (backdrop != null) backdrop.destroy();
+ remove(backdrop);
backdrop = null;
this.clear();
@@ -578,16 +594,27 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
*/
function propagateEvent(event:ScriptEvent):Void
{
- if (this.currentDialogueBox != null)
+ if (this.currentDialogueBox != null && this.currentDialogueBox.exists)
{
ScriptEventDispatcher.callEvent(this.currentDialogueBox, event);
}
- if (this.currentSpeaker != null)
+ if (this.currentSpeaker != null && this.currentSpeaker.exists)
{
ScriptEventDispatcher.callEvent(this.currentSpeaker, event);
}
}
+ /**
+ * Calls `kill()` on the group's members and then on the group itself.
+ * You can revive this group later via `revive()` after this.
+ */
+ public override function revive():Void
+ {
+ super.revive();
+ this.alpha = 1;
+ this.visible = true;
+ }
+
/**
* Calls `kill()` on the group's members and then on the group itself.
* You can revive this group later via `revive()` after this.
@@ -599,6 +626,12 @@ class Conversation extends FlxSpriteGroup implements IDialogueScriptedClass impl
exists = false;
_skipTransformChildren = false;
if (group != null) group.kill();
+
+ if (outroTween != null)
+ {
+ outroTween.cancel();
+ outroTween = null;
+ }
}
public override function toString():String
diff --git a/source/funkin/play/cutscene/dialogue/DialogueBox.hx b/source/funkin/play/cutscene/dialogue/DialogueBox.hx
index 6f8a0086a..fe4f13be0 100644
--- a/source/funkin/play/cutscene/dialogue/DialogueBox.hx
+++ b/source/funkin/play/cutscene/dialogue/DialogueBox.hx
@@ -4,10 +4,11 @@ import flixel.FlxSprite;
import funkin.data.IRegistryEntry;
import flixel.group.FlxSpriteGroup;
import flixel.graphics.frames.FlxFramesCollection;
-import flixel.text.FlxText;
+import funkin.graphics.FunkinSprite;
import flixel.addons.text.FlxTypeText;
import funkin.util.assets.FlxAnimationUtil;
import funkin.modding.events.ScriptEvent;
+import funkin.audio.FunkinSound;
import funkin.modding.IScriptedClass.IDialogueScriptedClass;
import flixel.util.FlxColor;
import funkin.data.dialogue.DialogueBoxData;
@@ -111,9 +112,6 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple
this.y = 0;
this.alpha = 1;
- this.boxSprite = new FlxSprite(0, 0);
- add(this.boxSprite);
-
loadSpritesheet();
loadAnimations();
@@ -122,6 +120,14 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple
function loadSpritesheet():Void
{
+ if (this.boxSprite != null)
+ {
+ remove(this.boxSprite);
+ this.boxSprite = null;
+ }
+
+ this.boxSprite = new FunkinSprite(0, 0);
+
trace('[DIALOGUE BOX] Loading spritesheet ${_data.assetPath} for ${id}');
var tex:FlxFramesCollection = Paths.getSparrowAtlas(_data.assetPath);
@@ -146,6 +152,8 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple
this.flipY = _data.flipY;
this.globalOffsets = _data.offsets;
this.setScale(_data.scale);
+
+ add(this.boxSprite);
}
public function setText(newText:String):Void
@@ -190,6 +198,34 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple
this.boxSprite.updateHitbox();
}
+ /**
+ * Calls `kill()` on the group's members and then on the group itself.
+ * You can revive this group later via `revive()` after this.
+ */
+ public override function kill():Void
+ {
+ super.kill();
+ if (this.boxSprite != null)
+ {
+ this.boxSprite.kill();
+ this.boxSprite = null;
+ }
+ if (this.textDisplay != null)
+ {
+ this.textDisplay.kill();
+ this.textDisplay = null;
+ }
+ this.clear();
+ }
+
+ public override function revive():Void
+ {
+ super.revive();
+
+ this.visible = true;
+ this.alpha = 1.0;
+ }
+
function loadAnimations():Void
{
trace('[DIALOGUE BOX] Loading ${_data.animations.length} animations for ${id}');
@@ -246,7 +282,8 @@ class DialogueBox extends FlxSpriteGroup implements IDialogueScriptedClass imple
textDisplay.setFormat(_data.text.fontFamily, _data.text.size, FlxColor.fromString(_data.text.color), LEFT, SHADOW,
FlxColor.fromString(_data.text.shadowColor ?? '#00000000'), false);
textDisplay.borderSize = _data.text.shadowWidth ?? 2;
- textDisplay.sounds = [FlxG.sound.load(Paths.sound('pixelText'), 0.6)];
+ // TODO: Add an option to configure this.
+ textDisplay.sounds = [FunkinSound.load(Paths.sound('pixelText'), 0.6)];
textDisplay.completeCallback = onTypingComplete;
diff --git a/source/funkin/play/cutscene/dialogue/Speaker.hx b/source/funkin/play/cutscene/dialogue/Speaker.hx
index d5bffd7b0..0d29481c4 100644
--- a/source/funkin/play/cutscene/dialogue/Speaker.hx
+++ b/source/funkin/play/cutscene/dialogue/Speaker.hx
@@ -106,6 +106,26 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRe
loadAnimations();
}
+ /**
+ * Calls `kill()` on the group's members and then on the group itself.
+ * You can revive this group later via `revive()` after this.
+ */
+ public override function kill():Void
+ {
+ super.kill();
+ }
+
+ public override function revive():Void
+ {
+ super.revive();
+
+ this.visible = true;
+ this.alpha = 1.0;
+
+ loadSpritesheet();
+ loadAnimations();
+ }
+
function loadSpritesheet():Void
{
trace('[SPEAKER] Loading spritesheet ${_data.assetPath} for ${id}');
@@ -198,25 +218,25 @@ class Speaker extends FlxSprite implements IDialogueScriptedClass implements IRe
// If the animation exists, we're good.
if (hasAnimation(name)) return name;
- trace('[BOPPER] Animation "$name" does not exist!');
+ FlxG.log.notice('Speaker tried to play animation "$name" that does not exist, stripping suffixes...');
// Attempt to strip a `-alt` suffix, if it exists.
if (name.lastIndexOf('-') != -1)
{
var correctName = name.substring(0, name.lastIndexOf('-'));
- trace('[BOPPER] Attempting to fallback to "$correctName"');
+ FlxG.log.notice('Speaker tried to play animation "$name" that does not exist, stripping suffixes...');
return correctAnimationName(correctName);
}
else
{
if (name != 'idle')
{
- trace('[BOPPER] Attempting to fallback to "idle"');
+ FlxG.log.warn('Speaker tried to play animation "$name" that does not exist, fallback to idle...');
return correctAnimationName('idle');
}
else
{
- trace('[BOPPER] Failing animation playback.');
+ FlxG.log.error('Speaker tried to play animation "idle" that does not exist! This is bad!');
return null;
}
}
diff --git a/source/funkin/play/event/FocusCameraSongEvent.hx b/source/funkin/play/event/FocusCameraSongEvent.hx
index 847df4a60..d4ab4f24f 100644
--- a/source/funkin/play/event/FocusCameraSongEvent.hx
+++ b/source/funkin/play/event/FocusCameraSongEvent.hx
@@ -1,5 +1,6 @@
package funkin.play.event;
+import flixel.tweens.FlxEase;
// Data from the chart
import funkin.data.song.SongData;
import funkin.data.song.SongData.SongEventData;
@@ -57,6 +58,9 @@ class FocusCameraSongEvent extends SongEvent
// Does nothing if there is no PlayState camera or stage.
if (PlayState.instance == null || PlayState.instance.currentStage == null) return;
+ // Does nothing if we are minimal mode.
+ if (PlayState.instance.isMinimalMode) return;
+
var posX:Null = data.getFloat('x');
if (posX == null) posX = 0.0;
var posY:Null = data.getFloat('y');
@@ -66,6 +70,13 @@ class FocusCameraSongEvent extends SongEvent
if (char == null) char = cast data.value;
+ var useTween:Null = data.getBool('useTween');
+ if (useTween == null) useTween = false;
+ var duration:Null = data.getFloat('duration');
+ if (duration == null) duration = 4.0;
+ var ease:Null = data.getString('ease');
+ if (ease == null) ease = 'linear';
+
switch (char)
{
case -1: // Position
@@ -114,6 +125,26 @@ class FocusCameraSongEvent extends SongEvent
default:
trace('Unknown camera focus: ' + data);
}
+
+ if (useTween)
+ {
+ switch (ease)
+ {
+ case 'INSTANT':
+ PlayState.instance.tweenCameraToFollowPoint(0);
+ default:
+ var durSeconds = Conductor.instance.stepLengthMs * duration / 1000;
+
+ var easeFunction:NullFloat> = Reflect.field(FlxEase, ease);
+ if (easeFunction == null)
+ {
+ trace('Invalid ease function: $ease');
+ return;
+ }
+
+ PlayState.instance.tweenCameraToFollowPoint(durSeconds, easeFunction);
+ }
+ }
}
public override function getTitle():String
@@ -155,6 +186,51 @@ class FocusCameraSongEvent extends SongEvent
step: 10.0,
type: SongEventFieldType.FLOAT,
units: "px"
+ },
+ {
+ name: 'useTween',
+ title: 'Use Tween',
+ type: SongEventFieldType.BOOL,
+ defaultValue: false
+ },
+ {
+ name: 'duration',
+ title: 'Duration',
+ defaultValue: 4.0,
+ step: 0.5,
+ type: SongEventFieldType.FLOAT,
+ units: 'steps'
+ },
+ {
+ name: 'ease',
+ title: 'Easing Type',
+ defaultValue: 'linear',
+ type: SongEventFieldType.ENUM,
+ keys: [
+ 'Linear' => 'linear',
+ 'Instant' => 'INSTANT',
+ 'Quad In' => 'quadIn',
+ 'Quad Out' => 'quadOut',
+ 'Quad In/Out' => 'quadInOut',
+ 'Cube In' => 'cubeIn',
+ 'Cube Out' => 'cubeOut',
+ 'Cube In/Out' => 'cubeInOut',
+ 'Quart In' => 'quartIn',
+ 'Quart Out' => 'quartOut',
+ 'Quart In/Out' => 'quartInOut',
+ 'Quint In' => 'quintIn',
+ 'Quint Out' => 'quintOut',
+ 'Quint In/Out' => 'quintInOut',
+ 'Smooth Step In' => 'smoothStepIn',
+ 'Smooth Step Out' => 'smoothStepOut',
+ 'Smooth Step In/Out' => 'smoothStepInOut',
+ 'Sine In' => 'sineIn',
+ 'Sine Out' => 'sineOut',
+ 'Sine In/Out' => 'sineInOut',
+ 'Elastic In' => 'elasticIn',
+ 'Elastic Out' => 'elasticOut',
+ 'Elastic In/Out' => 'elasticInOut',
+ ]
}
]);
}
diff --git a/source/funkin/play/event/ZoomCameraSongEvent.hx b/source/funkin/play/event/ZoomCameraSongEvent.hx
index 809130499..b913aebe7 100644
--- a/source/funkin/play/event/ZoomCameraSongEvent.hx
+++ b/source/funkin/play/event/ZoomCameraSongEvent.hx
@@ -52,26 +52,36 @@ class ZoomCameraSongEvent extends SongEvent
super('ZoomCamera');
}
+ static final DEFAULT_ZOOM:Float = 1.0;
+ static final DEFAULT_DURATION:Float = 4.0;
+ static final DEFAULT_MODE:String = 'direct';
+ static final DEFAULT_EASE:String = 'linear';
+
public override function handleEvent(data:SongEventData):Void
{
// Does nothing if there is no PlayState camera or stage.
- if (PlayState.instance == null) return;
+ if (PlayState.instance == null || PlayState.instance.currentStage == null) return;
- var zoom:Null = data.getFloat('zoom');
- if (zoom == null) zoom = 1.0;
- var duration:Null = data.getFloat('duration');
- if (duration == null) duration = 4.0;
+ // Does nothing if we are minimal mode.
+ if (PlayState.instance.isMinimalMode) return;
- var ease:Null = data.getString('ease');
- if (ease == null) ease = 'linear';
+ var zoom:Float = data.getFloat('zoom') ?? DEFAULT_ZOOM;
+
+ var duration:Float = data.getFloat('duration') ?? DEFAULT_DURATION;
+
+ var mode:String = data.getString('mode') ?? DEFAULT_MODE;
+ var isDirectMode:Bool = mode == 'direct';
+
+ var ease:String = data.getString('ease') ?? DEFAULT_EASE;
// If it's a string, check the value.
switch (ease)
{
case 'INSTANT':
- // Set the zoom. Use defaultCameraZoom to prevent breaking camera bops.
- PlayState.instance.defaultCameraZoom = zoom * FlxCamera.defaultZoom;
+ PlayState.instance.tweenCameraZoom(zoom, 0, isDirectMode);
default:
+ var durSeconds = Conductor.instance.stepLengthMs * duration / 1000;
+
var easeFunction:NullFloat> = Reflect.field(FlxEase, ease);
if (easeFunction == null)
{
@@ -79,8 +89,7 @@ class ZoomCameraSongEvent extends SongEvent
return;
}
- FlxTween.tween(PlayState.instance, {defaultCameraZoom: zoom * FlxCamera.defaultZoom}, (Conductor.instance.stepLengthMs * duration / 1000),
- {ease: easeFunction});
+ PlayState.instance.tweenCameraZoom(zoom, durSeconds, isDirectMode, easeFunction);
}
}
@@ -93,8 +102,9 @@ class ZoomCameraSongEvent extends SongEvent
* ```
* {
* 'zoom': FLOAT, // Target zoom level.
- * 'duration': FLOAT, // Optional duration in steps
- * 'ease': ENUM, // Optional easing function
+ * 'duration': FLOAT, // Optional duration in steps.
+ * 'mode': ENUM, // Whether to set additive zoom or direct zoom.
+ * 'ease': ENUM, // Optional easing function.
* }
* @return SongEventSchema
*/
@@ -117,6 +127,13 @@ class ZoomCameraSongEvent extends SongEvent
type: SongEventFieldType.FLOAT,
units: 'steps'
},
+ {
+ name: 'mode',
+ title: 'Mode',
+ defaultValue: 'direct',
+ type: SongEventFieldType.ENUM,
+ keys: ['Additive' => 'additive', 'Direct' => 'direct']
+ },
{
name: 'ease',
title: 'Easing Type',
diff --git a/source/funkin/play/notes/NoteSplash.hx b/source/funkin/play/notes/NoteSplash.hx
index 0ff8076c8..2411e5615 100644
--- a/source/funkin/play/notes/NoteSplash.hx
+++ b/source/funkin/play/notes/NoteSplash.hx
@@ -35,6 +35,7 @@ class NoteSplash extends FlxSprite
*/
function setup():Void
{
+ if (frameCollection?.parent?.isDestroyed ?? false) frameCollection = null;
if (frameCollection == null) preloadFrames();
this.frames = frameCollection;
@@ -75,6 +76,8 @@ class NoteSplash extends FlxSprite
this.playAnimation('splash${variant}Right');
}
+ if (animation.curAnim == null) return;
+
// Vary the speed of the animation a bit.
animation.curAnim.frameRate = FRAMERATE_DEFAULT + FlxG.random.int(-FRAMERATE_VARIANCE, FRAMERATE_VARIANCE);
diff --git a/source/funkin/play/notes/NoteSprite.hx b/source/funkin/play/notes/NoteSprite.hx
index 0368b18e9..b16b88466 100644
--- a/source/funkin/play/notes/NoteSprite.hx
+++ b/source/funkin/play/notes/NoteSprite.hx
@@ -4,9 +4,10 @@ import funkin.data.song.SongData.SongNoteData;
import funkin.play.notes.notestyle.NoteStyle;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.FlxSprite;
+import funkin.graphics.FunkinSprite;
import funkin.graphics.shaders.HSVShader;
-class NoteSprite extends FlxSprite
+class NoteSprite extends FunkinSprite
{
static final DIRECTION_COLORS:Array = ['purple', 'blue', 'green', 'red'];
@@ -15,26 +16,53 @@ class NoteSprite extends FlxSprite
var hsvShader:HSVShader;
/**
- * The time at which the note should be hit, in milliseconds.
+ * The strum time at which the note should be hit, in milliseconds.
*/
- public var strumTime(default, set):Float;
+ public var strumTime(get, set):Float;
+
+ function get_strumTime():Float
+ {
+ return this.noteData?.time ?? 0.0;
+ }
function set_strumTime(value:Float):Float
{
- this.strumTime = value;
- return this.strumTime;
+ if (this.noteData == null) return value;
+ return this.noteData.time = value;
+ }
+
+ /**
+ * The length for which the note should be held, in milliseconds.
+ * Defaults to 0 for single notes.
+ */
+ public var length(get, set):Float;
+
+ function get_length():Float
+ {
+ return this.noteData?.length ?? 0.0;
+ }
+
+ function set_length(value:Float):Float
+ {
+ if (this.noteData == null) return value;
+ return this.noteData.length = value;
}
/**
* An extra attribute for the note.
* For example, whether the note is an "alt" note, or whether it has custom behavior on hit.
*/
- public var kind(default, set):String;
+ public var kind(get, set):Null;
+
+ function get_kind():Null
+ {
+ return this.noteData?.kind;
+ }
function set_kind(value:String):String
{
- this.kind = value;
- return this.kind;
+ if (this.noteData == null) return value;
+ return this.noteData.kind = value;
}
/**
@@ -99,16 +127,13 @@ class NoteSprite extends FlxSprite
*/
public var handledMiss:Bool;
- public function new(noteStyle:NoteStyle, strumTime:Float = 0, direction:Int = 0)
+ public function new(noteStyle:NoteStyle, direction:Int = 0)
{
super(0, -9999);
- this.strumTime = strumTime;
this.direction = direction;
this.hsvShader = new HSVShader();
- if (this.strumTime < 0) this.strumTime = 0;
-
setupNoteGraphic(noteStyle);
// Disables the update() function for performance.
diff --git a/source/funkin/play/notes/Strumline.hx b/source/funkin/play/notes/Strumline.hx
index 4538696dc..aa87fa2e4 100644
--- a/source/funkin/play/notes/Strumline.hx
+++ b/source/funkin/play/notes/Strumline.hx
@@ -1,5 +1,6 @@
package funkin.play.notes;
+import flixel.util.FlxSignal.FlxTypedSignal;
import flixel.FlxG;
import funkin.play.notes.notestyle.NoteStyle;
import flixel.group.FlxSpriteGroup;
@@ -39,6 +40,10 @@ class Strumline extends FlxSpriteGroup
return FlxG.height / 0.45;
}
+ /**
+ * Whether this strumline is controlled by the player's inputs.
+ * False means it's controlled by the opponent or Bot Play.
+ */
public var isPlayer:Bool;
/**
@@ -56,6 +61,8 @@ class Strumline extends FlxSpriteGroup
public var holdNotes:FlxTypedSpriteGroup;
+ public var onNoteIncoming:FlxTypedSignalVoid>;
+
var strumlineNotes:FlxTypedSpriteGroup;
var noteSplashes:FlxTypedSpriteGroup;
var noteHoldCovers:FlxTypedSpriteGroup;
@@ -113,6 +120,8 @@ class Strumline extends FlxSpriteGroup
this.refresh();
+ this.onNoteIncoming = new FlxTypedSignalVoid>();
+
for (i in 0...KEY_COUNT)
{
var child:StrumlineNote = new StrumlineNote(noteStyle, isPlayer, DIRECTIONS[i]);
@@ -398,6 +407,8 @@ class Strumline extends FlxSpriteGroup
}
nextNoteIndex = noteIndex + 1; // Increment the nextNoteIndex rather than splicing the array, because splicing is slow.
+
+ onNoteIncoming.dispatch(noteSprite);
}
// Update rendering of notes.
@@ -746,7 +757,6 @@ class Strumline extends FlxSpriteGroup
if (noteSprite != null)
{
- noteSprite.strumTime = note.time;
noteSprite.direction = note.getDirection();
noteSprite.noteData = note;
diff --git a/source/funkin/play/notes/notestyle/NoteStyle.hx b/source/funkin/play/notes/notestyle/NoteStyle.hx
index 34c1ce9c3..d0cc09f6a 100644
--- a/source/funkin/play/notes/notestyle/NoteStyle.hx
+++ b/source/funkin/play/notes/notestyle/NoteStyle.hx
@@ -4,6 +4,7 @@ import flixel.graphics.frames.FlxAtlasFrames;
import flixel.graphics.frames.FlxFramesCollection;
import funkin.data.animation.AnimationData;
import funkin.data.IRegistryEntry;
+import funkin.graphics.FunkinSprite;
import funkin.data.notestyle.NoteStyleData;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
@@ -100,6 +101,14 @@ class NoteStyle implements IRegistryEntry
function buildNoteFrames(force:Bool = false):FlxAtlasFrames
{
+ if (!FunkinSprite.isTextureCached(Paths.image(getNoteAssetPath())))
+ {
+ FlxG.log.warn('Note texture is not cached: ${getNoteAssetPath()}');
+ }
+
+ // Purge the note frames if the cached atlas is invalid.
+ if (noteFrames?.parent?.isDestroyed ?? false) noteFrames = null;
+
if (noteFrames != null && !force) return noteFrames;
noteFrames = Paths.getSparrowAtlas(getNoteAssetPath(), getNoteAssetLibrary());
@@ -109,8 +118,6 @@ class NoteStyle implements IRegistryEntry
throw 'Could not load note frames for note style: $id';
}
- noteFrames.parent.persist = true;
-
return noteFrames;
}
diff --git a/source/funkin/play/song/Song.hx b/source/funkin/play/song/Song.hx
index 970aebc57..567c388c7 100644
--- a/source/funkin/play/song/Song.hx
+++ b/source/funkin/play/song/Song.hx
@@ -1,6 +1,5 @@
package funkin.play.song;
-import flixel.sound.FlxSound;
import funkin.audio.VoicesGroup;
import funkin.audio.FunkinSound;
import funkin.data.IRegistryEntry;
@@ -13,9 +12,8 @@ import funkin.data.song.SongData.SongOffsets;
import funkin.data.song.SongData.SongTimeChange;
import funkin.data.song.SongData.SongTimeFormat;
import funkin.data.song.SongRegistry;
-import funkin.data.song.SongRegistry;
+import funkin.modding.IScriptedClass.IPlayStateScriptedClass;
import funkin.modding.events.ScriptEvent;
-import funkin.modding.IScriptedClass;
import funkin.util.SortUtil;
import openfl.utils.Assets;
@@ -31,14 +29,44 @@ import openfl.utils.Assets;
@:nullSafety
class Song implements IPlayStateScriptedClass implements IRegistryEntry
{
- public static final DEFAULT_SONGNAME:String = "Unknown";
- public static final DEFAULT_ARTIST:String = "Unknown";
+ /**
+ * The default value for the song's name
+ */
+ public static final DEFAULT_SONGNAME:String = 'Unknown';
+
+ /**
+ * The default value for the song's artist
+ */
+ public static final DEFAULT_ARTIST:String = 'Unknown';
+
+ /**
+ * The default value for the song's time format
+ */
public static final DEFAULT_TIMEFORMAT:SongTimeFormat = SongTimeFormat.MILLISECONDS;
+
+ /**
+ * The default value for the song's divisions
+ */
public static final DEFAULT_DIVISIONS:Null = null;
+
+ /**
+ * The default value for whether the song loops.
+ */
public static final DEFAULT_LOOPED:Bool = false;
- public static final DEFAULT_STAGE:String = "mainStage";
+
+ /**
+ * The default value for the song's playable stage.
+ */
+ public static final DEFAULT_STAGE:String = 'mainStage';
+
+ /**
+ * The default value for the song's scroll speed.
+ */
public static final DEFAULT_SCROLLSPEED:Float = 1.0;
+ /**
+ * The internal ID of the song.
+ */
public final id:String;
/**
@@ -53,6 +81,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry;
final difficulties:Map;
+ /**
+ * The list of variations a song has.
+ */
public var variations(get, never):Array;
function get_variations():Array
@@ -65,6 +96,9 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry = fetchVariationMetadata(id, vari);
if (variMeta != null) _metadata.set(variMeta.variation, variMeta);
}
}
@@ -115,32 +152,87 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry, variations:Array, charts:Map,
- validScore:Bool = false):Song
+ includeScript:Bool = true, validScore:Bool = false):Song
{
- var result:Song = new Song(songId);
+ @:privateAccess
+ var result:Null;
+
+ if (includeScript && SongRegistry.instance.isScriptedEntry(songId))
+ {
+ var songClassName:String = SongRegistry.instance.getScriptedEntryClassName(songId);
+
+ @:privateAccess
+ result = SongRegistry.instance.createScriptedEntry(songClassName);
+ }
+ else
+ {
+ @:privateAccess
+ result = SongRegistry.instance.createEntry(songId);
+ }
+
+ if (result == null) throw 'ERROR: Could not build Song instance ($songId), is the attached script bad?';
result._metadata.clear();
for (meta in metadata)
+ {
result._metadata.set(meta.variation, meta);
+ }
result.difficulties.clear();
result.populateDifficulties();
for (variation => chartData in charts)
+ {
result.applyChartData(chartData, variation);
+ }
result.validScore = validScore;
return result;
}
+ /**
+ * Retrieve a list of the raw metadata for the song.
+ * @return The metadata JSON objects for the song's variations.
+ */
public function getRawMetadata():Array
{
return _metadata.values();
}
+ /**
+ * List the album IDs for each variation of the song.
+ * @return A map of variation IDs to album IDs.
+ */
+ public function listAlbums():Map
+ {
+ var result:Map = new Map();
+
+ for (difficultyId in difficulties.keys())
+ {
+ var meta:Null = difficulties.get(difficultyId);
+ if (meta != null && meta.album != null)
+ {
+ result.set(difficultyId, meta.album);
+ }
+ }
+
+ return result;
+ }
+
/**
* Populate the difficulty data from the provided metadata.
* Does not load chart data (that is triggered later when we want to play the song).
@@ -192,6 +284,7 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry):Array
+ public function listDifficulties(?variationId:String, ?variationIds:Array, showHidden:Bool = false):Array
{
if (variationIds == null) variationIds = [];
if (variationId != null) variationIds.push(variationId);
@@ -314,6 +410,15 @@ class Song implements IPlayStateScriptedClass implements IRegistryEntry 2)
diff --git a/source/funkin/play/stage/Bopper.hx b/source/funkin/play/stage/Bopper.hx
index 1bc0632f9..262aff7bc 100644
--- a/source/funkin/play/stage/Bopper.hx
+++ b/source/funkin/play/stage/Bopper.hx
@@ -167,10 +167,7 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
function update_shouldAlternate():Void
{
- if (hasAnimation('danceLeft'))
- {
- this.shouldAlternate = true;
- }
+ this.shouldAlternate = hasAnimation('danceLeft');
}
/**
@@ -228,33 +225,42 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
/**
* Ensure that a given animation exists before playing it.
- * Will gracefully check for name, then name with stripped suffixes, then 'idle', then fail to play.
- * @param name
+ * Will gracefully check for name, then name with stripped suffixes, then fail to play.
+ * @param name The animation name to attempt to correct.
+ * @param fallback Instead of failing to play, try to play this animation instead.
*/
- function correctAnimationName(name:String):String
+ function correctAnimationName(name:String, ?fallback:String):String
{
// If the animation exists, we're good.
if (hasAnimation(name)) return name;
- trace('[BOPPER] Animation "$name" does not exist!');
+ FlxG.log.notice('Bopper tried to play animation "$name" that does not exist, stripping suffixes...');
// Attempt to strip a `-alt` suffix, if it exists.
if (name.lastIndexOf('-') != -1)
{
var correctName = name.substring(0, name.lastIndexOf('-'));
- trace('[BOPPER] Attempting to fallback to "$correctName"');
+ FlxG.log.notice('Bopper tried to play animation "$name" that does not exist, stripping suffixes...');
return correctAnimationName(correctName);
}
else
{
- if (name != 'idle')
+ if (fallback != null)
{
- trace('[BOPPER] Attempting to fallback to "idle"');
- return correctAnimationName('idle');
+ if (fallback == name)
+ {
+ FlxG.log.error('Bopper tried to play animation "$name" that does not exist! This is bad!');
+ return null;
+ }
+ else
+ {
+ FlxG.log.warn('Bopper tried to play animation "$name" that does not exist, fallback to idle...');
+ return correctAnimationName('idle');
+ }
}
else
{
- trace('[BOPPER] Failing animation playback.');
+ FlxG.log.error('Bopper tried to play animation "$name" that does not exist! This is bad!');
return null;
}
}
@@ -353,7 +359,9 @@ class Bopper extends StageProp implements IPlayStateScriptedClass
public function onGameOver(event:ScriptEvent) {}
- public function onNoteHit(event:NoteScriptEvent) {}
+ public function onNoteIncoming(event:NoteScriptEvent) {}
+
+ public function onNoteHit(event:HitNoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent) {}
diff --git a/source/funkin/play/stage/Stage.hx b/source/funkin/play/stage/Stage.hx
index af5765b25..db42b0dd3 100644
--- a/source/funkin/play/stage/Stage.hx
+++ b/source/funkin/play/stage/Stage.hx
@@ -109,9 +109,11 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
{
getBoyfriend().resetCharacter(true);
// Reapply the camera offsets.
- var charData = _data.characters.bf;
- getBoyfriend().cameraFocusPoint.x += charData.cameraOffsets[0];
- getBoyfriend().cameraFocusPoint.y += charData.cameraOffsets[1];
+ var stageCharData:StageDataCharacter = _data.characters.bf;
+ var finalScale:Float = getBoyfriend().getBaseScale() * stageCharData.scale;
+ getBoyfriend().setScale(finalScale);
+ getBoyfriend().cameraFocusPoint.x += stageCharData.cameraOffsets[0];
+ getBoyfriend().cameraFocusPoint.y += stageCharData.cameraOffsets[1];
}
else
{
@@ -121,17 +123,21 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
{
getGirlfriend().resetCharacter(true);
// Reapply the camera offsets.
- var charData = _data.characters.gf;
- getGirlfriend().cameraFocusPoint.x += charData.cameraOffsets[0];
- getGirlfriend().cameraFocusPoint.y += charData.cameraOffsets[1];
+ var stageCharData:StageDataCharacter = _data.characters.gf;
+ var finalScale:Float = getBoyfriend().getBaseScale() * stageCharData.scale;
+ getGirlfriend().setScale(finalScale);
+ getGirlfriend().cameraFocusPoint.x += stageCharData.cameraOffsets[0];
+ getGirlfriend().cameraFocusPoint.y += stageCharData.cameraOffsets[1];
}
if (getDad() != null)
{
getDad().resetCharacter(true);
// Reapply the camera offsets.
- var charData = _data.characters.dad;
- getDad().cameraFocusPoint.x += charData.cameraOffsets[0];
- getDad().cameraFocusPoint.y += charData.cameraOffsets[1];
+ var stageCharData:StageDataCharacter = _data.characters.dad;
+ var finalScale:Float = getBoyfriend().getBaseScale() * stageCharData.scale;
+ getDad().setScale(finalScale);
+ getDad().cameraFocusPoint.x += stageCharData.cameraOffsets[0];
+ getDad().cameraFocusPoint.y += stageCharData.cameraOffsets[1];
}
// Reset positions of named props.
@@ -185,9 +191,9 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
switch (dataProp.animType)
{
case 'packer':
- propSprite.frames = Paths.getPackerAtlas(dataProp.assetPath);
+ propSprite.loadPacker(dataProp.assetPath);
default: // 'sparrow'
- propSprite.frames = Paths.getSparrowAtlas(dataProp.assetPath);
+ propSprite.loadSparrow(dataProp.assetPath);
}
}
else if (isSolidColor)
@@ -209,7 +215,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
else
{
// Initalize static sprite.
- propSprite.loadGraphic(Paths.image(dataProp.assetPath));
+ propSprite.loadTexture(dataProp.assetPath);
// Disables calls to update() for a performance boost.
propSprite.active = false;
@@ -226,7 +232,7 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
switch (dataProp.scale)
{
case Left(value):
- propSprite.scale.set(value);
+ propSprite.scale.set(value, value);
case Right(values):
propSprite.scale.set(values[0], values[1]);
@@ -390,22 +396,25 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
#end
// Apply position and z-index.
- var charData:StageDataCharacter = null;
+ var stageCharData:StageDataCharacter = null;
switch (charType)
{
case BF:
this.characters.set('bf', character);
- charData = _data.characters.bf;
+ stageCharData = _data.characters.bf;
character.flipX = !character.getDataFlipX();
+ character.name = 'bf';
character.initHealthIcon(false);
case GF:
this.characters.set('gf', character);
- charData = _data.characters.gf;
+ stageCharData = _data.characters.gf;
character.flipX = character.getDataFlipX();
+ character.name = 'gf';
case DAD:
this.characters.set('dad', character);
- charData = _data.characters.dad;
+ stageCharData = _data.characters.dad;
character.flipX = character.getDataFlipX();
+ character.name = 'dad';
character.initHealthIcon(true);
default:
this.characters.set(character.characterId, character);
@@ -415,15 +424,15 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
// This ensures positioning is based on the idle animation.
character.resetCharacter(true);
- if (charData != null)
+ if (stageCharData != null)
{
- character.zIndex = charData.zIndex;
+ character.zIndex = stageCharData.zIndex;
// Start with the per-stage character position.
// Subtracting the origin ensures characters are positioned relative to their feet.
// Subtracting the global offset allows positioning on a per-character basis.
- character.x = charData.position[0] - character.characterOrigin.x + character.globalOffsets[0];
- character.y = charData.position[1] - character.characterOrigin.y + character.globalOffsets[1];
+ character.x = stageCharData.position[0] - character.characterOrigin.x + character.globalOffsets[0];
+ character.y = stageCharData.position[1] - character.characterOrigin.y + character.globalOffsets[1];
@:privateAccess(funkin.play.stage.Bopper)
{
@@ -432,15 +441,17 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
character.originalPosition.y = character.y + character.animOffsets[1];
}
- character.cameraFocusPoint.x += charData.cameraOffsets[0];
- character.cameraFocusPoint.y += charData.cameraOffsets[1];
+ var finalScale = character.getBaseScale() * stageCharData.scale;
+ character.setScale(finalScale); // Don't use scale.set for characters!
+ character.cameraFocusPoint.x += stageCharData.cameraOffsets[0];
+ character.cameraFocusPoint.y += stageCharData.cameraOffsets[1];
#if debug
// Draw the debug icon at the character's feet.
if (charType == BF || charType == DAD)
{
- debugIcon.x = charData.position[0];
- debugIcon.y = charData.position[1];
+ debugIcon.x = stageCharData.position[0];
+ debugIcon.y = stageCharData.position[1];
debugIcon2.x = character.x;
debugIcon2.y = character.y;
}
@@ -634,7 +645,30 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
*/
public function dispatchToCharacters(event:ScriptEvent):Void
{
- for (characterId in characters.keys())
+ var charList = this.characters.keys().array();
+
+ // Dad, then BF, then GF, in that order.
+
+ if (charList.contains('dad'))
+ {
+ dispatchToCharacter('dad', event);
+ charList.remove('dad');
+ }
+
+ if (charList.contains('bf'))
+ {
+ dispatchToCharacter('bf', event);
+ charList.remove('bf');
+ }
+
+ if (charList.contains('gf'))
+ {
+ dispatchToCharacter('gf', event);
+ charList.remove('gf');
+ }
+
+ // Then the rest of the characters, if any.
+ for (characterId in charList)
{
dispatchToCharacter(characterId, event);
}
@@ -840,7 +874,9 @@ class Stage extends FlxSpriteGroup implements IPlayStateScriptedClass implements
public function onCountdownEnd(event:CountdownScriptEvent) {}
- public function onNoteHit(event:NoteScriptEvent) {}
+ public function onNoteIncoming(event:NoteScriptEvent) {}
+
+ public function onNoteHit(event:HitNoteScriptEvent) {}
public function onNoteMiss(event:NoteScriptEvent) {}
diff --git a/source/funkin/save/Save.hx b/source/funkin/save/Save.hx
index b1468bd29..34680a7f5 100644
--- a/source/funkin/save/Save.hx
+++ b/source/funkin/save/Save.hx
@@ -11,8 +11,7 @@ import funkin.ui.debug.charting.ChartEditorState.ChartEditorTheme;
import thx.semver.Version;
@:nullSafety
-@:forward(volume, mute)
-abstract Save(RawSaveData)
+class Save
{
// Version 2.0.2 adds attributes to `optionsChartEditor`, that should return default values if they are null.
public static final SAVE_DATA_VERSION:thx.semver.Version = "2.0.2";
@@ -25,6 +24,20 @@ abstract Save(RawSaveData)
static final SAVE_PATH_LEGACY:String = 'ninjamuffin99';
static final SAVE_NAME_LEGACY:String = 'funkin';
+ public static var instance(get, never):Save;
+ static var _instance:Null = null;
+
+ static function get_instance():Save
+ {
+ if (_instance == null)
+ {
+ _instance = new Save(FlxG.save.data);
+ }
+ return _instance;
+ }
+
+ var data:RawSaveData;
+
public static function load():Void
{
trace("[SAVE] Loading save...");
@@ -33,86 +46,87 @@ abstract Save(RawSaveData)
loadFromSlot(1);
}
- public static function get():Save
- {
- return FlxG.save.data;
- }
-
/**
* Constructing a new Save will load the default values.
*/
- public function new()
+ public function new(data:RawSaveData)
{
- this =
- {
- version: Save.SAVE_DATA_VERSION,
+ this.data = data;
- volume: 1.0,
- mute: false,
+ if (this.data == null) data = Save.getDefault();
+ }
- api:
- {
- newgrounds:
- {
- sessionId: null,
- }
- },
- scores:
- {
- // No saved scores.
- levels: [],
- songs: [],
- },
- options:
- {
- // Reasonable defaults.
- naughtyness: true,
- downscroll: false,
- flashingLights: true,
- zoomCamera: true,
- debugDisplay: false,
- autoPause: true,
- inputOffset: 0,
- audioVisualOffset: 0,
+ public static function getDefault():RawSaveData
+ {
+ return {
+ version: Save.SAVE_DATA_VERSION,
- controls:
- {
- // Leave controls blank so defaults are loaded.
- p1:
- {
- keyboard: {},
- gamepad: {},
- },
- p2:
- {
- keyboard: {},
- gamepad: {},
- },
- },
- },
+ volume: 1.0,
+ mute: false,
- mods:
- {
- // No mods enabled.
- enabledMods: [],
- modOptions: [],
- },
+ api:
+ {
+ newgrounds:
+ {
+ sessionId: null,
+ }
+ },
+ scores:
+ {
+ // No saved scores.
+ levels: [],
+ songs: [],
+ },
+ options:
+ {
+ // Reasonable defaults.
+ naughtyness: true,
+ downscroll: false,
+ flashingLights: true,
+ zoomCamera: true,
+ debugDisplay: false,
+ autoPause: true,
+ inputOffset: 0,
+ audioVisualOffset: 0,
- optionsChartEditor:
- {
- // Reasonable defaults.
- previousFiles: [],
- noteQuant: 3,
- chartEditorLiveInputStyle: ChartEditorLiveInputStyle.None,
- theme: ChartEditorTheme.Light,
- playtestStartTime: false,
- downscroll: false,
- metronomeVolume: 1.0,
- hitsoundsEnabledPlayer: true,
- hitsoundsEnabledOpponent: true,
- themeMusic: true
- },
- };
+ controls:
+ {
+ // Leave controls blank so defaults are loaded.
+ p1:
+ {
+ keyboard: {},
+ gamepad: {},
+ },
+ p2:
+ {
+ keyboard: {},
+ gamepad: {},
+ },
+ },
+ },
+
+ mods:
+ {
+ // No mods enabled.
+ enabledMods: [],
+ modOptions: [],
+ },
+
+ optionsChartEditor:
+ {
+ // Reasonable defaults.
+ previousFiles: [],
+ noteQuant: 3,
+ chartEditorLiveInputStyle: ChartEditorLiveInputStyle.None,
+ theme: ChartEditorTheme.Light,
+ playtestStartTime: false,
+ downscroll: false,
+ metronomeVolume: 1.0,
+ hitsoundVolumePlayer: 1.0,
+ hitsoundVolumeOpponent: 1.0,
+ themeMusic: true
+ },
+ };
}
/**
@@ -122,7 +136,7 @@ abstract Save(RawSaveData)
function get_options():SaveDataOptions
{
- return this.options;
+ return data.options;
}
/**
@@ -132,7 +146,7 @@ abstract Save(RawSaveData)
function get_modOptions():Map
{
- return this.mods.modOptions;
+ return data.mods.modOptions;
}
/**
@@ -142,249 +156,232 @@ abstract Save(RawSaveData)
function get_ngSessionId():Null
{
- return this.api.newgrounds.sessionId;
+ return data.api.newgrounds.sessionId;
}
function set_ngSessionId(value:Null):Null
{
- this.api.newgrounds.sessionId = value;
+ data.api.newgrounds.sessionId = value;
flush();
- return this.api.newgrounds.sessionId;
+ return data.api.newgrounds.sessionId;
}
public var enabledModIds(get, set):Array;
function get_enabledModIds():Array
{
- return this.mods.enabledMods;
+ return data.mods.enabledMods;
}
function set_enabledModIds(value:Array):Array
{
- this.mods.enabledMods = value;
+ data.mods.enabledMods = value;
flush();
- return this.mods.enabledMods;
+ return data.mods.enabledMods;
}
public var chartEditorPreviousFiles(get, set):Array;
function get_chartEditorPreviousFiles():Array
{
- if (this.optionsChartEditor.previousFiles == null) this.optionsChartEditor.previousFiles = [];
+ if (data.optionsChartEditor.previousFiles == null) data.optionsChartEditor.previousFiles = [];
- return this.optionsChartEditor.previousFiles;
+ return data.optionsChartEditor.previousFiles;
}
function set_chartEditorPreviousFiles(value:Array):Array
{
// Set and apply.
- this.optionsChartEditor.previousFiles = value;
+ data.optionsChartEditor.previousFiles = value;
flush();
- return this.optionsChartEditor.previousFiles;
+ return data.optionsChartEditor.previousFiles;
}
public var chartEditorHasBackup(get, set):Bool;
function get_chartEditorHasBackup():Bool
{
- if (this.optionsChartEditor.hasBackup == null) this.optionsChartEditor.hasBackup = false;
+ if (data.optionsChartEditor.hasBackup == null) data.optionsChartEditor.hasBackup = false;
- return this.optionsChartEditor.hasBackup;
+ return data.optionsChartEditor.hasBackup;
}
function set_chartEditorHasBackup(value:Bool):Bool
{
// Set and apply.
- this.optionsChartEditor.hasBackup = value;
+ data.optionsChartEditor.hasBackup = value;
flush();
- return this.optionsChartEditor.hasBackup;
+ return data.optionsChartEditor.hasBackup;
}
public var chartEditorNoteQuant(get, set):Int;
function get_chartEditorNoteQuant():Int
{
- if (this.optionsChartEditor.noteQuant == null) this.optionsChartEditor.noteQuant = 3;
+ if (data.optionsChartEditor.noteQuant == null) data.optionsChartEditor.noteQuant = 3;
- return this.optionsChartEditor.noteQuant;
+ return data.optionsChartEditor.noteQuant;
}
function set_chartEditorNoteQuant(value:Int):Int
{
// Set and apply.
- this.optionsChartEditor.noteQuant = value;
+ data.optionsChartEditor.noteQuant = value;
flush();
- return this.optionsChartEditor.noteQuant;
+ return data.optionsChartEditor.noteQuant;
}
public var chartEditorLiveInputStyle(get, set):ChartEditorLiveInputStyle;
function get_chartEditorLiveInputStyle():ChartEditorLiveInputStyle
{
- if (this.optionsChartEditor.chartEditorLiveInputStyle == null) this.optionsChartEditor.chartEditorLiveInputStyle = ChartEditorLiveInputStyle.None;
+ if (data.optionsChartEditor.chartEditorLiveInputStyle == null) data.optionsChartEditor.chartEditorLiveInputStyle = ChartEditorLiveInputStyle.None;
- return this.optionsChartEditor.chartEditorLiveInputStyle;
+ return data.optionsChartEditor.chartEditorLiveInputStyle;
}
function set_chartEditorLiveInputStyle(value:ChartEditorLiveInputStyle):ChartEditorLiveInputStyle
{
// Set and apply.
- this.optionsChartEditor.chartEditorLiveInputStyle = value;
+ data.optionsChartEditor.chartEditorLiveInputStyle = value;
flush();
- return this.optionsChartEditor.chartEditorLiveInputStyle;
+ return data.optionsChartEditor.chartEditorLiveInputStyle;
}
public var chartEditorDownscroll(get, set):Bool;
function get_chartEditorDownscroll():Bool
{
- if (this.optionsChartEditor.downscroll == null) this.optionsChartEditor.downscroll = false;
+ if (data.optionsChartEditor.downscroll == null) data.optionsChartEditor.downscroll = false;
- return this.optionsChartEditor.downscroll;
+ return data.optionsChartEditor.downscroll;
}
function set_chartEditorDownscroll(value:Bool):Bool
{
// Set and apply.
- this.optionsChartEditor.downscroll = value;
+ data.optionsChartEditor.downscroll = value;
flush();
- return this.optionsChartEditor.downscroll;
+ return data.optionsChartEditor.downscroll;
}
public var chartEditorPlaytestStartTime(get, set):Bool;
function get_chartEditorPlaytestStartTime():Bool
{
- if (this.optionsChartEditor.playtestStartTime == null) this.optionsChartEditor.playtestStartTime = false;
+ if (data.optionsChartEditor.playtestStartTime == null) data.optionsChartEditor.playtestStartTime = false;
- return this.optionsChartEditor.playtestStartTime;
+ return data.optionsChartEditor.playtestStartTime;
}
function set_chartEditorPlaytestStartTime(value:Bool):Bool
{
// Set and apply.
- this.optionsChartEditor.playtestStartTime = value;
+ data.optionsChartEditor.playtestStartTime = value;
flush();
- return this.optionsChartEditor.playtestStartTime;
+ return data.optionsChartEditor.playtestStartTime;
}
public var chartEditorTheme(get, set):ChartEditorTheme;
function get_chartEditorTheme():ChartEditorTheme
{
- if (this.optionsChartEditor.theme == null) this.optionsChartEditor.theme = ChartEditorTheme.Light;
+ if (data.optionsChartEditor.theme == null) data.optionsChartEditor.theme = ChartEditorTheme.Light;
- return this.optionsChartEditor.theme;
+ return data.optionsChartEditor.theme;
}
function set_chartEditorTheme(value:ChartEditorTheme):ChartEditorTheme
{
// Set and apply.
- this.optionsChartEditor.theme = value;
+ data.optionsChartEditor.theme = value;
flush();
- return this.optionsChartEditor.theme;
+ return data.optionsChartEditor.theme;
}
public var chartEditorMetronomeVolume(get, set):Float;
function get_chartEditorMetronomeVolume():Float
{
- if (this.optionsChartEditor.metronomeVolume == null) this.optionsChartEditor.metronomeVolume = 1.0;
+ if (data.optionsChartEditor.metronomeVolume == null) data.optionsChartEditor.metronomeVolume = 1.0;
- return this.optionsChartEditor.metronomeVolume;
+ return data.optionsChartEditor.metronomeVolume;
}
function set_chartEditorMetronomeVolume(value:Float):Float
{
// Set and apply.
- this.optionsChartEditor.metronomeVolume = value;
+ data.optionsChartEditor.metronomeVolume = value;
flush();
- return this.optionsChartEditor.metronomeVolume;
+ return data.optionsChartEditor.metronomeVolume;
}
- public var chartEditorHitsoundVolume(get, set):Float;
+ public var chartEditorHitsoundVolumePlayer(get, set):Float;
- function get_chartEditorHitsoundVolume():Float
+ function get_chartEditorHitsoundVolumePlayer():Float
{
- if (this.optionsChartEditor.hitsoundVolume == null) this.optionsChartEditor.hitsoundVolume = 1.0;
+ if (data.optionsChartEditor.hitsoundVolumePlayer == null) data.optionsChartEditor.hitsoundVolumePlayer = 1.0;
- return this.optionsChartEditor.hitsoundVolume;
+ return data.optionsChartEditor.hitsoundVolumePlayer;
}
- function set_chartEditorHitsoundVolume(value:Float):Float
+ function set_chartEditorHitsoundVolumePlayer(value:Float):Float
{
// Set and apply.
- this.optionsChartEditor.hitsoundVolume = value;
+ data.optionsChartEditor.hitsoundVolumePlayer = value;
flush();
- return this.optionsChartEditor.hitsoundVolume;
+ return data.optionsChartEditor.hitsoundVolumePlayer;
}
- public var chartEditorHitsoundsEnabledPlayer(get, set):Bool;
+ public var chartEditorHitsoundVolumeOpponent(get, set):Float;
- function get_chartEditorHitsoundsEnabledPlayer():Bool
+ function get_chartEditorHitsoundVolumeOpponent():Float
{
- if (this.optionsChartEditor.hitsoundsEnabledPlayer == null) this.optionsChartEditor.hitsoundsEnabledPlayer = true;
+ if (data.optionsChartEditor.hitsoundVolumeOpponent == null) data.optionsChartEditor.hitsoundVolumeOpponent = 1.0;
- return this.optionsChartEditor.hitsoundsEnabledPlayer;
+ return data.optionsChartEditor.hitsoundVolumeOpponent;
}
- function set_chartEditorHitsoundsEnabledPlayer(value:Bool):Bool
+ function set_chartEditorHitsoundVolumeOpponent(value:Float):Float
{
// Set and apply.
- this.optionsChartEditor.hitsoundsEnabledPlayer = value;
+ data.optionsChartEditor.hitsoundVolumeOpponent = value;
flush();
- return this.optionsChartEditor.hitsoundsEnabledPlayer;
- }
-
- public var chartEditorHitsoundsEnabledOpponent(get, set):Bool;
-
- function get_chartEditorHitsoundsEnabledOpponent():Bool
- {
- if (this.optionsChartEditor.hitsoundsEnabledOpponent == null) this.optionsChartEditor.hitsoundsEnabledOpponent = true;
-
- return this.optionsChartEditor.hitsoundsEnabledOpponent;
- }
-
- function set_chartEditorHitsoundsEnabledOpponent(value:Bool):Bool
- {
- // Set and apply.
- this.optionsChartEditor.hitsoundsEnabledOpponent = value;
- flush();
- return this.optionsChartEditor.hitsoundsEnabledOpponent;
+ return data.optionsChartEditor.hitsoundVolumeOpponent;
}
public var chartEditorThemeMusic(get, set):Bool;
function get_chartEditorThemeMusic():Bool
{
- if (this.optionsChartEditor.themeMusic == null) this.optionsChartEditor.themeMusic = true;
+ if (data.optionsChartEditor.themeMusic == null) data.optionsChartEditor.themeMusic = true;
- return this.optionsChartEditor.themeMusic;
+ return data.optionsChartEditor.themeMusic;
}
function set_chartEditorThemeMusic(value:Bool):Bool
{
// Set and apply.
- this.optionsChartEditor.themeMusic = value;
+ data.optionsChartEditor.themeMusic = value;
flush();
- return this.optionsChartEditor.themeMusic;
+ return data.optionsChartEditor.themeMusic;
}
public var chartEditorPlaybackSpeed(get, set):Float;
function get_chartEditorPlaybackSpeed():Float
{
- if (this.optionsChartEditor.playbackSpeed == null) this.optionsChartEditor.playbackSpeed = 1.0;
+ if (data.optionsChartEditor.playbackSpeed == null) data.optionsChartEditor.playbackSpeed = 1.0;
- return this.optionsChartEditor.playbackSpeed;
+ return data.optionsChartEditor.playbackSpeed;
}
function set_chartEditorPlaybackSpeed(value:Float):Float
{
// Set and apply.
- this.optionsChartEditor.playbackSpeed = value;
+ data.optionsChartEditor.playbackSpeed = value;
flush();
- return this.optionsChartEditor.playbackSpeed;
+ return data.optionsChartEditor.playbackSpeed;
}
/**
@@ -396,11 +393,11 @@ abstract Save(RawSaveData)
*/
public function getLevelScore(levelId:String, difficultyId:String = 'normal'):Null
{
- var level = this.scores.levels.get(levelId);
+ var level = data.scores.levels.get(levelId);
if (level == null)
{
level = [];
- this.scores.levels.set(levelId, level);
+ data.scores.levels.set(levelId, level);
}
return level.get(difficultyId);
@@ -411,11 +408,11 @@ abstract Save(RawSaveData)
*/
public function setLevelScore(levelId:String, difficultyId:String, score:SaveScoreData):Void
{
- var level = this.scores.levels.get(levelId);
+ var level = data.scores.levels.get(levelId);
if (level == null)
{
level = [];
- this.scores.levels.set(levelId, level);
+ data.scores.levels.set(levelId, level);
}
level.set(difficultyId, score);
@@ -424,11 +421,11 @@ abstract Save(RawSaveData)
public function isLevelHighScore(levelId:String, difficultyId:String = 'normal', score:SaveScoreData):Bool
{
- var level = this.scores.levels.get(levelId);
+ var level = data.scores.levels.get(levelId);
if (level == null)
{
level = [];
- this.scores.levels.set(levelId, level);
+ data.scores.levels.set(levelId, level);
}
var currentScore = level.get(difficultyId);
@@ -467,11 +464,11 @@ abstract Save(RawSaveData)
*/
public function getSongScore(songId:String, difficultyId:String = 'normal'):Null
{
- var song = this.scores.songs.get(songId);
+ var song = data.scores.songs.get(songId);
if (song == null)
{
song = [];
- this.scores.songs.set(songId, song);
+ data.scores.songs.set(songId, song);
}
return song.get(difficultyId);
}
@@ -481,11 +478,11 @@ abstract Save(RawSaveData)
*/
public function setSongScore(songId:String, difficultyId:String, score:SaveScoreData):Void
{
- var song = this.scores.songs.get(songId);
+ var song = data.scores.songs.get(songId);
if (song == null)
{
song = [];
- this.scores.songs.set(songId, song);
+ data.scores.songs.set(songId, song);
}
song.set(difficultyId, score);
@@ -501,11 +498,11 @@ abstract Save(RawSaveData)
*/
public function isSongHighScore(songId:String, difficultyId:String = 'normal', score:SaveScoreData):Bool
{
- var song = this.scores.songs.get(songId);
+ var song = data.scores.songs.get(songId);
if (song == null)
{
song = [];
- this.scores.songs.set(songId, song);
+ data.scores.songs.set(songId, song);
}
var currentScore = song.get(difficultyId);
@@ -546,9 +543,9 @@ abstract Save(RawSaveData)
switch (inputType)
{
case Keys:
- return (playerId == 0) ? this.options.controls.p1.keyboard : this.options.controls.p2.keyboard;
+ return (playerId == 0) ? data.options.controls.p1.keyboard : data.options.controls.p2.keyboard;
case Gamepad(_):
- return (playerId == 0) ? this.options.controls.p1.gamepad : this.options.controls.p2.gamepad;
+ return (playerId == 0) ? data.options.controls.p1.gamepad : data.options.controls.p2.gamepad;
}
}
@@ -566,20 +563,20 @@ abstract Save(RawSaveData)
case Keys:
if (playerId == 0)
{
- this.options.controls.p1.keyboard = controls;
+ data.options.controls.p1.keyboard = controls;
}
else
{
- this.options.controls.p2.keyboard = controls;
+ data.options.controls.p2.keyboard = controls;
}
case Gamepad(_):
if (playerId == 0)
{
- this.options.controls.p1.gamepad = controls;
+ data.options.controls.p1.gamepad = controls;
}
else
{
- this.options.controls.p2.gamepad = controls;
+ data.options.controls.p2.gamepad = controls;
}
}
@@ -600,6 +597,36 @@ abstract Save(RawSaveData)
}
}
+ /**
+ * The user's current volume setting.
+ */
+ public var volume(get, set):Float;
+
+ function get_volume():Float
+ {
+ return data.volume;
+ }
+
+ function set_volume(value:Float):Float
+ {
+ return data.volume = value;
+ }
+
+ /**
+ * Whether the user's volume is currently muted.
+ */
+ public var mute(get, set):Bool;
+
+ function get_mute():Bool
+ {
+ return data.mute;
+ }
+
+ function set_mute(value:Bool):Bool
+ {
+ return data.mute = value;
+ }
+
/**
* Call this to make sure the save data is written to disk.
*/
@@ -625,17 +652,22 @@ abstract Save(RawSaveData)
if (legacySaveData != null)
{
trace('[SAVE] Found legacy save data, converting...');
- FlxG.save.mergeData(SaveDataMigrator.migrateFromLegacy(legacySaveData));
+ var gameSave = SaveDataMigrator.migrate(legacySaveData);
+ @:privateAccess
+ FlxG.save.mergeData(gameSave.data);
+ }
+ else
+ {
+ trace('[SAVE] No legacy save data found.');
}
}
else
{
trace('[SAVE] Loaded save data.');
- FlxG.save.mergeData(SaveDataMigrator.migrate(FlxG.save.data));
+ @:privateAccess
+ var gameSave = SaveDataMigrator.migrate(FlxG.save.data);
+ FlxG.save.mergeData(gameSave.data);
}
-
- trace('[SAVE] Done loading save data.');
- trace(FlxG.save.data);
}
static function fetchLegacySaveData():Null
@@ -924,12 +956,6 @@ typedef SaveControlsData =
*/
var ?CUTSCENE_ADVANCE:Array;
- /**
- * Keybind for skipping a cutscene.
- * @default `Escape`
- */
- var ?CUTSCENE_SKIP:Array;
-
/**
* Keybind for increasing volume.
* @default `Plus`
@@ -1004,10 +1030,16 @@ typedef SaveDataChartEditorOptions =
var ?metronomeVolume:Float;
/**
- * Hitsound volume in the Chart Editor.
+ * Hitsound volume (player) in the Chart Editor.
* @default `1.0`
*/
- var ?hitsoundVolume:Float;
+ var ?hitsoundVolumePlayer:Float;
+
+ /**
+ * Hitsound volume (opponent) in the Chart Editor.
+ * @default `1.0`
+ */
+ var ?hitsoundVolumeOpponent:Float;
/**
* If true, playtest songs from the current position in the Chart Editor.
@@ -1015,18 +1047,6 @@ typedef SaveDataChartEditorOptions =
*/
var ?playtestStartTime:Bool;
- /**
- * Player note hit sounds in the Chart Editor.
- * @default `true`
- */
- var ?hitsoundsEnabledPlayer:Bool;
-
- /**
- * Opponent note hit sounds in the Chart Editor.
- * @default `true`
- */
- var ?hitsoundsEnabledOpponent:Bool;
-
/**
* Theme music in the Chart Editor.
* @default `true`
diff --git a/source/funkin/save/migrator/RawSaveData_v1_0_0.hx b/source/funkin/save/migrator/RawSaveData_v1_0_0.hx
index b71102cce..a516f944a 100644
--- a/source/funkin/save/migrator/RawSaveData_v1_0_0.hx
+++ b/source/funkin/save/migrator/RawSaveData_v1_0_0.hx
@@ -35,7 +35,6 @@ typedef SaveControlsData_v1_0_0 =
var ?ACCEPT:Array;
var ?BACK:Array;
var ?CUTSCENE_ADVANCE:Array;
- var ?CUTSCENE_SKIP:Array;
var ?NOTE_DOWN:Array;
var ?NOTE_LEFT:Array;
var ?NOTE_RIGHT:Array;
diff --git a/source/funkin/save/migrator/SaveDataMigrator.hx b/source/funkin/save/migrator/SaveDataMigrator.hx
index 92bee4ceb..00637d52a 100644
--- a/source/funkin/save/migrator/SaveDataMigrator.hx
+++ b/source/funkin/save/migrator/SaveDataMigrator.hx
@@ -19,21 +19,21 @@ class SaveDataMigrator
{
trace('[SAVE] No version found in save data! Returning blank data.');
trace(inputData);
- return new Save();
+ return new Save(Save.getDefault());
}
else
{
if (VersionUtil.validateVersion(version, Save.SAVE_DATA_VERSION_RULE))
{
- // Simply cast the structured data.
- var save:Save = inputData;
+ // Simply import the structured data.
+ var save:Save = new Save(inputData);
return save;
}
else
{
trace('[SAVE] Invalid save data version! Returning blank data.');
trace(inputData);
- return new Save();
+ return new Save(Save.getDefault());
}
}
}
@@ -45,7 +45,7 @@ class SaveDataMigrator
{
var inputSaveData:RawSaveData_v1_0_0 = cast inputData;
- var result:Save = new Save();
+ var result:Save = new Save(Save.getDefault());
result.volume = inputSaveData.volume;
result.mute = inputSaveData.mute;
@@ -272,7 +272,6 @@ class SaveDataMigrator
ACCEPT: controlsData?.keys?.ACCEPT ?? null,
BACK: controlsData?.keys?.BACK ?? null,
CUTSCENE_ADVANCE: controlsData?.keys?.CUTSCENE_ADVANCE ?? null,
- CUTSCENE_SKIP: controlsData?.keys?.CUTSCENE_SKIP ?? null,
NOTE_DOWN: controlsData?.keys?.NOTE_DOWN ?? null,
NOTE_LEFT: controlsData?.keys?.NOTE_LEFT ?? null,
NOTE_RIGHT: controlsData?.keys?.NOTE_RIGHT ?? null,
@@ -293,7 +292,6 @@ class SaveDataMigrator
ACCEPT: controlsData?.pad?.ACCEPT ?? null,
BACK: controlsData?.pad?.BACK ?? null,
CUTSCENE_ADVANCE: controlsData?.pad?.CUTSCENE_ADVANCE ?? null,
- CUTSCENE_SKIP: controlsData?.pad?.CUTSCENE_SKIP ?? null,
NOTE_DOWN: controlsData?.pad?.NOTE_DOWN ?? null,
NOTE_LEFT: controlsData?.pad?.NOTE_LEFT ?? null,
NOTE_RIGHT: controlsData?.pad?.NOTE_RIGHT ?? null,
diff --git a/source/funkin/ui/MusicBeatSubState.hx b/source/funkin/ui/MusicBeatSubState.hx
index 91822c557..ab62a082d 100644
--- a/source/funkin/ui/MusicBeatSubState.hx
+++ b/source/funkin/ui/MusicBeatSubState.hx
@@ -1,6 +1,6 @@
package funkin.ui;
-import flixel.addons.transition.FlxTransitionableSubState;
+import flixel.addons.transition.FlxTransitionableState;
import flixel.FlxSubState;
import flixel.text.FlxText;
import funkin.ui.mainmenu.MainMenuState;
@@ -20,7 +20,16 @@ class MusicBeatSubState extends FlxSubState implements IEventHandler
{
public var leftWatermarkText:FlxText = null;
public var rightWatermarkText:FlxText = null;
- public var conductorInUse:Conductor = Conductor.instance;
+
+ public var conductorInUse(get, default):Conductor;
+
+ var _conductorInUse:Null;
+
+ function get_conductorInUse():Conductor
+ {
+ if (_conductorInUse == null) return Conductor.instance;
+ return _conductorInUse;
+ }
public function new(bgColor:FlxColor = FlxColor.TRANSPARENT)
{
diff --git a/source/funkin/ui/debug/MemoryCounter.hx b/source/funkin/ui/debug/MemoryCounter.hx
index 312d853e7..b25b55645 100644
--- a/source/funkin/ui/debug/MemoryCounter.hx
+++ b/source/funkin/ui/debug/MemoryCounter.hx
@@ -1,5 +1,6 @@
package funkin.ui.debug;
+import funkin.util.MemoryUtil;
import openfl.text.TextFormat;
import openfl.system.System;
import openfl.text.TextField;
@@ -35,7 +36,7 @@ class MemoryCounter extends TextField
@:noCompletion
#if !flash override #end function __enterFrame(deltaTime:Float):Void
{
- var mem:Float = Math.round(System.totalMemory / BYTES_PER_MEG / ROUND_TO) * ROUND_TO;
+ var mem:Float = Math.round(MemoryUtil.getMemoryUsed() / BYTES_PER_MEG / ROUND_TO) * ROUND_TO;
if (mem > memPeak) memPeak = mem;
diff --git a/source/funkin/ui/debug/anim/DebugBoundingState.hx b/source/funkin/ui/debug/anim/DebugBoundingState.hx
index 5561a9dcd..46a095a65 100644
--- a/source/funkin/ui/debug/anim/DebugBoundingState.hx
+++ b/source/funkin/ui/debug/anim/DebugBoundingState.hx
@@ -40,6 +40,7 @@ import openfl.utils.ByteArray;
import funkin.input.Cursor;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.util.SortUtil;
+import haxe.ui.core.Screen;
using flixel.util.FlxSpriteUtil;
@@ -75,21 +76,19 @@ class DebugBoundingState extends FlxState
var uiStuff:Component;
+ var haxeUIFocused(get, default):Bool = false;
+
+ function get_haxeUIFocused():Bool
+ {
+ // get the screen position, according to the HUD camera, temp default to FlxG.camera juuust in case?
+ var hudMousePos:FlxPoint = FlxG.mouse.getScreenPosition(hudCam ?? FlxG.camera);
+ return Screen.instance.hasSolidComponentUnderPoint(hudMousePos.x, hudMousePos.y);
+ }
+
override function create()
{
Paths.setCurrentLevel('week1');
- var str = Paths.xml('ui/animation-editor/offset-editor-view');
- uiStuff = RuntimeComponentBuilder.fromAsset(str);
-
- // uiStuff.findComponent("btnViewSpriteSheet").onClick = _ -> curView = SPRITESHEET;
- var dropdown:DropDown = cast uiStuff.findComponent("swapper");
- dropdown.onChange = function(e:UIEvent) {
- trace(e.type);
- curView = cast e.data.curView;
- trace(e.data);
- // trace(e.data);
- };
// lv.
// lv.onChange = function(e:UIEvent)
// {
@@ -106,23 +105,40 @@ class DebugBoundingState extends FlxState
hudCam = new FlxCamera();
hudCam.bgColor.alpha = 0;
- FlxG.cameras.add(hudCam, false);
-
bg = FlxGridOverlay.create(10, 10);
// bg = new FlxSprite().makeGraphic(FlxG.width, FlxG.height, FlxColor.GREEN);
bg.scrollFactor.set();
add(bg);
- initSpritesheetView();
- initOffsetView();
+ // we are setting this as the default draw camera only temporarily, to trick haxeui
+ FlxG.cameras.add(hudCam);
- Cursor.show();
+ var str = Paths.xml('ui/animation-editor/offset-editor-view');
+ uiStuff = RuntimeComponentBuilder.fromAsset(str);
+
+ // uiStuff.findComponent("btnViewSpriteSheet").onClick = _ -> curView = SPRITESHEET;
+ var dropdown:DropDown = cast uiStuff.findComponent("swapper");
+ dropdown.onChange = function(e:UIEvent) {
+ trace(e.type);
+ curView = cast e.data.curView;
+ trace(e.data);
+ // trace(e.data);
+ };
uiStuff.cameras = [hudCam];
add(uiStuff);
+ // sets the default camera back to FlxG.camera, since we set it to hudCamera for haxeui stuf
+ FlxG.cameras.setDefaultDrawTarget(FlxG.camera, true);
+ FlxG.cameras.setDefaultDrawTarget(hudCam, false);
+
+ initSpritesheetView();
+ initOffsetView();
+
+ Cursor.show();
+
super.create();
}
@@ -359,7 +375,7 @@ class DebugBoundingState extends FlxState
offsetView.visible = true;
offsetView.active = true;
offsetControls();
- mouseOffsetMovement();
+ if (!haxeUIFocused) mouseOffsetMovement();
}
if (FlxG.keys.justPressed.H) hudCam.visible = !hudCam.visible;
@@ -367,7 +383,7 @@ class DebugBoundingState extends FlxState
if (FlxG.keys.justPressed.F4) FlxG.switchState(() -> new MainMenuState());
MouseUtil.mouseCamDrag();
- MouseUtil.mouseWheelZoom();
+ if (!haxeUIFocused) MouseUtil.mouseWheelZoom();
// bg.scale.x = FlxG.camera.zoom;
// bg.scale.y = FlxG.camera.zoom;
diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx
index 48a6e70c9..bdc0d311e 100644
--- a/source/funkin/ui/debug/charting/ChartEditorState.hx
+++ b/source/funkin/ui/debug/charting/ChartEditorState.hx
@@ -310,10 +310,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
this.songLengthInMs = value;
- // Make sure playhead doesn't go outside the song.
- if (playheadPositionInMs > songLengthInMs) playheadPositionInMs = songLengthInMs;
-
- onSongLengthChanged();
+ updateGridHeight();
return this.songLengthInMs;
}
@@ -595,11 +592,22 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
var playtestPracticeMode:Bool = false;
+ /**
+ * If true, playtesting a chart will make the computer do it for you!
+ */
+ var playtestBotPlayMode:Bool = false;
+
/**
* Enables or disables the "debugger" popup that appears when you run into a flixel error.
*/
var enabledDebuggerPopup:Bool = true;
+ /**
+ * Whether song scripts should be enabled during playtesting.
+ * You should probably check the box if the song has custom mechanics.
+ */
+ var playtestSongScripts:Bool = true;
+
// Visuals
/**
@@ -704,19 +712,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var metronomeVolume:Float = 1.0;
/**
- * The volume to play hitsounds at.
+ * The volume to play the player's hitsounds at.
*/
- var hitsoundVolume:Float = 1.0;
+ var hitsoundVolumePlayer:Float = 1.0;
/**
- * Whether hitsounds are enabled for the player.
+ * The volume to play the opponent's hitsounds at.
*/
- var hitsoundsEnabledPlayer:Bool = true;
-
- /**
- * Whether hitsounds are enabled for the opponent.
- */
- var hitsoundsEnabledOpponent:Bool = true;
+ var hitsoundVolumeOpponent:Float = 1.0;
/**
* Whether hitsounds are enabled for at least one character.
@@ -725,7 +728,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function get_hitsoundsEnabled():Bool
{
- return hitsoundsEnabledPlayer || hitsoundsEnabledOpponent;
+ return hitsoundVolumePlayer + hitsoundVolumeOpponent > 0;
}
// Auto-save
@@ -875,6 +878,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
var noteDisplayDirty:Bool = true;
+ var noteTooltipsDirty:Bool = true;
+
/**
* Whether the selected charactesr have been modified and the health icons need to be updated.
*/
@@ -928,12 +933,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function get_shouldShowBackupAvailableDialog():Bool
{
- return Save.get().chartEditorHasBackup;
+ return Save.instance.chartEditorHasBackup;
}
function set_shouldShowBackupAvailableDialog(value:Bool):Bool
{
- return Save.get().chartEditorHasBackup = value;
+ return Save.instance.chartEditorHasBackup = value;
}
/**
@@ -1399,7 +1404,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
function get_currentSongId():String
{
- return currentSongName.toLowerKebabCase().replace('.', '').replace(' ', '-');
+ return currentSongName.toLowerKebabCase().replace(' ', '-').sanitize();
}
var currentSongArtist(get, set):String;
@@ -1538,6 +1543,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Make sure view is updated when the variation changes.
noteDisplayDirty = true;
notePreviewDirty = true;
+ noteTooltipsDirty = true;
notePreviewViewportBoundsDirty = true;
switchToCurrentInstrumental();
@@ -1559,6 +1565,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Make sure view is updated when the difficulty changes.
noteDisplayDirty = true;
notePreviewDirty = true;
+ noteTooltipsDirty = true;
notePreviewViewportBoundsDirty = true;
// Make sure the difficulty we selected is in the list of difficulties.
@@ -1757,30 +1764,30 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
*/
var menubarItemVolumeMetronome:Slider;
- /**
- * The `Audio -> Enable Player Hitsounds` menu checkbox.
- */
- var menubarItemPlayerHitsounds:MenuCheckBox;
-
- /**
- * The `Audio -> Enable Opponent Hitsounds` menu checkbox.
- */
- var menubarItemOpponentHitsounds:MenuCheckBox;
-
/**
* The `Audio -> Play Theme Music` menu checkbox.
*/
var menubarItemThemeMusic:MenuCheckBox;
/**
- * The `Audio -> Hitsound Volume` label.
+ * The `Audio -> Player Hitsound Volume` label.
*/
- var menubarLabelVolumeHitsounds:Label;
+ var menubarLabelVolumeHitsoundPlayer:Label;
/**
- * The `Audio -> Hitsound Volume` slider.
+ * The `Audio -> Enemy Hitsound Volume` label.
*/
- var menubarItemVolumeHitsounds:Slider;
+ var menubarLabelVolumeHitsoundOpponent:Label;
+
+ /**
+ * The `Audio -> Player Hitsound Volume` slider.
+ */
+ var menubarItemVolumeHitsoundPlayer:Slider;
+
+ /**
+ * The `Audio -> Enemy Hitsound Volume` slider.
+ */
+ var menubarItemVolumeHitsoundOpponent:Slider;
/**
* The `Audio -> Instrumental Volume` label.
@@ -2171,7 +2178,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
public function loadPreferences():Void
{
- var save:Save = Save.get();
+ var save:Save = Save.instance;
if (previousWorkingFilePaths[0] == null)
{
@@ -2187,9 +2194,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
playtestStartTime = save.chartEditorPlaytestStartTime;
currentTheme = save.chartEditorTheme;
metronomeVolume = save.chartEditorMetronomeVolume;
- hitsoundVolume = save.chartEditorHitsoundVolume;
- hitsoundsEnabledPlayer = save.chartEditorHitsoundsEnabledPlayer;
- hitsoundsEnabledOpponent = save.chartEditorHitsoundsEnabledOpponent;
+ hitsoundVolumePlayer = save.chartEditorHitsoundVolumePlayer;
+ hitsoundVolumePlayer = save.chartEditorHitsoundVolumeOpponent;
this.welcomeMusic.active = save.chartEditorThemeMusic;
// audioInstTrack.volume = save.chartEditorInstVolume;
@@ -2200,7 +2206,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
public function writePreferences(hasBackup:Bool):Void
{
- var save:Save = Save.get();
+ var save:Save = Save.instance;
// Can't use filter() because of null safety checking!
var filteredWorkingFilePaths:Array = [];
@@ -2217,9 +2223,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
save.chartEditorPlaytestStartTime = playtestStartTime;
save.chartEditorTheme = currentTheme;
save.chartEditorMetronomeVolume = metronomeVolume;
- save.chartEditorHitsoundVolume = hitsoundVolume;
- save.chartEditorHitsoundsEnabledPlayer = hitsoundsEnabledPlayer;
- save.chartEditorHitsoundsEnabledOpponent = hitsoundsEnabledOpponent;
+ save.chartEditorHitsoundVolumePlayer = hitsoundVolumePlayer;
+ save.chartEditorHitsoundVolumeOpponent = hitsoundVolumeOpponent;
save.chartEditorThemeMusic = this.welcomeMusic.active;
// save.chartEditorInstVolume = audioInstTrack.volume;
@@ -2848,12 +2853,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
menuBarItemInputStyleNone.onClick = function(event:UIEvent) {
currentLiveInputStyle = None;
};
+ menuBarItemInputStyleNone.selected = currentLiveInputStyle == None;
menuBarItemInputStyleNumberKeys.onClick = function(event:UIEvent) {
currentLiveInputStyle = NumberKeys;
};
+ menuBarItemInputStyleNumberKeys.selected = currentLiveInputStyle == NumberKeys;
menuBarItemInputStyleWASD.onClick = function(event:UIEvent) {
- currentLiveInputStyle = WASD;
+ currentLiveInputStyle = WASDKeys;
};
+ menuBarItemInputStyleWASD.selected = currentLiveInputStyle == WASDKeys;
menubarItemAbout.onClick = _ -> this.openAboutDialog();
menubarItemWelcomeDialog.onClick = _ -> this.openWelcomeDialog(true);
@@ -2912,24 +2920,25 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
};
menubarItemVolumeMetronome.value = Std.int(metronomeVolume * 100);
- menubarItemPlayerHitsounds.onChange = event -> hitsoundsEnabledPlayer = event.value;
- menubarItemPlayerHitsounds.selected = hitsoundsEnabledPlayer;
-
- menubarItemOpponentHitsounds.onChange = event -> hitsoundsEnabledOpponent = event.value;
- menubarItemOpponentHitsounds.selected = hitsoundsEnabledOpponent;
-
menubarItemThemeMusic.onChange = event -> {
this.welcomeMusic.active = event.value;
fadeInWelcomeMusic(WELCOME_MUSIC_FADE_IN_DELAY, WELCOME_MUSIC_FADE_IN_DURATION);
};
menubarItemThemeMusic.selected = this.welcomeMusic.active;
- menubarItemVolumeHitsound.onChange = event -> {
+ menubarItemVolumeHitsoundPlayer.onChange = event -> {
var volume:Float = event.value.toFloat() / 100.0;
- hitsoundVolume = volume;
- menubarLabelVolumeHitsound.text = 'Hitsound - ${Std.int(event.value)}%';
+ hitsoundVolumePlayer = volume;
+ menubarLabelVolumeHitsoundPlayer.text = 'Player - ${Std.int(event.value)}%';
};
- menubarItemVolumeHitsound.value = Std.int(hitsoundVolume * 100);
+ menubarItemVolumeHitsoundPlayer.value = Std.int(hitsoundVolumePlayer * 100);
+
+ menubarItemVolumeHitsoundOpponent.onChange = event -> {
+ var volume:Float = event.value.toFloat() / 100.0;
+ hitsoundVolumeOpponent = volume;
+ menubarLabelVolumeHitsoundOpponent.text = 'Enemy - ${Std.int(event.value)}%';
+ };
+ menubarItemVolumeHitsoundOpponent.value = Std.int(hitsoundVolumeOpponent * 100);
menubarItemVolumeInstrumental.onChange = event -> {
var volume:Float = event.value.toFloat() / 100.0;
@@ -2951,7 +2960,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
menubarItemPlaybackSpeed.onChange = event -> {
var pitch:Float = (event.value.toFloat() * 2.0) / 100.0;
- pitch = Math.floor(pitch / 0.25) * 0.25; // Round to nearest 0.25.
+ pitch = Math.floor(pitch / 0.05) * 0.05; // Round to nearest 5%
+ pitch = Math.max(0.05, Math.min(2.0, pitch)); // Clamp to 5% to 200%
#if FLX_PITCH
if (audioInstTrack != null) audioInstTrack.pitch = pitch;
audioVocalTrackGroup.pitch = pitch;
@@ -3417,7 +3427,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Update the event sprite's position.
eventSprite.updateEventPosition(renderedEvents);
// Update the sprite's graphic. TODO: Is this inefficient?
- eventSprite.playAnimation(eventSprite.eventData.event);
+ eventSprite.playAnimation(eventSprite.eventData.eventKind);
}
else
{
@@ -3657,8 +3667,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
selectionSquare.width = eventSprite.width;
selectionSquare.height = eventSprite.height;
}
+
+ // Additional cleanup on notes.
+ if (noteTooltipsDirty) eventSprite.updateTooltipText();
}
+ noteTooltipsDirty = false;
+
// Sort the notes DESCENDING. This keeps the sustain behind the associated note.
renderedNotes.sort(FlxSort.byY, FlxSort.DESCENDING); // TODO: .group.insertionSort()
@@ -4530,14 +4545,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
// Create an event and place it in the chart.
// TODO: Figure out configuring event data.
- var newEventData:SongEventData = new SongEventData(cursorSnappedMs, eventKindToPlace, eventDataToPlace.clone());
+ var newEventData:SongEventData = new SongEventData(cursorSnappedMs, eventKindToPlace, eventDataToPlace.copy());
performCommand(new AddEventsCommand([newEventData], FlxG.keys.pressed.CONTROL));
}
else
{
// Create a note and place it in the chart.
- var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace.clone());
+ var newNoteData:SongNoteData = new SongNoteData(cursorSnappedMs, cursorColumn, 0, noteKindToPlace);
performCommand(new AddNotesCommand([newNoteData], FlxG.keys.pressed.CONTROL));
@@ -4678,9 +4693,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var eventData:SongEventData = gridGhostEvent.eventData != null ? gridGhostEvent.eventData : new SongEventData(cursorMs, eventKindToPlace, null);
- if (eventKindToPlace != eventData.event)
+ if (eventKindToPlace != eventData.eventKind)
{
- eventData.event = eventKindToPlace;
+ eventData.eventKind = eventKindToPlace;
}
eventData.time = cursorSnappedMs;
@@ -4942,7 +4957,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
// Place notes at the playhead.
switch (currentLiveInputStyle)
{
- case ChartEditorLiveInputStyle.WASD:
+ case ChartEditorLiveInputStyle.WASDKeys:
if (FlxG.keys.justPressed.A) placeNoteAtPlayhead(4);
if (FlxG.keys.justPressed.S) placeNoteAtPlayhead(5);
if (FlxG.keys.justPressed.W) placeNoteAtPlayhead(6);
@@ -5245,6 +5260,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
}
// Would bind Ctrl+A and Ctrl+D here, but they are already bound to Select All and Select None.
}
+ else
+ {
+ trace('Ignoring keybinds for View menu items because we are in live input mode (${currentLiveInputStyle}).');
+ }
}
/**
@@ -5267,14 +5286,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
{
// F1 = Open Help
if (FlxG.keys.justPressed.F1) this.openUserGuideDialog();
-
- // DEBUG KEYBIND: Ctrl + Alt + Shift + L = Crash the game.
- #if debug
- if (FlxG.keys.pressed.CONTROL && FlxG.keys.pressed.ALT && FlxG.keys.pressed.SHIFT && FlxG.keys.justPressed.L)
- {
- throw "DEBUG: Crashing the chart editor!";
- }
- #end
}
function handleQuickWatch():Void
@@ -5317,10 +5328,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var startTimestamp:Float = 0;
if (playtestStartTime) startTimestamp = scrollPositionInMs + playheadPositionInMs;
+ var playbackRate:Float = ((menubarItemPlaybackSpeed.value ?? 1.0) * 2.0) / 100.0;
+ playbackRate = Math.floor(playbackRate / 0.05) * 0.05; // Round to nearest 5%
+ playbackRate = Math.max(0.05, Math.min(2.0, playbackRate)); // Clamp to 5% to 200%
+
var targetSong:Song;
try
{
- targetSong = Song.buildRaw(currentSongId, songMetadata.values(), availableVariations, songChartData, false);
+ targetSong = Song.buildRaw(currentSongId, songMetadata.values(), availableVariations, songChartData, playtestSongScripts, false);
}
catch (e)
{
@@ -5328,9 +5343,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
return;
}
- LogStyle.WARNING.openConsole = enabledDebuggerPopup;
- LogStyle.ERROR.openConsole = enabledDebuggerPopup;
-
// TODO: Rework asset system so we can remove this.
switch (currentSongStage)
{
@@ -5348,7 +5360,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
Paths.setCurrentLevel('week6');
case 'tankmanBattlefield':
Paths.setCurrentLevel('week7');
- case 'phillyStreets' | 'phillyBlazin':
+ case 'phillyStreets' | 'phillyBlazin' | 'phillyBlazin2':
Paths.setCurrentLevel('weekend1');
}
@@ -5364,8 +5376,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
targetDifficulty: selectedDifficulty,
targetVariation: selectedVariation,
practiceMode: playtestPracticeMode,
+ botPlayMode: playtestBotPlayMode,
minimalMode: minimal,
startTimestamp: startTimestamp,
+ playbackRate: playbackRate,
overrideMusic: true,
});
@@ -5511,8 +5525,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
this.switchToInstrumental(currentInstrumentalId, currentSongMetadata.playData.characters.player, currentSongMetadata.playData.characters.opponent);
}
- function onSongLengthChanged():Void
+ public function updateGridHeight():Void
{
+ // Make sure playhead doesn't go outside the song after we update the grid height.
+ if (playheadPositionInMs > songLengthInMs) playheadPositionInMs = songLengthInMs;
+
if (gridTiledSprite != null)
{
gridTiledSprite.height = songLengthInPixels;
@@ -5929,7 +5946,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
var tempNote:NoteSprite = new NoteSprite(NoteStyleRegistry.instance.fetchDefault());
tempNote.noteData = noteData;
tempNote.scrollFactor.set(0, 0);
- var event:NoteScriptEvent = new NoteScriptEvent(NOTE_HIT, tempNote, 1, true);
+ var event:NoteScriptEvent = new HitNoteScriptEvent(tempNote, 0.0, 0, 'perfect', 0);
dispatchEvent(event);
// Calling event.cancelEvent() skips all the other logic! Neat!
@@ -5939,9 +5956,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
switch (noteData.getStrumlineIndex())
{
case 0: // Player
- if (hitsoundsEnabledPlayer) this.playSound(Paths.sound('chartingSounds/hitNotePlayer'), hitsoundVolume);
+ if (hitsoundVolumePlayer > 0) this.playSound(Paths.sound('chartingSounds/hitNotePlayer'), hitsoundVolumePlayer);
case 1: // Opponent
- if (hitsoundsEnabledOpponent) this.playSound(Paths.sound('chartingSounds/hitNoteOpponent'), hitsoundVolume);
+ if (hitsoundVolumeOpponent > 0) this.playSound(Paths.sound('chartingSounds/hitNoteOpponent'), hitsoundVolumeOpponent);
}
}
}
@@ -6139,7 +6156,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
}
/**
- * Available input modes for the chart editor state.
+ * Available input modes for the chart editor state. Numbers/arrows/WASD available for other keybinds.
*/
enum ChartEditorLiveInputStyle
{
@@ -6154,9 +6171,9 @@ enum ChartEditorLiveInputStyle
NumberKeys;
/**
- * WASD to place notes on opponent's side, arrow keys to place notes on player's side.
+ * WASD to place notes on opponent's side, Arrow keys to place notes on player's side.
*/
- WASD;
+ WASDKeys;
}
typedef ChartEditorParams =
diff --git a/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx b/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx
index bd832fab3..a3728f337 100644
--- a/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/ChangeStartingBPMCommand.hx
@@ -40,6 +40,8 @@ class ChangeStartingBPMCommand implements ChartEditorCommand
state.scrollPositionInPixels = 0;
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
+
+ state.updateGridHeight();
}
public function undo(state:ChartEditorState):Void
@@ -62,6 +64,8 @@ class ChangeStartingBPMCommand implements ChartEditorCommand
state.scrollPositionInPixels = 0;
Conductor.instance.mapTimeChanges(state.currentSongMetadata.timeChanges);
+
+ state.updateGridHeight();
}
public function shouldAddToHistory(state:ChartEditorState):Bool
diff --git a/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx b/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx
index 88f73cfed..423295f1a 100644
--- a/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/SelectItemsCommand.hx
@@ -34,11 +34,11 @@ class SelectItemsCommand implements ChartEditorCommand
}
// If we just selected one or more events (and no notes), then we should make the event data toolbox display the event data for the selected event.
- if (this.notes.length == 0 && this.events.length >= 1)
+ if (this.notes.length == 0 && this.events.length == 1)
{
var eventSelected = this.events[0];
- state.eventKindToPlace = eventSelected.event;
+ state.eventKindToPlace = eventSelected.eventKind;
// This code is here to parse event data that's not built as a struct for some reason.
// TODO: Clean this up or get rid of it.
@@ -46,7 +46,7 @@ class SelectItemsCommand implements ChartEditorCommand
var defaultKey = null;
if (eventSchema == null)
{
- trace('[WARNING] Event schema not found for event ${eventSelected.event}.');
+ trace('[WARNING] Event schema not found for event ${eventSelected.eventKind}.');
}
else
{
@@ -60,7 +60,7 @@ class SelectItemsCommand implements ChartEditorCommand
}
// If we just selected one or more notes (and no events), then we should make the note data toolbox display the note data for the selected note.
- if (this.events.length == 0 && this.notes.length >= 1)
+ if (this.events.length == 0 && this.notes.length == 1)
{
var noteSelected = this.notes[0];
diff --git a/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx b/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx
index 5cc89e137..73cf80fa0 100644
--- a/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx
+++ b/source/funkin/ui/debug/charting/commands/SetItemSelectionCommand.hx
@@ -31,11 +31,11 @@ class SetItemSelectionCommand implements ChartEditorCommand
state.currentEventSelection = events;
// If we just selected one or more events (and no notes), then we should make the event data toolbox display the event data for the selected event.
- if (this.notes.length == 0 && this.events.length >= 1)
+ if (this.notes.length == 0 && this.events.length == 1)
{
var eventSelected = this.events[0];
- state.eventKindToPlace = eventSelected.event;
+ state.eventKindToPlace = eventSelected.eventKind;
// This code is here to parse event data that's not built as a struct for some reason.
// TODO: Clean this up or get rid of it.
@@ -43,7 +43,7 @@ class SetItemSelectionCommand implements ChartEditorCommand
var defaultKey = null;
if (eventSchema == null)
{
- trace('[WARNING] Event schema not found for event ${eventSelected.event}.');
+ trace('[WARNING] Event schema not found for event ${eventSelected.eventKind}.');
}
else
{
@@ -51,13 +51,18 @@ class SetItemSelectionCommand implements ChartEditorCommand
}
var eventData = eventSelected.valueAsStruct(defaultKey);
- state.eventDataToPlace = eventData;
+ var eventDataClone = Reflect.copy(eventData);
+
+ if (eventDataClone != null)
+ {
+ state.eventDataToPlace = eventDataClone;
+ }
state.refreshToolbox(ChartEditorState.CHART_EDITOR_TOOLBOX_EVENT_DATA_LAYOUT);
}
// IF we just selected one or more notes (and no events), then we should make the note data toolbox display the note data for the selected note.
- if (this.events.length == 0 && this.notes.length >= 1)
+ if (this.events.length == 0 && this.notes.length == 1)
{
var noteSelected = this.notes[0];
diff --git a/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx
index e3dae37cf..c996079bc 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorEventSprite.hx
@@ -133,7 +133,7 @@ class ChartEditorEventSprite extends FlxSprite
public function playAnimation(?name:String):Void
{
- if (name == null) name = eventData?.event ?? DEFAULT_EVENT;
+ if (name == null) name = eventData?.eventKind ?? DEFAULT_EVENT;
var correctedName = correctAnimationName(name);
this.animation.play(correctedName);
@@ -160,12 +160,11 @@ class ChartEditorEventSprite extends FlxSprite
else
{
this.visible = true;
- playAnimation(value.event);
+ playAnimation(value.eventKind);
this.eventData = value;
// Update the position to match the note data.
updateEventPosition();
- // Update the tooltip text.
- this.tooltip.tipData = {text: this.eventData.buildTooltip()};
+ updateTooltipText();
return this.eventData;
}
}
@@ -188,6 +187,13 @@ class ChartEditorEventSprite extends FlxSprite
this.updateTooltipPosition();
}
+ public function updateTooltipText():Void
+ {
+ if (this.eventData == null) return;
+ if (this.isGhost) return;
+ this.tooltip.tipData = {text: this.eventData.buildTooltip()};
+ }
+
public function updateTooltipPosition():Void
{
// No tooltip for ghost sprites.
diff --git a/source/funkin/ui/debug/charting/components/ChartEditorMeasureTicks.hx b/source/funkin/ui/debug/charting/components/ChartEditorMeasureTicks.hx
index 1a76d1e22..2d8b62cbd 100644
--- a/source/funkin/ui/debug/charting/components/ChartEditorMeasureTicks.hx
+++ b/source/funkin/ui/debug/charting/components/ChartEditorMeasureTicks.hx
@@ -58,9 +58,15 @@ class ChartEditorMeasureTicks extends FlxTypedSpriteGroup
var measureNumberInViewport = Math.floor(viewTopPosition / ChartEditorState.GRID_SIZE / Conductor.instance.stepsPerMeasure) + 1;
var measureNumberPosition = measureNumberInViewport * ChartEditorState.GRID_SIZE * Conductor.instance.stepsPerMeasure;
- measureNumber.text = '${measureNumberInViewport + 1}';
measureNumber.y = measureNumberPosition + this.y;
+ // Show the measure number only if it isn't beneath the end of the note grid.
+ // Using measureNumber + 1 because the cut-off bar at the bottom is technically a bar, but it looks bad if a measure number shows up there.
+ if ((measureNumberInViewport + 1) < chartEditorState.songLengthInSteps / Conductor.instance.stepsPerMeasure)
+ measureNumber.text = '${measureNumberInViewport + 1}';
+ else
+ measureNumber.text = '';
+
// trace(measureNumber.text + ' at ' + measureNumber.y);
}
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
index 363dc1567..1e1d165f3 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorAudioHandler.hx
@@ -7,6 +7,7 @@ import funkin.audio.FunkinSound;
import funkin.play.character.BaseCharacter.CharacterType;
import funkin.util.FileUtil;
import funkin.util.assets.SoundUtil;
+import funkin.util.TimerUtil;
import funkin.audio.waveform.WaveformData;
import funkin.audio.waveform.WaveformDataParser;
import funkin.audio.waveform.WaveformSprite;
@@ -128,41 +129,41 @@ class ChartEditorAudioHandler
public static function switchToInstrumental(state:ChartEditorState, instId:String = '', playerId:String, opponentId:String):Bool
{
- var perfA = haxe.Timer.stamp();
+ var perfA:Float = TimerUtil.start();
var result:Bool = playInstrumental(state, instId);
if (!result) return false;
- var perfB = haxe.Timer.stamp();
+ var perfB:Float = TimerUtil.start();
stopExistingVocals(state);
- var perfC = haxe.Timer.stamp();
+ var perfC:Float = TimerUtil.start();
result = playVocals(state, BF, playerId, instId);
- var perfD = haxe.Timer.stamp();
+ var perfD:Float = TimerUtil.start();
// if (!result) return false;
result = playVocals(state, DAD, opponentId, instId);
// if (!result) return false;
- var perfE = haxe.Timer.stamp();
+ var perfE:Float = TimerUtil.start();
state.hardRefreshOffsetsToolbox();
- var perfF = haxe.Timer.stamp();
+ var perfF:Float = TimerUtil.start();
state.hardRefreshFreeplayToolbox();
- var perfG = haxe.Timer.stamp();
+ var perfG:Float = TimerUtil.start();
- trace('Switched to instrumental in ${perfB - perfA} seconds.');
- trace('Stopped existing vocals in ${perfC - perfB} seconds.');
- trace('Played BF vocals in ${perfD - perfC} seconds.');
- trace('Played DAD vocals in ${perfE - perfD} seconds.');
- trace('Hard refreshed offsets toolbox in ${perfF - perfE} seconds.');
- trace('Hard refreshed freeplay toolbox in ${perfG - perfF} seconds.');
+ trace('Switched to instrumental in ${TimerUtil.seconds(perfA, perfB)}.');
+ trace('Stopped existing vocals in ${TimerUtil.seconds(perfB, perfC)}.');
+ trace('Played BF vocals in ${TimerUtil.seconds(perfC, perfD)}.');
+ trace('Played DAD vocals in ${TimerUtil.seconds(perfD, perfE)}.');
+ trace('Hard refreshed offsets toolbox in ${TimerUtil.seconds(perfE, perfF)}.');
+ trace('Hard refreshed freeplay toolbox in ${TimerUtil.seconds(perfF, perfG)}.');
return true;
}
@@ -174,10 +175,9 @@ class ChartEditorAudioHandler
{
if (instId == '') instId = 'default';
var instTrackData:Null = state.audioInstTrackData.get(instId);
- var perfA = haxe.Timer.stamp();
+ var perfStart:Float = TimerUtil.start();
var instTrack:Null = SoundUtil.buildSoundFromBytes(instTrackData);
- var perfB = haxe.Timer.stamp();
- trace('Built instrumental track in ${perfB - perfA} seconds.');
+ trace('Built instrumental track in ${TimerUtil.seconds(perfStart)} seconds.');
if (instTrack == null) return false;
stopExistingInstrumental(state);
@@ -205,10 +205,9 @@ class ChartEditorAudioHandler
{
var trackId:String = '${charId}${instId == '' ? '' : '-${instId}'}';
var vocalTrackData:Null = state.audioVocalTrackData.get(trackId);
- var perfStart = haxe.Timer.stamp();
+ var perfStart:Float = TimerUtil.start();
var vocalTrack:Null = SoundUtil.buildSoundFromBytes(vocalTrackData);
- var perfEnd = haxe.Timer.stamp();
- trace('Built vocal track in ${perfEnd - perfStart} seconds.');
+ trace('Built vocal track in ${TimerUtil.seconds(perfStart)}.');
if (state.audioVocalTrackGroup == null) state.audioVocalTrackGroup = new VoicesGroup();
@@ -219,10 +218,9 @@ class ChartEditorAudioHandler
case BF:
state.audioVocalTrackGroup.addPlayerVoice(vocalTrack);
- var perfStart = haxe.Timer.stamp();
+ var perfStart:Float = TimerUtil.start();
var waveformData:Null = vocalTrack.waveformData;
- var perfEnd = haxe.Timer.stamp();
- trace('Interpreted waveform data in ${perfEnd - perfStart} seconds.');
+ trace('Interpreted waveform data in ${TimerUtil.seconds(perfStart)}.');
if (waveformData != null)
{
@@ -246,10 +244,9 @@ class ChartEditorAudioHandler
case DAD:
state.audioVocalTrackGroup.addOpponentVoice(vocalTrack);
- var perfStart = haxe.Timer.stamp();
+ var perfStart:Float = TimerUtil.start();
var waveformData:Null = vocalTrack.waveformData;
- var perfEnd = haxe.Timer.stamp();
- trace('Interpreted waveform data in ${perfEnd - perfStart} seconds.');
+ trace('Interpreted waveform data in ${TimerUtil.seconds(perfStart)}.');
if (waveformData != null)
{
@@ -299,16 +296,14 @@ class ChartEditorAudioHandler
*/
public static function playSound(_state:ChartEditorState, path:String, volume:Float = 1.0):Void
{
- var snd:FlxSound = FlxG.sound.list.recycle(FlxSound) ?? new FlxSound();
var asset:Null = FlxG.sound.cache(path);
if (asset == null)
{
trace('WARN: Failed to play sound $path, asset not found.');
return;
}
- snd.loadEmbedded(asset);
+ var snd:FunkinSound = FunkinSound.load(asset);
snd.autoDestroy = true;
- FlxG.sound.list.add(snd);
snd.play(true);
snd.volume = volume;
}
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx
index 970f021ac..b84c68f8d 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorDialogHandler.hx
@@ -808,7 +808,8 @@ class ChartEditorDialogHandler
}
songVariationMetadataEntry.onClick = onClickMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel);
#if FILE_DROP_SUPPORTED
- addDropHandler(songVariationMetadataEntry, onDropFileMetadataVariation.bind(variation).bind(songVariationMetadataEntryLabel));
+ state.addDropHandler({component: songVariationMetadataEntry, handler: onDropFileMetadataVariation.bind(variation)
+ .bind(songVariationMetadataEntryLabel)});
#end
chartContainerB.addComponent(songVariationMetadataEntry);
@@ -832,7 +833,11 @@ class ChartEditorDialogHandler
}
songVariationChartDataEntry.onClick = onClickChartDataVariation.bind(variation).bind(songVariationChartDataEntryLabel);
#if FILE_DROP_SUPPORTED
- addDropHandler(songVariationChartDataEntry, onDropFileChartDataVariation.bind(variation).bind(songVariationChartDataEntryLabel));
+ state.addDropHandler(
+ {
+ component: songVariationChartDataEntry,
+ handler: onDropFileChartDataVariation.bind(variation).bind(songVariationChartDataEntryLabel)
+ });
#end
chartContainerB.addComponent(songVariationChartDataEntry);
}
diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx
index f32cc2bfb..8c7b1a8c1 100644
--- a/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx
+++ b/source/funkin/ui/debug/charting/handlers/ChartEditorToolboxHandler.hx
@@ -299,6 +299,15 @@ class ChartEditorToolboxHandler
state.playtestStartTime = checkboxStartTime.selected;
};
+ var checkboxBotPlay:Null = toolbox.findComponent('playtestBotPlayCheckbox', CheckBox);
+ if (checkboxBotPlay == null) throw 'ChartEditorToolboxHandler.buildToolboxPlaytestPropertiesLayout() - Could not find playtestBotPlayCheckbox component.';
+
+ checkboxBotPlay.selected = state.playtestBotPlayMode;
+
+ checkboxBotPlay.onClick = _ -> {
+ state.playtestBotPlayMode = checkboxBotPlay.selected;
+ };
+
var checkboxDebugger:Null = toolbox.findComponent('playtestDebuggerCheckbox', CheckBox);
if (checkboxDebugger == null) throw 'ChartEditorToolboxHandler.buildToolboxPlaytestPropertiesLayout() - Could not find playtestDebuggerCheckbox component.';
@@ -309,6 +318,17 @@ class ChartEditorToolboxHandler
state.enabledDebuggerPopup = checkboxDebugger.selected;
};
+ var checkboxSongScripts:Null = toolbox.findComponent('playtestSongScriptsCheckbox', CheckBox);
+
+ if (checkboxSongScripts == null)
+ throw 'ChartEditorToolboxHandler.buildToolboxPlaytestPropertiesLayout() - Could not find playtestSongScriptsCheckbox component.';
+
+ state.playtestSongScripts = checkboxSongScripts.selected;
+
+ checkboxSongScripts.onClick = _ -> {
+ state.playtestSongScripts = checkboxSongScripts.selected;
+ };
+
return toolbox;
}
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx
index 7b163ad3d..f0949846d 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorEventDataToolbox.hx
@@ -90,7 +90,7 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
// Edit the event data of any selected events.
for (event in chartEditorState.currentEventSelection)
{
- event.event = chartEditorState.eventKindToPlace;
+ event.eventKind = chartEditorState.eventKindToPlace;
event.value = chartEditorState.eventDataToPlace;
}
chartEditorState.saveDataDirty = true;
@@ -237,6 +237,11 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
{
value = event.target.value.value;
}
+ else if (field.type == BOOL)
+ {
+ var chk:CheckBox = cast event.target;
+ value = cast(chk.selected, Null); // Need to cast to nullable bool or the compiler will get mad.
+ }
trace('ChartEditorToolboxHandler.buildEventDataFormFromSchema() - ${event.target.id} = ${value}');
@@ -253,14 +258,15 @@ class ChartEditorEventDataToolbox extends ChartEditorBaseToolbox
// Edit the event data of any existing events.
if (!_initializing && chartEditorState.currentEventSelection.length > 0)
{
- for (event in chartEditorState.currentEventSelection)
+ for (songEvent in chartEditorState.currentEventSelection)
{
- event.event = chartEditorState.eventKindToPlace;
- event.value = chartEditorState.eventDataToPlace;
+ songEvent.eventKind = chartEditorState.eventKindToPlace;
+ songEvent.value = Reflect.copy(chartEditorState.eventDataToPlace);
}
chartEditorState.saveDataDirty = true;
chartEditorState.noteDisplayDirty = true;
chartEditorState.notePreviewDirty = true;
+ chartEditorState.noteTooltipsDirty = true;
}
}
}
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorFreeplayToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorFreeplayToolbox.hx
index c384e7a6d..c65781259 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorFreeplayToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorFreeplayToolbox.hx
@@ -1,21 +1,22 @@
package funkin.ui.debug.charting.toolboxes;
+import flixel.addons.display.FlxTiledSprite;
+import flixel.math.FlxMath;
import funkin.audio.SoundGroup;
+import funkin.audio.waveform.WaveformDataParser;
+import funkin.ui.debug.charting.commands.SetFreeplayPreviewCommand;
+import funkin.ui.haxeui.components.WaveformPlayer;
+import funkin.ui.freeplay.FreeplayState;
+import funkin.util.TimerUtil;
+import haxe.ui.backend.flixel.components.SpriteWrapper;
import haxe.ui.components.Button;
import haxe.ui.components.HorizontalSlider;
import haxe.ui.components.Label;
-import flixel.addons.display.FlxTiledSprite;
-import flixel.math.FlxMath;
import haxe.ui.components.NumberStepper;
import haxe.ui.components.Slider;
-import haxe.ui.backend.flixel.components.SpriteWrapper;
-import funkin.ui.debug.charting.commands.SetFreeplayPreviewCommand;
-import funkin.ui.haxeui.components.WaveformPlayer;
-import funkin.audio.waveform.WaveformDataParser;
import haxe.ui.containers.VBox;
import haxe.ui.containers.Absolute;
import haxe.ui.containers.ScrollView;
-import funkin.ui.freeplay.FreeplayState;
import haxe.ui.containers.Frame;
import haxe.ui.core.Screen;
import haxe.ui.events.DragEvent;
@@ -288,12 +289,12 @@ class ChartEditorFreeplayToolbox extends ChartEditorBaseToolbox
// Build player waveform.
// waveformMusic.waveform.forceUpdate = true;
- var perfStart = haxe.Timer.stamp();
- var waveformData1 = playerVoice.waveformData;
- var waveformData2 = opponentVoice?.waveformData ?? playerVoice.waveformData; // this null check is for songs that only have 1 vocals file!
+ var perfStart:Float = TimerUtil.start();
+ var waveformData1 = playerVoice?.waveformData;
+ var waveformData2 = opponentVoice?.waveformData ?? playerVoice?.waveformData; // this null check is for songs that only have 1 vocals file!
var waveformData3 = chartEditorState.audioInstTrack.waveformData;
- var waveformData = waveformData1.merge(waveformData2).merge(waveformData3);
- trace('Waveform data merging took: ${haxe.Timer.stamp() - perfStart} seconds');
+ var waveformData = waveformData3.merge(waveformData1).merge(waveformData2);
+ trace('Waveform data merging took: ${TimerUtil.seconds(perfStart)}');
waveformMusic.waveform.waveformData = waveformData;
// Set the width and duration to render the full waveform, with the clipRect applied we only render a segment of it.
diff --git a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx
index fd9209294..46721edfa 100644
--- a/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx
+++ b/source/funkin/ui/debug/charting/toolboxes/ChartEditorOffsetsToolbox.hx
@@ -270,24 +270,21 @@ class ChartEditorOffsetsToolbox extends ChartEditorBaseToolbox
// Build player waveform.
// waveformPlayer.waveform.forceUpdate = true;
- waveformPlayer.waveform.waveformData = playerVoice.waveformData;
+ waveformPlayer.waveform.waveformData = playerVoice?.waveformData;
// Set the width and duration to render the full waveform, with the clipRect applied we only render a segment of it.
- waveformPlayer.waveform.duration = playerVoice.length / Constants.MS_PER_SEC;
+ waveformPlayer.waveform.duration = (playerVoice?.length ?? 1000.0) / Constants.MS_PER_SEC;
// Build opponent waveform.
// waveformOpponent.waveform.forceUpdate = true;
// note: if song only has one set of vocals (Vocals.ogg/mp3) then this is null and crashes charting editor
// so we null check
- if (opponentVoice != null)
- {
- waveformOpponent.waveform.waveformData = opponentVoice.waveformData;
- waveformOpponent.waveform.duration = opponentVoice.length / Constants.MS_PER_SEC;
- }
+ waveformOpponent.waveform.waveformData = opponentVoice?.waveformData;
+ waveformOpponent.waveform.duration = (opponentVoice?.length ?? 1000.0) / Constants.MS_PER_SEC;
// Build instrumental waveform.
// waveformInstrumental.waveform.forceUpdate = true;
waveformInstrumental.waveform.waveformData = chartEditorState.audioInstTrack.waveformData;
- waveformInstrumental.waveform.duration = instTrack.length / Constants.MS_PER_SEC;
+ waveformInstrumental.waveform.duration = (instTrack?.length ?? 1000.0) / Constants.MS_PER_SEC;
addOffsetsToAudioPreview();
}
diff --git a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
index b26082f98..d2a0a053e 100644
--- a/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
+++ b/source/funkin/ui/debug/charting/util/ChartEditorDropdowns.hx
@@ -117,18 +117,32 @@ class ChartEditorDropdowns
"ugh" => "Ugh (Week 7)",
"hehPrettyGood" => "Heh, Pretty Good (Week 7)",
// Weekend 1
- "weekend-1-lightcan" => "Light Can (2hot)",
- "weekend-1-kickcan" => "Kick Can (2hot)",
- "weekend-1-kneecan" => "Knee Can (2hot)",
- "weekend-1-cockgun" => "Cock Gun (2hot)",
- "weekend-1-firegun" => "Fire Gun (2hot)",
- "weekend-1-punchlow" => "Punch Low (Blazin)",
- "weekend-1-punchhigh" => "Punch High (Blazin)",
- "weekend-1-punchlowblocked" => "Punch Low Blocked (Blazin)",
- "weekend-1-punchhighblocked" => "Punch High Blocked (Blazin)",
- "weekend-1-dodgelow" => "Dodge Low (Blazin)",
- "weekend-1-blockhigh" => "Block High (Blazin)",
- "weekend-1-fakeout" => "Fakeout (Blazin)",
+ "weekend-1-punchhigh" => "Punch High (Blazin')",
+ "weekend-1-punchhighdodged" => "Punch High (Dodge) (Blazin')",
+ "weekend-1-punchhighblocked" => "Punch High (Block) (Blazin')",
+ "weekend-1-punchhighspin" => "Punch High (Spin) (Blazin')",
+ "weekend-1-punchlow" => "Punch Low (Blazin')",
+ "weekend-1-punchlowdodged" => "Punch Low (Dodge) (Blazin')",
+ "weekend-1-punchlowblocked" => "Punch Low (Block) (Blazin')",
+ "weekend-1-punchlowspin" => "Punch High (Spin) (Blazin')",
+ "weekend-1-picouppercutprep" => "Pico Uppercut (Prep) (Blazin')",
+ "weekend-1-picouppercut" => "Pico Uppercut (Blazin')",
+ "weekend-1-blockhigh" => "Block High (Blazin')",
+ "weekend-1-blocklow" => "Block Low (Blazin')",
+ "weekend-1-blockspin" => "Block High (Spin) (Blazin')",
+ "weekend-1-dodgehigh" => "Dodge High (Blazin')",
+ "weekend-1-dodgelow" => "Dodge Low (Blazin')",
+ "weekend-1-dodgespin" => "Dodge High (Spin) (Blazin')",
+ "weekend-1-hithigh" => "Hit High (Blazin')",
+ "weekend-1-hitlow" => "Hit Low (Blazin')",
+ "weekend-1-hitspin" => "Hit High (Spin) (Blazin')",
+ "weekend-1-darnelluppercutprep" => "Darnell Uppercut (Prep) (Blazin')",
+ "weekend-1-darnelluppercut" => "Darnell Uppercut (Blazin')",
+ "weekend-1-idle" => "Idle (Blazin')",
+ "weekend-1-fakeout" => "Fakeout (Blazin')",
+ "weekend-1-taunt" => "Taunt (If Fakeout) (Blazin')",
+ "weekend-1-tauntforce" => "Taunt (Forced) (Blazin')",
+ "weekend-1-reversefakeout" => "Fakeout (Reverse) (Blazin')",
];
public static function populateDropdownWithNoteKinds(dropDown:DropDown, startingKindId:String):DropDownEntry
@@ -147,6 +161,8 @@ class ChartEditorDropdowns
dropDown.dataSource.add(value);
}
+ dropDown.dataSource.sort('id', ASCENDING);
+
return returnValue;
}
diff --git a/source/funkin/ui/debug/dialogue/ConversationDebugState.hx b/source/funkin/ui/debug/dialogue/ConversationDebugState.hx
index 33a6f365a..c2edcca5a 100644
--- a/source/funkin/ui/debug/dialogue/ConversationDebugState.hx
+++ b/source/funkin/ui/debug/dialogue/ConversationDebugState.hx
@@ -11,6 +11,7 @@ import funkin.data.dialogue.DialogueBoxData;
import funkin.data.dialogue.DialogueBoxRegistry;
import funkin.data.dialogue.SpeakerData;
import funkin.data.dialogue.SpeakerRegistry;
+import funkin.data.freeplay.AlbumRegistry;
import funkin.play.cutscene.dialogue.Conversation;
import funkin.play.cutscene.dialogue.DialogueBox;
import funkin.play.cutscene.dialogue.Speaker;
@@ -36,11 +37,24 @@ class ConversationDebugState extends MusicBeatState
public override function create():Void
{
- conversation = ConversationRegistry.instance.fetchEntry(conversationId);
- conversation.completeCallback = onConversationComplete;
- add(conversation);
+ super.create();
+ startConversation();
+ }
- ScriptEventDispatcher.callEvent(conversation, new ScriptEvent(CREATE, false));
+ function startConversation():Void
+ {
+ if (conversation != null) return;
+
+ conversation = ConversationRegistry.instance.fetchEntry(conversationId);
+ if (conversation == null) return;
+ if (!conversation.alive) conversation.revive();
+
+ conversation.zIndex = 1000;
+ add(conversation);
+ refresh();
+
+ var event:ScriptEvent = new ScriptEvent(CREATE, false);
+ ScriptEventDispatcher.callEvent(conversation, event);
}
function onConversationComplete():Void
@@ -61,15 +75,17 @@ class ConversationDebugState extends MusicBeatState
if (conversation != null)
{
- if (controls.CUTSCENE_ADVANCE) conversation.advanceConversation();
-
- if (controls.CUTSCENE_SKIP)
+ if (controls.CUTSCENE_ADVANCE)
{
- conversation.trySkipConversation(elapsed);
+ conversation.advanceConversation();
}
- else
+ else if (controls.PAUSE)
{
- conversation.trySkipConversation(-1);
+ conversation.kill();
+ remove(conversation);
+ conversation = null;
+
+ FlxG.switchState(() -> new ConversationDebugState());
}
}
}
diff --git a/source/funkin/ui/debug/stage/StageOffsetSubState.hx b/source/funkin/ui/debug/stage/StageOffsetSubState.hx
index e8a5d0a23..fa5056220 100644
--- a/source/funkin/ui/debug/stage/StageOffsetSubState.hx
+++ b/source/funkin/ui/debug/stage/StageOffsetSubState.hx
@@ -49,8 +49,11 @@ class StageOffsetSubState extends HaxeUISubState
{
super.create();
+ var playState = PlayState.instance;
+
FlxG.mouse.visible = true;
- PlayState.instance.pauseMusic();
+ playState.pauseMusic();
+ playState.cancelAllCameraTweens();
FlxG.camera.target = null;
setupUIListeners();
@@ -63,8 +66,8 @@ class StageOffsetSubState extends HaxeUISubState
// add(uiStuff);
- PlayState.instance.persistentUpdate = true;
- component.cameras = [PlayState.instance.camHUD];
+ playState.persistentUpdate = true;
+ component.cameras = [playState.camHUD];
// uiStuff.cameras = [PlayState.instance.camHUD];
// btn.cameras = [PlayState.instance.camHUD];
@@ -72,7 +75,7 @@ class StageOffsetSubState extends HaxeUISubState
var layerList:ListView = findComponent("prop-layers");
- for (thing in PlayState.instance.currentStage)
+ for (thing in playState.currentStage)
{
var prop:StageProp = cast thing;
if (prop != null && prop.name != null)
diff --git a/source/funkin/ui/freeplay/Album.hx b/source/funkin/ui/freeplay/Album.hx
new file mode 100644
index 000000000..7291c7357
--- /dev/null
+++ b/source/funkin/ui/freeplay/Album.hx
@@ -0,0 +1,89 @@
+package funkin.ui.freeplay;
+
+import funkin.data.freeplay.AlbumData;
+import funkin.data.freeplay.AlbumRegistry;
+import funkin.data.IRegistryEntry;
+import flixel.graphics.FlxGraphic;
+
+/**
+ * A class representing the data for an album as displayed in Freeplay.
+ */
+class Album implements IRegistryEntry
+{
+ /**
+ * The internal ID for this album.
+ */
+ public final id:String;
+
+ /**
+ * The full data for an album.
+ */
+ public final _data:AlbumData;
+
+ public function new(id:String)
+ {
+ this.id = id;
+ this._data = _fetchData(id);
+
+ if (_data == null)
+ {
+ throw 'Could not parse album data for id: $id';
+ }
+ }
+
+ /**
+ * Return the name of the album.
+ * @
+ */
+ public function getAlbumName():String
+ {
+ return _data.name;
+ }
+
+ /**
+ * Return the artists of the album.
+ * @return The list of artists
+ */
+ public function getAlbumArtists():Array
+ {
+ return _data.artists;
+ }
+
+ /**
+ * Get the asset key for the album art.
+ * @return The asset key
+ */
+ public function getAlbumArtAssetKey():String
+ {
+ return _data.albumArtAsset;
+ }
+
+ /**
+ * Get the album art as a graphic, ready to apply to a sprite.
+ * @return The built graphic
+ */
+ public function getAlbumArtGraphic():FlxGraphic
+ {
+ return FlxG.bitmap.add(Paths.image(getAlbumArtAssetKey()));
+ }
+
+ /**
+ * Get the asset key for the album title.
+ */
+ public function getAlbumTitleAssetKey():String
+ {
+ return _data.albumTitleAsset;
+ }
+
+ public function toString():String
+ {
+ return 'Album($id)';
+ }
+
+ public function destroy():Void {}
+
+ static function _fetchData(id:String):Null
+ {
+ return AlbumRegistry.instance.parseEntryDataWithMigration(id, AlbumRegistry.instance.fetchEntryVersion(id));
+ }
+}
diff --git a/source/funkin/ui/freeplay/AlbumRoll.hx b/source/funkin/ui/freeplay/AlbumRoll.hx
new file mode 100644
index 000000000..a1e63c9a1
--- /dev/null
+++ b/source/funkin/ui/freeplay/AlbumRoll.hx
@@ -0,0 +1,192 @@
+package funkin.ui.freeplay;
+
+import flixel.FlxSprite;
+import flixel.group.FlxSpriteGroup;
+import flixel.util.FlxSort;
+import flixel.tweens.FlxTween;
+import flixel.util.FlxTimer;
+import flixel.tweens.FlxEase;
+import funkin.data.freeplay.AlbumRegistry;
+import funkin.graphics.FunkinSprite;
+import funkin.util.SortUtil;
+import openfl.utils.Assets;
+
+/**
+ * The graphic for the album roll in the FreeplayState.
+ * Simply set `albumID` to fetch the required data and update the textures.
+ */
+class AlbumRoll extends FlxSpriteGroup
+{
+ /**
+ * The ID of the album to display.
+ * Modify this value to automatically update the album art and title.
+ */
+ public var albumId(default, set):String;
+
+ function set_albumId(value:String):String
+ {
+ if (this.albumId != value)
+ {
+ this.albumId = value;
+ updateAlbum();
+ }
+
+ return value;
+ }
+
+ var albumArt:FunkinSprite;
+ var albumTitle:FunkinSprite;
+ var difficultyStars:DifficultyStars;
+
+ var _exitMovers:Null;
+
+ var albumData:Album;
+
+ public function new()
+ {
+ super();
+
+ albumTitle = new FunkinSprite(947, 491);
+ albumTitle.visible = true;
+ albumTitle.zIndex = 200;
+ add(albumTitle);
+
+ difficultyStars = new DifficultyStars(140, 39);
+
+ difficultyStars.stars.visible = true;
+ albumTitle.visible = false;
+ // albumArtist.visible = false;
+
+ // var albumArtist:FlxSprite = new FlxSprite(1010, 607).loadGraphic(Paths.image('freeplay/albumArtist-kawaisprite'));
+ }
+
+ /**
+ * Load the album data by ID and update the textures.
+ */
+ function updateAlbum():Void
+ {
+ albumData = AlbumRegistry.instance.fetchEntry(albumId);
+
+ if (albumData == null)
+ {
+ FlxG.log.warn('Could not find album data for album ID: ${albumId}');
+
+ return;
+ };
+
+ if (albumArt != null)
+ {
+ FlxTween.cancelTweensOf(albumArt);
+ albumArt.visible = false;
+ albumArt.destroy();
+ remove(albumArt);
+ }
+
+ // Paths.animateAtlas('freeplay/albumRoll'),
+ albumArt = FunkinSprite.create(1500, 360, albumData.getAlbumArtAssetKey());
+ albumArt.setGraphicSize(262, 262); // Magic number for size IG
+ albumArt.zIndex = 100;
+
+ // playIntro();
+ add(albumArt);
+
+ applyExitMovers();
+
+ if (Assets.exists(Paths.image(albumData.getAlbumTitleAssetKey())))
+ {
+ albumTitle.loadGraphic(Paths.image(albumData.getAlbumTitleAssetKey()));
+ }
+ else
+ {
+ albumTitle.visible = false;
+ }
+
+ refresh();
+ }
+
+ public function refresh():Void
+ {
+ sort(SortUtil.byZIndex, FlxSort.ASCENDING);
+ }
+
+ /**
+ * Apply exit movers for the album roll.
+ * @param exitMovers The exit movers to apply.
+ */
+ public function applyExitMovers(?exitMovers:FreeplayState.ExitMoverData):Void
+ {
+ if (exitMovers == null)
+ {
+ exitMovers = _exitMovers;
+ }
+ else
+ {
+ _exitMovers = exitMovers;
+ }
+
+ if (exitMovers == null) return;
+
+ exitMovers.set([albumArt],
+ {
+ x: FlxG.width,
+ speed: 0.4,
+ wait: 0
+ });
+ 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
+ });
+ }
+
+ /**
+ * Play the intro animation on the album art.
+ */
+ public function playIntro():Void
+ {
+ albumArt.visible = true;
+ FlxTween.tween(albumArt, {x: 950, y: 320, angle: -340}, 0.5, {ease: FlxEase.elasticOut});
+
+ albumTitle.visible = false;
+ new FlxTimer().start(0.75, function(_) {
+ showTitle();
+ });
+ }
+
+ public function setDifficultyStars(?difficulty:Int):Void
+ {
+ if (difficulty == null) return;
+
+ difficultyStars.difficulty = difficulty;
+ }
+
+ public function showTitle():Void
+ {
+ albumTitle.visible = true;
+ }
+
+ /**
+ * Make the album stars visible.
+ */
+ public function showStars():Void
+ {
+ // albumArtist.visible = false;
+ difficultyStars.stars.visible = false;
+ }
+}
diff --git a/source/funkin/ui/freeplay/DJBoyfriend.hx b/source/funkin/ui/freeplay/DJBoyfriend.hx
index 2417cdf9a..55f43d2ef 100644
--- a/source/funkin/ui/freeplay/DJBoyfriend.hx
+++ b/source/funkin/ui/freeplay/DJBoyfriend.hx
@@ -4,7 +4,7 @@ import flixel.FlxSprite;
import flixel.util.FlxSignal;
import funkin.util.assets.FlxAnimationUtil;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
-import flixel.system.FlxSound;
+import flixel.sound.FlxSound;
import flixel.util.FlxTimer;
import funkin.audio.FlxStreamSound;
@@ -156,8 +156,6 @@ class DJBoyfriend extends FlxAtlasSprite
function setupAnimations():Void
{
- // frames = FlxAnimationUtil.combineFramesCollections(Paths.getSparrowAtlas('freeplay/bfFreeplay'), Paths.getSparrowAtlas('freeplay/bf-freeplay-afk'));
-
// animation.addByPrefix('intro', "boyfriend dj intro", 24, false);
addOffset('boyfriend dj intro', 8, 3);
diff --git a/source/funkin/ui/freeplay/FreeplayFlames.hx b/source/funkin/ui/freeplay/FreeplayFlames.hx
index a116fb813..c20d85898 100644
--- a/source/funkin/ui/freeplay/FreeplayFlames.hx
+++ b/source/funkin/ui/freeplay/FreeplayFlames.hx
@@ -23,7 +23,7 @@ class FreeplayFlames extends FlxSpriteGroup
{
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.addByPrefix("flame", "fire loop full instance 1", FlxG.random.int(23, 25), false);
flame.animation.play("flame");
flame.visible = false;
flameCount = 0;
diff --git a/source/funkin/ui/freeplay/FreeplayScore.hx b/source/funkin/ui/freeplay/FreeplayScore.hx
index e266efca1..413b182e0 100644
--- a/source/funkin/ui/freeplay/FreeplayScore.hx
+++ b/source/funkin/ui/freeplay/FreeplayScore.hx
@@ -111,7 +111,7 @@ class ScoreNum extends FlxSprite
for (i in 0...10)
{
var stringNum:String = numToString[i];
- animation.addByPrefix(stringNum, stringNum, 24, false);
+ animation.addByPrefix(stringNum, '$stringNum DIGITAL', 24, false);
}
this.digit = initDigit;
diff --git a/source/funkin/ui/freeplay/FreeplayState.hx b/source/funkin/ui/freeplay/FreeplayState.hx
index 39cab8759..fc11eec28 100644
--- a/source/funkin/ui/freeplay/FreeplayState.hx
+++ b/source/funkin/ui/freeplay/FreeplayState.hx
@@ -1,20 +1,14 @@
package funkin.ui.freeplay;
-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.group.FlxGroup;
import flixel.group.FlxGroup.FlxTypedGroup;
-import flixel.group.FlxSpriteGroup;
+import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.input.touch.FlxTouch;
import flixel.math.FlxAngle;
-import flixel.math.FlxMath;
-import funkin.graphics.FunkinCamera;
import flixel.math.FlxPoint;
import flixel.system.debug.watch.Tracker.TrackerProfile;
import flixel.text.FlxText;
@@ -23,36 +17,26 @@ import flixel.tweens.FlxTween;
import flixel.util.FlxColor;
import flixel.util.FlxSpriteUtil;
import flixel.util.FlxTimer;
+import funkin.audio.FunkinSound;
import funkin.data.level.LevelRegistry;
import funkin.data.song.SongRegistry;
-import funkin.graphics.adobeanimate.FlxAtlasSprite;
+import funkin.graphics.FunkinCamera;
+import funkin.graphics.FunkinSprite;
import funkin.graphics.shaders.AngleMask;
import funkin.graphics.shaders.HSVShader;
import funkin.graphics.shaders.PureColor;
import funkin.graphics.shaders.StrokeShader;
import funkin.input.Controls;
-import funkin.input.Controls.Control;
-import funkin.play.components.HealthIcon;
-import funkin.play.PlayState;
import funkin.play.PlayStatePlaylist;
import funkin.play.song.Song;
import funkin.save.Save;
import funkin.save.Save.SaveScoreData;
import funkin.ui.AtlasText;
-import funkin.ui.freeplay.BGScrollingText;
-import funkin.ui.freeplay.DifficultyStars;
-import funkin.ui.freeplay.DJBoyfriend;
-import funkin.ui.freeplay.FreeplayScore;
-import funkin.ui.freeplay.LetterSort;
-import funkin.ui.freeplay.SongMenuItem;
import funkin.ui.mainmenu.MainMenuState;
-import funkin.ui.MusicBeatState;
import funkin.ui.MusicBeatSubState;
import funkin.ui.transition.LoadingState;
import funkin.ui.transition.StickerSubState;
import funkin.util.MathUtil;
-import funkin.util.MathUtil;
-import lime.app.Future;
import lime.utils.Assets;
/**
@@ -63,6 +47,9 @@ typedef FreeplayStateParams =
?character:String,
};
+/**
+ * The state for the freeplay menu, allowing the player to select any song to play.
+ */
class FreeplayState extends MusicBeatSubState
{
//
@@ -118,30 +105,31 @@ class FreeplayState extends MusicBeatSubState
var grpDifficulties:FlxTypedSpriteGroup;
var coolColors:Array = [
- 0xff9271fd,
- 0xff9271fd,
- 0xff223344,
+ 0xFF9271FD,
+ 0xFF9271FD,
+ 0xFF223344,
0xFF941653,
- 0xFFfc96d7,
- 0xFFa0d1ff,
- 0xffff78bf,
- 0xfff6b604
+ 0xFFFC96D7,
+ 0xFFA0D1FF,
+ 0xFFFF78BF,
+ 0xFFF6B604
];
var grpSongs:FlxTypedGroup;
var grpCapsules:FlxTypedGroup;
var curCapsule:SongMenuItem;
var curPlaying:Bool = false;
- var ostName:FlxText;
- var difficultyStars:DifficultyStars;
var displayedVariations:Array;
var dj:DJBoyfriend;
+ var ostName:FlxText;
+ var albumRoll:AlbumRoll;
+
var letterSort:LetterSort;
var typing:FlxInputText;
- var exitMovers:Map, MoveData> = new Map();
+ var exitMovers:ExitMoverData = new Map();
var stickerSubState:StickerSubState;
@@ -177,7 +165,7 @@ class FreeplayState extends MusicBeatSubState
#if discord_rpc
// Updating Discord Rich Presence
- DiscordClient.changePresence("In the Menus", null);
+ DiscordClient.changePresence('In the Menus', null);
#end
var isDebug:Bool = false;
@@ -186,17 +174,14 @@ class FreeplayState extends MusicBeatSubState
isDebug = true;
#end
- if (FlxG.sound.music == null || (FlxG.sound.music != null && !FlxG.sound.music.playing))
- {
- FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
- }
+ FunkinSound.playMusic('freakyMenu');
// Add a null entry that represents the RANDOM option
songs.push(null);
// TODO: This makes custom variations disappear from Freeplay. Figure out a better solution later.
// Default character (BF) shows default and Erect variations. Pico shows only Pico variations.
- displayedVariations = (currentCharacter == "bf") ? [Constants.DEFAULT_VARIATION, "erect"] : [currentCharacter];
+ displayedVariations = (currentCharacter == 'bf') ? [Constants.DEFAULT_VARIATION, 'erect'] : [currentCharacter];
// programmatically adds the songs via LevelRegistry and SongRegistry
for (levelId in LevelRegistry.instance.listBaseGameLevelIds())
@@ -206,7 +191,7 @@ class FreeplayState extends MusicBeatSubState
var song:Song = SongRegistry.instance.fetchEntry(songId);
// Only display songs which actually have available charts for the current character.
- var availableDifficultiesForSong = song.listDifficulties(displayedVariations);
+ var availableDifficultiesForSong:Array = song.listDifficulties(displayedVariations);
if (availableDifficultiesForSong.length == 0) continue;
songs.push(new FreeplaySongData(levelId, songId, song, displayedVariations));
@@ -226,17 +211,17 @@ class FreeplayState extends MusicBeatSubState
trace(FlxG.camera.initialZoom);
trace(FlxCamera.defaultZoom);
- var pinkBack:FlxSprite = new FlxSprite().loadGraphic(Paths.image('freeplay/pinkBack'));
- pinkBack.color = 0xFFffd4e9; // sets it to pink!
+ var pinkBack:FunkinSprite = FunkinSprite.create('freeplay/pinkBack');
+ pinkBack.color = 0xFFFFD4E9; // sets it to pink!
pinkBack.x -= pinkBack.width;
FlxTween.tween(pinkBack, {x: 0}, 0.6, {ease: FlxEase.quartOut});
add(pinkBack);
- var orangeBackShit:FlxSprite = new FlxSprite(84, 440).makeGraphic(Std.int(pinkBack.width), 75, 0xFFfeda00);
+ var orangeBackShit:FunkinSprite = new FunkinSprite(84, 440).makeSolidColor(Std.int(pinkBack.width), 75, 0xFFFEDA00);
add(orangeBackShit);
- var alsoOrangeLOL:FlxSprite = new FlxSprite(0, orangeBackShit.y).makeGraphic(100, Std.int(orangeBackShit.height), 0xFFffd400);
+ var alsoOrangeLOL:FunkinSprite = new FunkinSprite(0, orangeBackShit.y).makeSolidColor(100, Std.int(orangeBackShit.height), 0xFFFFD400);
add(alsoOrangeLOL);
exitMovers.set([pinkBack, orangeBackShit, alsoOrangeLOL],
@@ -255,10 +240,10 @@ class FreeplayState extends MusicBeatSubState
add(grpTxtScrolls);
grpTxtScrolls.visible = false;
- FlxG.debugger.addTrackerProfile(new TrackerProfile(BGScrollingText, ["x", "y", "speed", "size"]));
+ 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;
+ var moreWays:BGScrollingText = new BGScrollingText(0, 160, 'HOT BLOODED IN MORE WAYS THAN ONE', FlxG.width, true, 43);
+ moreWays.funnyColor = 0xFFFFF383;
moreWays.speed = 6.8;
grpTxtScrolls.add(moreWays);
@@ -268,8 +253,8 @@ class FreeplayState extends MusicBeatSubState
speed: 0.4,
});
- var funnyScroll:BGScrollingText = new BGScrollingText(0, 220, "BOYFRIEND", FlxG.width / 2, false, 60);
- funnyScroll.funnyColor = 0xFFff9963;
+ var funnyScroll:BGScrollingText = new BGScrollingText(0, 220, 'BOYFRIEND', FlxG.width / 2, false, 60);
+ funnyScroll.funnyColor = 0xFFFF9963;
funnyScroll.speed = -3.8;
grpTxtScrolls.add(funnyScroll);
@@ -281,7 +266,7 @@ class FreeplayState extends MusicBeatSubState
wait: 0
});
- var txtNuts:BGScrollingText = new BGScrollingText(0, 285, "PROTECT YO NUTS", FlxG.width / 2, true, 43);
+ 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],
@@ -290,8 +275,8 @@ class FreeplayState extends MusicBeatSubState
speed: 0.4,
});
- var funnyScroll2:BGScrollingText = new BGScrollingText(0, 335, "BOYFRIEND", FlxG.width / 2, false, 60);
- funnyScroll2.funnyColor = 0xFFff9963;
+ var funnyScroll2:BGScrollingText = new BGScrollingText(0, 335, 'BOYFRIEND', FlxG.width / 2, false, 60);
+ funnyScroll2.funnyColor = 0xFFFF9963;
funnyScroll2.speed = -3.8;
grpTxtScrolls.add(funnyScroll2);
@@ -301,8 +286,8 @@ class FreeplayState extends MusicBeatSubState
speed: 0.5,
});
- var moreWays2:BGScrollingText = new BGScrollingText(0, 397, "HOT BLOODED IN MORE WAYS THAN ONE", FlxG.width, true, 43);
- moreWays2.funnyColor = 0xFFfff383;
+ var moreWays2:BGScrollingText = new BGScrollingText(0, 397, 'HOT BLOODED IN MORE WAYS THAN ONE', FlxG.width, true, 43);
+ moreWays2.funnyColor = 0xFFFFF383;
moreWays2.speed = 6.8;
grpTxtScrolls.add(moreWays2);
@@ -312,8 +297,8 @@ class FreeplayState extends MusicBeatSubState
speed: 0.4
});
- var funnyScroll3:BGScrollingText = new BGScrollingText(0, orangeBackShit.y + 10, "BOYFRIEND", FlxG.width / 2, 60);
- funnyScroll3.funnyColor = 0xFFfea400;
+ var funnyScroll3:BGScrollingText = new BGScrollingText(0, orangeBackShit.y + 10, 'BOYFRIEND', FlxG.width / 2, 60);
+ funnyScroll3.funnyColor = 0xFFFEA400;
funnyScroll3.speed = -3.8;
grpTxtScrolls.add(funnyScroll3);
@@ -329,8 +314,10 @@ class FreeplayState extends MusicBeatSubState
x: -dj.width * 1.6,
speed: 0.5
});
+
// TODO: Replace this.
- if (currentCharacter == "pico") dj.visible = false;
+ if (currentCharacter == 'pico') dj.visible = false;
+
add(dj);
var bgDad:FlxSprite = new FlxSprite(pinkBack.width * 0.75, 0).loadGraphic(Paths.image('freeplay/freeplayBGdad'));
@@ -388,62 +375,23 @@ class FreeplayState extends MusicBeatSubState
if (diffSprite.difficultyId == currentDifficulty) diffSprite.visible = true;
}
- // NOTE: This is an AtlasSprite because we use an animation to bring it into view.
- // TODO: Add the ability to select the album graphic.
- var albumArt:FlxAtlasSprite = new FlxAtlasSprite(640, 360, Paths.animateAtlas("freeplay/albumRoll"));
- albumArt.visible = false;
- add(albumArt);
+ albumRoll = new AlbumRoll();
+ albumRoll.albumId = 'volume1';
+ add(albumRoll);
- 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'));
- 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);
+ albumRoll.applyExitMovers(exitMovers);
var overhangStuff:FlxSprite = new FlxSprite().makeGraphic(FlxG.width, 64, FlxColor.BLACK);
overhangStuff.y -= overhangStuff.height;
add(overhangStuff);
FlxTween.tween(overhangStuff, {y: 0}, 0.3, {ease: FlxEase.quartOut});
- var fnfFreeplay:FlxText = new FlxText(8, 8, 0, "FREEPLAY", 48);
- fnfFreeplay.font = "VCR OSD Mono";
+ var fnfFreeplay:FlxText = new FlxText(8, 8, 0, 'FREEPLAY', 48);
+ fnfFreeplay.font = 'VCR OSD Mono';
fnfFreeplay.visible = false;
- ostName = new FlxText(8, 8, FlxG.width - 8 - 8, "OFFICIAL OST", 48);
- ostName.font = "VCR OSD Mono";
+ ostName = new FlxText(8, 8, FlxG.width - 8 - 8, 'OFFICIAL OST', 48);
+ ostName.font = 'VCR OSD Mono';
ostName.alignment = RIGHT;
ostName.visible = false;
@@ -455,21 +403,21 @@ class FreeplayState extends MusicBeatSubState
wait: 0
});
- var sillyStroke = new StrokeShader(0xFFFFFFFF, 2, 2);
+ var sillyStroke:StrokeShader = new StrokeShader(0xFFFFFFFF, 2, 2);
fnfFreeplay.shader = sillyStroke;
add(fnfFreeplay);
add(ostName);
var fnfHighscoreSpr:FlxSprite = new FlxSprite(860, 70);
fnfHighscoreSpr.frames = Paths.getSparrowAtlas('freeplay/highscore');
- fnfHighscoreSpr.animation.addByPrefix("highscore", "highscore", 24, false);
+ fnfHighscoreSpr.animation.addByPrefix('highscore', 'highscore small instance 1', 24, false);
fnfHighscoreSpr.visible = false;
fnfHighscoreSpr.setGraphicSize(0, Std.int(fnfHighscoreSpr.height * 1));
fnfHighscoreSpr.updateHitbox();
add(fnfHighscoreSpr);
new FlxTimer().start(FlxG.random.float(12, 50), function(tmr) {
- fnfHighscoreSpr.animation.play("highscore");
+ fnfHighscoreSpr.animation.play('highscore');
tmr.time = FlxG.random.float(20, 60);
}, 0);
@@ -480,7 +428,7 @@ class FreeplayState extends MusicBeatSubState
var clearBoxSprite:FlxSprite = new FlxSprite(1165, 65).loadGraphic(Paths.image('freeplay/clearBox'));
add(clearBoxSprite);
- txtCompletion = new AtlasText(1185, 87, "69", AtlasFont.FREEPLAY_CLEAR);
+ txtCompletion = new AtlasText(1185, 87, '69', AtlasFont.FREEPLAY_CLEAR);
txtCompletion.visible = false;
add(txtCompletion);
@@ -497,9 +445,9 @@ class FreeplayState extends MusicBeatSubState
letterSort.changeSelectionCallback = (str) -> {
switch (str)
{
- case "fav":
+ case 'fav':
generateSongList({filterType: FAVORITE}, true);
- case "ALL":
+ case 'ALL':
generateSongList(null, true);
default:
generateSongList({filterType: REGEXP, filterData: str}, true);
@@ -515,25 +463,20 @@ 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();
- };
+ albumRoll.playIntro();
- new FlxTimer().start(1, function(_) {
- albumTitle.visible = true;
+ new FlxTimer().start(0.75, function(_) {
+ albumRoll.showTitle();
});
new FlxTimer().start(35 / 24, function(_) {
- albumArtist.visible = true;
- difficultyStars.stars.visible = true;
+ albumRoll.showStars();
});
FlxTween.tween(grpDifficulties, {x: 90}, 0.6, {ease: FlxEase.quartOut});
- var diffSelLeft = new DifficultySelector(20, grpDifficulties.y - 10, false, controls);
- var diffSelRight = new DifficultySelector(325, grpDifficulties.y - 10, true, controls);
+ var diffSelLeft:DifficultySelector = new DifficultySelector(20, grpDifficulties.y - 10, false, controls);
+ var diffSelRight:DifficultySelector = new DifficultySelector(325, grpDifficulties.y - 10, true, controls);
add(diffSelLeft);
add(diffSelRight);
@@ -563,7 +506,7 @@ class FreeplayState extends MusicBeatSubState
});
});
- pinkBack.color = 0xFFffd863;
+ pinkBack.color = 0xFFFFD863;
bgDad.visible = true;
orangeBackShit.visible = true;
alsoOrangeLOL.visible = true;
@@ -572,9 +515,9 @@ class FreeplayState extends MusicBeatSubState
generateSongList(null, false);
- var swag:Alphabet = new Alphabet(1, 0, "swag");
+ // var swag:Alphabet = new Alphabet(1, 0, 'swag');
- var funnyCam = new FunkinCamera(0, 0, FlxG.width, FlxG.height);
+ var funnyCam:FunkinCamera = new FunkinCamera(0, 0, FlxG.width, FlxG.height);
funnyCam.bgColor = FlxColor.TRANSPARENT;
FlxG.cameras.add(funnyCam);
@@ -589,12 +532,20 @@ class FreeplayState extends MusicBeatSubState
});
}
- public function generateSongList(?filterStuff:SongFilter, force:Bool = false)
+ /**
+ * Given the current filter, rebuild the current song list.
+ *
+ * @param filterStuff A filter to apply to the song list (regex, startswith, all, favorite)
+ * @param force
+ */
+ public function generateSongList(?filterStuff:SongFilter, force:Bool = false):Void
{
curSelected = 1;
for (cap in grpCapsules.members)
+ {
cap.kill();
+ }
var tempSongs:Array = songs;
@@ -605,7 +556,7 @@ class FreeplayState extends MusicBeatSubState
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");
+ var filterRegexp:EReg = new EReg('^[' + filterStuff.filterData + '].*', 'i');
tempSongs = tempSongs.filter(str -> {
if (str == null) return true; // Random
return filterRegexp.match(str.songName);
@@ -661,14 +612,19 @@ class FreeplayState extends MusicBeatSubState
funnyMenu.favIcon.visible = tempSongs[i].isFav;
funnyMenu.hsvShader = hsvShader;
- if (i < 8) funnyMenu.initJumpIn(Math.min(i, 4), force);
+ if (i < 8)
+ {
+ funnyMenu.initJumpIn(Math.min(i, 4), force);
+ }
else
+ {
funnyMenu.forcePosition();
+ }
grpCapsules.add(funnyMenu);
}
- FlxG.console.registerFunction("changeSelection", changeSelection);
+ FlxG.console.registerFunction('changeSelection', changeSelection);
rememberSelection();
@@ -692,7 +648,7 @@ class FreeplayState extends MusicBeatSubState
var busy:Bool = false; // Set to true once the user has pressed enter to select a song.
- override function update(elapsed:Float)
+ override function update(elapsed:Float):Void
{
super.update(elapsed);
@@ -700,7 +656,7 @@ class FreeplayState extends MusicBeatSubState
{
if (songs[curSelected] != null)
{
- var realShit = curSelected;
+ var realShit:Int = curSelected;
songs[curSelected].isFav = !songs[curSelected].isFav;
if (songs[curSelected].isFav)
{
@@ -709,7 +665,7 @@ class FreeplayState extends MusicBeatSubState
ease: FlxEase.elasticOut,
onComplete: _ -> {
grpCapsules.members[realShit].favIcon.visible = true;
- grpCapsules.members[realShit].favIcon.animation.play("fav");
+ grpCapsules.members[realShit].favIcon.animation.play('fav');
}
});
}
@@ -773,9 +729,9 @@ class FreeplayState extends MusicBeatSubState
{
if (busy) return;
- var upP = controls.UI_UP_P;
- var downP = controls.UI_DOWN_P;
- var accepted = controls.ACCEPT;
+ var upP:Bool = controls.UI_UP_P;
+ var downP:Bool = controls.UI_DOWN_P;
+ var accepted:Bool = controls.ACCEPT;
if (FlxG.onMobile)
{
@@ -787,14 +743,14 @@ class FreeplayState extends MusicBeatSubState
}
if (touch.pressed)
{
- var dx = initTouchPos.x - touch.screenX;
- var dy = initTouchPos.y - touch.screenY;
+ var dx:Float = initTouchPos.x - touch.screenX;
+ var dy:Float = initTouchPos.y - touch.screenY;
- var angle = Math.atan2(dy, dx);
- var length = Math.sqrt(dx * dx + dy * dy);
+ var angle:Float = Math.atan2(dy, dx);
+ var length:Float = Math.sqrt(dx * dx + dy * dy);
- FlxG.watch.addQuick("LENGTH", length);
- FlxG.watch.addQuick("ANGLE", Math.round(FlxAngle.asDegrees(angle)));
+ FlxG.watch.addQuick('LENGTH', length);
+ FlxG.watch.addQuick('ANGLE', Math.round(FlxAngle.asDegrees(angle)));
}
}
@@ -859,9 +815,14 @@ class FreeplayState extends MusicBeatSubState
{
spamTimer = 0;
- if (controls.UI_UP) changeSelection(-1);
+ if (controls.UI_UP)
+ {
+ changeSelection(-1);
+ }
else
+ {
changeSelection(1);
+ }
}
}
else if (spamTimer >= 0.9) spamming = true;
@@ -900,16 +861,6 @@ class FreeplayState extends MusicBeatSubState
changeDiff(1);
}
- // TODO: DEBUG REMOVE THIS
- if (FlxG.keys.justPressed.P)
- {
- var newParams:FreeplayStateParams =
- {
- character: currentCharacter == "bf" ? "pico" : "bf",
- };
- openSubState(new funkin.ui.transition.StickerSubState(null, (sticker) -> new funkin.ui.freeplay.FreeplayState(newParams, sticker)));
- }
-
if (controls.BACK && !typing.hasFocus)
{
FlxTween.globalManager.clear();
@@ -975,18 +926,18 @@ class FreeplayState extends MusicBeatSubState
public override function destroy():Void
{
super.destroy();
- var daSong = songs[curSelected];
+ var daSong:Null = songs[curSelected];
if (daSong != null)
{
clearDaCache(daSong.songName);
}
}
- function changeDiff(change:Int = 0)
+ function changeDiff(change:Int = 0):Void
{
touchTimer = 0;
- var currentDifficultyIndex = diffIdsCurrent.indexOf(currentDifficulty);
+ var currentDifficultyIndex:Int = diffIdsCurrent.indexOf(currentDifficulty);
if (currentDifficultyIndex == -1) currentDifficultyIndex = diffIdsCurrent.indexOf(Constants.DEFAULT_DIFFICULTY);
@@ -997,10 +948,10 @@ class FreeplayState extends MusicBeatSubState
currentDifficulty = diffIdsCurrent[currentDifficultyIndex];
- var daSong = songs[curSelected];
+ var daSong:Null = songs[curSelected];
if (daSong != null)
{
- var songScore:SaveScoreData = Save.get().getSongScore(songs[curSelected].songId, currentDifficulty);
+ var songScore:SaveScoreData = Save.instance.getSongScore(songs[curSelected].songId, currentDifficulty);
intendedScore = songScore?.score ?? 0;
intendedCompletion = songScore?.accuracy ?? 0.0;
rememberedDifficulty = currentDifficulty;
@@ -1061,11 +1012,12 @@ class FreeplayState extends MusicBeatSubState
}
// Set the difficulty star count on the right.
- difficultyStars.difficulty = daSong?.songRating ?? difficultyStars.difficulty; // yay haxe 4.3
+ albumRoll.setDifficultyStars(daSong?.songRating);
+ albumRoll.albumId = daSong?.albumId ?? Constants.DEFAULT_ALBUM_ID;
}
// Clears the cache of songs, frees up memory, they' ll have to be loaded in later tho function clearDaCache(actualSongTho:String)
- function clearDaCache(actualSongTho:String)
+ function clearDaCache(actualSongTho:String):Void
{
for (song in songs)
{
@@ -1080,7 +1032,7 @@ class FreeplayState extends MusicBeatSubState
function capsuleOnConfirmRandom(randomCapsule:SongMenuItem):Void
{
- trace("RANDOM SELECTED");
+ trace('RANDOM SELECTED');
busy = true;
letterSort.inputEnabled = false;
@@ -1096,7 +1048,7 @@ class FreeplayState extends MusicBeatSubState
if (availableSongCapsules.length == 0)
{
- trace("No songs available!");
+ trace('No songs available!');
busy = false;
letterSort.inputEnabled = true;
FlxG.sound.play(Paths.sound('cancelMenu'));
@@ -1142,6 +1094,12 @@ class FreeplayState extends MusicBeatSubState
targetSong: targetSong,
targetDifficulty: targetDifficulty,
targetVariation: targetVariation,
+ practiceMode: false,
+ minimalMode: false,
+ // TODO: Make these an option! It's currently only accessible via chart editor.
+ // startTimestamp: 0.0,
+ // playbackRate: 0.5,
+ // botPlayMode: true,
}, true);
});
}
@@ -1162,27 +1120,26 @@ class FreeplayState extends MusicBeatSubState
}
// Set the difficulty star count on the right.
- var daSong = songs[curSelected];
- difficultyStars.difficulty = daSong?.songRating ?? 0;
+ var daSong:Null = songs[curSelected];
+ albumRoll.setDifficultyStars(daSong?.songRating ?? 0);
}
- function changeSelection(change:Int = 0)
+ function changeSelection(change:Int = 0):Void
{
- // NGio.logEvent('Fresh');
FlxG.sound.play(Paths.sound('scrollMenu'), 0.4);
// FlxG.sound.playMusic(Paths.inst(songs[curSelected].songName));
- var prevSelected = curSelected;
+ var prevSelected:Int = curSelected;
curSelected += change;
if (curSelected < 0) curSelected = grpCapsules.countLiving() - 1;
if (curSelected >= grpCapsules.countLiving()) curSelected = 0;
- var daSongCapsule = grpCapsules.members[curSelected];
+ var daSongCapsule:SongMenuItem = grpCapsules.members[curSelected];
if (daSongCapsule.songData != null)
{
- var songScore:SaveScoreData = Save.get().getSongScore(daSongCapsule.songData.songId, currentDifficulty);
+ var songScore:SaveScoreData = Save.instance.getSongScore(daSongCapsule.songData.songId, currentDifficulty);
intendedScore = songScore?.score ?? 0;
intendedCompletion = songScore?.accuracy ?? 0.0;
diffIdsCurrent = daSongCapsule.songData.songDifficulties;
@@ -1221,7 +1178,7 @@ class FreeplayState extends MusicBeatSubState
// TODO: Stream the instrumental of the selected song?
if (prevSelected == 0)
{
- FlxG.sound.playMusic(Paths.music('freakyMenu/freakyMenu'));
+ FunkinSound.playMusic('freakyMenu');
FlxG.sound.music.fadeIn(2, 0, 0.8);
}
}
@@ -1230,6 +1187,9 @@ class FreeplayState extends MusicBeatSubState
}
}
+/**
+ * The difficulty selector arrows to the left and right of the difficulty.
+ */
class DifficultySelector extends FlxSprite
{
var controls:Controls;
@@ -1242,7 +1202,7 @@ class DifficultySelector extends FlxSprite
this.controls = controls;
frames = Paths.getSparrowAtlas('freeplay/freeplaySelector');
- animation.addByPrefix('shine', "arrow pointer loop", 24);
+ animation.addByPrefix('shine', 'arrow pointer loop', 24);
animation.play('shine');
whiteShader = new PureColor(FlxColor.WHITE);
@@ -1252,7 +1212,7 @@ class DifficultySelector extends FlxSprite
flipX = flipped;
}
- override function update(elapsed:Float)
+ override function update(elapsed:Float):Void
{
if (flipX && controls.UI_RIGHT_P) moveShitDown();
if (!flipX && controls.UI_LEFT_P) moveShitDown();
@@ -1260,7 +1220,7 @@ class DifficultySelector extends FlxSprite
super.update(elapsed);
}
- function moveShitDown()
+ function moveShitDown():Void
{
offset.y -= 5;
@@ -1276,34 +1236,62 @@ class DifficultySelector extends FlxSprite
}
}
+/**
+ * Structure for the current song filter.
+ */
typedef SongFilter =
{
var filterType:FilterType;
var ?filterData:Dynamic;
}
+/**
+ * Possible types to use for the song filter.
+ */
enum abstract FilterType(String)
{
- var STARTSWITH;
- var REGEXP;
- var FAVORITE;
- var ALL;
+ /**
+ * Filter to songs which start with a string
+ */
+ public var STARTSWITH;
+
+ /**
+ * Filter to songs which match a regular expression
+ */
+ public var REGEXP;
+
+ /**
+ * Filter to songs which are favorited
+ */
+ public var FAVORITE;
+
+ /**
+ * Filter to all songs
+ */
+ public var ALL;
}
+/**
+ * Data about a specific song in the freeplay menu.
+ */
class FreeplaySongData
{
+ /**
+ * Whether or not the song has been favorited.
+ */
public var isFav:Bool = false;
var song:Song;
- public var levelId(default, null):String = "";
- public var songId(default, null):String = "";
+ public var levelId(default, null):String = '';
+ public var songId(default, null):String = '';
public var songDifficulties(default, null):Array = [];
- public var songName(default, null):String = "";
- public var songCharacter(default, null):String = "";
+ public var songName(default, null):String = '';
+ public var songCharacter(default, null):String = '';
public var songRating(default, null):Int = 0;
+ public var albumId(default, null):String = '';
public var currentDifficulty(default, set):String = Constants.DEFAULT_DIFFICULTY;
public var displayedVariations(default, null):Array = [Constants.DEFAULT_VARIATION];
@@ -1327,19 +1315,28 @@ class FreeplaySongData
updateValues(displayedVariations);
}
- function updateValues(displayedVariations:Array):Void
+ function updateValues(variations:Array):Void
{
- this.songDifficulties = song.listDifficulties(displayedVariations);
+ this.songDifficulties = song.listDifficulties(variations);
if (!this.songDifficulties.contains(currentDifficulty)) currentDifficulty = Constants.DEFAULT_DIFFICULTY;
- var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, displayedVariations);
+ var songDifficulty:SongDifficulty = song.getDifficulty(currentDifficulty, variations);
if (songDifficulty == null) return;
this.songName = songDifficulty.songName;
this.songCharacter = songDifficulty.characters.opponent;
this.songRating = songDifficulty.difficultyRating;
+ this.albumId = songDifficulty.album;
}
}
+/**
+ * The map storing information about the exit movers.
+ */
+typedef ExitMoverData = Map, MoveData>;
+
+/**
+ * The data for an exit mover.
+ */
typedef MoveData =
{
var ?x:Float;
@@ -1348,8 +1345,14 @@ typedef MoveData =
var ?wait:Float;
}
+/**
+ * The sprite for the difficulty
+ */
class DifficultySprite extends FlxSprite
{
+ /**
+ * The difficulty id which this sprite represents.
+ */
public var difficultyId:String;
public function new(diffId:String)
diff --git a/source/funkin/ui/freeplay/ScriptedAlbum.hx b/source/funkin/ui/freeplay/ScriptedAlbum.hx
new file mode 100644
index 000000000..737f97ad2
--- /dev/null
+++ b/source/funkin/ui/freeplay/ScriptedAlbum.hx
@@ -0,0 +1,9 @@
+package funkin.ui.freeplay;
+
+/**
+ * A script that can be tied to an Album.
+ * Create a scripted class that extends Album to use this.
+ * This allows you to customize how a specific album appears.
+ */
+@:hscriptClass
+class ScriptedAlbum extends funkin.ui.freeplay.Album implements polymod.hscript.HScriptedClass {}
diff --git a/source/funkin/ui/freeplay/SongMenuItem.hx b/source/funkin/ui/freeplay/SongMenuItem.hx
index 06d113468..c20d81328 100644
--- a/source/funkin/ui/freeplay/SongMenuItem.hx
+++ b/source/funkin/ui/freeplay/SongMenuItem.hx
@@ -65,25 +65,26 @@ class SongMenuItem extends FlxSpriteGroup
var rank:String = FlxG.random.getObject(ranks);
ranking = new FlxSprite(capsule.width * 0.84, 30);
- ranking.loadGraphic(Paths.image("freeplay/ranks/" + rank));
+ ranking.loadGraphic(Paths.image('freeplay/ranks/' + rank));
ranking.scale.x = ranking.scale.y = realScaled;
- ranking.alpha = 0.75;
+ // ranking.alpha = 0.75;
+ ranking.visible = false;
ranking.origin.set(capsule.origin.x - ranking.x, capsule.origin.y - ranking.y);
add(ranking);
grpHide.add(ranking);
switch (rank)
{
- case "perfect":
+ case 'perfect':
ranking.x -= 10;
}
grayscaleShader = new Grayscale(1);
- diffRatingSprite = new FlxSprite(145, 90).loadGraphic(Paths.image("freeplay/diffRatings/diff00"));
+ diffRatingSprite = new FlxSprite(145, 90).loadGraphic(Paths.image('freeplay/diffRatings/diff00'));
diffRatingSprite.shader = grayscaleShader;
- diffRatingSprite.visible = false;
- add(diffRatingSprite);
+ // TODO: Readd once ratings are fully implemented
+ // add(diffRatingSprite);
diffRatingSprite.origin.set(capsule.origin.x - diffRatingSprite.x, capsule.origin.y - diffRatingSprite.y);
grpHide.add(diffRatingSprite);
@@ -104,7 +105,7 @@ class SongMenuItem extends FlxSpriteGroup
favIcon = new FlxSprite(400, 40);
favIcon.frames = Paths.getSparrowAtlas('freeplay/favHeart');
- favIcon.animation.addByPrefix('fav', "favorite heart", 24, false);
+ favIcon.animation.addByPrefix('fav', 'favorite heart', 24, false);
favIcon.animation.play('fav');
favIcon.setGraphicSize(50, 50);
favIcon.visible = false;
@@ -114,10 +115,11 @@ class SongMenuItem extends FlxSpriteGroup
setVisibleGrp(false);
}
- function updateDifficultyRating(newRating:Int)
+ function updateDifficultyRating(newRating:Int):Void
{
var ratingPadded:String = newRating < 10 ? '0$newRating' : '$newRating';
diffRatingSprite.loadGraphic(Paths.image('freeplay/diffRatings/diff${ratingPadded}'));
+ diffRatingSprite.visible = false;
}
function set_hsvShader(value:HSVShader):HSVShader
@@ -129,7 +131,7 @@ class SongMenuItem extends FlxSpriteGroup
return value;
}
- function textAppear()
+ function textAppear():Void
{
songText.scale.x = 1.7;
songText.scale.y = 0.2;
@@ -144,7 +146,7 @@ class SongMenuItem extends FlxSpriteGroup
});
}
- function setVisibleGrp(value:Bool)
+ function setVisibleGrp(value:Bool):Void
{
for (spr in grpHide.members)
{
@@ -156,7 +158,7 @@ class SongMenuItem extends FlxSpriteGroup
updateSelected();
}
- public function init(?x:Float, ?y:Float, songData:Null