diff --git a/.gitattributes b/.gitattributes
index 94f480de94..e7a218c89d 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1 +1,3 @@
-* text=auto eol=lf
\ No newline at end of file
+* text=auto eol=lf
+*.hxc linguist-language=Haxe
+*.hxp linguist-language=Haxe
diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md
deleted file mode 100644
index efa81b56de..0000000000
--- a/.github/ISSUE_TEMPLATE/bug.md
+++ /dev/null
@@ -1,44 +0,0 @@
----
-name: Bug Report
-about: Report a bug or critical performance issue
-title: 'Bug Report: [DESCRIBE YOUR BUG IN DETAIL HERE]'
-labels: bug
----
-
-[weed]: <> (FILL THIS ISSUE THING OUT AS MUCH AS POSSIBLE)
-[weed]: <> (OR ELSE YOUR ISSUE WILL BE LESS LIKELY TO BE SOLVED!)
-[weed]: <> (DO NOT POST ABOUT ISSUES FROM OTHER FNF MOD ENGINES! I CANNOT AND PROBABLY WON'T SOLVE THOSE!)
-[weed]: <> (GO TO THEIR RESPECTIVE GITHUB ISSUES AND REPORT THEM THERE LOL!)
-
-[weed]: <> (ALSO MAKE SURE THAT YOU USE PROPER LABELS, IF YOU'RE RUNNING INTO COMPILER ISSUES, USE THE compiler issue LABEL!!!)
-
-#### Please check for duplicates or similar issues, as well performing simple troubleshooting steps (such as clearing cookies, clearing AppData, trying another browser) before submitting an issue.
-### If you are playing the game in a browser, what site are you playing it from?
-
-[weed]: <> (Put an X in the [ ] thingies to fill out checkbox!)
-[weed]: <> (something like [x] pretty much, don't screw up or you will look stupid)
-
-- [ ] [Newgrounds](https://www.newgrounds.com/portal/view/770371)
-- [ ] [Itch.io](https://ninja-muffin24.itch.io/funkin)? Specify below
-- - [ ] Windows
-- - [ ] Mac
-- - [ ] Linux
-
-### If you are playing the game in a browser, what browser are you using?
-
-[weed]: <> (Again, put an x in the [ ] box!)
-
-- [ ] Google Chrome (or chomium based like Brave, vivaldi, MS Edge)
-- [ ] Firefox
-- [ ] Safari
-
-## What version of the game are you using? Look in the bottom left corner of the main menu. (ex: 0.2.7, 0.2.1, shit like that)
-
-
-## Have you identified any steps to reproduce the bug? If so, please describe them below in as much detail as possible. Use images if possible.
-
-## Please describe your issue. Provide extensive detail and images if possible.
-
-
-
-## If you're game is FROZEN and you're playing a web version, press F12 to open up browser dev window, and go to console, and copy-paste whatever red error you're getting
diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml
new file mode 100644
index 0000000000..6be3f1245e
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug.yml
@@ -0,0 +1,62 @@
+name: Bug Report
+description: Report a bug or an issue in the game.
+labels: ["type: minor bug", "status: pending triage"]
+title: "Bug Report: "
+body:
+ - type: checkboxes
+ attributes:
+ label: Issue Checklist
+ options:
+ - label: I have properly named the issue
+ - label: I have checked the issues/discussions pages to see if the issue has been previously reported
+
+ - type: dropdown
+ attributes:
+ label: What platform are you using?
+ options:
+ - Newgrounds (Web)
+ - Itch.io (Web)
+ - Itch.io (Downloadable Build) - Windows
+ - Itch.io (Downloadable Build) - MacOS
+ - Itch.io (Downloadable Build) - Linux
+ validations:
+ required: true
+
+ - type: dropdown
+ attributes:
+ label: If you are playing on a browser, which one are you using?
+ options:
+ - Google Chrome
+ - Microsoft Edge
+ - Firefox
+ - Opera
+ - Safari
+ - Other (Specify below)
+
+ - type: input
+ attributes:
+ label: Version
+ description: What version are you using?
+ placeholder: ex. 0.4.1
+ validations:
+ required: true
+
+ - type: markdown
+ attributes:
+ value: "## Describe your bug."
+
+ - type: markdown
+ attributes:
+ value: "### Please do not report issues from other engines. These must be reported in their respective repositories."
+
+ - type: markdown
+ attributes:
+ value: "#### Provide as many details as you can."
+
+ - type: textarea
+ attributes:
+ label: Context (Provide images, videos, etc.)
+
+ - type: textarea
+ attributes:
+ label: Steps to reproduce (or crash logs, errors, etc.)
diff --git a/.github/ISSUE_TEMPLATE/compiling.md b/.github/ISSUE_TEMPLATE/compiling.md
deleted file mode 100644
index 14aea44a7c..0000000000
--- a/.github/ISSUE_TEMPLATE/compiling.md
+++ /dev/null
@@ -1,25 +0,0 @@
----
-name: Compiling help
-about: If you need help compiling the game, and you're running into issues. (Look through the 'compiling help' label in case it's been solved!)
-title: 'Compiling help: [BRIEF DESCRIPTION / ERROR MESSAGE OUTPUT]'
-labels: compiling help
----
-
-[weed]: <> (FILL THIS ISSUE THING OUT AS MUCH AS POSSIBLE)
-[weed]: <> (OR ELSE YOUR ISSUE WILL BE LESS LIKELY TO BE SOLVED!)
-[weed]: <> (DO NOT POST ABOUT ISSUES FROM OTHER FNF MOD ENGINES! I CANNOT AND PROBABLY WON'T SOLVE THOSE!)
-[weed]: <> (GO TO THEIR RESPECTIVE GITHUB ISSUES AND REPORT THEM THERE LOL!)
-
-#### Please check for duplicates or similar compiler issues by filtering for 'compiler help'
-
-[weed]: <> (Put an X in the [ ] thingies to fill out checkbox!)
-[weed]: <> (something like [x] pretty much, don't screw up or you will look stupid)
-
-
-- [ ] Windows
-- [ ] Mac
-- [ ] Linux
-- [ ] HTML5
-
-## Please describe your issue. Provide extensive detail and images if possible.
-
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000..3ba13e0cec
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1 @@
+blank_issues_enabled: false
diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml
new file mode 100644
index 0000000000..6fb7c94374
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/crash.yml
@@ -0,0 +1,70 @@
+name: Crash Report
+description: Report a crash that occurred while playing the game.
+labels: ["type: major bug", "status: pending triage"]
+title: "Crash Report: "
+body:
+ - type: checkboxes
+ attributes:
+ label: Issue Checklist
+ options:
+ - label: I have properly named the issue
+ - label: I have checked the issues/discussions pages to see if the issue has been previously reported
+
+ - type: dropdown
+ attributes:
+ label: What platform are you using?
+ options:
+ - Newgrounds (Web)
+ - Itch.io (Web)
+ - Itch.io (Downloadable Build) - Windows
+ - Itch.io (Downloadable Build) - MacOS
+ - Itch.io (Downloadable Build) - Linux
+ validations:
+ required: true
+
+ - type: dropdown
+ attributes:
+ label: If you are playing on a browser, which one are you using?
+ options:
+ - Google Chrome
+ - Microsoft Edge
+ - Firefox
+ - Opera
+ - Safari
+ - Other (Specify below)
+
+ - type: input
+ attributes:
+ label: Version
+ description: What version are you using?
+ placeholder: ex. 0.4.1
+ validations:
+ required: true
+
+ - type: markdown
+ attributes:
+ value: "## Describe your issue."
+
+ - type: markdown
+ attributes:
+ value: "### Please do not report issues from other engines. These must be reported in their respective repositories."
+
+ - type: markdown
+ attributes:
+ value: "#### Provide as many details as you can."
+
+ - type: textarea
+ attributes:
+ label: Context (Provide screenshots or videos of the crash happening)
+
+ - type: textarea
+ attributes:
+ label: Steps to reproduce
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: Crash logs (can be found in the logs folder where Funkin.exe is)
+ validations:
+ required: true
diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md
deleted file mode 100644
index e1cc3ae0df..0000000000
--- a/.github/ISSUE_TEMPLATE/enhancement.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: Enhancement
-about: Suggest a new feature
-title: 'Enhancement: '
-labels: enhancement
----
-#### Please check for duplicates or similar issues before creating this issue.
-## What is your suggestion, and why should it be implemented?
diff --git a/.github/ISSUE_TEMPLATE/enhancement.yml b/.github/ISSUE_TEMPLATE/enhancement.yml
new file mode 100644
index 0000000000..816e4a12b1
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/enhancement.yml
@@ -0,0 +1,15 @@
+name: Enhancement
+description: Suggest a new feature.
+labels: ["type: enhancement", "status: pending triage"]
+title: "Enhancement: "
+body:
+ - type: checkboxes
+ attributes:
+ label: Issue Checklist
+ options:
+ - label: I have properly named the enhancement
+ - label: I have checked the issues/discussions pages to see if the enhancement has been previously suggested
+
+ - type: textarea
+ attributes:
+ label: What is your suggestion, and why should it be implemented?
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
deleted file mode 100644
index a257c217a7..0000000000
--- a/.github/ISSUE_TEMPLATE/question.md
+++ /dev/null
@@ -1,12 +0,0 @@
----
-name: Question
-about: Ask a general question
-title: 'Question: '
-labels: question
----
-
-[weed]: <> (This isn't a place for AMA type questions, if you want to ask any of the devs something, reach out to them on twitter prob )
-[weed]: <> (any biz bullshit can go to cameron.taylor.ninja@gmail.com)
-
-#### Please check for duplicates or similar issues before asking your question.
-## What is your question?
diff --git a/.github/PULL_REQUEST_TEMPLATE/bug.md b/.github/PULL_REQUEST_TEMPLATE/bug.md
deleted file mode 100644
index 41914c5ede..0000000000
--- a/.github/PULL_REQUEST_TEMPLATE/bug.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-name: Bug Fix
-about: Fix a bug or critical performance issue
-title: 'Bug Fix: '
-labels: bug
----
-#### Please check for duplicates or similar PRs before creating this issue.
-## Does this PR close any issue(s)? If so, link them below.
-
-## Briefly describe the issue(s) fixed.
diff --git a/.github/PULL_REQUEST_TEMPLATE/enhancement.md b/.github/PULL_REQUEST_TEMPLATE/enhancement.md
deleted file mode 100644
index e208deefea..0000000000
--- a/.github/PULL_REQUEST_TEMPLATE/enhancement.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-name: Enhancement
-about: Add a new feature
-title: 'Enhancement: '
-labels: enhancement
----
-#### Please check for duplicates or similar PRs before creating this issue.
-## Does this PR close any issue(s)? If so, link them below.
-
-## What do your change(s) add, and why should they be implemented?
diff --git a/.github/actions/setup-haxe/action.yml b/.github/actions/setup-haxe/action.yml
index 5a9f7b293a..a6c21f6a96 100644
--- a/.github/actions/setup-haxe/action.yml
+++ b/.github/actions/setup-haxe/action.yml
@@ -44,7 +44,7 @@ runs:
g++ \
libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \
libgl-dev libgl1-mesa-dev \
- libasound2-dev
+ libasound2-dev libpulse-dev
ln -s /usr/lib/x86_64-linux-gnu/libffi.so.8 /usr/lib/x86_64-linux-gnu/libffi.so.6 || true
- name: Install linux-specific dependencies
if: ${{ runner.os == 'Linux' && contains(inputs.targets, 'linux') }}
@@ -56,12 +56,17 @@ runs:
shell: bash
run: |
echo "TIMER_HAXELIB=$(date +%s)" >> "$GITHUB_ENV"
- haxelib --debug --never install haxelib 4.1.0 --global
- haxelib --debug --never deleterepo || true
+ haxelib fixrepo --global || true
+ haxelib --debug --never --global install haxelib 4.1.0
+ haxelib --debug --global set haxelib 4.1.0
+ haxelib --global remove haxelib git || true
+ haxelib --global remove hmm || true
+ rm -rf .haxelib
+ haxelib --debug --never --global git haxelib https://github.com/FunkinCrew/haxelib.git funkin-patches --skip-dependencies
+ haxelib --debug --never --global git hmm https://github.com/FunkinCrew/hmm funkin-patches
haxelib --debug --never newrepo
+ haxelib version
echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV"
- haxelib --debug --never git haxelib https://github.com/HaxeFoundation/haxelib.git master
- haxelib --debug --global install hmm
echo "TIMER_DEPS=$(date +%s)" >> "$GITHUB_ENV"
- name: Restore cached dependencies
@@ -75,7 +80,12 @@ runs:
name: Prep git for dependency install
uses: gacts/run-and-post-run@v1
with:
- run: git config --global 'url.https://x-access-token:${{ inputs.gh-token }}@github.com/.insteadOf' https://github.com/
+ run: |
+ git config --global --name-only --get-regexp 'url\.https\:\/\/x-access-token:.+@github\.com\/\.insteadOf' \
+ | xargs -I {} git config --global --unset {}
+
+ git config -l --show-scope --show-origin
+ git config --global 'url.https://x-access-token:${{ inputs.gh-token }}@github.com/.insteadOf' https://github.com/
post: git config --global --unset 'url.https://x-access-token:${{ inputs.gh-token }}@github.com/.insteadOf'
- if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }}
diff --git a/.github/changed-lines-count-labeler.yml b/.github/changed-lines-count-labeler.yml
new file mode 100644
index 0000000000..6f890f5342
--- /dev/null
+++ b/.github/changed-lines-count-labeler.yml
@@ -0,0 +1,12 @@
+# Add 'small' to any changes below 10 lines
+small:
+ max: 9
+
+# Add 'medium' to any changes between 10 and 100 lines
+medium:
+ min: 10
+ max: 99
+
+# Add 'large' to any changes for more than 100 lines
+large:
+ min: 100
diff --git a/.github/labeler.yml b/.github/labeler.yml
new file mode 100644
index 0000000000..e8250b4e77
--- /dev/null
+++ b/.github/labeler.yml
@@ -0,0 +1,11 @@
+# Add Documentation tag to PR's changing markdown files, or anyhting in the docs folder
+Documentation:
+- changed-files:
+ - any-glob-to-any-file:
+ - docs/*
+ - '**/*.md'
+
+# Adds Haxe tag to PR's changing haxe code files
+Haxe:
+- changed-files:
+ - any-glob-to-any-file: '**/*.hx'
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000000..8e8a45e195
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,6 @@
+
+## Does this PR close any issues? If so, link them below.
+
+## Briefly describe the issue(s) fixed.
+
+## Include any relevant screenshots or videos.
diff --git a/.github/workflows/build-game.yml b/.github/workflows/build-game.yml
index 07802557ce..dff9a369d3 100644
--- a/.github/workflows/build-game.yml
+++ b/.github/workflows/build-game.yml
@@ -45,7 +45,11 @@ jobs:
uses: ./.github/actions/setup-haxe
with:
gh-token: ${{ steps.app_token.outputs.token }}
-
+ - name: Setup HXCPP dev commit
+ run: |
+ cd .haxelib/hxcpp/git/tools/hxcpp
+ haxe compile.hxml
+ cd ../../../../..
- name: Build game
if: ${{ matrix.target == 'windows' }}
run: |
@@ -107,7 +111,9 @@ jobs:
name: Install dependencies
run: |
git config --global 'url.https://x-access-token:${{ steps.app_token.outputs.token }}@github.com/.insteadOf' https://github.com/
+ git config --global advice.detachedHead false
haxelib --global run hmm install -q
+ cd .haxelib/hxcpp/git/tools/hxcpp && haxe compile.hxml
- if: ${{ matrix.target != 'html5' }}
name: Restore hxcpp cache
diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml
new file mode 100644
index 0000000000..a861af5781
--- /dev/null
+++ b/.github/workflows/labeler.yml
@@ -0,0 +1,27 @@
+name: "Pull Request Labeler"
+on:
+- pull_request_target
+
+jobs:
+ labeler:
+ permissions:
+ contents: read
+ pull-requests: write
+ runs-on: ubuntu-latest
+ steps:
+ - name: Set basic labels
+ uses: actions/labeler@v5
+ with:
+ sync-labels: true
+ changed-lines-count-labeler:
+ permissions:
+ contents: read
+ pull-requests: write
+ runs-on: ubuntu-latest
+ name: An action for automatically labelling pull requests based on the changed lines count
+ steps:
+ - name: Set change count labels
+ uses: vkirilichev/changed-lines-count-labeler@v0.2
+ with:
+ repo-token: ${{ secrets.GITHUB_TOKEN }}
+ configuration-path: .github/changed-lines-count-labeler.yml
diff --git a/.gitignore b/.gitignore
index 84585eee0f..ae402bdee4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,3 +13,4 @@ shitAudio/
node_modules/
package.json
package-lock.json
+.aider*
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 74f72b8261..6dc1dc0082 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -3,10 +3,17 @@
"configurations": [
{
// Launch in native/CPP on Windows/OSX/Linux
- "name": "Lime",
+ "name": "Lime Build+Debug",
"type": "lime",
"request": "launch"
},
+ {
+ // Launch in native/CPP on Windows/OSX/Linux
+ "name": "Lime Debug (No Build)",
+ "type": "lime",
+ "request": "launch",
+ "preLaunchTask": null
+ },
{
// Launch in browser
"name": "HTML5 Debug",
diff --git a/.vscode/settings.json b/.vscode/settings.json
index a8a67245b2..227cb94ec2 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -94,12 +94,12 @@
{
"label": "Windows / Debug",
"target": "windows",
- "args": ["-debug", "-DFORCE_DEBUG_VERSION"]
+ "args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Linux / Debug",
"target": "linux",
- "args": ["-debug", "-DFORCE_DEBUG_VERSION"]
+ "args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug",
@@ -109,7 +109,7 @@
{
"label": "Windows / Debug (FlxAnimate Test)",
"target": "windows",
- "args": ["-debug", "-DANIMATE", "-DFORCE_DEBUG_VERSION"]
+ "args": ["-debug", "-DANIMATE", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (FlxAnimate Test)",
@@ -119,7 +119,7 @@
{
"label": "Windows / Debug (Straight to Freeplay)",
"target": "windows",
- "args": ["-debug", "-DFREEPLAY", "-DFORCE_DEBUG_VERSION"]
+ "args": ["-debug", "-DFREEPLAY", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Freeplay)",
@@ -132,13 +132,13 @@
"args": [
"-debug",
"-DSONG=bopeebo -DDIFFICULTY=normal",
- "-DFORCE_DEBUG_VERSION"
+ "-DFEATURE_DEBUG_FUNCTIONS"
]
},
{
"label": "Windows / Debug (Straight to Play - 2hot)",
"target": "windows",
- "args": ["-debug", "-DSONG=2hot", "-DFORCE_DEBUG_VERSION"]
+ "args": ["-debug", "-DSONG=2hot", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Play - Bopeebo Normal)",
@@ -148,17 +148,22 @@
{
"label": "Windows / Debug (Conversation Test)",
"target": "windows",
- "args": ["-debug", "-DDIALOGUE", "-DFORCE_DEBUG_VERSION"]
+ "args": ["-debug", "-DDIALOGUE", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Conversation Test)",
"target": "hl",
"args": ["-debug", "-DDIALOGUE"]
},
+ {
+ "label": "Windows / Debug (Results Screen Test)",
+ "target": "windows",
+ "args": ["-debug", "-DRESULTS"]
+ },
{
"label": "Windows / Debug (Straight to Chart Editor)",
"target": "windows",
- "args": ["-debug", "-DCHARTING", "-DFORCE_DEBUG_VERSION"]
+ "args": ["-debug", "-DCHARTING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Chart Editor)",
@@ -168,12 +173,12 @@
{
"label": "Windows / Debug (Straight to Animation Editor)",
"target": "windows",
- "args": ["-debug", "-DANIMDEBUG", "-DFORCE_DEBUG_VERSION"]
+ "args": ["-debug", "-DANIMDEBUG", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Windows / Debug (Debug hxCodec)",
"target": "windows",
- "args": ["-debug", "-DHXC_LIBVLC_LOGGING", "-DFORCE_DEBUG_VERSION"]
+ "args": ["-debug", "-DHXC_LIBVLC_LOGGING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Animation Editor)",
@@ -183,7 +188,7 @@
{
"label": "Windows / Debug (Latency Test)",
"target": "windows",
- "args": ["-debug", "-DLATENCY", "-DFORCE_DEBUG_VERSION"]
+ "args": ["-debug", "-DLATENCY", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Latency Test)",
@@ -193,7 +198,7 @@
{
"label": "Windows / Debug (Waveform Test)",
"target": "windows",
- "args": ["-debug", "-DWAVEFORM", "-DFORCE_DEBUG_VERSION"]
+ "args": ["-debug", "-DWAVEFORM", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Windows / Release",
@@ -213,17 +218,17 @@
{
"label": "HTML5 / Debug",
"target": "html5",
- "args": ["-debug", "-DFORCE_DEBUG_VERSION"]
+ "args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HTML5 / Debug (Watch)",
"target": "html5",
- "args": ["-debug", "-watch", "-DFORCE_DEBUG_VERSION"]
+ "args": ["-debug", "-watch", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "macOS / Debug",
"target": "mac",
- "args": ["-debug", "-DFORCE_DEBUG_VERSION"]
+ "args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "macOS / Release",
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a852ca82d0..a2031ba24c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,11 +4,171 @@ All notable changes will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [0.5.0] - 2024-09-12
+### Added
+- Added a new Character Select screen to switch between playable characters in Freeplay
+ - Modding isn't 100% there but we're working on it!
+- Added Pico as a playable character! Unlock him by completing Weekend 1 (if you haven't already done that)
+ - The songs from Weekend 1 have moved; you must now switch to Pico in Freeplay to access them
+- Added 10 new Pico remixes! Access them by selecting Pico from in the Character Select screen
+ - Bopeebo (Pico Mix)
+ - Fresh (Pico Mix)
+ - DadBattle (Pico Mix)
+ - Spookeez (Pico Mix)
+ - South (Pico Mix)
+ - Philly Nice (Pico Mix)
+ - Blammed (Pico Mix)
+ - Eggnog (Pico Mix)
+ - Ugh (Pico Mix)
+ - Guns (Pico Mix)
+- Added 1 new Boyfriend remix! Access it by selecting Pico from in the Character Select screen
+ - Darnell (BF Mix)
+- Added 2 new Erect remixes! Access them by switching difficulty on the song
+ - Cocoa Erect
+ - Ugh Erect
+- Implemented support for a new Instrumental Selector in Freeplay
+ - Beating a Pico remix lets you use that instrumental when playing as Boyfriend
+- Added the first batch of Erect Stages! These graphical overhauls of the original stages will be used when playing Erect remixes and Pico remixes
+ - Week 1 Erect Stage
+ - Week 2 Erect Stage
+ - Week 3 Erect Stage
+ - Week 4 Erect Stage
+ - Week 5 Erect Stage
+ - Weekend 1 Erect Stage
+- Implemented alternate animations and music for Pico in the results screen.
+ - These display on Pico remixes, as well as when playing Weekend 1.
+- Implemented support for scripted Note Kinds. You can use HScript define a different note style to display for these notes as well as custom behavior. (community feature by lemz1)
+- Implemented support for Numeric and Selector options in the Options menu. (community feature by FlooferLand)
+## Changed
+- Girlfriend and Nene now perform previously unused animations when you achieve a large combo, or drop a large combo.
+- The pixel character icons in the Freeplay menu now display an animation!
+- Altered how Week 6 displays sprites to make things look more retro.
+- Character offsets are now independent of the character's scale.
+ - This should resolve issues with offsets when porting characters from older mods.
+ - Pixel character offsets have been modified to compensate.
+- Note style data can now specify custom combo count graphics, judgement graphics, countdown graphics, and countdown audio. (community feature by anysad)
+ - These were previously using hardcoded values based on whether the stage was `school` or `schoolEvil`.
+- The `danceEvery` property of characters and stage props can now use values with a precision of `0.25`, to play their idle animation up to four times per beat.
+- Reworked the JSON merging system in Polymod; you can now include JSONPatch files under `_merge` in your mod folder to add, modify, or remove values in a JSON without replacing it entirely!
+- Cutscenes now automatically pause when tabbing out (community fix by AbnormalPoof)
+- Characters will now respect the `danceEvery` property (community fix by gamerbross)
+- The F5 function now reloads the current song's chart data from disc (community feature by gamerbross)
+- Refactored the compilation guide and added common troubleshooting steps (community fix by Hundrec)
+- Made several layout improvements and fixes to the Animation Offsets editor in the Debug menu (community fix by gamerbross)
+- Fixed a bug where the Back sound would be not played when leaving the Story menu and Options menu (community fix by AppleHair)
+- Animation offsets no longer directly modify the `x` and `y` position of props, which makes props work better with tweens (community fix by Sword352)
+- The YEAH! events in Tutorial now use chart events rather than being hard-coded (community fix by anysad)
+- The player's Score now displays commas in it (community fix by loggo)
+## Fixed
+- Fixed an issue where songs with no notes would crash on the Results screen.
+- Fixed an issue where the old icon easter egg would not work properly on pixel levels.
+- Fixed an issue where you could play notes during the Thorns cutscene.
+- Fixed an issue where the Heart icon when favoriting a song in Freeplay would be malformed.
+- Fixed an issue where Pico's death animation displays a faint blue background (community fix by doggogit)
+- Fixed an issue where mod songs would not play a preview in the Freeplay menu (community fix by KarimAkra)
+- Fixed an issue where the Memory Usage counter could overflow and display a negative number (community fix by KarimAkra)
+- Fixed an issue where pressing the Chart Editor keybind while playtesting a chart would reset the chart editor (community fix by gamerbross)
+- Fixed a crash bug when pressing F5 after seeing the sticker transition (community fix by gamerbross)
+- Fixed an issue where the Story Mode menu couldn't be scrolled with a mouse (community fix by JVNpixels)
+- Fixed an issue causing the song to majorly desync sometimes (community fix by Burgerballs)
+- Fixed an issue where the Freeplay song preview would not respect the instrumental ID specified in the song metadata (community fix by AppleHair)
+- Fixed an issue where Tankman's icon wouldn't display in the Chart Editor (community fix by hundrec)
+- Fixed an issue where pausing the game during a camera zoom would zoom the pause menu. (community fix by gamerbros)
+- Fixed an issue where certain UI elements would not flash at a consistent rate (community fix by cyn0x8)
+- Fixed an issue where the game would not use the placeholder health icon as a fallback (community fix by gamerbross)
+- Fixed an issue where the chart editor could get stuck creating a hold note when using Live Inputs (community fix by gamerbross)
+- Fixed an issue where character graphics could not be placed in week folders (community fix by 7oltan)
+- Fixed a crash issue when a Freeplay song has no `Normal` difficulty (community fix by Applehair and gamerbross)
+- Fixed an issue in Story Mode where a song that isn't valid for the current variation could be selected (community fix by Applehair)
+
+## [0.4.1] - 2024-06-12
+### Added
+- Pressing ESCAPE on the title screen on desktop now exits the game, allowing you to exit the game while in fullscreen on desktop
+- Freeplay menu controls (favoriting and switching categories) are now rebindable from the Options menu, and now have default binds on controllers.
+### Changed
+- Highscores and ranks are now saved separately, which fixes the issue where people would overwrite their saves with higher scores,
+which would remove their rank if they had a lower one.
+- A-Bot speaker now reacts to the user's volume preference on desktop (thanks to [M7theguy for the issue report/suggestion](https://github.com/FunkinCrew/Funkin/issues/2744)!)
+- On Freeplay, heart icons are shifted to the right when you favorite a song that has no rank on it.
+- Only play `scrollMenu` sound effect when there's a real change on the freeplay menu ([thanks gamerbross for the PR!](https://github.com/FunkinCrew/Funkin/pull/2741))
+- Gave antialiasing to the edge of the dad graphic on Freeplay
+- Rearranged some controls in the controls menu
+- Made several chart revisions
+ - Re-enabled custom camera events in Roses (Erect/Nightmare)
+ - Tweaked the chart for Lit Up (Hard)
+ - Corrected the difficulty ratings for M.I.L.F. (Easy/Normal/Hard)
+### Fixed
+- Fixed an issue in the controls menu where some control binds would overlap their names
+- Fixed crash when attempting to exit the gameover screen when also attempting to retry the song ([thanks DMMaster636 for the PR!](https://github.com/FunkinCrew/Funkin/pull/2709))
+- Fix botplay sustain release bug ([thanks Hundrec!](Fix botplay sustain release bug #2683))
+- Fix for the camera not pausing during a gameplay pause ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2684))
+- Fixed issue where Pico's gameplay sprite would unintentionally appear on the gameover screen when dying on 2Hot from an explosion
+- Freeplay previews properly fade volume during the BF idle animation
+- Fixed bug where Dadbattle incorrectly appeared as Dadbattle Erect when returning to freeplay on Hard
+- Fixed 2Hot not appearing under the "#" category in Freeplay menu
+- Fixed a bug where the Chart Editor would crash when attempting to select an event with the Event toolbox open
+- Improved offsets for Pico and Tankman opponents so they don't slide around as much.
+- Fixed the black "temp" graphic on freeplay from being incorrectly sized / masked, now it's identical to the dad freeplay graphic
+
+## [0.4.0] - 2024-06-06
+### Added
+- 2 new Erect remixes, Eggnog and Satin Panties. Check them out from the Freeplay menu!
+- Major visual improvements to the Results screen, with additional animations and audio based on your performance.
+- Major visual improvements to the Freeplay screen, with song difficulty ratings and player rank displays.
+ - Freeplay now plays a preview of songs when you hover over them.
+- Added a Charter field to the chart format, to allow for crediting the creator of a level's chart.
+ - You can see who charted a song from the Pause menu.
+- Added a new Scroll Speed chart event to change the note speed mid-song (thanks burgerballs!)
+### Changed
+- Tweaked the charts for several songs:
+ - Tutorial (increased the note speed slightly)
+ - Spookeez
+ - Monster
+ - Winter Horrorland
+ - M.I.L.F.
+ - Senpai (increased the note speed)
+ - Roses
+ - Thorns (increased the note speed slightly)
+ - Ugh
+ - Stress
+ - Lit Up
+- Favorite songs marked in Freeplay are now stored between sessions.
+- The Freeplay easter eggs are now easier to see.
+- In the event that the game cannot load your save data, it will now perform a backup before clearing it, so that we can try to repair it in the future.
+- Custom note styles are now properly supported for songs; add new notestyles via JSON, then select it for use from the Chart Editor Metadata toolbox. (thanks Keoiki!)
+- Health icons now support a Winning frame without requiring a spritesheet, simply include a third frame in the icon file. (thanks gamerbross!)
+ - Remember that for more complex behaviors such as animations or transitions, you should use an XML file to define each frame.
+- Improved the Event Toolbox in the Chart Editor; dropdowns are now bigger, include search field, and display elements in alphabetical order rather than a random order.
+### Fixed
+- Fixed an issue where Nene's visualizer would not play on Desktop builds
+- Fixed a bug where the game would silently fail to load saves on HTML5
+- Fixed some bugs with the props on the Story Menu not bopping properly
+- Additional fixes to the Loading bar on HTML5 (thanks lemz1!)
+- Fixed several bugs with the TitleState, including missing music when returning from the Main Menu (thanks gamerbross!)
+- Fixed a camera bug in the Main Menu (thanks richTrash21!)
+- Fixed a bug where changing difficulties in Story mode wouldn't update the score (thanks sectorA!)
+- Fixed a crash in Freeplay caused by a level referencing an invalid song (thanks gamerbross!)
+- Fixed a bug where pressing the volume keys would stop the Toy commercial (thanks gamerbross!)
+- Fixed a bug where the Chart Editor Playtest would crash when losing (thanks gamerbross!)
+- Fixed a bug where hold notes would display improperly in the Chart Editor when downscroll was enabled for gameplay (thanks gamerbross!)
+- Fixed a bug where hold notes would be positioned wrong on downscroll (thanks MaybeMaru!)
+- Removed a large number of unused imports to optimize builds (thanks Ethan-makes-music!)
+- Improved debug logging for unscripted stages (thanks gamerbross!)
+- Made improvements to compiling documentation (thanks gedehari!)
+- Fixed a crash on Linux caused by an old version of hxCodec (thanks Noobz4Life!)
+- Optimized animation handling for characters (thanks richTrash21!)
+- Made improvements to compiling documentation (thanks gedehari!)
+- Fixed an issue where the Chart Editor would use an incorrect instrumental on imported Legacy songs (thanks gamerbross!)
+- Fixed a camera bug in the Main Menu (thanks richTrash21!)
+- Fixed a bug where opening the game from the command line would crash the preloader (thanks NotHyper474!)
+- Fixed a bug where characters would sometimes use the wrong scale value (thanks PurSnake!)
+- Additional bug fixes and optimizations.
+
## [0.3.3] - 2024-05-14
### Changed
- Cleaned up some code in `PlayAnimationSongEvent.hx` (thanks BurgerBalls!)
### Fixed
-- Fix Web Loading Bar (thanks lemz1!)
+- Fixes to the Loading bar on HTML5 (thanks lemz1!)
- Don't allow any more inputs when exiting freeplay (thanks gamerbros!)
- Fixed using mouse wheel to scroll on freeplay (thanks JugieNoob!)
- Fixed the reset's of the health icons, score, and notes when re-entering gameplay from gameover (thanks ImCodist!)
@@ -16,11 +176,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed camera stutter once a wipe transition to the Main Menu completes (thanks ImCodist!)
- Fixed an issue where hold note would be invisible for a single frame (thanks ImCodist!)
- Fix tween accumulation on title screen when pressing Y multiple times (thanks TheGaloXx!)
-- Fix for a game over easter egg so you don't accidentally exit it when viewing
- Fix a crash when querying FlxG.state in the crash handler
+- Fix for a game over easter egg so you don't accidentally exit it when viewing
- Fix an issue where the Freeplay menu never displays 100% clear
+- Fix an issue where Weekend 1 Pico attempted to retrieve a missing asset.
+- Fix an issue where duplicate keybinds would be stoed, potentially causing a crash
- Chart debug key now properly returns you to the previous chart editor session if you were playtesting a chart (thanks nebulazorua!)
-- Hopefully fixed Freeplay crashes on AMD gpu's
+- Fix a crash on Freeplay found on AMD graphics cards
## [0.3.2] - 2024-05-03
### Added
diff --git a/Project.xml b/Project.xml
deleted file mode 100644
index 24cdac2700..0000000000
--- a/Project.xml
+++ /dev/null
@@ -1,265 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -->
- -->
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/README.md b/README.md
index 62794b9240..b1e16f6de6 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
# Friday Night Funkin'
-Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for Ludem Dare 47.
+Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for Ludum Dare 47.
-This game was made with love to Newgrounds and it's community. Extra love to Tom Fulp.
+This game was made with love to Newgrounds and its community. Extra love to Tom Fulp.
- [Playable web demo on Newgrounds!](https://www.newgrounds.com/portal/view/770371)
- [Demo download builds for Windows, Mac, and Linux from Itch.io!](https://ninja-muffin24.itch.io/funkin)
@@ -23,7 +23,7 @@ Full credits can be found in-game, or wherever the credits.json file is.
## Programming
- [ninjamuffin99](https://twitter.com/ninja_muffin99) - Lead Programmer
-- [MasterEric](https://twitter.com/EliteMasterEric) - Programmer
+- [EliteMasterEric](https://twitter.com/EliteMasterEric) - Programmer
- [MtH](https://twitter.com/emmnyaa) - Charting and Additional Programming
- [GeoKureli](https://twitter.com/Geokureli/) - Additional Programming
- Our contributors on GitHub
diff --git a/art b/art
index 66572f85d8..bfca2ea98d 160000
--- a/art
+++ b/art
@@ -1 +1 @@
-Subproject commit 66572f85d826ce2ec1d45468c12733b161237ffa
+Subproject commit bfca2ea98d11a0f4dee4a27b9390951fbc5701ea
diff --git a/assets b/assets
index 783f22e741..bc7009b424 160000
--- a/assets
+++ b/assets
@@ -1 +1 @@
-Subproject commit 783f22e741c85223da7f3f815b28fc4c6f240cbc
+Subproject commit bc7009b4242691faa5c4552f7ca8a2f28e8cb1d2
diff --git a/build/Dockerfile b/build/Dockerfile
index c545d1364b..3188701662 100644
--- a/build/Dockerfile
+++ b/build/Dockerfile
@@ -83,7 +83,7 @@ apt-fast install -y --no-install-recommends \
libc6-dev libffi-dev \
libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \
libgl-dev libgl1-mesa-dev \
- libasound2-dev \
+ libasound2-dev libpulse-dev \
libvlc-dev libvlccore-dev
EOF
@@ -137,8 +137,8 @@ ENV PATH="$HAXEPATH:$PATH"
RUN <` and `lime rebuild -debug`
+10. `lime test ` to build and launch the game for your platform (for example, `lime test windows`)
-# Troubleshooting
-
-While performing the process of compilation, you may experience one of the following issues:
-
-## PolymodHandler: extra field coreAssetRedirect
-
-```
-Installing funkin.vis from https://github.com/FunkinCrew/funkVis branch: 98c9db09f0bbfedfe67a84538a5814aaef80bdea
-Error: std@sys_remove_dir
-Execution error: command "haxelib --never git funkin.vis https://github.com/FunkinCrew/funkVis 98c9db09f0bbfedfe67a84538a5814aaef80bdea" failed with status: 1 in cwd
-```
-
-If you receive this error, you are on an outdated version of Polymod.
-
-To solve, you should try reinstalling Polymod:
-
-```
-haxelib run hmm reinstall --force polymod
-```
-
-You can also try deleting your `.haxelib` folder in your Funkin' project, then reinstalling all your Haxelibs to prevent any other errors:
-
-```
-rm -rf ./.haxelib
-haxelib run hmm reinstall --force
-```
+## Build Flags
-## PolymodHandler: Couldn't find a match for this asset library: (vlc)
+There are several useful build flags you can add to a build to affect how it works. A full list can be found in `project.hxp`, but here's information on some of them:
-```
-source/funkin/modding/PolymodErrorHandler.hx:84: [ERROR] Your Lime/OpenFL configuration is using custom asset libraries, and you provided frameworkParams in Polymod.init(), but we couldn't find a match for this asset library: (vlc)
-source/funkin/modding/PolymodHandler.hx:158: An error occurred! Failed when loading mods!
-source/funkin/util/logging/CrashHandler.hx:62: Error while handling crash: Null Object Reference
-```
+- `-debug` to build the game in debug mode. This automatically enables several useful debug features.
+ - This includes enabling in-game debug functions, disables compile-time optimizations, enabling asset redirection (see below), and enabling the VSCode debug server (which can slow the game on some machines but allows for powerful debugging through breakpoints).
+ - `-DGITHUB_BUILD` will enable in-game debug functions (such as the ability to time travel in a song by pressing `PgUp`/`PgDn`), without enabling the other stuff
+- `-DFEATURE_POLYMOD_MODS` or `-DNO_FEATURE_POLYMOD_MODS` to forcibly enable or disable modding support.
+- `-DREDIRECT_ASSETS_FOLDER` or `-DNO_REDIRECT_ASSETS_FOLDER` to forcibly enable or disable asset redirection.
+ - This feature causes the game to load exported assets from the project's assets folder rather than the exported one. Great for fast iteration, but the game
+- `-DFEATURE_DISCORD_RPC` or `-DNO_FEATURE_DISCORD_RPC` to forcibly enable or disable support for Discord Rich Presence.
+- `-DFEATURE_VIDEO_PLAYBACK` or `-DNO_FEATURE_VIDEO_PLAYBACK` to forcibly enable or disable video cutscene support.
+- `-DFEATURE_CHART_EDITOR` or `-DNO_FEATURE_CHART_EDITOR` to forcibly enable or disable the chart editor in the Debug menu.
+- `-DFEATURE_STAGE_EDITOR` to forcibly enable the experimental stage editor.
+- `-DFEATURE_GHOST_TAPPING` to forcibly enable an experimental gameplay change to the anti-mash system.
-This error is specific to Linux targets. If you receive this error, you are on an outdated verison of hxCodec.
-
-To solve, you should try reinstalling hxCodec:
-
-```
-haxelib run hmm reinstall --force hxCodec
-```
-
-You can also try deleting your `.haxelib` folder in your Funkin' project, then reinstalling all your Haxelibs to prevent any other errors:
-
-```
-rm -rf ./.haxelib
-haxelib run hmm reinstall --force
-```
-
-## Git: stream 0 was not closed cleanly: PROTOCOL_ERROR
-
-```
-error: RPC failed; curl 92 HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1)
-```
-
-If you receive this error while cloning, you may be experiencing issues with your network connection.
-
-To solve, you should try modifying your git configuration before cloning again:
+# Troubleshooting
-```
-git config --global http.postBuffer 4096M
-```
+If you experience any issues during the compilation process, DO NOT open an issue on GitHub. Instead, check the [Troubleshooting Guide](TROUBLESHOOTING.md) for steps on how to resolve common problems.
diff --git a/example_mods/introMod/_polymod_meta.json b/example_mods/introMod/_polymod_meta.json
index e0b03f1cd3..4dc0cd8044 100644
--- a/example_mods/introMod/_polymod_meta.json
+++ b/example_mods/introMod/_polymod_meta.json
@@ -3,7 +3,7 @@
"description": "An introductory mod.",
"contributors": [
{
- "name": "MasterEric"
+ "name": "EliteMasterEric"
}
],
"api_version": "0.1.0",
diff --git a/example_mods/testing123/_polymod_meta.json b/example_mods/testing123/_polymod_meta.json
index 4c0f177f9b..0a2ed042c6 100644
--- a/example_mods/testing123/_polymod_meta.json
+++ b/example_mods/testing123/_polymod_meta.json
@@ -3,7 +3,7 @@
"description": "Newgrounds? More like OLDGROUNDS lol.",
"contributors": [
{
- "name": "MasterEric"
+ "name": "EliteMasterEric"
}
],
"api_version": "0.1.0",
diff --git a/hmm.json b/hmm.json
index 288aa80b8b..d967a69b31 100644
--- a/hmm.json
+++ b/hmm.json
@@ -1,44 +1,53 @@
{
"dependencies": [
+ {
+ "name": "FlxPartialSound",
+ "type": "git",
+ "dir": null,
+ "ref": "a1eab7b9bf507b87200a3341719054fe427f3b15",
+ "url": "https://github.com/FunkinCrew/FlxPartialSound.git"
+ },
{
"name": "discord_rpc",
"type": "git",
"dir": null,
"ref": "2d83fa863ef0c1eace5f1cf67c3ac315d1a3a8a5",
- "url": "https://github.com/Aidan63/linc_discord-rpc"
+ "url": "https://github.com/FunkinCrew/linc_discord-rpc"
},
{
"name": "flixel",
"type": "git",
"dir": null,
- "ref": "a7d8e3bad89a0a3506a4714121f73d8e34522c49",
+ "ref": "f2b090d6c608471e730b051c8ee22b8b378964b1",
"url": "https://github.com/FunkinCrew/flixel"
},
{
"name": "flixel-addons",
"type": "git",
"dir": null,
- "ref": "a523c3b56622f0640933944171efed46929e360e",
+ "ref": "9c6fb47968e894eb36bf10e94725cd7640c49281",
"url": "https://github.com/FunkinCrew/flixel-addons"
},
{
"name": "flixel-text-input",
- "type": "haxelib",
- "version": "1.1.0"
+ "type": "git",
+ "dir": null,
+ "ref": "951a0103a17bfa55eed86703ce50b4fb0d7590bc",
+ "url": "https://github.com/FunkinCrew/flixel-text-input"
},
{
"name": "flixel-ui",
"type": "git",
"dir": null,
- "ref": "719b4f10d94186ed55f6fef1b6618d32abec8c15",
+ "ref": "27f1ba626f80a6282fa8a187115e79a4a2133dc2",
"url": "https://github.com/HaxeFlixel/flixel-ui"
},
{
"name": "flxanimate",
"type": "git",
"dir": null,
- "ref": "17e0d59fdbc2b6283a5c0e4df41f1c7f27b71c49",
- "url": "https://github.com/FunkinCrew/flxanimate"
+ "ref": "0654797e5eb7cd7de0c1b2dbaa1efe5a1e1d9412",
+ "url": "https://github.com/Dot-Stuff/flxanimate"
},
{
"name": "format",
@@ -49,9 +58,16 @@
"name": "funkin.vis",
"type": "git",
"dir": null,
- "ref": "2aa654b974507ab51ab1724d2d97e75726fd7d78",
+ "ref": "22b1ce089dd924f15cdc4632397ef3504d464e90",
"url": "https://github.com/FunkinCrew/funkVis"
},
+ {
+ "name": "grig.audio",
+ "type": "git",
+ "dir": "src",
+ "ref": "57f5d47f2533fd0c3dcd025a86cb86c0dfa0b6d2",
+ "url": "https://gitlab.com/haxe-grig/grig.audio.git"
+ },
{
"name": "hamcrest",
"type": "haxelib",
@@ -61,20 +77,22 @@
"name": "haxeui-core",
"type": "git",
"dir": null,
- "ref": "0212d8fdfcafeb5f0d5a41e1ddba8ff21d0e183b",
+ "ref": "22f7c5a8ffca90d4677cffd6e570f53761709fbc",
"url": "https://github.com/haxeui/haxeui-core"
},
{
"name": "haxeui-flixel",
"type": "git",
"dir": null,
- "ref": "63a906a6148958dbfde8c7b48d90b0693767fd95",
+ "ref": "28bb710d0ae5d94b5108787593052165be43b980",
"url": "https://github.com/haxeui/haxeui-flixel"
},
{
"name": "hscript",
- "type": "haxelib",
- "version": "2.5.0"
+ "type": "git",
+ "dir": null,
+ "ref": "12785398e2f07082f05034cb580682e5671442a2",
+ "url": "https://github.com/FunkinCrew/hscript"
},
{
"name": "hxCodec",
@@ -85,8 +103,10 @@
},
{
"name": "hxcpp",
- "type": "haxelib",
- "version": "4.3.2"
+ "type": "git",
+ "dir": null,
+ "ref": "904ea40643b050a5a154c5e4c33a83fd2aec18b1",
+ "url": "https://github.com/HaxeFoundation/hxcpp"
},
{
"name": "hxcpp-debug-server",
@@ -95,10 +115,17 @@
"ref": "147294123f983e35f50a966741474438069a7a8f",
"url": "https://github.com/FunkinCrew/hxcpp-debugger"
},
+ {
+ "name": "hxjsonast",
+ "type": "git",
+ "dir": null,
+ "ref": "20e72cc68c823496359775ac1f06500e67f189d5",
+ "url": "https://github.com/nadako/hxjsonast/"
+ },
{
"name": "hxp",
"type": "haxelib",
- "version": "1.2.2"
+ "version": "1.3.0"
},
{
"name": "json2object",
@@ -107,11 +134,25 @@
"ref": "a8c26f18463c98da32f744c214fe02273e1823fa",
"url": "https://github.com/FunkinCrew/json2object"
},
+ {
+ "name": "jsonpatch",
+ "type": "git",
+ "dir": null,
+ "ref": "f9b83215acd586dc28754b4ae7f69d4c06c3b4d3",
+ "url": "https://github.com/EliteMasterEric/jsonpatch"
+ },
+ {
+ "name": "jsonpath",
+ "type": "git",
+ "dir": null,
+ "ref": "7a24193717b36393458c15c0435bb7c4470ecdda",
+ "url": "https://github.com/EliteMasterEric/jsonpath"
+ },
{
"name": "lime",
"type": "git",
"dir": null,
- "ref": "872ff6db2f2d27c0243d4ff76802121ded550dd7",
+ "ref": "fe3368f611a84a19afc03011353945ae4da8fffd",
"url": "https://github.com/FunkinCrew/lime"
},
{
@@ -146,29 +187,29 @@
"name": "openfl",
"type": "git",
"dir": null,
- "ref": "228c1b5063911e2ad75cef6e3168ef0a4b9f9134",
+ "ref": "8306425c497766739510ab29e876059c96f77bd2",
"url": "https://github.com/FunkinCrew/openfl"
},
{
"name": "polymod",
"type": "git",
"dir": null,
- "ref": "8553b800965f225bb14c7ab8f04bfa9cdec362ac",
+ "ref": "0fbdf27fe124549730accd540cec8a183f8652c0",
"url": "https://github.com/larsiusprime/polymod"
},
{
"name": "thx.core",
"type": "git",
"dir": null,
- "ref": "22605ff44f01971d599641790d6bae4869f7d9f4",
- "url": "https://github.com/FunkinCrew/thx.core"
+ "ref": "76d87418fadd92eb8e1b61f004cff27d656e53dd",
+ "url": "https://github.com/fponticelli/thx.core"
},
{
"name": "thx.semver",
"type": "git",
"dir": null,
- "ref": "cf8d213589a2c7ce4a59b0fdba9e8ff36bc029fa",
- "url": "https://github.com/FunkinCrew/thx.semver"
+ "ref": "bdb191fe7cf745c02a980749906dbf22719e200b",
+ "url": "https://github.com/fponticelli/thx.semver"
}
]
}
diff --git a/project.hxp b/project.hxp
new file mode 100644
index 0000000000..1193a9cd42
--- /dev/null
+++ b/project.hxp
@@ -0,0 +1,1109 @@
+package;
+
+// I don't think we can import `funkin` classes here. Macros? Recursion? IDK.
+import hxp.*;
+import lime.tools.*;
+import sys.FileSystem;
+
+using StringTools;
+
+/**
+ * This HXP performs the functions of a Lime `project.xml` file,
+ * but it's written in Haxe rather than XML!
+ *
+ * This makes it far easier to organize, reuse, and refactor,
+ * and improves management of feature flag logic.
+ */
+@:nullSafety
+class Project extends HXProject {
+ //
+ // METADATA
+ //
+
+ /**
+ * The game's version number, as a Semantic Versioning string with no prefix.
+ * REMEMBER TO CHANGE THIS WHEN THE GAME UPDATES!
+ * You only have to change it here, the rest of the game will query this value.
+ */
+ static final VERSION:String = "0.5.0";
+
+ /**
+ * The game's name. Used as the default window title.
+ */
+ static final TITLE:String = "Friday Night Funkin'";
+
+ /**
+ * The name of the generated executable file.
+ * For example, `"Funkin"` will create a file called `Funkin.exe`.
+ */
+ static final EXECUTABLE_NAME:String = "Funkin";
+
+ /**
+ * The relative location of the source code.
+ */
+ static final SOURCE_DIR:String = "source";
+
+ /**
+ * The fully qualified class path for the game's preloader.
+ * Particularly important on HTML5 but we use it on all platforms.
+ */
+ static final PRELOADER:String = "funkin.ui.transition.preload.FunkinPreloader";
+
+ /**
+ * A package name used for identifying the app on various app stores.
+ */
+ static final PACKAGE_NAME:String = "me.funkin.fnf";
+
+ /**
+ * The fully qualified class path for the entry point class to execute when launching the game.
+ * It's where `public static function main():Void` goes.
+ */
+ static final MAIN_CLASS:String = "Main";
+
+ /**
+ * The company name for the game.
+ * This appears in metadata in places I think.
+ */
+ static final COMPANY:String = "The Funkin' Crew";
+
+ /**
+ * Path to the Haxe script run before building the game.
+ */
+ static final PREBUILD_HX:String = "source/Prebuild.hx";
+
+ /**
+ * Path to the Haxe script run after building the game.
+ */
+ static final POSTBUILD_HX:String = "source/Postbuild.hx";
+
+ /**
+ * Asset path globs to always exclude from asset libraries.
+ */
+ static final EXCLUDE_ASSETS:Array = [".*", "cvs", "thumbs.db", "desktop.ini", "*.hash", "*.md"];
+
+ /**
+ * Asset path globs to exclude on web platforms.
+ */
+ static final EXCLUDE_ASSETS_WEB:Array = ["*.ogg"];
+ /**
+ * Asset path globs to exclude on native platforms.
+ */
+ static final EXCLUDE_ASSETS_NATIVE:Array = ["*.mp3"];
+
+ //
+ // FEATURE FLAGS
+ // Inverse feature flags are automatically populated.
+ //
+
+ /**
+ * `-DGITHUB_BUILD`
+ * If this flag is enabled, the game will use the configuration used by GitHub Actions
+ * to generate playtest builds to be pushed to the launcher.
+ *
+ * This is generally used to forcibly enable debugging features,
+ * even when the game is built in release mode for performance reasons.
+ */
+ static final GITHUB_BUILD:FeatureFlag = "GITHUB_BUILD";
+
+ /**
+ * `-DREDIRECT_ASSETS_FOLDER`
+ * If this flag is enabled, the game will redirect the `assets` folder from the `export` folder
+ * to the `assets` folder at the root of the workspace.
+ * This is useful for ensuring hot reloaded changes don't get lost when rebuilding the game.
+ */
+ static final REDIRECT_ASSETS_FOLDER:FeatureFlag = "REDIRECT_ASSETS_FOLDER";
+
+ /**
+ * `-DTOUCH_HERE_TO_PLAY`
+ * If this flag is enabled, the game will display a prompt to the user after the preloader completes,
+ * requiring them to click anywhere on the screen to start the game.
+ * This is done to ensure that the audio context can initialize properly on HTML5. Not necessary on desktop.
+ */
+ static final TOUCH_HERE_TO_PLAY:FeatureFlag = "TOUCH_HERE_TO_PLAY";
+
+ /**
+ * `-DPRELOAD_ALL`
+ * Whether to preload all asset libraries.
+ * Disabled on web, enabled on desktop.
+ */
+ static final PRELOAD_ALL:FeatureFlag = "PRELOAD_ALL";
+
+ /**
+ * `-DEMBED_ASSETS`
+ * Whether to embed all asset libraries into the executable.
+ */
+ static final EMBED_ASSETS:FeatureFlag = "EMBED_ASSETS";
+
+ /**
+ * `-DHARDCODED_CREDITS`
+ * If this flag is enabled, the credits will be parsed and encoded in the game at compile time,
+ * rather than read from JSON data at runtime.
+ */
+ static final HARDCODED_CREDITS:FeatureFlag = "HARDCODED_CREDITS";
+
+ /**
+ * `-DFEATURE_DEBUG_FUNCTIONS`
+ * If this flag is enabled, the game will have all playtester-only debugging functionality enabled.
+ * This includes debug hotkeys like time travel in the Play State.
+ * By default, enabled on debug builds or playtester builds and disabled on release builds.
+ */
+ static final FEATURE_DEBUG_FUNCTIONS:FeatureFlag = "FEATURE_DEBUG_FUNCTIONS";
+
+ /**
+ * `-DFEATURE_DISCORD_RPC`
+ * If this flag is enabled, the game will enable the Discord Remote Procedure Call library.
+ * This is used to provide Discord Rich Presence support.
+ */
+ static final FEATURE_DISCORD_RPC:FeatureFlag = "FEATURE_DISCORD_RPC";
+
+ /**
+ * `-DFEATURE_NEWGROUNDS`
+ * If this flag is enabled, the game will enable the Newgrounds library.
+ * This is used to provide Medal and Leaderboard support.
+ */
+ static final FEATURE_NEWGROUNDS:FeatureFlag = "FEATURE_NEWGROUNDS";
+
+ /**
+ * `-DFEATURE_FUNKVIS`
+ * If this flag is enabled, the game will enable the Funkin Visualizer library.
+ * This is used to provide audio visualization like Nene's speaker.
+ * Disabling this will make some waveforms inactive.
+ */
+ static final FEATURE_FUNKVIS:FeatureFlag = "FEATURE_FUNKVIS";
+
+ /**
+ * `-DFEATURE_PARTIAL_SOUNDS`
+ * If this flag is enabled, the game will enable the FlxPartialSound library.
+ * This is used to provide audio previews in Freeplay.
+ * Disabling this will make those previews not play.
+ */
+ static final FEATURE_PARTIAL_SOUNDS:FeatureFlag = "FEATURE_PARTIAL_SOUNDS";
+
+ /**
+ * `-DFEATURE_VIDEO_PLAYBACK`
+ * If this flag is enabled, the game will enable support for video playback.
+ * This requires the hxCodec library on desktop platforms.
+ */
+ static final FEATURE_VIDEO_PLAYBACK:FeatureFlag = "FEATURE_VIDEO_PLAYBACK";
+
+ /**
+ * `-DFEATURE_FILE_DROP`
+ * If this flag is enabled, the game will support dragging and dropping files onto it for various features.
+ * Disabled on MacOS.
+ */
+ static final FEATURE_FILE_DROP:FeatureFlag = "FEATURE_FILE_DROP";
+
+ /**
+ * `-DFEATURE_OPEN_URL`
+ * If this flag is enabled, the game will support opening URLs (such as the merch page).
+ */
+ static final FEATURE_OPEN_URL:FeatureFlag = "FEATURE_OPEN_URL";
+
+ /**
+ * `-DFEATURE_CHART_EDITOR`
+ * If this flag is enabled, the Chart Editor will be accessible from the debug menu.
+ */
+ static final FEATURE_CHART_EDITOR:FeatureFlag = "FEATURE_CHART_EDITOR";
+
+ /**
+ * `-DFEATURE_STAGE_EDITOR`
+ * If this flag is enabled, the Stage Editor will be accessible from the debug menu.
+ */
+ static final FEATURE_STAGE_EDITOR:FeatureFlag = "FEATURE_STAGE_EDITOR";
+
+ /**
+ * `-DFEATURE_POLYMOD_MODS`
+ * If this flag is enabled, the game will enable the Polymod library's support for atomic mod loading from the `./mods` folder.
+ * If this flag is disabled, no mods will be loaded.
+ */
+ static final FEATURE_POLYMOD_MODS:FeatureFlag = "FEATURE_POLYMOD_MODS";
+
+ /**
+ * `-DFEATURE_GHOST_TAPPING`
+ * If this flag is enabled, misses will not be counted when it is not the player's turn.
+ * Misses are still counted when the player has notes to hit.
+ */
+ static final FEATURE_GHOST_TAPPING:FeatureFlag = "FEATURE_GHOST_TAPPING";
+
+ //
+ // CONFIGURATION FUNCTIONS
+ //
+
+ public function new() {
+ super();
+
+ flair();
+ configureApp();
+
+ displayTarget();
+ configureFeatureFlags();
+ configureCompileDefines();
+ configureIncludeMacros();
+ configureCustomMacros();
+ configureOutputDir();
+ configurePolymod();
+ configureHaxelibs();
+ configureAssets();
+ configureIcons();
+ }
+
+ /**
+ * Do something before building, display some ASCII or something IDK
+ */
+ function flair() {
+ // TODO: Implement this.
+ info("Friday Night Funkin'");
+ info("Initializing build...");
+
+ info("Target Version: " + VERSION);
+ info("Git Branch: " + getGitBranch());
+ info("Git Commit: " + getGitCommit());
+ info("Git Modified? " + getGitModified());
+ info("Display? " + isDisplay());
+ }
+
+ /**
+ * Apply basic project metadata, such as the game title and version number,
+ * as well as info like the package name and company (used by various app stores).
+ */
+ function configureApp() {
+ this.meta.title = TITLE;
+ this.meta.version = VERSION;
+ this.meta.packageName = PACKAGE_NAME;
+ this.meta.company = COMPANY;
+
+ this.app.main = MAIN_CLASS;
+ this.app.file = EXECUTABLE_NAME;
+ this.app.preloader = PRELOADER;
+
+ // Tell Lime where to look for the game's source code.
+ // If for some reason we have multiple source directories, we can add more entries here.
+ this.sources.push(SOURCE_DIR);
+
+ // Tell Lime to run some prebuild and postbuild scripts.
+ this.preBuildCallbacks.push(buildHaxeCLICommand(PREBUILD_HX));
+ this.postBuildCallbacks.push(buildHaxeCLICommand(POSTBUILD_HX));
+
+ // TODO: Should we provide this?
+ // this.meta.buildNumber = 0;
+
+ // These values are only used by the SWF target I think.
+ // this.app.path
+ // this.app.init
+ // this.app.swfVersion
+ // this.app.url
+
+ // These values are only used by... FIREFOX MARKETPLACE WHAT?
+ // this.meta.description = "";
+ // this.meta.companyId = COMPANY;
+ // this.meta.companyUrl = COMPANY;
+
+ // Configure the window.
+ // Automatically configure FPS.
+ this.window.fps = 60;
+ // Set the window size.
+ this.window.width = 1280;
+ this.window.height = 720;
+ // Black background on release builds, magenta on debug builds.
+ this.window.background = FEATURE_DEBUG_FUNCTIONS.isEnabled(this) ? 0xFFFF00FF : 0xFF000000;
+
+ this.window.hardware = true;
+ this.window.vsync = false;
+
+ if (isWeb()) {
+ this.window.resizable = true;
+ }
+
+ if (isDesktop()) {
+ this.window.orientation = Orientation.LANDSCAPE;
+ this.window.fullscreen = false;
+ this.window.resizable = true;
+ this.window.vsync = false;
+ }
+
+ if (isMobile()) {
+ this.window.orientation = Orientation.LANDSCAPE;
+ this.window.fullscreen = false;
+ this.window.resizable = false;
+ this.window.width = 0;
+ this.window.height = 0;
+ }
+ }
+
+ /**
+ * Log information about the configured target platform.
+ */
+ function displayTarget() {
+ // Display the target operating system.
+ switch (this.target) {
+ case Platform.WINDOWS:
+ info('Target Platform: Windows');
+ case Platform.MAC:
+ info('Target Platform: MacOS');
+ case Platform.LINUX:
+ info('Target Platform: Linux');
+ case Platform.ANDROID:
+ info('Target Platform: Android');
+ case Platform.IOS:
+ info('Target Platform: IOS');
+ case Platform.HTML5:
+ info('Target Platform: HTML5');
+ // See lime.tools.Platform for a full list.
+ // case Platform.EMSCRITEN: // A WebAssembly build might be interesting...
+ // case Platform.AIR:
+ // case Platform.BLACKBERRY:
+ // case Platform.CONSOLE_PC:
+ // case Platform.FIREFOX:
+ // case Platform.FLASH:
+ // case Platform.PS3:
+ // case Platform.PS4:
+ // case Platform.TIZEN:
+ // case Platform.TVOS:
+ // case Platform.VITA:
+ // case Platform.WEBOS:
+ // case Platform.WIIU:
+ // case Platform.XBOX1:
+ default:
+ error('Unsupported platform (got ${target})');
+ }
+
+ switch (this.platformType) {
+ case PlatformType.DESKTOP:
+ info('Platform Type: Desktop');
+ case PlatformType.MOBILE:
+ info('Platform Type: Mobile');
+ case PlatformType.WEB:
+ info('Platform Type: Web');
+ case PlatformType.CONSOLE:
+ info('Platform Type: Console');
+ default:
+ error('Unknown platform type (got ${platformType})');
+ }
+
+ // Print whether we are using HXCPP, HashLink, or something else.
+ if (isWeb()) {
+ info('Target Language: JavaScript (HTML5)');
+ } else if (isHashLink()) {
+ info('Target Language: HashLink');
+ } else if (isNeko()) {
+ info('Target Language: Neko');
+ } else if (isJava()) {
+ info('Target Language: Java');
+ } else if (isNodeJS()) {
+ info('Target Language: JavaScript (NodeJS)');
+ } else if (isCSharp()) {
+ info('Target Language: C#');
+ } else {
+ info('Target Language: C++');
+ }
+
+ for (arch in this.architectures) {
+ // Display the list of target architectures.
+ switch (arch) {
+ case Architecture.X86:
+ info('Architecture: x86');
+ case Architecture.X64:
+ info('Architecture: x64');
+ case Architecture.ARMV5:
+ info('Architecture: ARMv5');
+ case Architecture.ARMV6:
+ info('Architecture: ARMv6');
+ case Architecture.ARMV7:
+ info('Architecture: ARMv7');
+ case Architecture.ARMV7S:
+ info('Architecture: ARMv7S');
+ case Architecture.ARM64:
+ info('Architecture: ARMx64');
+ case Architecture.MIPS:
+ info('Architecture: MIPS');
+ case Architecture.MIPSEL:
+ info('Architecture: MIPSEL');
+ case null:
+ if (!isWeb()) {
+ error('Unsupported architecture (got null on non-web platform)');
+ } else {
+ info('Architecture: Web');
+ }
+ default:
+ error('Unsupported architecture (got ${arch})');
+ }
+ }
+ }
+
+ /**
+ * Apply various feature flags based on the target platform and the user-provided build flags.
+ */
+ function configureFeatureFlags() {
+ // You can explicitly override any of these.
+ // For example, `-DGITHUB_BUILD` or `-DNO_HARDCODED_CREDITS`
+
+ // Should be false unless explicitly requested.
+ GITHUB_BUILD.apply(this, false);
+ FEATURE_STAGE_EDITOR.apply(this, false);
+ FEATURE_NEWGROUNDS.apply(this, false);
+ FEATURE_GHOST_TAPPING.apply(this, false);
+
+ // Should be true unless explicitly requested.
+ HARDCODED_CREDITS.apply(this, true);
+ FEATURE_OPEN_URL.apply(this, true);
+ FEATURE_POLYMOD_MODS.apply(this, true);
+ FEATURE_FUNKVIS.apply(this, true);
+ FEATURE_PARTIAL_SOUNDS.apply(this, true);
+ FEATURE_VIDEO_PLAYBACK.apply(this, true);
+
+ // Should be true on debug builds or if GITHUB_BUILD is enabled.
+ FEATURE_DEBUG_FUNCTIONS.apply(this, isDebug() || GITHUB_BUILD.isEnabled(this));
+
+ // Should default to true on workspace builds and false on release builds.
+ REDIRECT_ASSETS_FOLDER.apply(this, isDebug() && isDesktop());
+
+ // Should be true on release, non-tester builds.
+ // We don't want testers to accidentally leak songs to their Discord friends!
+ // TODO: Re-enable this.
+ FEATURE_DISCORD_RPC.apply(this, false && !FEATURE_DEBUG_FUNCTIONS.isEnabled(this));
+
+ // Should be true only on web builds.
+ // Audio context issues only exist there.
+ TOUCH_HERE_TO_PLAY.apply(this, isWeb());
+
+ // Should be true only on web builds.
+ // Enabling embedding and preloading is required to preload assets properly.
+ EMBED_ASSETS.apply(this, isWeb());
+ PRELOAD_ALL.apply(this, !isWeb());
+
+ // Should be true except on MacOS.
+ // File drop doesn't work there.
+ FEATURE_FILE_DROP.apply(this, !isMac());
+
+ // Should be true except on web builds.
+ // Chart editor doesn't work there.
+ FEATURE_CHART_EDITOR.apply(this, !isWeb());
+ }
+
+ /**
+ * Set compilation flags which are not feature flags.
+ */
+ function configureCompileDefines() {
+ // Enable OpenFL's error handler. Required for the crash logger.
+ setHaxedef("openfl-enable-handle-error");
+
+ // Enable stack trace tracking. Good for debugging but has a (minor) performance impact.
+ setHaxedef("HXCPP_CHECK_POINTER");
+ setHaxedef("HXCPP_STACK_LINE");
+ setHaxedef("HXCPP_STACK_TRACE");
+ setHaxedef("hscriptPos");
+
+ setHaxedef("safeMode");
+
+ // If we aren't using the Flixel debugger, strip it out.
+ if (FEATURE_DEBUG_FUNCTIONS.isDisabled(this)) {
+ setHaxedef("FLX_NO_DEBUG");
+ }
+
+ // Disable the built in pause screen when unfocusing the game.
+ setHaxedef("FLX_NO_FOCUS_LOST_SCREEN");
+
+ // HaxeUI configuration.
+ setHaxedef("haxeui_no_mouse_reset");
+ setHaxedef("haxeui_focus_out_on_click"); // Unfocus a dialog when clicking out of it
+ setHaxedef("haxeui_dont_impose_base_class"); // Suppress a macro error
+
+ if (isRelease()) {
+ // Improve performance on Nape
+ // TODO: Do we even use Nape?
+ setHaxedef("NAPE_RELEASE_BUILD");
+ }
+
+ // Cleaner looking compiler errors.
+ setHaxedef("message.reporting", "pretty");
+ }
+
+ /**
+ * Set compilation flags which manage dead code elimination.
+ */
+ function configureIncludeMacros() {
+ // Disable dead code elimination.
+ // This prevents functions that are unused by the base game from being unavailable to HScript.
+ addHaxeFlag("-dce no");
+
+ // Forcibly include all Funkin' classes in builds.
+ // This prevents classes that are unused by the base game from being unavailable to HScript.
+ addHaxeMacro("include('funkin')");
+
+ // Ensure all HaxeUI components are available at runtime.
+ addHaxeMacro("include('haxe.ui.backend.flixel.components')");
+ addHaxeMacro("include('haxe.ui.core')");
+ addHaxeMacro("include('haxe.ui.components')");
+ addHaxeMacro("include('haxe.ui.containers')");
+ addHaxeMacro("include('haxe.ui.containers.dialogs')");
+ addHaxeMacro("include('haxe.ui.containers.menus')");
+ addHaxeMacro("include('haxe.ui.containers.properties')");
+
+ // Ensure all Flixel classes are available at runtime.
+ // Explicitly ignore packages which require additional dependencies.
+ addHaxeMacro("include('flixel', true, [ 'flixel.addons.editors.spine.*', 'flixel.addons.nape.*', 'flixel.system.macros.*' ])");
+ }
+
+ /**
+ * Set compilation flags which manage bespoke build-time macros.
+ */
+ function configureCustomMacros() {
+ // This macro allows addition of new functionality to existing Flixel. -->
+ addHaxeMacro("addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxBasic())', 'flixel.FlxBasic')");
+ }
+
+ function configureOutputDir() {
+ // Set the output directory. Depends on the target platform and build type.
+
+ var buildDir = 'export/${isDebug() ? 'debug' : 'release'}/';
+
+ info('Output directory: $buildDir');
+ // setenv('BUILD_DIR', buildDir);
+ app.path = buildDir;
+ }
+
+ function configurePolymod() {
+ // The file extension to use for script files.
+ setHaxedef("POLYMOD_SCRIPT_EXT", ".hscript");
+ // Which asset library to use for scripts.
+ setHaxedef("POLYMOD_SCRIPT_LIBRARY", "scripts");
+ // The base path from which scripts should be accessed.
+ setHaxedef("POLYMOD_ROOT_PATH", "scripts/");
+ // Determines the subdirectory of the mod folder used for file appending.
+ setHaxedef("POLYMOD_APPEND_FOLDER", "_append");
+ // Determines the subdirectory of the mod folder used for file merges.
+ setHaxedef("POLYMOD_MERGE_FOLDER", "_merge");
+ // Determines the file in the mod folder used for metadata.
+ setHaxedef("POLYMOD_MOD_METADATA_FILE", "_polymod_meta.json");
+ // Determines the file in the mod folder used for the icon.
+ setHaxedef("POLYMOD_MOD_ICON_FILE", "_polymod_icon.png");
+
+ if (isDebug()) {
+ // Turns on additional debug logging.
+ setHaxedef("POLYMOD_DEBUG");
+ }
+ }
+
+ function configureHaxelibs() {
+ // Don't enforce
+ addHaxelib('lime'); // Game engine backend
+ addHaxelib('openfl'); // Game engine backend
+
+ addHaxelib('flixel'); // Game engine
+
+ addHaxelib('flixel-addons'); // Additional utilities for Flixel
+ addHaxelib('hscript'); // Scripting
+ // addHaxelib('flixel-ui'); // UI framework (DEPRECATED)
+ addHaxelib('haxeui-core'); // UI framework
+ addHaxelib('haxeui-flixel'); // Integrate HaxeUI with Flixel
+ addHaxelib('flixel-text-input'); // Improved text field rendering for HaxeUI
+
+ addHaxelib('polymod'); // Modding framework
+ addHaxelib('flxanimate'); // Texture atlas rendering
+
+ addHaxelib('json2object'); // JSON parsing
+ addHaxelib('jsonpath'); // JSON parsing
+ addHaxelib('jsonpatch'); // JSON parsing
+ addHaxelib('thx.core'); // General utility library, "the lodash of Haxe"
+ addHaxelib('thx.semver'); // Version string handling
+
+ if (isDebug()) {
+ addHaxelib('hxcpp-debug-server'); // VSCode debug support
+ }
+
+ if (isDesktop() && !isHashLink() && FEATURE_VIDEO_PLAYBACK.isEnabled(this)) {
+ // hxCodec doesn't function on HashLink or non-desktop platforms
+ // It's also unnecessary if video playback is disabled
+ addHaxelib('hxCodec'); // Video playback
+ }
+
+ if (FEATURE_DISCORD_RPC.isEnabled(this)) {
+ addHaxelib('discord_rpc'); // Discord API
+ }
+
+ if (FEATURE_NEWGROUNDS.isEnabled(this)) {
+ addHaxelib('newgrounds'); // Newgrounds API
+ }
+
+ if (FEATURE_FUNKVIS.isEnabled(this)) {
+ addHaxelib('funkin.vis'); // Audio visualization
+ addHaxelib('grig.audio'); // Audio data utilities
+ }
+
+ if (FEATURE_PARTIAL_SOUNDS.isEnabled(this)) {
+ addHaxelib('FlxPartialSound'); // Partial sound
+ }
+ }
+
+ function configureAssets() {
+ var exclude = EXCLUDE_ASSETS.concat(isWeb() ? EXCLUDE_ASSETS_WEB : EXCLUDE_ASSETS_NATIVE);
+ var shouldPreload = PRELOAD_ALL.isEnabled(this);
+ var shouldEmbed = EMBED_ASSETS.isEnabled(this);
+
+ if (shouldEmbed) {
+ info('Embedding assets into executable...');
+ } else {
+ info('Including assets alongside executable...');
+ }
+
+ // Default asset library
+ var shouldPreloadDefault = true;
+ addAssetLibrary("default", shouldEmbed, shouldPreloadDefault);
+ addAssetPath("assets/preload", "assets", "default", ["*"], exclude, shouldEmbed);
+
+ // Font assets
+ var shouldEmbedFonts = true;
+ addAssetPath("assets/fonts", null, "default", ["*"], exclude, shouldEmbedFonts);
+
+ // Shared asset libraries
+ addAssetLibrary("songs", shouldEmbed, shouldPreload);
+ addAssetPath("assets/songs", "assets/songs", "songs", ["*"], exclude, shouldEmbed);
+ addAssetLibrary("shared", shouldEmbed, shouldPreload);
+ addAssetPath("assets/shared", "assets/shared", "shared", ["*"], exclude, shouldEmbed);
+ if (FEATURE_VIDEO_PLAYBACK.isEnabled(this)) {
+ var shouldEmbedVideos = false;
+ addAssetLibrary("videos", shouldEmbedVideos, shouldPreload);
+ addAssetPath("assets/videos", "assets/videos", "videos", ["*"], exclude, shouldEmbedVideos);
+ }
+
+ // Level asset libraries
+ addAssetLibrary("tutorial", shouldEmbed, shouldPreload);
+ addAssetPath("assets/tutorial", "assets/tutorial", "tutorial", ["*"], exclude, shouldEmbed);
+ addAssetLibrary("week1", shouldEmbed, shouldPreload);
+ addAssetPath("assets/week1", "assets/week1", "week1", ["*"], exclude, shouldEmbed);
+ addAssetLibrary("week2", shouldEmbed, shouldPreload);
+ addAssetPath("assets/week2", "assets/week2", "week2", ["*"], exclude, shouldEmbed);
+ addAssetLibrary("week3", shouldEmbed, shouldPreload);
+ addAssetPath("assets/week3", "assets/week3", "week3", ["*"], exclude, shouldEmbed);
+ addAssetLibrary("week4", shouldEmbed, shouldPreload);
+ addAssetPath("assets/week4", "assets/week4", "week4", ["*"], exclude, shouldEmbed);
+ addAssetLibrary("week5", shouldEmbed, shouldPreload);
+ addAssetPath("assets/week5", "assets/week5", "week5", ["*"], exclude, shouldEmbed);
+ addAssetLibrary("week6", shouldEmbed, shouldPreload);
+ addAssetPath("assets/week6", "assets/week6", "week6", ["*"], exclude, shouldEmbed);
+ addAssetLibrary("week7", shouldEmbed, shouldPreload);
+ addAssetPath("assets/week7", "assets/week7", "week7", ["*"], exclude, shouldEmbed);
+ addAssetLibrary("weekend1", shouldEmbed, shouldPreload);
+ addAssetPath("assets/weekend1", "assets/weekend1", "weekend1", ["*"], exclude, shouldEmbed);
+
+ // Art asset library (where README and CHANGELOG pull from)
+ var shouldEmbedArt = false;
+ var shouldPreloadArt = false;
+ addAssetLibrary("art", shouldEmbedArt, shouldPreloadArt);
+ addAsset("art/readme.txt", "do NOT readme.txt", "art", shouldEmbedArt);
+ addAsset("LICENSE.md", "LICENSE.md", "art", shouldEmbedArt);
+ addAsset("CHANGELOG.md", "CHANGELOG.md", "art", shouldEmbedArt);
+ }
+
+ /**
+ * Configure the application's favicon and executable icon.
+ */
+ function configureIcons() {
+ addIcon("art/icon16.png", 16);
+ addIcon("art/icon32.png", 32);
+ addIcon("art/icon64.png", 64);
+ addIcon("art/iconOG.png");
+ }
+
+ //
+ // HELPER FUNCTIONS
+ // Easy functions to make the code more readable.
+ //
+
+ public function isWeb():Bool {
+ return this.platformType == PlatformType.WEB;
+ }
+
+ public function isMobile():Bool {
+ return this.platformType == PlatformType.MOBILE;
+ }
+
+ public function isDesktop():Bool {
+ return this.platformType == PlatformType.DESKTOP;
+ }
+
+ public function isConsole():Bool {
+ return this.platformType == PlatformType.CONSOLE;
+ }
+
+ public function is32Bit():Bool {
+ return this.architectures.contains(Architecture.X86);
+ }
+
+ public function is64Bit():Bool {
+ return this.architectures.contains(Architecture.X64);
+ }
+
+ public function isWindows():Bool {
+ return this.target == Platform.WINDOWS;
+ }
+
+ public function isMac():Bool {
+ return this.target == Platform.MAC;
+ }
+
+ public function isLinux():Bool {
+ return this.target == Platform.LINUX;
+ }
+
+ public function isAndroid():Bool {
+ return this.target == Platform.ANDROID;
+ }
+
+ public function isIOS():Bool {
+ return this.target == Platform.IOS;
+ }
+
+ public function isHashLink():Bool {
+ return this.targetFlags.exists("hl");
+ }
+
+ public function isNeko():Bool {
+ return this.targetFlags.exists("neko");
+ }
+
+ public function isJava():Bool {
+ return this.targetFlags.exists("java");
+ }
+
+ public function isNodeJS():Bool {
+ return this.targetFlags.exists("nodejs");
+ }
+
+ public function isCSharp():Bool {
+ return this.targetFlags.exists("cs");
+ }
+
+ public function isDisplay():Bool {
+ return this.command == "display";
+ }
+
+ public function isDebug():Bool {
+ return this.debug;
+ }
+
+ public function isRelease():Bool {
+ return !isDebug();
+ }
+
+ public function getHaxedef(name:String):Null {
+ return this.haxedefs.get(name);
+ }
+
+ public function setHaxedef(name:String, ?value:String):Void {
+ if (value == null) value = "";
+
+ this.haxedefs.set(name, value);
+ }
+
+ public function unsetHaxedef(name:String):Void {
+ this.haxedefs.remove(name);
+ }
+
+ public function getDefine(name:String):Null {
+ return this.defines.get(name);
+ }
+
+ public function hasDefine(name:String):Bool {
+ return this.defines.exists(name);
+ }
+
+ /**
+ * Add a library to the list of dependencies for the project.
+ * @param name The name of the library to add.
+ * @param version The version of the library to add. Optional.
+ */
+ public function addHaxelib(name:String, version:String = ""):Void {
+ this.haxelibs.push(new Haxelib(name, version));
+ }
+
+ /**
+ * Add a `haxeflag` to the project.
+ */
+ public function addHaxeFlag(value:String):Void {
+ this.haxeflags.push(value);
+ }
+
+ /**
+ * Call a Haxe build macro.
+ */
+ public function addHaxeMacro(value:String):Void {
+ addHaxeFlag('--macro ${value}');
+ }
+
+ /**
+ * Add an icon to the project.
+ * @param icon The path to the icon.
+ * @param size The size of the icon. Optional.
+ */
+ public function addIcon(icon:String, ?size:Int):Void {
+ this.icons.push(new Icon(icon, size));
+ }
+
+ /**
+ * Add an asset to the game build.
+ * @param path The path the asset is located at.
+ * @param rename The path the asset should be placed.
+ * @param library The asset library to add the asset to. `null` = "default"
+ * @param embed Whether to embed the asset in the executable.
+ */
+ public function addAsset(path:String, ?rename:String, ?library:String, embed:Bool = false):Void {
+ // path, rename, type, embed, setDefaults
+ var asset = new Asset(path, rename, null, embed, true);
+ @:nullSafety(Off)
+ {
+ asset.library = library ?? "default";
+ }
+ this.assets.push(asset);
+ }
+
+ /**
+ * Add an entire path of assets to the game build.
+ * @param path The path the assets are located at.
+ * @param rename The path the assets should be placed.
+ * @param library The asset library to add the assets to. `null` = "default"
+ * @param include An optional array to include specific asset names.
+ * @param exclude An optional array to exclude specific asset names.
+ * @param embed Whether to embed the assets in the executable.
+ */
+ public function addAssetPath(path:String, ?rename:String, library:String, ?include:Array, ?exclude:Array, embed:Bool = false):Void {
+ // Argument parsing.
+ if (path == "") return;
+
+ if (include == null) include = [];
+
+ if (exclude == null) exclude = [];
+
+ var targetPath = rename ?? path;
+ if (targetPath != "") targetPath += "/";
+
+ // Validate path.
+ if (!sys.FileSystem.exists(path)) {
+ error('Could not find asset path "${path}".');
+ } else if (!sys.FileSystem.isDirectory(path)) {
+ error('Could not parse asset path "${path}", expected a directory.');
+ } else {
+ // info(' Found asset path "${path}".');
+ }
+
+ for (file in sys.FileSystem.readDirectory(path)) {
+ if (sys.FileSystem.isDirectory('${path}/${file}')) {
+ // Attempt to recursively add all assets in the directory.
+ if (this.filter(file, ["*"], exclude)) {
+ addAssetPath('${path}/${file}', '${targetPath}${file}', library, include, exclude, embed);
+ }
+ } else {
+ if (this.filter(file, include, exclude)) {
+ addAsset('${path}/${file}', '${targetPath}${file}', library, embed);
+ }
+ }
+ }
+ }
+
+ /**
+ * Add an asset library to the game build.
+ * @param name The name of the library.
+ * @param embed
+ * @param preload
+ */
+ public function addAssetLibrary(name:String, embed:Bool = false, preload:Bool = false):Void {
+ // sourcePath, name, type, embed, preload, generate, prefix
+ var sourcePath = '';
+ this.libraries.push(new Library(sourcePath, name, null, embed, preload, false, ""));
+ }
+
+ //
+ // PROCESS FUNCTIONS
+ //
+
+ /**
+ * A CLI command to run a command in the shell.
+ */
+ public function buildCLICommand(cmd:String):CLICommand {
+ return CommandHelper.fromSingleString(cmd);
+ }
+
+ /**
+ * A CLI command to run a Haxe script via `--interp`.
+ */
+ public function buildHaxeCLICommand(path:String):CLICommand {
+ return CommandHelper.interpretHaxe(path);
+ }
+
+ public function getGitCommit():String {
+ // Cannibalized from GitCommit.hx
+ var process = new sys.io.Process('git', ['rev-parse', 'HEAD']);
+ if (process.exitCode() != 0) {
+ var message = process.stderr.readAll().toString();
+ error('[ERROR] Could not determine current git commit; is this a proper Git repository?');
+ }
+
+ var commitHash:String = process.stdout.readLine();
+ var commitHashSplice:String = commitHash.substr(0, 7);
+
+ return commitHashSplice;
+ }
+
+ public function getGitBranch():String {
+ // Cannibalized from GitCommit.hx
+ var branchProcess = new sys.io.Process('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
+
+ if (branchProcess.exitCode() != 0) {
+ var message = branchProcess.stderr.readAll().toString();
+ error('Could not determine current git branch; is this a proper Git repository?');
+ }
+
+ var branchName:String = branchProcess.stdout.readLine();
+
+ return branchName;
+ }
+
+ public function getGitModified():Bool {
+ var branchProcess = new sys.io.Process('git', ['status', '--porcelain']);
+
+ if (branchProcess.exitCode() != 0) {
+ var message = branchProcess.stderr.readAll().toString();
+ error('Could not determine current git status; is this a proper Git repository?');
+ }
+
+ var output:String = '';
+ try {
+ output = branchProcess.stdout.readLine();
+ } catch (e) {
+ if (e.message == 'Eof') {
+ // Do nothing.
+ // Eof = No output.
+ } else {
+ // Rethrow other exceptions.
+ throw e;
+ }
+ }
+
+ return output.length > 0;
+ }
+
+ //
+ // LOGGING FUNCTIONS
+ //
+
+ /**
+ * Display an error message. This should stop the build process.
+ */
+ public function error(message:String):Void {
+ Log.error('${message}');
+ }
+
+ /**
+ * Display an info message. This should not interfere with the build process.
+ */
+ public function info(message:String):Void {
+ // CURSED: We have to disable info() log calls because of a bug.
+ // https://github.com/haxelime/lime-vscode-extension/issues/88
+
+ // Log.info('[INFO] ${message}');
+
+ // trace(message);
+ // Sys.println(message);
+ // Sys.stdout().writeString(message);
+ // Sys.stderr().writeString(message);
+ }
+}
+
+/**
+ * An object representing a feature flag, which can be enabled or disabled.
+ * Includes features such as automatic generation of compile defines and inversion.
+ */
+abstract FeatureFlag(String) {
+ static final INVERSE_PREFIX:String = "NO_";
+
+ public function new(input:String) {
+ this = input;
+ }
+
+ @:from
+ public static function fromString(input:String):FeatureFlag {
+ return new FeatureFlag(input);
+ }
+
+ /**
+ * Enable/disable a feature flag if it is unset, and handle the inverse flag.
+ * Doesn't override a feature flag that was set explicitly.
+ * @param enableByDefault Whether to enable this feature flag if it is unset.
+ */
+ public function apply(project:Project, enableByDefault:Bool = false):Void {
+ // TODO: Name this function better?
+
+ if (isEnabled(project)) {
+ // If this flag was already enabled, disable the inverse.
+ project.info('Enabling feature flag ${this}');
+ getInverse().disable(project, false);
+ } else if (getInverse().isEnabled(project)) {
+ // If the inverse flag was already enabled, disable this flag.
+ project.info('Disabling feature flag ${this}');
+ disable(project, false);
+ } else {
+ if (enableByDefault) {
+ // Enable this flag if it was unset, and disable the inverse.
+ project.info('Enabling feature flag ${this}');
+ enable(project, true);
+ } else {
+ // Disable this flag if it was unset, and enable the inverse.
+ project.info('Disabling feature flag ${this}');
+ disable(project, true);
+ }
+ }
+ }
+
+ /**
+ * Enable this feature flag by setting the appropriate compile define.
+ *
+ * @param project The project to modify.
+ * @param andInverse Also disable the feature flag's inverse.
+ */
+ public function enable(project:Project, andInverse:Bool = true) {
+ project.setHaxedef(this, "");
+ if (andInverse) {
+ getInverse().disable(project, false);
+ }
+ }
+
+ /**
+ * Disable this feature flag by removing the appropriate compile define.
+ *
+ * @param project The project to modify.
+ * @param andInverse Also enable the feature flag's inverse.
+ */
+ public function disable(project:Project, andInverse:Bool = true) {
+ project.unsetHaxedef(this);
+ if (andInverse) {
+ getInverse().enable(project, false);
+ }
+ }
+
+ /**
+ * Query if this feature flag is enabled.
+ * @param project The project to query.
+ */
+ public function isEnabled(project:Project):Bool {
+ // Check both Haxedefs and Defines for this flag.
+ return project.haxedefs.exists(this) || project.defines.exists(this);
+ }
+
+ /**
+ * Query if this feature flag's inverse is enabled.
+ */
+ public function isDisabled(project:Project):Bool {
+ return getInverse().isEnabled(project);
+ }
+
+ /**
+ * Return the inverse of this feature flag.
+ * @return A new feature flag that is the inverse of this one.
+ */
+ public function getInverse():FeatureFlag {
+ if (this.startsWith(INVERSE_PREFIX)) {
+ return this.substring(INVERSE_PREFIX.length);
+ }
+ return INVERSE_PREFIX + this;
+ }
+}
diff --git a/source/Main.hx b/source/Main.hx
index add5bbc67b..724b118f81 100644
--- a/source/Main.hx
+++ b/source/Main.hx
@@ -22,7 +22,7 @@ class Main extends Sprite
var gameHeight:Int = 720; // Height of the game in pixels (might be less / more in actual pixels depending on your zoom).
var initialState:Class = funkin.InitState; // The FlxState the game starts with.
var zoom:Float = -1; // If -1, zoom is automatically calculated to fit the window dimensions.
- #if web
+ #if (web || CHEEMS)
var framerate:Int = 60; // How many frames per second the game should run at.
#else
// TODO: This should probably be in the options menu?
@@ -66,6 +66,12 @@ class Main extends Sprite
function init(?event:Event):Void
{
+ #if web
+ // set this variable (which is a function) from the lime version at lime/_internal/backend/html5/HTML5Application.hx
+ // The framerate cap will more thoroughly initialize via Preferences in InitState.hx
+ funkin.Preferences.lockedFramerateFunction = untyped js.Syntax.code("window.requestAnimationFrame");
+ #end
+
if (hasEventListener(Event.ADDED_TO_STAGE))
{
removeEventListener(Event.ADDED_TO_STAGE, init);
@@ -113,7 +119,7 @@ class Main extends Sprite
addChild(game);
- #if debug
+ #if FEATURE_DEBUG_FUNCTIONS
game.debugger.interaction.addTool(new funkin.util.TrackerToolButtonUtil());
#end
diff --git a/source/funkin/Assets.hx b/source/funkin/Assets.hx
new file mode 100644
index 0000000000..5351676d4d
--- /dev/null
+++ b/source/funkin/Assets.hx
@@ -0,0 +1,38 @@
+package funkin;
+
+/**
+ * A wrapper around `openfl.utils.Assets` which disallows access to the harmful functions.
+ * Later we'll add Funkin-specific caching to this.
+ */
+class Assets
+{
+ public static function getText(path:String):String
+ {
+ return openfl.utils.Assets.getText(path);
+ }
+
+ public static function getMusic(path:String):openfl.media.Sound
+ {
+ return openfl.utils.Assets.getMusic(path);
+ }
+
+ public static function getBitmapData(path:String):openfl.display.BitmapData
+ {
+ return openfl.utils.Assets.getBitmapData(path);
+ }
+
+ public static function getBytes(path:String):haxe.io.Bytes
+ {
+ return openfl.utils.Assets.getBytes(path);
+ }
+
+ public static function exists(path:String, ?type:openfl.utils.AssetType):Bool
+ {
+ return openfl.utils.Assets.exists(path, type);
+ }
+
+ public static function list(type:openfl.utils.AssetType):Array
+ {
+ return openfl.utils.Assets.list(type);
+ }
+}
diff --git a/source/funkin/Conductor.hx b/source/funkin/Conductor.hx
index 0d63cb6cce..e73b2860c7 100644
--- a/source/funkin/Conductor.hx
+++ b/source/funkin/Conductor.hx
@@ -430,7 +430,7 @@ class Conductor
else if (currentTimeChange != null && this.songPosition > 0.0)
{
// roundDecimal prevents representing 8 as 7.9999999
- this.currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) + (this.songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6);
+ this.currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * Constants.STEPS_PER_BEAT) + (this.songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6);
this.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
this.currentMeasureTime = currentStepTime / stepsPerMeasure;
this.currentStep = Math.floor(currentStepTime);
@@ -564,7 +564,7 @@ class Conductor
if (ms >= timeChange.timeStamp)
{
lastTimeChange = timeChange;
- resultStep = lastTimeChange.beatTime * 4;
+ resultStep = lastTimeChange.beatTime * Constants.STEPS_PER_BEAT;
}
else
{
@@ -600,7 +600,7 @@ class Conductor
var lastTimeChange:SongTimeChange = timeChanges[0];
for (timeChange in timeChanges)
{
- if (stepTime >= timeChange.beatTime * 4)
+ if (stepTime >= timeChange.beatTime * Constants.STEPS_PER_BEAT)
{
lastTimeChange = timeChange;
resultMs = lastTimeChange.timeStamp;
@@ -613,7 +613,7 @@ class Conductor
}
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
- resultMs += (stepTime - lastTimeChange.beatTime * 4) * lastStepLengthMs;
+ resultMs += (stepTime - lastTimeChange.beatTime * Constants.STEPS_PER_BEAT) * lastStepLengthMs;
return resultMs;
}
diff --git a/source/funkin/InitState.hx b/source/funkin/InitState.hx
index 00d34fadba..f71de00f4a 100644
--- a/source/funkin/InitState.hx
+++ b/source/funkin/InitState.hx
@@ -1,5 +1,6 @@
package funkin;
+import funkin.data.freeplay.player.PlayerRegistry;
import funkin.ui.debug.charting.ChartEditorState;
import funkin.ui.transition.LoadingState;
import flixel.FlxState;
@@ -18,6 +19,7 @@ import funkin.play.PlayStatePlaylist;
import openfl.display.BitmapData;
import funkin.data.story.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
+import funkin.data.freeplay.style.FreeplayStyleRegistry;
import funkin.data.event.SongEventRegistry;
import funkin.data.stage.StageRegistry;
import funkin.data.dialogue.conversation.ConversationRegistry;
@@ -26,13 +28,14 @@ import funkin.data.dialogue.speaker.SpeakerRegistry;
import funkin.data.freeplay.album.AlbumRegistry;
import funkin.data.song.SongRegistry;
import funkin.play.character.CharacterData.CharacterDataParser;
+import funkin.play.notes.notekind.NoteKindManager;
import funkin.modding.module.ModuleHandler;
import funkin.ui.title.TitleState;
import funkin.util.CLIUtil;
import funkin.util.CLIUtil.CLIParams;
import funkin.util.TimerUtil;
import funkin.util.TrackerUtil;
-#if discord_rpc
+#if FEATURE_DISCORD_RPC
import Discord.DiscordClient;
#end
@@ -121,7 +124,7 @@ class InitState extends FlxState
//
// DISCORD API SETUP
//
- #if discord_rpc
+ #if FEATURE_DISCORD_RPC
DiscordClient.initialize();
Application.current.onExit.add(function(exitCode) {
@@ -142,7 +145,7 @@ 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 scripted Module behavior to a Flixel plugin.
- #if debug
+ #if FEATURE_DEBUG_FUNCTIONS
funkin.util.plugins.MemoryGCPlugin.initialize();
#end
funkin.util.plugins.EvacuateDebugPlugin.initialize();
@@ -164,9 +167,11 @@ class InitState extends FlxState
SongRegistry.instance.loadEntries();
LevelRegistry.instance.loadEntries();
NoteStyleRegistry.instance.loadEntries();
+ PlayerRegistry.instance.loadEntries();
ConversationRegistry.instance.loadEntries();
DialogueBoxRegistry.instance.loadEntries();
SpeakerRegistry.instance.loadEntries();
+ FreeplayStyleRegistry.instance.loadEntries();
AlbumRegistry.instance.loadEntries();
StageRegistry.instance.loadEntries();
@@ -174,6 +179,8 @@ class InitState extends FlxState
// Move it to use a BaseRegistry.
CharacterDataParser.loadCharacterCache();
+ NoteKindManager.loadScripts();
+
ModuleHandler.buildModuleCallbacks();
ModuleHandler.loadModuleCache();
ModuleHandler.callOnCreate();
@@ -214,6 +221,38 @@ class InitState extends FlxState
#elseif STAGEBUILD
// -DSTAGEBUILD
FlxG.switchState(() -> new funkin.ui.debug.stage.StageBuilderState());
+ #elseif RESULTS
+ // -DRESULTS
+ FlxG.switchState(() -> new funkin.play.ResultState(
+ {
+ storyMode: true,
+ title: "Cum Song Erect by Kawai Sprite",
+ songId: "cum",
+ characterId: "pico-playable",
+ difficultyId: "nightmare",
+ isNewHighscore: true,
+ scoreData:
+ {
+ score: 1_234_567,
+ tallies:
+ {
+ sick: 130,
+ good: 60,
+ bad: 69,
+ shit: 69,
+ missed: 69,
+ combo: 69,
+ maxCombo: 69,
+ totalNotesHit: 140,
+ totalNotes: 190
+ }
+ // 2400 total notes = 7% = LOSS
+ // 240 total notes = 79% = GOOD
+ // 230 total notes = 82% = GREAT
+ // 210 total notes = 91% = EXCELLENT
+ // 190 total notes = PERFECT
+ },
+ }));
#elseif ANIMDEBUG
// -DANIMDEBUG
FlxG.switchState(() -> new funkin.ui.debug.anim.DebugBoundingState());
@@ -337,11 +376,16 @@ class InitState extends FlxState
//
// 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
+ #if FEATURE_DEBUG_FUNCTIONS
+ trace('Initializing Flixel debugger...');
+
+ #if !debug
+ // Make errors less annoying on release builds.
LogStyle.ERROR.openConsole = false;
LogStyle.ERROR.errorSound = null;
+ #end
+
+ // Make errors and warnings less annoying.
LogStyle.WARNING.openConsole = false;
LogStyle.WARNING.errorSound = null;
diff --git a/source/funkin/Paths.hx b/source/funkin/Paths.hx
index 54a4b7acfa..285af7ca22 100644
--- a/source/funkin/Paths.hx
+++ b/source/funkin/Paths.hx
@@ -11,9 +11,16 @@ class Paths
{
static var currentLevel:Null = null;
- public static function setCurrentLevel(name:String):Void
+ public static function setCurrentLevel(name:Null):Void
{
- currentLevel = name.toLowerCase();
+ if (name == null)
+ {
+ currentLevel = null;
+ }
+ else
+ {
+ currentLevel = name.toLowerCase();
+ }
}
public static function stripLibrary(path:String):String
@@ -123,9 +130,17 @@ class Paths
return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.${Constants.EXT_SOUND}';
}
- public static function inst(song:String, ?suffix:String = ''):String
+ /**
+ * Gets the path to an `Inst.mp3/ogg` song instrumental from songs:assets/songs/`song`/
+ * @param song name of the song to get instrumental for
+ * @param suffix any suffix to add to end of song name, used for `-erect` variants usually
+ * @param withExtension if it should return with the audio file extension `.mp3` or `.ogg`.
+ * @return String
+ */
+ public static function inst(song:String, ?suffix:String = '', ?withExtension:Bool = true):String
{
- return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.${Constants.EXT_SOUND}';
+ var ext:String = withExtension ? '.${Constants.EXT_SOUND}' : '';
+ return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix$ext';
}
public static function image(key:String, ?library:String):String
@@ -153,3 +168,11 @@ class Paths
return FlxAtlasFrames.fromSpriteSheetPacker(image(key, library), file('images/$key.txt', library));
}
}
+
+enum abstract PathsFunction(String)
+{
+ var MUSIC;
+ var INST;
+ var VOICES;
+ var SOUND;
+}
diff --git a/source/funkin/Preferences.hx b/source/funkin/Preferences.hx
index b2050c6a24..daeded897d 100644
--- a/source/funkin/Preferences.hx
+++ b/source/funkin/Preferences.hx
@@ -128,6 +128,48 @@ class Preferences
return value;
}
+ public static var unlockedFramerate(get, set):Bool;
+
+ static function get_unlockedFramerate():Bool
+ {
+ return Save?.instance?.options?.unlockedFramerate;
+ }
+
+ static function set_unlockedFramerate(value:Bool):Bool
+ {
+ if (value != Save.instance.options.unlockedFramerate)
+ {
+ #if web
+ toggleFramerateCap(value);
+ #end
+ }
+
+ var save:Save = Save.instance;
+ save.options.unlockedFramerate = value;
+ save.flush();
+ return value;
+ }
+
+ #if web
+ // We create a haxe version of this just for readability.
+ // We use these to override `window.requestAnimationFrame` in Javascript to uncap the framerate / "animation" request rate
+ // Javascript is crazy since u can just do stuff like that lol
+
+ public static function unlockedFramerateFunction(callback, element)
+ {
+ var currTime = Date.now().getTime();
+ var timeToCall = 0;
+ var id = js.Browser.window.setTimeout(function() {
+ callback(currTime + timeToCall);
+ }, timeToCall);
+ return id;
+ }
+
+ // Lime already implements their own little framerate cap, so we can just use that
+ // This also gets set in the init function in Main.hx, since we need to definitely override it
+ public static var lockedFramerateFunction = untyped js.Syntax.code("window.requestAnimationFrame");
+ #end
+
/**
* Loads the user's preferences from the save data and apply them.
*/
@@ -137,6 +179,17 @@ class Preferences
FlxG.autoPause = Preferences.autoPause;
// Apply the debugDisplay setting (enables the FPS and RAM display).
toggleDebugDisplay(Preferences.debugDisplay);
+ #if web
+ toggleFramerateCap(Preferences.unlockedFramerate);
+ #end
+ }
+
+ static function toggleFramerateCap(unlocked:Bool):Void
+ {
+ #if web
+ var framerateFunction = unlocked ? unlockedFramerateFunction : lockedFramerateFunction;
+ untyped js.Syntax.code("window.requestAnimationFrame = framerateFunction;");
+ #end
}
static function toggleDebugDisplay(show:Bool):Void
diff --git a/source/funkin/api/discord/Discord.hx b/source/funkin/api/discord/Discord.hx
index a4d65684ee..9dd513bf79 100644
--- a/source/funkin/api/discord/Discord.hx
+++ b/source/funkin/api/discord/Discord.hx
@@ -1,13 +1,13 @@
package funkin.api.discord;
import Sys.sleep;
-#if discord_rpc
+#if FEATURE_DISCORD_RPC
import discord_rpc.DiscordRpc;
#end
class DiscordClient
{
- #if discord_rpc
+ #if FEATURE_DISCORD_RPC
public function new()
{
trace("Discord Client starting...");
diff --git a/source/funkin/api/newgrounds/NGUnsafe.hx b/source/funkin/api/newgrounds/NGUnsafe.hx
index 9616dfe187..81da7359e7 100644
--- a/source/funkin/api/newgrounds/NGUnsafe.hx
+++ b/source/funkin/api/newgrounds/NGUnsafe.hx
@@ -1,9 +1,5 @@
package funkin.api.newgrounds;
-import flixel.util.FlxSignal;
-import flixel.util.FlxTimer;
-import lime.app.Application;
-import openfl.display.Stage;
#if newgrounds
import io.newgrounds.NG;
import io.newgrounds.NGLite;
@@ -28,7 +24,7 @@ class NGUnsafe
NG.core.calls.event.logEvent(event).send();
trace('should have logged: ' + event);
#else
- #if debug
+ #if FEATURE_DEBUG_FUNCTIONS
trace('event:$event - not logged, missing NG.io lib');
#end
#end
@@ -43,7 +39,7 @@ class NGUnsafe
if (!medal.unlocked) medal.sendUnlock();
}
#else
- #if debug
+ #if FEATURE_DEBUG_FUNCTIONS
trace('medal:$id - not unlocked, missing NG.io lib');
#end
#end
@@ -67,7 +63,7 @@ class NGUnsafe
}
}
#else
- #if debug
+ #if FEATURE_DEBUG_FUNCTIONS
trace('Song:$song, Score:$score - not posted, missing NG.io lib');
#end
#end
diff --git a/source/funkin/api/newgrounds/NGio.hx b/source/funkin/api/newgrounds/NGio.hx
index c1f8ad3ba4..a866783c11 100644
--- a/source/funkin/api/newgrounds/NGio.hx
+++ b/source/funkin/api/newgrounds/NGio.hx
@@ -2,19 +2,11 @@ package funkin.api.newgrounds;
#if newgrounds
import flixel.util.FlxSignal;
-import flixel.util.FlxTimer;
import io.newgrounds.NG;
import io.newgrounds.NGLite;
-import io.newgrounds.components.ScoreBoardComponent.Period;
import io.newgrounds.objects.Error;
-import io.newgrounds.objects.Medal;
import io.newgrounds.objects.Score;
-import io.newgrounds.objects.ScoreBoard;
-import io.newgrounds.objects.events.Response;
-import io.newgrounds.objects.events.Result.GetCurrentVersionResult;
-import io.newgrounds.objects.events.Result.GetVersionResult;
import lime.app.Application;
-import openfl.display.Stage;
#end
/**
@@ -247,7 +239,7 @@ class NGio
NG.core.calls.event.logEvent(event).send();
trace('should have logged: ' + event);
#else
- #if debug
+ #if FEATURE_DEBUG_FUNCTIONS
trace('event:$event - not logged, missing NG.io lib');
#end
#end
@@ -262,7 +254,7 @@ class NGio
if (!medal.unlocked) medal.sendUnlock();
}
#else
- #if debug
+ #if FEATURE_DEBUG_FUNCTIONS
trace('medal:$id - not unlocked, missing NG.io lib');
#end
#end
@@ -286,7 +278,7 @@ class NGio
}
}
#else
- #if debug
+ #if FEATURE_DEBUG_FUNCTIONS
trace('Song:$song, Score:$score - not posted, missing NG.io lib');
#end
#end
diff --git a/source/funkin/audio/FunkinSound.hx b/source/funkin/audio/FunkinSound.hx
index df05cc3ef6..dae31cd07e 100644
--- a/source/funkin/audio/FunkinSound.hx
+++ b/source/funkin/audio/FunkinSound.hx
@@ -11,10 +11,14 @@ import funkin.audio.waveform.WaveformDataParser;
import funkin.data.song.SongData.SongMusicData;
import funkin.data.song.SongRegistry;
import funkin.util.tools.ICloneable;
+import funkin.util.flixel.sound.FlxPartialSound;
+import funkin.Paths.PathsFunction;
import openfl.Assets;
+import lime.app.Future;
+import lime.app.Promise;
import openfl.media.SoundMixer;
+
#if (openfl >= "8.0.0")
-import openfl.utils.AssetType;
#end
/**
@@ -223,12 +227,12 @@ class FunkinSound extends FlxSound implements ICloneable
// already paused before we lost focus.
if (_lostFocus && !_alreadyPaused)
{
- trace('Resuming audio (${this._label}) on focus!');
+ // trace('Resuming audio (${this._label}) on focus!');
resume();
}
else
{
- trace('Not resuming audio (${this._label}) on focus!');
+ // trace('Not resuming audio (${this._label}) on focus!');
}
_lostFocus = false;
}
@@ -238,7 +242,7 @@ class FunkinSound extends FlxSound implements ICloneable
*/
override function onFocusLost():Void
{
- trace('Focus lost, pausing audio!');
+ // trace('Focus lost, pausing audio!');
_lostFocus = true;
_alreadyPaused = _paused;
pause();
@@ -336,29 +340,86 @@ class FunkinSound extends FlxSound implements ICloneable
if (songMusicData != null)
{
Conductor.instance.mapTimeChanges(songMusicData.timeChanges);
+
+ if (songMusicData.looped != null && params.loop == null) params.loop = songMusicData.looped;
}
else
{
FlxG.log.warn('Tried and failed to find music metadata for $key');
}
}
+ var pathsFunction = params.pathsFunction ?? MUSIC;
+ var suffix = params.suffix ?? '';
+ var pathToUse = switch (pathsFunction)
+ {
+ case MUSIC: Paths.music('$key/$key');
+ case INST: Paths.inst('$key', suffix);
+ default: Paths.music('$key/$key');
+ }
- var music = FunkinSound.load(Paths.music('$key/$key'), params?.startingVolume ?? 1.0, params.loop ?? true, false, true);
- if (music != null)
+ var shouldLoadPartial = params.partialParams?.loadPartial ?? false;
+
+ // even if we arent' trying to partial load a song, we want to error out any songs in progress,
+ // so we don't get overlapping music if someone were to load a new song while a partial one is loading!
+
+ emptyPartialQueue();
+
+ if (shouldLoadPartial)
{
- FlxG.sound.music = music;
+ var music = FunkinSound.loadPartial(pathToUse, params.partialParams?.start ?? 0.0, params.partialParams?.end ?? 1.0, params?.startingVolume ?? 1.0,
+ params.loop ?? true, false, false, params.onComplete);
- // Prevent repeat update() and onFocus() calls.
- FlxG.sound.list.remove(FlxG.sound.music);
+ if (music != null)
+ {
+ partialQueue.push(music);
- return true;
+ @:nullSafety(Off)
+ music.future.onComplete(function(partialMusic:Null) {
+ FlxG.sound.music = partialMusic;
+ FlxG.sound.list.remove(FlxG.sound.music);
+
+ if (FlxG.sound.music != null && params.onLoad != null) params.onLoad();
+ });
+
+ return true;
+ }
+ else
+ {
+ return false;
+ }
}
else
{
- return false;
+ var music = FunkinSound.load(pathToUse, params?.startingVolume ?? 1.0, params.loop ?? true, false, true, params.onComplete);
+ if (music != null)
+ {
+ FlxG.sound.music = music;
+
+ // Prevent repeat update() and onFocus() calls.
+ FlxG.sound.list.remove(FlxG.sound.music);
+
+ if (FlxG.sound.music != null && params.onLoad != null) params.onLoad();
+
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+ }
+
+ public static function emptyPartialQueue():Void
+ {
+ while (partialQueue.length > 0)
+ {
+ @:nullSafety(Off)
+ partialQueue.pop().error("Cancel loading partial sound");
}
}
+ static var partialQueue:Array>> = [];
+
/**
* Creates a new `FunkinSound` object synchronously.
*
@@ -415,6 +476,51 @@ class FunkinSound extends FlxSound implements ICloneable
return sound;
}
+ /**
+ * Will load a section of a sound file, useful for Freeplay where we don't want to load all the bytes of a song
+ * @param path The path to the sound file
+ * @param start The start time of the sound file
+ * @param end The end time of the sound file
+ * @param volume Volume to start at
+ * @param looped Whether the sound file should loop
+ * @param autoDestroy Whether the sound file should be destroyed after it finishes playing
+ * @param autoPlay Whether the sound file should play immediately
+ * @param onComplete Callback when the sound finishes playing
+ * @param onLoad Callback when the sound finishes loading
+ * @return A FunkinSound object
+ */
+ public static function loadPartial(path:String, start:Float = 0, end:Float = 1, volume:Float = 1.0, looped:Bool = false, autoDestroy:Bool = false,
+ autoPlay:Bool = true, ?onComplete:Void->Void, ?onLoad:Void->Void):Promise>
+ {
+ var promise:lime.app.Promise> = new lime.app.Promise>();
+
+ // split the path and get only after first :
+ // we are bypassing the openfl/lime asset library fuss on web only
+ #if web
+ path = Paths.stripLibrary(path);
+ #end
+
+ var soundRequest = FlxPartialSound.partialLoadFromFile(path, start, end);
+
+ if (soundRequest == null)
+ {
+ promise.complete(null);
+ }
+ else
+ {
+ promise.future.onError(function(e) {
+ soundRequest.error("Sound loading was errored or cancelled");
+ });
+
+ soundRequest.future.onComplete(function(partialSound) {
+ var snd = FunkinSound.load(partialSound, volume, looped, autoDestroy, autoPlay, onComplete, onLoad);
+ promise.complete(snd);
+ });
+ }
+
+ return promise;
+ }
+
@:nullSafety(Off)
public override function destroy():Void
{
@@ -433,11 +539,12 @@ class FunkinSound extends FlxSound implements ICloneable
* Play a sound effect once, then destroy it.
* @param key
* @param volume
- * @return static function construct():FunkinSound
+ * @return A `FunkinSound` object, or `null` if the sound could not be loaded.
*/
- public static function playOnce(key:String, volume:Float = 1.0, ?onComplete:Void->Void, ?onLoad:Void->Void):Void
+ public static function playOnce(key:String, volume:Float = 1.0, ?onComplete:Void->Void, ?onLoad:Void->Void):Null
{
var result = FunkinSound.load(key, volume, false, true, true, onComplete, onLoad);
+ return result;
}
/**
@@ -462,6 +569,14 @@ class FunkinSound extends FlxSound implements ICloneable
return sound;
}
+
+ /**
+ * Produces a string representation suitable for debugging.
+ */
+ public override function toString():String
+ {
+ return 'FunkinSound(${this._label})';
+ }
}
/**
@@ -475,6 +590,12 @@ typedef FunkinSoundPlayMusicParams =
*/
var ?startingVolume:Float;
+ /**
+ * The suffix of the music file to play. Usually for "-erect" tracks when loading an INST file
+ * @default ``
+ */
+ var ?suffix:String;
+
/**
* Whether to override music if a different track is already playing.
* @default `false`
@@ -498,4 +619,22 @@ typedef FunkinSoundPlayMusicParams =
* @default `true`
*/
var ?mapTimeChanges:Bool;
+
+ /**
+ * Which Paths function to use to load a song
+ * @default `MUSIC`
+ */
+ var ?pathsFunction:PathsFunction;
+
+ var ?partialParams:PartialSoundParams;
+
+ var ?onComplete:Void->Void;
+ var ?onLoad:Void->Void;
+}
+
+typedef PartialSoundParams =
+{
+ var loadPartial:Bool;
+ var start:Float;
+ var end:Float;
}
diff --git a/source/funkin/audio/SoundGroup.hx b/source/funkin/audio/SoundGroup.hx
index 5fc2abe0ed..5d53fedd64 100644
--- a/source/funkin/audio/SoundGroup.hx
+++ b/source/funkin/audio/SoundGroup.hx
@@ -113,6 +113,11 @@ class SoundGroup extends FlxTypedGroup
public function play(forceRestart:Bool = false, startTime:Float = 0.0, ?endTime:Float)
{
forEachAlive(function(sound:FunkinSound) {
+ if (sound.length < startTime)
+ {
+ // trace('Queuing sound (${sound.toString()} past its length! Skipping...)');
+ return;
+ }
sound.play(forceRestart, startTime, endTime);
});
}
diff --git a/source/funkin/audio/VoicesGroup.hx b/source/funkin/audio/VoicesGroup.hx
index 5037ee1d0c..9a1e0e0c17 100644
--- a/source/funkin/audio/VoicesGroup.hx
+++ b/source/funkin/audio/VoicesGroup.hx
@@ -1,9 +1,7 @@
package funkin.audio;
-import funkin.audio.FunkinSound;
import flixel.group.FlxGroup.FlxTypedGroup;
import funkin.audio.waveform.WaveformData;
-import funkin.audio.waveform.WaveformDataParser;
class VoicesGroup extends SoundGroup
{
diff --git a/source/funkin/audio/visualize/ABotVis.hx b/source/funkin/audio/visualize/ABotVis.hx
index ca77dd58aa..4d2243b7c2 100644
--- a/source/funkin/audio/visualize/ABotVis.hx
+++ b/source/funkin/audio/visualize/ABotVis.hx
@@ -1,13 +1,9 @@
package funkin.audio.visualize;
-import funkin.audio.visualize.dsp.FFT;
import flixel.FlxSprite;
-import flixel.addons.plugin.taskManager.FlxTask;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
-import flixel.math.FlxMath;
import flixel.sound.FlxSound;
-import funkin.util.MathUtil;
import funkin.vis.dsp.SpectralAnalyzer;
import funkin.vis.audioclip.frontends.LimeAudioClip;
@@ -58,8 +54,15 @@ class ABotVis extends FlxTypedSpriteGroup
public function initAnalyzer()
{
@:privateAccess
- analyzer = new SpectralAnalyzer(7, new LimeAudioClip(cast snd._channel.__source), 0.01, 30);
- analyzer.maxDb = -35;
+ analyzer = new SpectralAnalyzer(snd._channel.__audioSource, 7, 0.1, 40);
+
+ #if desktop
+ // On desktop it uses FFT stuff that isn't as optimized as the direct browser stuff we use on HTML5
+ // So we want to manually change it!
+ analyzer.fftN = 256;
+ #end
+
+ // analyzer.maxDb = -35;
// analyzer.fftN = 2048;
}
@@ -83,9 +86,7 @@ class ABotVis extends FlxTypedSpriteGroup
override function draw()
{
- #if web
if (analyzer != null) drawFFT();
- #end
super.draw();
}
@@ -94,12 +95,18 @@ class ABotVis extends FlxTypedSpriteGroup
*/
function drawFFT():Void
{
- var levels = analyzer.getLevels(false);
+ var levels = analyzer.getLevels();
for (i in 0...min(group.members.length, levels.length))
{
var animFrame:Int = Math.round(levels[i].value * 5);
+ #if desktop
+ // Web version scales with the Flixel volume level.
+ // This line brings platform parity but looks worse.
+ // animFrame = Math.round(animFrame * FlxG.sound.volume);
+ #end
+
animFrame = Math.floor(Math.min(5, animFrame));
animFrame = Math.floor(Math.max(0, animFrame));
diff --git a/source/funkin/audio/visualize/PolygonVisGroup.hx b/source/funkin/audio/visualize/PolygonVisGroup.hx
index cc68f4ae08..bff8457965 100644
--- a/source/funkin/audio/visualize/PolygonVisGroup.hx
+++ b/source/funkin/audio/visualize/PolygonVisGroup.hx
@@ -1,6 +1,5 @@
package funkin.audio.visualize;
-import funkin.audio.visualize.PolygonSpectogram;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.sound.FlxSound;
diff --git a/source/funkin/audio/visualize/SpectogramSprite.hx b/source/funkin/audio/visualize/SpectogramSprite.hx
index 636c0726a6..615e80d956 100644
--- a/source/funkin/audio/visualize/SpectogramSprite.hx
+++ b/source/funkin/audio/visualize/SpectogramSprite.hx
@@ -8,8 +8,6 @@ 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 lime.system.ThreadPool;
import lime.utils.Int16Array;
using Lambda;
@@ -38,8 +36,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup
lengthOfShit = amnt;
regenLineShit();
-
- // makeGraphic(200, 200, FlxColor.BLACK);
}
public function regenLineShit():Void
@@ -89,8 +85,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup
{
checkAndSetBuffer();
- // vis.checkAndSetBuffer();
-
if (setBuffer)
{
var samplesToGen:Int = Std.int(sampleRate * seconds);
@@ -191,7 +185,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup
// a value between 10hz and 100Khz
var hzPicker:Float = Math.pow(10, powedShit);
- // var sampleApprox:Int = Std.int(FlxMath.remapToRange(i, 0, group.members.length, startingSample, startingSample + samplesToGen));
var remappedFreq:Int = Std.int(FlxMath.remapToRange(hzPicker, 0, 10000, 0, freqShit[0].length - 1));
group.members[i].x = prevLine.x;
@@ -211,8 +204,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup
var line = FlxPoint.get(prevLine.x - group.members[i].x, prevLine.y - group.members[i].y);
// dont draw a line until i figure out a nicer way to view da spikes and shit idk lol!
- // group.members[i].setGraphicSize(Std.int(Math.max(line.length, 1)), Std.int(1));
- // group.members[i].angle = line.degrees;
}
}
}
@@ -261,9 +252,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup
group.members[Std.int(remappedSample)].x = prevLine.x;
group.members[Std.int(remappedSample)].y = prevLine.y;
- // group.members[0].y = prevLine.y;
-
- // FlxSpriteUtil.drawLine(this, prevLine.x, prevLine.y, width * remappedSample, left * height / 2 + height / 2);
prevLine.x = (curAud.balanced * swagheight / 2 + swagheight / 2) + x;
prevLine.y = (Std.int(remappedSample) / lengthOfShit * daHeight) + y;
diff --git a/source/funkin/audio/visualize/VisShit.hx b/source/funkin/audio/visualize/VisShit.hx
index 204ced1e1a..83b9496aca 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 lime.system.ThreadPool;
import lime.utils.Int16Array;
import funkin.util.MathUtil;
@@ -73,9 +72,6 @@ class VisShit
freqOutput.push([]);
- // if (FlxG.keys.justPressed.M)
- // trace(FFT.rfft(chunk).map(z -> z.scale(1 / fs).magnitude));
-
// find spectral peaks and their instantaneous frequencies
for (k => s in freqs)
{
@@ -91,7 +87,6 @@ class VisShit
if (freq < maxFreq) freqOutput[indexOfArray].push(power);
//
}
- // haxe.Log.trace("", null);
indexOfArray++;
// move to next (overlapping) chunk
@@ -122,7 +117,7 @@ class VisShit
{
// Math.pow3
@:privateAccess
- var buf = snd._channel.__source.buffer;
+ var buf = snd._channel.__audioSource.buffer;
// @:privateAccess
audioData = cast buf.data; // jank and hacky lol! kinda busted on HTML5 also!!
diff --git a/source/funkin/audio/visualize/dsp/FFT.hx b/source/funkin/audio/visualize/dsp/FFT.hx
index dc75acb814..40ee9cb8c3 100644
--- a/source/funkin/audio/visualize/dsp/FFT.hx
+++ b/source/funkin/audio/visualize/dsp/FFT.hx
@@ -1,7 +1,5 @@
package funkin.audio.visualize.dsp;
-import funkin.audio.visualize.dsp.Complex;
-
using funkin.audio.visualize.dsp.OffsetArray;
using funkin.audio.visualize.dsp.Signal;
diff --git a/source/funkin/audio/waveform/WaveformData.hx b/source/funkin/audio/waveform/WaveformData.hx
index 1f649b4728..a939f91bf9 100644
--- a/source/funkin/audio/waveform/WaveformData.hx
+++ b/source/funkin/audio/waveform/WaveformData.hx
@@ -1,7 +1,5 @@
package funkin.audio.waveform;
-import funkin.util.MathUtil;
-
@:nullSafety
class WaveformData
{
diff --git a/source/funkin/audio/waveform/WaveformDataParser.hx b/source/funkin/audio/waveform/WaveformDataParser.hx
index 5aa54d744e..ca421581b7 100644
--- a/source/funkin/audio/waveform/WaveformDataParser.hx
+++ b/source/funkin/audio/waveform/WaveformDataParser.hx
@@ -16,7 +16,7 @@ class WaveformDataParser
// Method 1. This only works if the sound has been played before.
@:privateAccess
- var soundBuffer:Null = sound?._channel?.__source?.buffer;
+ var soundBuffer:Null = sound?._channel?.__audioSource?.buffer;
if (soundBuffer == null)
{
diff --git a/source/funkin/audio/waveform/WaveformSprite.hx b/source/funkin/audio/waveform/WaveformSprite.hx
index 32ced2fbd3..8eaba8117d 100644
--- a/source/funkin/audio/waveform/WaveformSprite.hx
+++ b/source/funkin/audio/waveform/WaveformSprite.hx
@@ -1,7 +1,5 @@
package funkin.audio.waveform;
-import funkin.audio.waveform.WaveformData;
-import funkin.audio.waveform.WaveformDataParser;
import funkin.graphics.rendering.MeshRender;
import flixel.util.FlxColor;
diff --git a/source/funkin/data/BaseRegistry.hx b/source/funkin/data/BaseRegistry.hx
index 118516bec3..83413ad00a 100644
--- a/source/funkin/data/BaseRegistry.hx
+++ b/source/funkin/data/BaseRegistry.hx
@@ -263,7 +263,7 @@ abstract class BaseRegistry & Constructible
+ public function parseEntryDataWithMigration(id:String, version:Null):Null
{
if (version == null)
{
diff --git a/source/funkin/data/dialogue/conversation/ConversationData.hx b/source/funkin/data/dialogue/conversation/ConversationData.hx
index 30e3f451be..6505198367 100644
--- a/source/funkin/data/dialogue/conversation/ConversationData.hx
+++ b/source/funkin/data/dialogue/conversation/ConversationData.hx
@@ -1,7 +1,5 @@
package funkin.data.dialogue.conversation;
-import funkin.data.animation.AnimationData;
-
/**
* A type definition for the data for a specific conversation.
* It includes things like what dialogue boxes to use, what text to display, and what animations to play.
diff --git a/source/funkin/data/dialogue/conversation/ConversationRegistry.hx b/source/funkin/data/dialogue/conversation/ConversationRegistry.hx
index ca072897fd..fad1e43adc 100644
--- a/source/funkin/data/dialogue/conversation/ConversationRegistry.hx
+++ b/source/funkin/data/dialogue/conversation/ConversationRegistry.hx
@@ -1,7 +1,6 @@
package funkin.data.dialogue.conversation;
import funkin.play.cutscene.dialogue.Conversation;
-import funkin.data.dialogue.conversation.ConversationData;
import funkin.play.cutscene.dialogue.ScriptedConversation;
class ConversationRegistry extends BaseRegistry
diff --git a/source/funkin/data/event/SongEventRegistry.hx b/source/funkin/data/event/SongEventRegistry.hx
index 9b01635574..5ee2d39fa6 100644
--- a/source/funkin/data/event/SongEventRegistry.hx
+++ b/source/funkin/data/event/SongEventRegistry.hx
@@ -46,7 +46,7 @@ class SongEventRegistry
if (event != null)
{
- trace(' Loaded built-in song event: (${event.id})');
+ trace(' Loaded built-in song event: ${event.id}');
eventCache.set(event.id, event);
}
else
@@ -59,9 +59,9 @@ class SongEventRegistry
static function registerScriptedEvents()
{
var scriptedEventClassNames:Array = ScriptedSongEvent.listScriptClasses();
+ trace('Instantiating ${scriptedEventClassNames.length} scripted song events...');
if (scriptedEventClassNames == null || scriptedEventClassNames.length == 0) return;
- trace('Instantiating ${scriptedEventClassNames.length} scripted song events...');
for (eventCls in scriptedEventClassNames)
{
var event:SongEvent = ScriptedSongEvent.init(eventCls, "UKNOWN");
diff --git a/source/funkin/data/freeplay/player/CHANGELOG.md b/source/funkin/data/freeplay/player/CHANGELOG.md
new file mode 100644
index 0000000000..7a31e11ca9
--- /dev/null
+++ b/source/funkin/data/freeplay/player/CHANGELOG.md
@@ -0,0 +1,9 @@
+# Freeplay Playable Character Data Schema Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [1.0.0]
+Initial release.
diff --git a/source/funkin/data/freeplay/player/PlayerData.hx b/source/funkin/data/freeplay/player/PlayerData.hx
new file mode 100644
index 0000000000..de293c24e3
--- /dev/null
+++ b/source/funkin/data/freeplay/player/PlayerData.hx
@@ -0,0 +1,380 @@
+package funkin.data.freeplay.player;
+
+import funkin.data.animation.AnimationData;
+
+@:nullSafety
+class PlayerData
+{
+ /**
+ * The sematic version number of the player data JSON format.
+ * Supports fancy comparisons like NPM does it's neat.
+ */
+ @:default(funkin.data.freeplay.player.PlayerRegistry.PLAYER_DATA_VERSION)
+ public var version:String;
+
+ /**
+ * A readable name for this playable character.
+ */
+ public var name:String = 'Unknown';
+
+ /**
+ * The character IDs this character is associated with.
+ * Only songs that use these characters will show up in Freeplay.
+ */
+ @:default([])
+ public var ownedChars:Array = [];
+
+ /**
+ * Whether to show songs with character IDs that aren't associated with any specific character.
+ */
+ @:optional
+ @:default(false)
+ public var showUnownedChars:Bool = false;
+
+ /**
+ * Which freeplay style to use for this character.
+ */
+ @:optional
+ @:default("bf")
+ public var freeplayStyle:String = Constants.DEFAULT_FREEPLAY_STYLE;
+
+ /**
+ * Data for displaying this character in the Freeplay menu.
+ * If null, display no DJ.
+ */
+ @:optional
+ public var freeplayDJ:Null = null;
+
+ /**
+ * Data for displaying this character in the Character Select menu.
+ * If null, exclude from Character Select.
+ */
+ @:optional
+ public var charSelect:Null = null;
+
+ /**
+ * Data for displaying this character in the results screen.
+ */
+ @:optional
+ public var results:Null = null;
+
+ /**
+ * Whether this character is unlocked by default.
+ * Use a ScriptedPlayableCharacter to add custom logic.
+ */
+ @:optional
+ @:default(true)
+ public var unlocked:Bool = true;
+
+ public function new()
+ {
+ this.version = PlayerRegistry.PLAYER_DATA_VERSION;
+ }
+
+ /**
+ * Convert this StageData into a JSON string.
+ */
+ public function serialize(pretty:Bool = true):String
+ {
+ // Update generatedBy and version before writing.
+ updateVersionToLatest();
+
+ var writer = new json2object.JsonWriter();
+ return writer.write(this, pretty ? ' ' : null);
+ }
+
+ public function updateVersionToLatest():Void
+ {
+ this.version = PlayerRegistry.PLAYER_DATA_VERSION;
+ }
+}
+
+class PlayerFreeplayDJData
+{
+ var assetPath:String;
+ var animations:Array;
+
+ @:optional
+ @:default("BOYFRIEND")
+ var text1:String;
+
+ @:optional
+ @:default("HOT BLOODED IN MORE WAYS THAN ONE")
+ var text2:String;
+
+ @:optional
+ @:default("PROTECT YO NUTS")
+ var text3:String;
+
+ @:jignored
+ var animationMap:Map;
+
+ @:jignored
+ var prefixToOffsetsMap:Map>;
+
+ @:optional
+ var charSelect:Null;
+
+ @:optional
+ var cartoon:Null;
+
+ @:optional
+ var fistPump:Null;
+
+ public function new()
+ {
+ animationMap = new Map();
+ }
+
+ function mapAnimations()
+ {
+ if (animationMap == null) animationMap = new Map();
+ if (prefixToOffsetsMap == null) prefixToOffsetsMap = new Map();
+
+ animationMap.clear();
+ prefixToOffsetsMap.clear();
+ for (anim in animations)
+ {
+ animationMap.set(anim.name, anim);
+ prefixToOffsetsMap.set(anim.prefix, anim.offsets);
+ }
+ }
+
+ public function getAtlasPath():String
+ {
+ return Paths.animateAtlas(assetPath);
+ }
+
+ public function getFreeplayDJText(index:Int):String
+ {
+ switch (index)
+ {
+ case 1:
+ return text1;
+ case 2:
+ return text2;
+ case 3:
+ return text3;
+ default:
+ return '';
+ }
+ }
+
+ public function getAnimationPrefix(name:String):Null
+ {
+ if (animationMap.size() == 0) mapAnimations();
+
+ var anim = animationMap.get(name);
+ if (anim == null) return null;
+ return anim.prefix;
+ }
+
+ public function getAnimationOffsetsByPrefix(?prefix:String):Array
+ {
+ if (prefixToOffsetsMap.size() == 0) mapAnimations();
+ if (prefix == null) return [0, 0];
+ return prefixToOffsetsMap.get(prefix);
+ }
+
+ public function getAnimationOffsets(name:String):Array
+ {
+ return getAnimationOffsetsByPrefix(getAnimationPrefix(name));
+ }
+
+ // TODO: These should really be frame labels, ehe.
+
+ public function getCartoonSoundClickFrame():Int
+ {
+ return cartoon?.soundClickFrame ?? 80;
+ }
+
+ public function getCartoonSoundCartoonFrame():Int
+ {
+ return cartoon?.soundCartoonFrame ?? 85;
+ }
+
+ public function getCartoonLoopBlinkFrame():Int
+ {
+ return cartoon?.loopBlinkFrame ?? 112;
+ }
+
+ public function getCartoonLoopFrame():Int
+ {
+ return cartoon?.loopFrame ?? 166;
+ }
+
+ public function getCartoonChannelChangeFrame():Int
+ {
+ return cartoon?.channelChangeFrame ?? 60;
+ }
+
+ public function getFistPumpIntroStartFrame():Int
+ {
+ return fistPump?.introStartFrame ?? 0;
+ }
+
+ public function getFistPumpIntroEndFrame():Int
+ {
+ return fistPump?.introEndFrame ?? 0;
+ }
+
+ public function getFistPumpLoopStartFrame():Int
+ {
+ return fistPump?.loopStartFrame ?? 0;
+ }
+
+ public function getFistPumpLoopEndFrame():Int
+ {
+ return fistPump?.loopEndFrame ?? 0;
+ }
+
+ public function getFistPumpIntroBadStartFrame():Int
+ {
+ return fistPump?.introBadStartFrame ?? 0;
+ }
+
+ public function getFistPumpIntroBadEndFrame():Int
+ {
+ return fistPump?.introBadEndFrame ?? 0;
+ }
+
+ public function getFistPumpLoopBadStartFrame():Int
+ {
+ return fistPump?.loopBadStartFrame ?? 0;
+ }
+
+ public function getFistPumpLoopBadEndFrame():Int
+ {
+ return fistPump?.loopBadEndFrame ?? 0;
+ }
+
+ public function getCharSelectTransitionDelay():Float
+ {
+ return charSelect?.transitionDelay ?? 0.25;
+ }
+}
+
+class PlayerCharSelectData
+{
+ /**
+ * A zero-indexed number for the character's preferred position in the grid.
+ * 0 = top left, 4 = center, 8 = bottom right
+ * In the event of a conflict, the first character alphabetically gets it,
+ * and others get shifted over.
+ */
+ @:optional
+ public var position:Null;
+}
+
+typedef PlayerResultsData =
+{
+ var music:PlayerResultsMusicData;
+
+ var perfect:Array;
+ var excellent:Array;
+ var great:Array;
+ var good:Array;
+ var loss:Array;
+};
+
+typedef PlayerResultsMusicData =
+{
+ @:optional
+ var PERFECT_GOLD:String;
+
+ @:optional
+ var PERFECT:String;
+
+ @:optional
+ var EXCELLENT:String;
+
+ @:optional
+ var GREAT:String;
+
+ @:optional
+ var GOOD:String;
+
+ @:optional
+ var SHIT:String;
+}
+
+typedef PlayerResultsAnimationData =
+{
+ /**
+ * `sparrow` or `animate` or whatever
+ */
+ var renderType:String;
+
+ var assetPath:String;
+
+ @:optional
+ @:default([0, 0])
+ var offsets:Array;
+
+ @:optional
+ @:default(500)
+ var zIndex:Int;
+
+ @:optional
+ @:default(0.0)
+ var delay:Float;
+
+ @:optional
+ @:default(1.0)
+ var scale:Float;
+
+ @:optional
+ @:default('')
+ var startFrameLabel:Null;
+
+ @:optional
+ @:default(true)
+ var looped:Bool;
+
+ @:optional
+ var loopFrame:Null;
+
+ @:optional
+ var loopFrameLabel:Null;
+};
+
+typedef PlayerFreeplayDJCharSelectData =
+{
+ var transitionDelay:Float;
+}
+
+typedef PlayerFreeplayDJCartoonData =
+{
+ var soundClickFrame:Int;
+ var soundCartoonFrame:Int;
+ var loopBlinkFrame:Int;
+ var loopFrame:Int;
+ var channelChangeFrame:Int;
+}
+
+typedef PlayerFreeplayDJFistPumpData =
+{
+ @:default(0)
+ var introStartFrame:Int;
+
+ @:default(4)
+ var introEndFrame:Int;
+
+ @:default(4)
+ var loopStartFrame:Int;
+
+ @:default(-1)
+ var loopEndFrame:Int;
+
+ @:default(0)
+ var introBadStartFrame:Int;
+
+ @:default(4)
+ var introBadEndFrame:Int;
+
+ @:default(4)
+ var loopBadStartFrame:Int;
+
+ @:default(-1)
+ var loopBadEndFrame:Int;
+};
diff --git a/source/funkin/data/freeplay/player/PlayerRegistry.hx b/source/funkin/data/freeplay/player/PlayerRegistry.hx
new file mode 100644
index 0000000000..76b1c25c15
--- /dev/null
+++ b/source/funkin/data/freeplay/player/PlayerRegistry.hx
@@ -0,0 +1,208 @@
+package funkin.data.freeplay.player;
+
+import funkin.data.freeplay.player.PlayerData;
+import funkin.ui.freeplay.charselect.PlayableCharacter;
+import funkin.ui.freeplay.charselect.ScriptedPlayableCharacter;
+import funkin.save.Save;
+
+class PlayerRegistry extends BaseRegistry
+{
+ /**
+ * The current version string for the stage data format.
+ * Handle breaking changes by incrementing this value
+ * and adding migration to the `migratePlayerData()` function.
+ */
+ public static final PLAYER_DATA_VERSION:thx.semver.Version = "1.0.0";
+
+ public static final PLAYER_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
+
+ public static var instance(get, never):PlayerRegistry;
+ static var _instance:Null = null;
+
+ static function get_instance():PlayerRegistry
+ {
+ if (_instance == null) _instance = new PlayerRegistry();
+ return _instance;
+ }
+
+ /**
+ * A mapping between stage character IDs and Freeplay playable character IDs.
+ */
+ var ownedCharacterIds:Map = [];
+
+ public function new()
+ {
+ super('PLAYER', 'players', PLAYER_DATA_VERSION_RULE);
+ }
+
+ public override function loadEntries():Void
+ {
+ super.loadEntries();
+
+ for (playerId in listEntryIds())
+ {
+ var player = fetchEntry(playerId);
+ if (player == null) continue;
+
+ var currentPlayerCharIds = player.getOwnedCharacterIds();
+ for (characterId in currentPlayerCharIds)
+ {
+ ownedCharacterIds.set(characterId, playerId);
+ }
+ }
+
+ log('Loaded ${countEntries()} playable characters with ${ownedCharacterIds.size()} associations.');
+ }
+
+ public function countUnlockedCharacters():Int
+ {
+ var count = 0;
+
+ for (charId in listEntryIds())
+ {
+ var player = fetchEntry(charId);
+ if (player == null) continue;
+
+ if (player.isUnlocked()) count++;
+ }
+
+ return count;
+ }
+
+ public function hasNewCharacter():Bool
+ {
+ var charactersSeen = Save.instance.charactersSeen.clone();
+
+ for (charId in listEntryIds())
+ {
+ var player = fetchEntry(charId);
+ if (player == null) continue;
+
+ if (!player.isUnlocked()) continue;
+ if (charactersSeen.contains(charId)) continue;
+
+ // This character is unlocked but we haven't seen them in Freeplay yet.
+ return true;
+ }
+
+ // Fallthrough case.
+ return false;
+ }
+
+ public function listNewCharacters():Array
+ {
+ var charactersSeen = Save.instance.charactersSeen.clone();
+ var result = [];
+
+ for (charId in listEntryIds())
+ {
+ var player = fetchEntry(charId);
+ if (player == null) continue;
+
+ if (!player.isUnlocked()) continue;
+ if (charactersSeen.contains(charId)) continue;
+
+ // This character is unlocked but we haven't seen them in Freeplay yet.
+ result.push(charId);
+ }
+
+ return result;
+ }
+
+ /**
+ * Get the playable character associated with a given stage character.
+ * @param characterId The stage character ID.
+ * @return The playable character.
+ */
+ public function getCharacterOwnerId(characterId:Null):Null
+ {
+ if (characterId == null) return null;
+ return ownedCharacterIds[characterId];
+ }
+
+ /**
+ * Return true if the given stage character is associated with a specific playable character.
+ * If so, the level should only appear if that character is selected in Freeplay.
+ * @param characterId The stage character ID.
+ * @return Whether the character is owned by any one character.
+ */
+ public function isCharacterOwned(characterId:String):Bool
+ {
+ return ownedCharacterIds.exists(characterId);
+ }
+
+ /**
+ * Read, parse, and validate the JSON data and produce the corresponding data object.
+ */
+ public function parseEntryData(id:String):Null
+ {
+ // JsonParser does not take type parameters,
+ // otherwise this function would be in BaseRegistry.
+ var parser = 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.
+ */
+ public function parseEntryDataRaw(contents:String, ?fileName:String):Null
+ {
+ var parser = 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):PlayableCharacter
+ {
+ return ScriptedPlayableCharacter.init(clsName, "unknown");
+ }
+
+ function getScriptedClassNames():Array
+ {
+ return ScriptedPlayableCharacter.listScriptClasses();
+ }
+
+ /**
+ * A list of all the playable characters from the base game, in order.
+ */
+ public function listBaseGamePlayerIds():Array
+ {
+ return ["bf", "pico"];
+ }
+
+ /**
+ * A list of all installed playable characters that are not from the base game.
+ */
+ public function listModdedPlayerIds():Array
+ {
+ return listEntryIds().filter(function(id:String):Bool {
+ return listBaseGamePlayerIds().indexOf(id) == -1;
+ });
+ }
+}
diff --git a/source/funkin/data/freeplay/style/CHANGELOG.md b/source/funkin/data/freeplay/style/CHANGELOG.md
new file mode 100644
index 0000000000..8fe9f7eb88
--- /dev/null
+++ b/source/funkin/data/freeplay/style/CHANGELOG.md
@@ -0,0 +1,9 @@
+# Freeplay Style Data Schema Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [1.0.0]
+Initial release.
diff --git a/source/funkin/data/freeplay/style/FreeplayStyleData.hx b/source/funkin/data/freeplay/style/FreeplayStyleData.hx
new file mode 100644
index 0000000000..1af1982173
--- /dev/null
+++ b/source/funkin/data/freeplay/style/FreeplayStyleData.hx
@@ -0,0 +1,48 @@
+package funkin.data.freeplay.style;
+
+import funkin.data.animation.AnimationData;
+
+/**
+ * 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 FreeplayStyleData =
+{
+ /**
+ * Semantic version for style data.
+ */
+ public var version:String;
+
+ /**
+ * Asset key for the background image.
+ */
+ public var bgAsset:String;
+
+ /**
+ * Asset key for the difficulty selector image.
+ */
+ public var selectorAsset:String;
+
+ /**
+ * Asset key for the numbers shown at the top right of the screen.
+ */
+ public var numbersAsset:String;
+
+ /**
+ * Asset key for the freeplay capsules.
+ */
+ public var capsuleAsset:String;
+
+ /**
+ * Color data for the capsule text outline.
+ * the order of this array goes as follows: [DESELECTED, SELECTED]
+ */
+ public var capsuleTextColors:Array;
+
+ /**
+ * Delay time after confirming a song selection, before entering PlayState.
+ * Useful for letting longer animations play out.
+ */
+ public var startDelay:Float;
+}
diff --git a/source/funkin/data/freeplay/style/FreeplayStyleRegistry.hx b/source/funkin/data/freeplay/style/FreeplayStyleRegistry.hx
new file mode 100644
index 0000000000..626e2ac393
--- /dev/null
+++ b/source/funkin/data/freeplay/style/FreeplayStyleRegistry.hx
@@ -0,0 +1,84 @@
+package funkin.data.freeplay.style;
+
+import funkin.ui.freeplay.FreeplayStyle;
+import funkin.data.freeplay.style.FreeplayStyleData;
+import funkin.ui.freeplay.ScriptedFreeplayStyle;
+
+class FreeplayStyleRegistry extends BaseRegistry
+{
+ /**
+ * The current version string for the style data format.
+ * Handle breaking changes by incrementing this value
+ * and adding migration to the `migrateStyleData()` function.
+ */
+ public static final FREEPLAYSTYLE_DATA_VERSION:thx.semver.Version = '1.0.0';
+
+ public static final FREEPLAYSTYLE_DATA_VERSION_RULE:thx.semver.VersionRule = '1.0.x';
+
+ public static final instance:FreeplayStyleRegistry = new FreeplayStyleRegistry();
+
+ public function new()
+ {
+ super('FREEPLAYSTYLE', 'ui/freeplay/styles', FREEPLAYSTYLE_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):FreeplayStyle
+ {
+ return ScriptedFreeplayStyle.init(clsName, 'unknown');
+ }
+
+ function getScriptedClassNames():Array
+ {
+ return ScriptedFreeplayStyle.listScriptClasses();
+ }
+}
diff --git a/source/funkin/data/notestyle/CHANGELOG.md b/source/funkin/data/notestyle/CHANGELOG.md
new file mode 100644
index 0000000000..d85c11cad7
--- /dev/null
+++ b/source/funkin/data/notestyle/CHANGELOG.md
@@ -0,0 +1,31 @@
+# Note Style Data Schema Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [1.1.0]
+### Added
+- Added several new `assets`:
+ - `countdownThree`
+ - `countdownTwo`
+ - `countdownOne`
+ - `countdownGo`
+ - `judgementSick`
+ - `judgementGood`
+ - `judgementBad`
+ - `judgementShit`
+ - `comboNumber0`
+ - `comboNumber1`
+ - `comboNumber2`
+ - `comboNumber3`
+ - `comboNumber4`
+ - `comboNumber5`
+ - `comboNumber6`
+ - `comboNumber7`
+ - `comboNumber8`
+ - `comboNumber9`
+
+## [1.0.0]
+Initial version.
diff --git a/source/funkin/data/notestyle/NoteStyleData.hx b/source/funkin/data/notestyle/NoteStyleData.hx
index 04fda67ca6..39162b896e 100644
--- a/source/funkin/data/notestyle/NoteStyleData.hx
+++ b/source/funkin/data/notestyle/NoteStyleData.hx
@@ -74,6 +74,84 @@ typedef NoteStyleAssetsData =
*/
@:optional
var holdNoteCover:NoteStyleAssetData;
+
+ /**
+ * The THREE sound (and an optional pre-READY graphic).
+ */
+ @:optional
+ var countdownThree:NoteStyleAssetData;
+
+ /**
+ * The TWO sound and READY graphic.
+ */
+ @:optional
+ var countdownTwo:NoteStyleAssetData;
+
+ /**
+ * The ONE sound and SET graphic.
+ */
+ @:optional
+ var countdownOne:NoteStyleAssetData;
+
+ /**
+ * The GO sound and GO! graphic.
+ */
+ @:optional
+ var countdownGo:NoteStyleAssetData;
+
+ /**
+ * The SICK! judgement.
+ */
+ @:optional
+ var judgementSick:NoteStyleAssetData;
+
+ /**
+ * The GOOD! judgement.
+ */
+ @:optional
+ var judgementGood:NoteStyleAssetData;
+
+ /**
+ * The BAD! judgement.
+ */
+ @:optional
+ var judgementBad:NoteStyleAssetData;
+
+ /**
+ * The SHIT! judgement.
+ */
+ @:optional
+ var judgementShit:NoteStyleAssetData;
+
+ @:optional
+ var comboNumber0:NoteStyleAssetData;
+
+ @:optional
+ var comboNumber1:NoteStyleAssetData;
+
+ @:optional
+ var comboNumber2:NoteStyleAssetData;
+
+ @:optional
+ var comboNumber3:NoteStyleAssetData;
+
+ @:optional
+ var comboNumber4:NoteStyleAssetData;
+
+ @:optional
+ var comboNumber5:NoteStyleAssetData;
+
+ @:optional
+ var comboNumber6:NoteStyleAssetData;
+
+ @:optional
+ var comboNumber7:NoteStyleAssetData;
+
+ @:optional
+ var comboNumber8:NoteStyleAssetData;
+
+ @:optional
+ var comboNumber9:NoteStyleAssetData;
}
/**
@@ -109,10 +187,19 @@ typedef NoteStyleAssetData =
@:optional
var isPixel:Bool;
+ /**
+ * If true, animations will be played on the graphic.
+ * @default `false` to save performance.
+ */
+ @:default(false)
+ @:optional
+ var animated:Bool;
+
/**
* The structure of this data depends on the asset.
*/
- var data:T;
+ @:optional
+ var data:Null;
}
typedef NoteStyleData_Note =
@@ -123,7 +210,14 @@ typedef NoteStyleData_Note =
var right:UnnamedAnimationData;
}
+typedef NoteStyleData_Countdown =
+{
+ var audioPath:String;
+}
+
typedef NoteStyleData_HoldNote = {}
+typedef NoteStyleData_Judgement = {}
+typedef NoteStyleData_ComboNum = {}
/**
* Data on animations for each direction of the strumline.
diff --git a/source/funkin/data/notestyle/NoteStyleRegistry.hx b/source/funkin/data/notestyle/NoteStyleRegistry.hx
index 5e9fa9a3dd..36d1b92001 100644
--- a/source/funkin/data/notestyle/NoteStyleRegistry.hx
+++ b/source/funkin/data/notestyle/NoteStyleRegistry.hx
@@ -11,9 +11,9 @@ class NoteStyleRegistry extends BaseRegistry
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateNoteStyleData()` function.
*/
- public static final NOTE_STYLE_DATA_VERSION:thx.semver.Version = "1.0.0";
+ public static final NOTE_STYLE_DATA_VERSION:thx.semver.Version = "1.1.0";
- public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
+ public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.1.x";
public static var instance(get, never):NoteStyleRegistry;
static var _instance:Null = null;
diff --git a/source/funkin/data/song/CHANGELOG.md b/source/funkin/data/song/CHANGELOG.md
index 3cd3af0700..86c9f6912a 100644
--- a/source/funkin/data/song/CHANGELOG.md
+++ b/source/funkin/data/song/CHANGELOG.md
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [2.2.4]
+### Added
+- Added `playData.characters.opponentVocals` to specify which vocal track(s) to play for the opponent.
+ - If the value isn't present, it will use the `playData.characters.opponent`, but if it is present, it will be used (even if it's empty, in which case no vocals will be used for the opponent)
+- Added `playData.characters.playerVocals` to specify which vocal track(s) to play for the player.
+ - If the value isn't present, it will use the `playData.characters.player`, but if it is present, it will be used (even if it's empty, in which case no vocals will be used for the player)
+- Added `offsets.altVocals` field to apply vocal offsets when alternate instrumentals are used.
+
+
+## [2.2.3]
+### Added
+- Added `charter` field to denote authorship of a chart.
+
## [2.2.2]
### Added
- Added `playData.previewStart` and `playData.previewEnd` fields to specify when in the song should the song's audio should be played as a preview in Freeplay.
diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx
index 26380947af..074ed0b440 100644
--- a/source/funkin/data/song/SongData.hx
+++ b/source/funkin/data/song/SongData.hx
@@ -30,6 +30,9 @@ class SongMetadata implements ICloneable
@:default("Unknown")
public var artist:String;
+ @:optional
+ public var charter:Null = null;
+
@:optional
@:default(96)
public var divisions:Null; // Optional field
@@ -53,6 +56,8 @@ class SongMetadata implements ICloneable
@:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY)
public var generatedBy:String;
+ @:optional
+ @:default(funkin.data.song.SongData.SongTimeFormat.MILLISECONDS)
public var timeFormat:SongTimeFormat;
public var timeChanges:Array;
@@ -112,14 +117,23 @@ class SongMetadata implements ICloneable
*/
public function serialize(pretty:Bool = true):String
{
+ // Update generatedBy and version before writing.
+ updateVersionToLatest();
+
var ignoreNullOptionals = true;
var writer = new json2object.JsonWriter(ignoreNullOptionals);
- // I believe @:jignored should be iggnored by the writer?
+ // I believe @:jignored should be ignored by the writer?
// var output = this.clone();
// output.variation = null; // Not sure how to make a field optional on the reader and ignored on the writer.
return writer.write(this, pretty ? ' ' : null);
}
+ public function updateVersionToLatest():Void
+ {
+ this.version = SongRegistry.SONG_METADATA_VERSION;
+ this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
+ }
+
/**
* Produces a string representation suitable for debugging.
*/
@@ -243,18 +257,27 @@ class SongOffsets implements ICloneable
public var altInstrumentals:Map;
/**
- * The offset, in milliseconds, to apply to the song's vocals, relative to the chart.
+ * The offset, in milliseconds, to apply to the song's vocals, relative to the song's base instrumental.
* These are applied ON TOP OF the instrumental offset.
*/
@:optional
@:default([])
public var vocals:Map;
- public function new(instrumental:Float = 0.0, ?altInstrumentals:Map, ?vocals:Map)
+ /**
+ * The offset, in milliseconds, to apply to the songs vocals, relative to each alternate instrumental.
+ * This is useful for the circumstance where, for example, an alt instrumental has a few seconds of lead in before the song starts.
+ */
+ @:optional
+ @:default([])
+ public var altVocals:Map>;
+
+ public function new(instrumental:Float = 0.0, ?altInstrumentals:Map, ?vocals:Map, ?altVocals:Map>)
{
this.instrumental = instrumental;
this.altInstrumentals = altInstrumentals == null ? new Map() : altInstrumentals;
this.vocals = vocals == null ? new Map() : vocals;
+ this.altVocals = altVocals == null ? new Map>() : altVocals;
}
public function getInstrumentalOffset(?instrumental:String):Float
@@ -279,11 +302,19 @@ class SongOffsets implements ICloneable
return value;
}
- public function getVocalOffset(charId:String):Float
+ public function getVocalOffset(charId:String, ?instrumental:String):Float
{
- if (!this.vocals.exists(charId)) return 0.0;
-
- return this.vocals.get(charId);
+ if (instrumental == null)
+ {
+ if (!this.vocals.exists(charId)) return 0.0;
+ return this.vocals.get(charId);
+ }
+ else
+ {
+ if (!this.altVocals.exists(instrumental)) return 0.0;
+ if (!this.altVocals.get(instrumental).exists(charId)) return 0.0;
+ return this.altVocals.get(instrumental).get(charId);
+ }
}
public function setVocalOffset(charId:String, value:Float):Float
@@ -306,7 +337,7 @@ class SongOffsets implements ICloneable
*/
public function toString():String
{
- return 'SongOffsets(${this.instrumental}ms, ${this.altInstrumentals}, ${this.vocals})';
+ return 'SongOffsets(${this.instrumental}ms, ${this.altInstrumentals}, ${this.vocals}, ${this.altVocals})';
}
}
@@ -368,6 +399,12 @@ class SongMusicData implements ICloneable
this.variation = variation == null ? Constants.DEFAULT_VARIATION : variation;
}
+ public function updateVersionToLatest():Void
+ {
+ this.version = SongRegistry.SONG_MUSIC_DATA_VERSION;
+ this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
+ }
+
public function clone():SongMusicData
{
var result:SongMusicData = new SongMusicData(this.songName, this.artist, this.variation);
@@ -509,12 +546,26 @@ class SongCharacterData implements ICloneable
@:default([])
public var altInstrumentals:Array = [];
- public function new(player:String = '', girlfriend:String = '', opponent:String = '', instrumental:String = '')
+ @:optional
+ public var opponentVocals:Null> = null;
+
+ @:optional
+ public var playerVocals:Null> = null;
+
+ public function new(player:String = '', girlfriend:String = '', opponent:String = '', instrumental:String = '', ?altInstrumentals:Array,
+ ?opponentVocals:Array, ?playerVocals:Array)
{
this.player = player;
this.girlfriend = girlfriend;
this.opponent = opponent;
this.instrumental = instrumental;
+
+ this.altInstrumentals = altInstrumentals;
+ this.opponentVocals = opponentVocals;
+ this.playerVocals = playerVocals;
+
+ if (opponentVocals == null) this.opponentVocals = [opponent];
+ if (playerVocals == null) this.playerVocals = [player];
}
public function clone():SongCharacterData
@@ -600,11 +651,20 @@ class SongChartData implements ICloneable
*/
public function serialize(pretty:Bool = true):String
{
+ // Update generatedBy and version before writing.
+ updateVersionToLatest();
+
var ignoreNullOptionals = true;
var writer = new json2object.JsonWriter(ignoreNullOptionals);
return writer.write(this, pretty ? ' ' : null);
}
+ public function updateVersionToLatest():Void
+ {
+ this.version = SongRegistry.SONG_CHART_DATA_VERSION;
+ this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
+ }
+
public function clone():SongChartData
{
// We have to manually perform the deep clone here because Map.deepClone() doesn't work.
@@ -693,18 +753,6 @@ class SongEventDataRaw implements ICloneable
{
return new SongEventDataRaw(this.time, this.eventKind, this.value);
}
-}
-
-/**
- * Wrap SongEventData in an abstract so we can overload operators.
- */
-@:forward(time, eventKind, value, activated, getStepTime, clone)
-abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
-{
- public function new(time:Float, eventKind:String, value:Dynamic = null)
- {
- this = new SongEventDataRaw(time, eventKind, value);
- }
public function valueAsStruct(?defaultKey:String = "key"):Dynamic
{
@@ -728,27 +776,27 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
}
}
- public inline function getHandler():Null
+ public function getHandler():Null
{
return SongEventRegistry.getEvent(this.eventKind);
}
- public inline function getSchema():Null
+ public function getSchema():Null
{
return SongEventRegistry.getEventSchema(this.eventKind);
}
- public inline function getDynamic(key:String):Null
+ public function getDynamic(key:String):Null
{
return this.value == null ? null : Reflect.field(this.value, key);
}
- public inline function getBool(key:String):Null
+ public function getBool(key:String):Null
{
return this.value == null ? null : cast Reflect.field(this.value, key);
}
- public inline function getInt(key:String):Null
+ public function getInt(key:String):Null
{
if (this.value == null) return null;
var result = Reflect.field(this.value, key);
@@ -758,7 +806,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return cast result;
}
- public inline function getFloat(key:String):Null
+ public function getFloat(key:String):Null
{
if (this.value == null) return null;
var result = Reflect.field(this.value, key);
@@ -768,17 +816,17 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return cast result;
}
- public inline function getString(key:String):String
+ public function getString(key:String):String
{
return this.value == null ? null : cast Reflect.field(this.value, key);
}
- public inline function getArray(key:String):Array
+ public function getArray(key:String):Array
{
return this.value == null ? null : cast Reflect.field(this.value, key);
}
- public inline function getBoolArray(key:String):Array
+ public function getBoolArray(key:String):Array
{
return this.value == null ? null : cast Reflect.field(this.value, key);
}
@@ -810,6 +858,19 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return result;
}
+}
+
+/**
+ * Wrap SongEventData in an abstract so we can overload operators.
+ */
+@:forward(time, eventKind, value, activated, getStepTime, clone, getHandler, getSchema, getDynamic, getBool, getInt, getFloat, getString, getArray,
+ getBoolArray, buildTooltip, valueAsStruct)
+abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
+{
+ public function new(time:Float, eventKind:String, value:Dynamic = null)
+ {
+ this = new SongEventDataRaw(time, eventKind, value);
+ }
public function clone():SongEventData
{
@@ -922,12 +983,18 @@ class SongNoteDataRaw implements ICloneable
return this.kind = value;
}
- public function new(time:Float, data:Int, length:Float = 0, kind:String = '')
+ @:alias("p")
+ @:default([])
+ @:optional
+ public var params:Array;
+
+ public function new(time:Float, data:Int, length:Float = 0, kind:String = '', ?params:Array)
{
this.time = time;
this.data = data;
this.length = length;
this.kind = kind;
+ this.params = params ?? [];
}
/**
@@ -1022,9 +1089,19 @@ class SongNoteDataRaw implements ICloneable
_stepLength = null;
}
+ public function cloneParams():Array
+ {
+ var params:Array = [];
+ for (param in this.params)
+ {
+ params.push(param.clone());
+ }
+ return params;
+ }
+
public function clone():SongNoteDataRaw
{
- return new SongNoteDataRaw(this.time, this.data, this.length, this.kind);
+ return new SongNoteDataRaw(this.time, this.data, this.length, this.kind, cloneParams());
}
public function toString():String
@@ -1040,9 +1117,9 @@ class SongNoteDataRaw implements ICloneable
@:forward
abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
{
- public function new(time:Float, data:Int, length:Float = 0, kind:String = '')
+ public function new(time:Float, data:Int, length:Float = 0, kind:String = '', ?params:Array)
{
- this = new SongNoteDataRaw(time, data, length, kind);
+ this = new SongNoteDataRaw(time, data, length, kind, params);
}
public static function buildDirectionName(data:Int, strumlineSize:Int = 4):String
@@ -1086,7 +1163,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
if (other.kind == '' || this.kind == null) return false;
}
- return this.time == other.time && this.data == other.data && this.length == other.length;
+ return this.time == other.time && this.data == other.data && this.length == other.length && this.params == other.params;
}
@:op(A != B)
@@ -1105,7 +1182,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
if (other.kind == '') return true;
}
- return this.time != other.time || this.data != other.data || this.length != other.length;
+ return this.time != other.time || this.data != other.data || this.length != other.length || this.params != other.params;
}
@:op(A > B)
@@ -1142,7 +1219,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
public function clone():SongNoteData
{
- return new SongNoteData(this.time, this.data, this.length, this.kind);
+ return new SongNoteData(this.time, this.data, this.length, this.kind, this.params);
}
/**
@@ -1154,3 +1231,30 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
+ (this.kind != '' ? ' [kind: ${this.kind}])' : ')');
}
}
+
+class NoteParamData implements ICloneable
+{
+ @:alias("n")
+ public var name:String;
+
+ @:alias("v")
+ @:jcustomparse(funkin.data.DataParse.dynamicValue)
+ @:jcustomwrite(funkin.data.DataWrite.dynamicValue)
+ public var value:Dynamic;
+
+ public function new(name:String, value:Dynamic)
+ {
+ this.name = name;
+ this.value = value;
+ }
+
+ public function clone():NoteParamData
+ {
+ return new NoteParamData(this.name, this.value);
+ }
+
+ public function toString():String
+ {
+ return 'NoteParamData(${this.name}, ${this.value})';
+ }
+}
diff --git a/source/funkin/data/song/SongRegistry.hx b/source/funkin/data/song/SongRegistry.hx
index 277dcd9e1f..e7cab246c6 100644
--- a/source/funkin/data/song/SongRegistry.hx
+++ b/source/funkin/data/song/SongRegistry.hx
@@ -20,7 +20,7 @@ class SongRegistry extends BaseRegistry
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateStageData()` function.
*/
- public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.2";
+ public static final SONG_METADATA_VERSION:thx.semver.Version = "2.2.4";
public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.2.x";
diff --git a/source/funkin/data/song/importer/ChartManifestData.hx b/source/funkin/data/song/importer/ChartManifestData.hx
index dd0d284798..04b5a1b69b 100644
--- a/source/funkin/data/song/importer/ChartManifestData.hx
+++ b/source/funkin/data/song/importer/ChartManifestData.hx
@@ -61,10 +61,18 @@ class ChartManifestData
*/
public function serialize(pretty:Bool = true):String
{
+ // Update generatedBy and version before writing.
+ updateVersionToLatest();
+
var writer = new json2object.JsonWriter();
return writer.write(this, pretty ? ' ' : null);
}
+ public function updateVersionToLatest():Void
+ {
+ this.version = CHART_MANIFEST_DATA_VERSION;
+ }
+
public static function deserialize(contents:String):Null
{
var parser = new json2object.JsonParser();
diff --git a/source/funkin/data/song/importer/FNFLegacyImporter.hx b/source/funkin/data/song/importer/FNFLegacyImporter.hx
index ab2abda8e1..96a1051cc4 100644
--- a/source/funkin/data/song/importer/FNFLegacyImporter.hx
+++ b/source/funkin/data/song/importer/FNFLegacyImporter.hx
@@ -36,7 +36,7 @@ class FNFLegacyImporter
{
trace('Migrating song metadata from FNF Legacy.');
- var songMetadata:SongMetadata = new SongMetadata('Import', 'Kawai Sprite', 'default');
+ var songMetadata:SongMetadata = new SongMetadata('Import', Constants.DEFAULT_ARTIST, 'default');
var hadError:Bool = false;
@@ -65,7 +65,7 @@ class FNFLegacyImporter
songMetadata.timeChanges = rebuildTimeChanges(songData);
- songMetadata.playData.characters = new SongCharacterData(songData?.song?.player1 ?? 'bf', 'gf', songData?.song?.player2 ?? 'dad', 'mom');
+ songMetadata.playData.characters = new SongCharacterData(songData?.song?.player1 ?? 'bf', 'gf', songData?.song?.player2 ?? 'dad');
return songMetadata;
}
@@ -199,6 +199,8 @@ class FNFLegacyImporter
{
// Handle the dumb logic for mustHitSection.
var noteData = note.data;
+ if (noteData < 0) continue; // Exclude Psych event notes.
+ if (noteData > (STRUMLINE_SIZE * 2)) noteData = noteData % (2 * STRUMLINE_SIZE); // Handle other engine event notes.
// Flip notes if mustHitSection is FALSE (not true lol).
if (!mustHitSection)
diff --git a/source/funkin/data/stage/StageData.hx b/source/funkin/data/stage/StageData.hx
index 22b883c759..eda8e31481 100644
--- a/source/funkin/data/stage/StageData.hx
+++ b/source/funkin/data/stage/StageData.hx
@@ -58,9 +58,17 @@ class StageData
*/
public function serialize(pretty:Bool = true):String
{
+ // Update generatedBy and version before writing.
+ updateVersionToLatest();
+
var writer = new json2object.JsonWriter();
return writer.write(this, pretty ? ' ' : null);
}
+
+ public function updateVersionToLatest():Void
+ {
+ this.version = StageRegistry.STAGE_DATA_VERSION;
+ }
}
typedef StageDataCharacters =
@@ -132,12 +140,12 @@ typedef StageDataProp =
* If not zero, this prop will play an animation every X beats of the song.
* This requires animations to be defined. If `danceLeft` and `danceRight` are defined,
* they will alternated between, otherwise the `idle` animation will be used.
- *
- * @default 0
+ * Supports up to 0.25 precision.
+ * @default 0.0
*/
- @:default(0)
+ @:default(0.0)
@:optional
- var danceEvery:Int;
+ var danceEvery:Float;
/**
* How much the prop scrolls relative to the camera. Used to create a parallax effect.
diff --git a/source/funkin/data/stage/StageRegistry.hx b/source/funkin/data/stage/StageRegistry.hx
index a033712960..87113ef051 100644
--- a/source/funkin/data/stage/StageRegistry.hx
+++ b/source/funkin/data/stage/StageRegistry.hx
@@ -93,8 +93,8 @@ class StageRegistry extends BaseRegistry
public function listBaseGameStageIds():Array
{
return [
- "mainStage", "spookyMansion", "phillyTrain", "limoRide", "mallXmas", "mallEvil", "school", "schoolEvil", "tankmanBattlefield", "phillyStreets",
- "phillyBlazin",
+ "mainStage", "mainStageErect", "spookyMansion", "phillyTrain", "phillyTrainErect", "limoRide", "limoRideErect", "mallXmas", "mallXmasErect", "mallEvil",
+ "school", "schoolEvil", "tankmanBattlefield", "phillyStreets", "phillyStreetsErect", "phillyBlazin",
];
}
diff --git a/source/funkin/data/story/level/LevelData.hx b/source/funkin/data/story/level/LevelData.hx
index ceb2cc054c..d1b00bbe6e 100644
--- a/source/funkin/data/story/level/LevelData.hx
+++ b/source/funkin/data/story/level/LevelData.hx
@@ -91,11 +91,13 @@ typedef LevelPropData =
/**
* The frequency to bop at, in beats.
- * @default 1 = every beat, 2 = every other beat, etc.
+ * 1 = every beat, 2 = every other beat, etc.
+ * Supports up to 0.25 precision.
+ * @default 1.0
*/
- @:default(1)
+ @:default(1.0)
@:optional
- var danceEvery:Int;
+ var danceEvery:Float;
/**
* The offset on the position to render the prop at.
diff --git a/source/funkin/effects/IntervalShake.hx b/source/funkin/effects/IntervalShake.hx
new file mode 100644
index 0000000000..545739cc33
--- /dev/null
+++ b/source/funkin/effects/IntervalShake.hx
@@ -0,0 +1,240 @@
+package funkin.effects;
+
+import flixel.FlxObject;
+import flixel.util.FlxDestroyUtil.IFlxDestroyable;
+import flixel.util.FlxPool;
+import flixel.util.FlxTimer;
+import flixel.math.FlxPoint;
+import flixel.util.FlxAxes;
+import flixel.tweens.FlxEase.EaseFunction;
+import flixel.math.FlxMath;
+
+/**
+ * pretty much a copy of FlxFlicker geared towards making sprites
+ * shake around at a set interval and slow down over time.
+ */
+class IntervalShake implements IFlxDestroyable
+{
+ static var _pool:FlxPool = new FlxPool(IntervalShake.new);
+
+ /**
+ * Internal map for looking up which objects are currently shaking and getting their shake data.
+ */
+ static var _boundObjects:Map = new Map();
+
+ /**
+ * An effect that shakes the sprite on a set interval and a starting intensity that goes down over time.
+ *
+ * @param Object The object to shake.
+ * @param Duration How long to shake for (in seconds). `0` means "forever".
+ * @param Interval In what interval to update the shake position. Set to `FlxG.elapsed` if `<= 0`!
+ * @param StartIntensity The starting intensity of the shake.
+ * @param EndIntensity The ending intensity of the shake.
+ * @param Ease Control the easing of the intensity over the shake.
+ * @param CompletionCallback Callback on shake completion
+ * @param ProgressCallback Callback on each shake interval
+ * @return The `IntervalShake` object. `IntervalShake`s are pooled internally, so beware of storing references.
+ */
+ public static function shake(Object:FlxObject, Duration:Float = 1, Interval:Float = 0.04, StartIntensity:Float = 0, EndIntensity:Float = 0,
+ Ease:EaseFunction, ?CompletionCallback:IntervalShake->Void, ?ProgressCallback:IntervalShake->Void):IntervalShake
+ {
+ if (isShaking(Object))
+ {
+ // if (ForceRestart)
+ // {
+ // stopShaking(Object);
+ // }
+ // else
+ // {
+ // Ignore this call if object is already flickering.
+ return _boundObjects[Object];
+ // }
+ }
+
+ if (Interval <= 0)
+ {
+ Interval = FlxG.elapsed;
+ }
+
+ var shake:IntervalShake = _pool.get();
+ shake.start(Object, Duration, Interval, StartIntensity, EndIntensity, Ease, CompletionCallback, ProgressCallback);
+ return _boundObjects[Object] = shake;
+ }
+
+ /**
+ * Returns whether the object is shaking or not.
+ *
+ * @param Object The object to test.
+ */
+ public static function isShaking(Object:FlxObject):Bool
+ {
+ return _boundObjects.exists(Object);
+ }
+
+ /**
+ * Stops shaking the object.
+ *
+ * @param Object The object to stop shaking.
+ */
+ public static function stopShaking(Object:FlxObject):Void
+ {
+ var boundShake:IntervalShake = _boundObjects[Object];
+ if (boundShake != null)
+ {
+ boundShake.stop();
+ }
+ }
+
+ /**
+ * The shaking object.
+ */
+ public var object(default, null):FlxObject;
+
+ /**
+ * The shaking timer. You can check how many seconds has passed since shaking started etc.
+ */
+ public var timer(default, null):FlxTimer;
+
+ /**
+ * The starting intensity of the shake.
+ */
+ public var startIntensity(default, null):Float;
+
+ /**
+ * The ending intensity of the shake.
+ */
+ public var endIntensity(default, null):Float;
+
+ /**
+ * How long to shake for (in seconds). `0` means "forever".
+ */
+ public var duration(default, null):Float;
+
+ /**
+ * The interval of the shake.
+ */
+ public var interval(default, null):Float;
+
+ /**
+ * Defines on what axes to `shake()`. Default value is `XY` / both.
+ */
+ public var axes(default, null):FlxAxes;
+
+ /**
+ * Defines the initial position of the object at the beginning of the shake effect.
+ */
+ public var initialOffset(default, null):FlxPoint;
+
+ /**
+ * The callback that will be triggered after the shake has completed.
+ */
+ public var completionCallback(default, null):IntervalShake->Void;
+
+ /**
+ * The callback that will be triggered every time the object shakes.
+ */
+ public var progressCallback(default, null):IntervalShake->Void;
+
+ /**
+ * The easing of the intensity over the shake.
+ */
+ public var ease(default, null):EaseFunction;
+
+ /**
+ * Nullifies the references to prepare object for reuse and avoid memory leaks.
+ */
+ public function destroy():Void
+ {
+ object = null;
+ timer = null;
+ ease = null;
+ completionCallback = null;
+ progressCallback = null;
+ }
+
+ /**
+ * Starts shaking behavior.
+ */
+ function start(Object:FlxObject, Duration:Float = 1, Interval:Float = 0.04, StartIntensity:Float = 0, EndIntensity:Float = 0, Ease:EaseFunction,
+ ?CompletionCallback:IntervalShake->Void, ?ProgressCallback:IntervalShake->Void):Void
+ {
+ object = Object;
+ duration = Duration;
+ interval = Interval;
+ completionCallback = CompletionCallback;
+ startIntensity = StartIntensity;
+ endIntensity = EndIntensity;
+ initialOffset = new FlxPoint(Object.x, Object.y);
+ ease = Ease;
+ axes = FlxAxes.XY;
+ _secondsSinceStart = 0;
+ timer = new FlxTimer().start(interval, shakeProgress, Std.int(duration / interval));
+ }
+
+ /**
+ * Prematurely ends shaking.
+ */
+ public function stop():Void
+ {
+ timer.cancel();
+ // object.visible = true;
+ object.x = initialOffset.x;
+ object.y = initialOffset.y;
+ release();
+ }
+
+ /**
+ * Unbinds the object from shaking and releases it into pool for reuse.
+ */
+ function release():Void
+ {
+ _boundObjects.remove(object);
+ _pool.put(this);
+ }
+
+ public var _secondsSinceStart(default, null):Float = 0;
+
+ public var scale(default, null):Float = 0;
+
+ /**
+ * Just a helper function for shake() to update object's position.
+ */
+ function shakeProgress(timer:FlxTimer):Void
+ {
+ _secondsSinceStart += interval;
+ scale = _secondsSinceStart / duration;
+ if (ease != null)
+ {
+ scale = 1 - ease(scale);
+ // trace(scale);
+ }
+
+ var curIntensity:Float = 0;
+ curIntensity = FlxMath.lerp(endIntensity, startIntensity, scale);
+
+ if (axes.x) object.x = initialOffset.x + FlxG.random.float((-curIntensity) * object.width, (curIntensity) * object.width);
+ if (axes.y) object.y = initialOffset.y + FlxG.random.float((-curIntensity) * object.width, (curIntensity) * object.width);
+
+ // object.visible = !object.visible;
+
+ if (progressCallback != null) progressCallback(this);
+
+ if (timer.loops > 0 && timer.loopsLeft == 0)
+ {
+ object.x = initialOffset.x;
+ object.y = initialOffset.y;
+ if (completionCallback != null)
+ {
+ completionCallback(this);
+ }
+
+ if (this.timer == timer) release();
+ }
+ }
+
+ /**
+ * Internal constructor. Use static methods.
+ */
+ @:keep
+ function new() {}
+}
diff --git a/source/funkin/effects/RetroCameraFade.hx b/source/funkin/effects/RetroCameraFade.hx
new file mode 100644
index 0000000000..d4c1da5efc
--- /dev/null
+++ b/source/funkin/effects/RetroCameraFade.hx
@@ -0,0 +1,106 @@
+package funkin.effects;
+
+import flixel.util.FlxTimer;
+import flixel.FlxCamera;
+import openfl.filters.ColorMatrixFilter;
+
+class RetroCameraFade
+{
+ // im lazy, but we only use this for week 6
+ // and also sorta yoinked for djflixel, lol !
+ public static function fadeWhite(camera:FlxCamera, camSteps:Int = 5, time:Float = 1):Void
+ {
+ var steps:Int = 0;
+ var stepsTotal:Int = camSteps;
+
+ new FlxTimer().start(time / stepsTotal, _ -> {
+ var V:Float = (1 / stepsTotal) * steps;
+ if (steps == stepsTotal) V = 1;
+
+ var matrix = [
+ 1, 0, 0, 0, V * 255,
+ 0, 1, 0, 0, V * 255,
+ 0, 0, 1, 0, V * 255,
+ 0, 0, 0, 1, 0
+ ];
+ camera.filters = [new ColorMatrixFilter(matrix)];
+ steps++;
+ }, stepsTotal + 1);
+ }
+
+ public static function fadeFromWhite(camera:FlxCamera, camSteps:Int = 5, time:Float = 1):Void
+ {
+ var steps:Int = camSteps;
+ var stepsTotal:Int = camSteps;
+
+ var matrixDerp = [
+ 1, 0, 0, 0, 1.0 * 255,
+ 0, 1, 0, 0, 1.0 * 255,
+ 0, 0, 1, 0, 1.0 * 255,
+ 0, 0, 0, 1, 0
+ ];
+ camera.filters = [new ColorMatrixFilter(matrixDerp)];
+
+ new FlxTimer().start(time / stepsTotal, _ -> {
+ var V:Float = (1 / stepsTotal) * steps;
+ if (steps == stepsTotal) V = 1;
+
+ var matrix = [
+ 1, 0, 0, 0, V * 255,
+ 0, 1, 0, 0, V * 255,
+ 0, 0, 1, 0, V * 255,
+ 0, 0, 0, 1, 0
+ ];
+ camera.filters = [new ColorMatrixFilter(matrix)];
+ steps--;
+ }, camSteps);
+ }
+
+ public static function fadeToBlack(camera:FlxCamera, camSteps:Int = 5, time:Float = 1):Void
+ {
+ var steps:Int = 0;
+ var stepsTotal:Int = camSteps;
+
+ new FlxTimer().start(time / stepsTotal, _ -> {
+ var V:Float = (1 / stepsTotal) * steps;
+ if (steps == stepsTotal) V = 1;
+
+ var matrix = [
+ 1, 0, 0, 0, -V * 255,
+ 0, 1, 0, 0, -V * 255,
+ 0, 0, 1, 0, -V * 255,
+ 0, 0, 0, 1, 0
+ ];
+ camera.filters = [new ColorMatrixFilter(matrix)];
+ steps++;
+ }, camSteps);
+ }
+
+ public static function fadeBlack(camera:FlxCamera, camSteps:Int = 5, time:Float = 1):Void
+ {
+ var steps:Int = camSteps;
+ var stepsTotal:Int = camSteps;
+
+ var matrixDerp = [
+ 1, 0, 0, 0, -1.0 * 255,
+ 0, 1, 0, 0, -1.0 * 255,
+ 0, 0, 1, 0, -1.0 * 255,
+ 0, 0, 0, 1, 0
+ ];
+ camera.filters = [new ColorMatrixFilter(matrixDerp)];
+
+ new FlxTimer().start(time / stepsTotal, _ -> {
+ var V:Float = (1 / stepsTotal) * steps;
+ if (steps == stepsTotal) V = 1;
+
+ var matrix = [
+ 1, 0, 0, 0, -V * 255,
+ 0, 1, 0, 0, -V * 255,
+ 0, 0, 1, 0, -V * 255,
+ 0, 0, 0, 1, 0
+ ];
+ camera.filters = [new ColorMatrixFilter(matrix)];
+ steps--;
+ }, camSteps + 1);
+ }
+}
diff --git a/source/funkin/graphics/FlxFilteredSprite.hx b/source/funkin/graphics/FlxFilteredSprite.hx
new file mode 100644
index 0000000000..ea0376c3d0
--- /dev/null
+++ b/source/funkin/graphics/FlxFilteredSprite.hx
@@ -0,0 +1,419 @@
+package funkin.graphics;
+
+import flixel.FlxBasic;
+import flixel.FlxCamera;
+import flixel.FlxG;
+import flixel.FlxSprite;
+import flixel.graphics.FlxGraphic;
+import flixel.graphics.frames.FlxFrame;
+import flixel.math.FlxMatrix;
+import flixel.math.FlxPoint;
+import flixel.math.FlxRect;
+import flixel.util.FlxColor;
+import lime.graphics.cairo.Cairo;
+import openfl.display.BitmapData;
+import openfl.display.BlendMode;
+import openfl.display.DisplayObjectRenderer;
+import openfl.display.Graphics;
+import openfl.display.OpenGLRenderer;
+import openfl.display._internal.Context3DGraphics;
+import openfl.display3D.Context3D;
+import openfl.display3D.Context3DClearMask;
+import openfl.filters.BitmapFilter;
+import openfl.filters.BlurFilter;
+import openfl.geom.ColorTransform;
+import openfl.geom.Matrix;
+import openfl.geom.Point;
+import openfl.geom.Rectangle;
+#if (js && html5)
+import lime._internal.graphics.ImageCanvasUtil;
+import openfl.display.CanvasRenderer;
+import openfl.display._internal.CanvasGraphics as GfxRenderer;
+#else
+import openfl.display.CairoRenderer;
+import openfl.display._internal.CairoGraphics as GfxRenderer;
+#end
+
+/**
+ * A modified `FlxSprite` that supports filters.
+ * The name's pretty much self-explanatory.
+ */
+@:access(openfl.geom.Rectangle)
+@:access(openfl.filters.BitmapFilter)
+@:access(flixel.graphics.frames.FlxFrame)
+class FlxFilteredSprite extends FlxSprite
+{
+ @:noCompletion var _renderer:FlxAnimateFilterRenderer = new FlxAnimateFilterRenderer();
+
+ @:noCompletion var _filterMatrix:FlxMatrix;
+
+ /**
+ * An `Array` of shader filters (aka `BitmapFilter`).
+ */
+ public var filters(default, set):Array;
+
+ /**
+ * a flag to update the image with the filters.
+ * Useful when trying to render a shader at all times.
+ */
+ public var filterDirty:Bool = false;
+
+ @:noCompletion var filtered:Bool;
+
+ @:noCompletion var _blankFrame:FlxFrame;
+
+ var _filterBmp1:BitmapData;
+ var _filterBmp2:BitmapData;
+
+ override public function update(elapsed:Float)
+ {
+ super.update(elapsed);
+ if (!filterDirty && filters != null)
+ {
+ for (filter in filters)
+ {
+ if (filter.__renderDirty)
+ {
+ filterDirty = true;
+ break;
+ }
+ }
+ }
+ }
+
+ @:noCompletion
+ override function initVars():Void
+ {
+ super.initVars();
+ _filterMatrix = new FlxMatrix();
+ filters = null;
+ filtered = false;
+ }
+
+ override public function draw():Void
+ {
+ checkEmptyFrame();
+
+ if (alpha == 0 || _frame.type == FlxFrameType.EMPTY) return;
+
+ if (dirty) // rarely
+ calcFrame(useFramePixels);
+
+ if (filterDirty) filterFrame();
+
+ for (camera in cameras)
+ {
+ if (!camera.visible || !camera.exists || !isOnScreen(camera)) continue;
+
+ getScreenPosition(_point, camera).subtractPoint(offset);
+
+ if (isSimpleRender(camera)) drawSimple(camera);
+ else
+ drawComplex(camera);
+
+ #if FLX_DEBUG
+ FlxBasic.visibleCount++;
+ #end
+ }
+
+ #if FLX_DEBUG
+ if (FlxG.debugger.drawDebug) drawDebug();
+ #end
+ }
+
+ @:noCompletion
+ override function drawComplex(camera:FlxCamera):Void
+ {
+ _frame.prepareMatrix(_matrix, FlxFrameAngle.ANGLE_0, checkFlipX(), checkFlipY());
+ _matrix.concat(_filterMatrix);
+ _matrix.translate(-origin.x, -origin.y);
+ _matrix.scale(scale.x, scale.y);
+
+ if (bakedRotationAngle <= 0)
+ {
+ updateTrig();
+
+ if (angle != 0) _matrix.rotateWithTrig(_cosAngle, _sinAngle);
+ }
+
+ _point.add(origin.x, origin.y);
+ _matrix.translate(_point.x, _point.y);
+
+ if (isPixelPerfectRender(camera))
+ {
+ _matrix.tx = Math.floor(_matrix.tx);
+ _matrix.ty = Math.floor(_matrix.ty);
+ }
+
+ camera.drawPixels((filtered) ? _blankFrame : _frame, framePixels, _matrix, colorTransform, blend, antialiasing, shader);
+ }
+
+ @:noCompletion
+ function filterFrame()
+ {
+ filterDirty = false;
+ _filterMatrix.identity();
+
+ if (filters != null && filters.length > 0)
+ {
+ _flashRect.setEmpty();
+
+ for (filter in filters)
+ {
+ _flashRect.__expand(-filter.__leftExtension,
+ -filter.__topExtension, filter.__leftExtension
+ + filter.__rightExtension,
+ filter.__topExtension
+ + filter.__bottomExtension);
+ }
+ _flashRect.width += frameWidth;
+ _flashRect.height += frameHeight;
+ if (_blankFrame == null) _blankFrame = new FlxFrame(null);
+
+ if (_blankFrame.parent == null || _flashRect.width > _blankFrame.parent.width || _flashRect.height > _blankFrame.parent.height)
+ {
+ if (_blankFrame.parent != null)
+ {
+ _blankFrame.parent.destroy();
+ _filterBmp1.dispose();
+ _filterBmp2.dispose();
+ }
+
+ _blankFrame.parent = FlxGraphic.fromRectangle(Math.ceil(_flashRect.width * 1.25), Math.ceil(_flashRect.height * 1.25), 0, true);
+ _filterBmp1 = new BitmapData(_blankFrame.parent.width, _blankFrame.parent.height, 0);
+ _filterBmp2 = new BitmapData(_blankFrame.parent.width, _blankFrame.parent.height, 0);
+ }
+ _blankFrame.offset.copyFrom(_frame.offset);
+ _blankFrame.parent.bitmap = _renderer.applyFilter(_blankFrame.parent.bitmap, _filterBmp1, _filterBmp2, frame.parent.bitmap, filters, _flashRect,
+ frame.frame.copyToFlash());
+ _blankFrame.frame = FlxRect.get(0, 0, _blankFrame.parent.bitmap.width, _blankFrame.parent.bitmap.height);
+ _filterMatrix.translate(_flashRect.x, _flashRect.y);
+ _frame = _blankFrame.copyTo();
+ filtered = true;
+ }
+ else
+ {
+ resetFrame();
+ filtered = false;
+ }
+ }
+
+ @:noCompletion
+ function set_filters(value:Array)
+ {
+ if (filters != value) filterDirty = true;
+
+ return filters = value;
+ }
+
+ @:noCompletion
+ override function set_frame(value:FlxFrame)
+ {
+ if (value != frame) filterDirty = true;
+
+ return super.set_frame(value);
+ }
+
+ override public function destroy()
+ {
+ super.destroy();
+ }
+}
+
+@:noCompletion
+@:access(openfl.display.OpenGLRenderer)
+@:access(openfl.filters.BitmapFilter)
+@:access(openfl.geom.Rectangle)
+@:access(openfl.display.Stage)
+@:access(openfl.display.Graphics)
+@:access(openfl.display.Shader)
+@:access(openfl.display.BitmapData)
+@:access(openfl.geom.ColorTransform)
+@:access(openfl.display.DisplayObject)
+@:access(openfl.display3D.Context3D)
+@:access(openfl.display.CanvasRenderer)
+@:access(openfl.display.CairoRenderer)
+@:access(openfl.display3D.Context3D)
+class FlxAnimateFilterRenderer
+{
+ var renderer:OpenGLRenderer;
+ var context:Context3D;
+
+ public function new()
+ {
+ // context = new openfl.display3D.Context3D(null);
+ renderer = new OpenGLRenderer(FlxG.game.stage.context3D);
+ renderer.__worldTransform = new Matrix();
+ renderer.__worldColorTransform = new ColorTransform();
+ }
+
+ @:noCompletion function setRenderer(renderer:DisplayObjectRenderer, rect:Rectangle)
+ {
+ @:privateAccess
+ if (true)
+ {
+ var displayObject = FlxG.game;
+ var pixelRatio = FlxG.game.stage.__renderer.__pixelRatio;
+
+ var offsetX = rect.x > 0 ? Math.ceil(rect.x) : Math.floor(rect.x);
+ var offsetY = rect.y > 0 ? Math.ceil(rect.y) : Math.floor(rect.y);
+ if (renderer.__worldTransform == null)
+ {
+ renderer.__worldTransform = new Matrix();
+ renderer.__worldColorTransform = new ColorTransform();
+ }
+ if (displayObject.__cacheBitmapColorTransform == null) displayObject.__cacheBitmapColorTransform = new ColorTransform();
+
+ renderer.__stage = displayObject.stage;
+
+ renderer.__allowSmoothing = true;
+ renderer.__setBlendMode(NORMAL);
+ renderer.__worldAlpha = 1 / displayObject.__worldAlpha;
+
+ renderer.__worldTransform.identity();
+ renderer.__worldTransform.invert();
+ renderer.__worldTransform.concat(new Matrix());
+ renderer.__worldTransform.tx -= offsetX;
+ renderer.__worldTransform.ty -= offsetY;
+ renderer.__worldTransform.scale(pixelRatio, pixelRatio);
+
+ renderer.__pixelRatio = pixelRatio;
+ }
+ }
+
+ public function applyFilter(target:BitmapData = null, target1:BitmapData = null, target2:BitmapData = null, bmp:BitmapData, filters:Array,
+ rect:Rectangle, bmpRect:Rectangle)
+ {
+ if (filters == null || filters.length == 0) return bmp;
+
+ renderer.__setBlendMode(NORMAL);
+ renderer.__worldAlpha = 1;
+
+ if (renderer.__worldTransform == null)
+ {
+ renderer.__worldTransform = new Matrix();
+ renderer.__worldColorTransform = new ColorTransform();
+ }
+ renderer.__worldTransform.identity();
+ renderer.__worldColorTransform.__identity();
+
+ var bitmap:BitmapData = (target == null) ? new BitmapData(Math.ceil(rect.width * 1.25), Math.ceil(rect.height * 1.25), true, 0) : target;
+
+ var bitmap2 = (target1 == null) ? new BitmapData(Math.ceil(rect.width * 1.25), Math.ceil(rect.height * 1.25), true, 0) : target1,
+ bitmap3 = (target2 == null) ? bitmap2.clone() : target2;
+ renderer.__setRenderTarget(bitmap);
+
+ bmp.__renderTransform.translate(Math.abs(rect.x) - bmpRect.x, Math.abs(rect.y) - bmpRect.y);
+ bmpRect.x = Math.abs(rect.x);
+ bmpRect.y = Math.abs(rect.y);
+
+ var bestResolution = renderer.__context3D.__backBufferWantsBestResolution;
+ renderer.__context3D.__backBufferWantsBestResolution = false;
+ renderer.__scissorRect(bmpRect);
+ renderer.__renderFilterPass(bmp, renderer.__defaultDisplayShader, true);
+ renderer.__scissorRect();
+
+ renderer.__context3D.__backBufferWantsBestResolution = bestResolution;
+
+ bmp.__renderTransform.identity();
+
+ var shader, cacheBitmap = null;
+ for (filter in filters)
+ {
+ if (filter.__preserveObject)
+ {
+ renderer.__setRenderTarget(bitmap3);
+ renderer.__renderFilterPass(bitmap, renderer.__defaultDisplayShader, filter.__smooth);
+ }
+
+ for (i in 0...filter.__numShaderPasses)
+ {
+ shader = filter.__initShader(renderer, i, (filter.__preserveObject) ? bitmap3 : null);
+ renderer.__setBlendMode(filter.__shaderBlendMode);
+ renderer.__setRenderTarget(bitmap2);
+ renderer.__renderFilterPass(bitmap, shader, filter.__smooth);
+
+ cacheBitmap = bitmap;
+ bitmap = bitmap2;
+ bitmap2 = cacheBitmap;
+ }
+ filter.__renderDirty = false;
+ }
+ if (target1 == null) bitmap2.dispose();
+ if (target2 == null) bitmap3.dispose();
+
+ // var gl = renderer.__gl;
+
+ // var renderBuffer = bitmap.getTexture(renderer.__context3D);
+ // @:privateAccess
+ // gl.readPixels(0, 0, bitmap.width, bitmap.height, renderBuffer.__format, gl.UNSIGNED_BYTE, bitmap.image.data);
+ // bitmap.image.version = 0;
+ // @:privateAccess
+ // bitmap.__textureVersion = -1;
+
+ return bitmap;
+ }
+
+ public function applyBlend(blend:BlendMode, bitmap:BitmapData)
+ {
+ bitmap.__update(false, true);
+ var bmp = new BitmapData(bitmap.width, bitmap.height, 0);
+
+ #if (js && html5)
+ ImageCanvasUtil.convertToCanvas(bmp.image);
+ @:privateAccess
+ var renderer = new CanvasRenderer(bmp.image.buffer.__srcContext);
+ #else
+ var renderer = new CairoRenderer(new Cairo(bmp.getSurface()));
+ #end
+
+ // setRenderer(renderer, bmp.rect);
+
+ var m = new Matrix();
+ var c = new ColorTransform();
+ renderer.__allowSmoothing = true;
+ renderer.__overrideBlendMode = blend;
+ renderer.__worldTransform = m;
+ renderer.__worldAlpha = 1;
+ renderer.__worldColorTransform = c;
+
+ renderer.__setBlendMode(blend);
+ #if (js && html5)
+ bmp.__drawCanvas(bitmap, renderer);
+ #else
+ bmp.__drawCairo(bitmap, renderer);
+ #end
+
+ return bitmap;
+ }
+
+ public function graphicstoBitmapData(gfx:Graphics)
+ {
+ if (gfx.__bounds == null) return null;
+ // var cacheRTT = renderer.__context3D.__state.renderToTexture;
+ // var cacheRTTDepthStencil = renderer.__context3D.__state.renderToTextureDepthStencil;
+ // var cacheRTTAntiAlias = renderer.__context3D.__state.renderToTextureAntiAlias;
+ // var cacheRTTSurfaceSelector = renderer.__context3D.__state.renderToTextureSurfaceSelector;
+
+ // var bmp = new BitmapData(Math.ceil(gfx.__width), Math.ceil(gfx.__height), 0);
+ // renderer.__context3D.setRenderToTexture(bmp.getTexture(renderer.__context3D));
+ // gfx.__owner.__renderTransform.identity();
+ // gfx.__renderTransform.identity();
+ // Context3DGraphics.render(gfx, renderer);
+ GfxRenderer.render(gfx, cast renderer.__softwareRenderer);
+ var bmp = gfx.__bitmap;
+
+ gfx.__bitmap = null;
+
+ // if (cacheRTT != null)
+ // {
+ // renderer.__context3D.setRenderToTexture(cacheRTT, cacheRTTDepthStencil, cacheRTTAntiAlias, cacheRTTSurfaceSelector);
+ // }
+ // else
+ // {
+ // renderer.__context3D.setRenderToBackBuffer();
+ // }
+
+ return bmp;
+ }
+}
diff --git a/source/funkin/graphics/FunkinSprite.hx b/source/funkin/graphics/FunkinSprite.hx
index bfd2e80285..521553527c 100644
--- a/source/funkin/graphics/FunkinSprite.hx
+++ b/source/funkin/graphics/FunkinSprite.hx
@@ -7,6 +7,10 @@ import flixel.tweens.FlxTween;
import openfl.display3D.textures.TextureBase;
import funkin.graphics.framebuffer.FixedBitmapData;
import openfl.display.BitmapData;
+import flixel.math.FlxRect;
+import flixel.math.FlxPoint;
+import flixel.graphics.frames.FlxFrame;
+import flixel.FlxCamera;
/**
* An FlxSprite with additional functionality.
@@ -269,6 +273,103 @@ class FunkinSprite extends FlxSprite
return result;
}
+ @:access(flixel.FlxCamera)
+ override function getBoundingBox(camera:FlxCamera):FlxRect
+ {
+ getScreenPosition(_point, camera);
+
+ _rect.set(_point.x, _point.y, width, height);
+ _rect = camera.transformRect(_rect);
+
+ if (isPixelPerfectRender(camera))
+ {
+ _rect.width = _rect.width / this.scale.x;
+ _rect.height = _rect.height / this.scale.y;
+ _rect.x = _rect.x / this.scale.x;
+ _rect.y = _rect.y / this.scale.y;
+ _rect.floor();
+ _rect.x = _rect.x * this.scale.x;
+ _rect.y = _rect.y * this.scale.y;
+ _rect.width = _rect.width * this.scale.x;
+ _rect.height = _rect.height * this.scale.y;
+ }
+
+ return _rect;
+ }
+
+ /**
+ * Returns the screen position of this object.
+ *
+ * @param result Optional arg for the returning point
+ * @param camera The desired "screen" coordinate space. If `null`, `FlxG.camera` is used.
+ * @return The screen position of this object.
+ */
+ public override function getScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint
+ {
+ if (result == null) result = FlxPoint.get();
+
+ if (camera == null) camera = FlxG.camera;
+
+ result.set(x, y);
+ if (pixelPerfectPosition)
+ {
+ _rect.width = _rect.width / this.scale.x;
+ _rect.height = _rect.height / this.scale.y;
+ _rect.x = _rect.x / this.scale.x;
+ _rect.y = _rect.y / this.scale.y;
+ _rect.round();
+ _rect.x = _rect.x * this.scale.x;
+ _rect.y = _rect.y * this.scale.y;
+ _rect.width = _rect.width * this.scale.x;
+ _rect.height = _rect.height * this.scale.y;
+ }
+
+ return result.subtract(camera.scroll.x * scrollFactor.x, camera.scroll.y * scrollFactor.y);
+ }
+
+ override function drawSimple(camera:FlxCamera):Void
+ {
+ getScreenPosition(_point, camera).subtractPoint(offset);
+ if (isPixelPerfectRender(camera))
+ {
+ _point.x = _point.x / this.scale.x;
+ _point.y = _point.y / this.scale.y;
+ _point.round();
+
+ _point.x = _point.x * this.scale.x;
+ _point.y = _point.y * this.scale.y;
+ }
+
+ _point.copyToFlash(_flashPoint);
+ camera.copyPixels(_frame, framePixels, _flashRect, _flashPoint, colorTransform, blend, antialiasing);
+ }
+
+ override function drawComplex(camera:FlxCamera):Void
+ {
+ _frame.prepareMatrix(_matrix, FlxFrameAngle.ANGLE_0, checkFlipX(), checkFlipY());
+ _matrix.translate(-origin.x, -origin.y);
+ _matrix.scale(scale.x, scale.y);
+
+ if (bakedRotationAngle <= 0)
+ {
+ updateTrig();
+
+ if (angle != 0) _matrix.rotateWithTrig(_cosAngle, _sinAngle);
+ }
+
+ getScreenPosition(_point, camera).subtractPoint(offset);
+ _point.add(origin.x, origin.y);
+ _matrix.translate(_point.x, _point.y);
+
+ if (isPixelPerfectRender(camera))
+ {
+ _matrix.tx = Math.round(_matrix.tx / this.scale.x) * this.scale.x;
+ _matrix.ty = Math.round(_matrix.ty / this.scale.y) * this.scale.y;
+ }
+
+ camera.drawPixels(_frame, framePixels, _matrix, colorTransform, blend, antialiasing, shader);
+ }
+
public override function destroy():Void
{
frames = null;
diff --git a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx
index 8a77c1c859..952fa8b717 100644
--- a/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx
+++ b/source/funkin/graphics/adobeanimate/FlxAtlasSprite.hx
@@ -4,8 +4,12 @@ import flixel.util.FlxSignal.FlxTypedSignal;
import flxanimate.FlxAnimate;
import flxanimate.FlxAnimate.Settings;
import flxanimate.frames.FlxAnimateFrames;
+import flixel.graphics.frames.FlxFrame;
+import flixel.system.FlxAssets.FlxGraphicAsset;
import openfl.display.BitmapData;
import openfl.utils.Assets;
+import flixel.math.FlxPoint;
+import flxanimate.animate.FlxKeyFrame;
/**
* A sprite which provides convenience functions for rendering a texture atlas with animations.
@@ -18,16 +22,21 @@ class FlxAtlasSprite extends FlxAnimate
FrameRate: 24.0,
Reversed: false,
// ?OnComplete:Void -> Void,
- ShowPivot: #if debug false #else false #end,
+ ShowPivot: false,
Antialiasing: true,
ScrollFactor: null,
// Offset: new FlxPoint(0, 0), // This is just FlxSprite.offset
};
/**
- * Signal dispatched when an animation finishes playing.
+ * Signal dispatched when an animation advances to the next frame.
*/
- public var onAnimationFinish:FlxTypedSignalVoid> = new FlxTypedSignalVoid>();
+ public var onAnimationFrame:FlxTypedSignalInt->Void> = new FlxTypedSignal();
+
+ /**
+ * Signal dispatched when a non-looping animation finishes playing.
+ */
+ public var onAnimationComplete:FlxTypedSignalVoid> = new FlxTypedSignal();
var currentAnimation:String;
@@ -42,19 +51,28 @@ class FlxAtlasSprite extends FlxAnimate
throw 'Null path specified for FlxAtlasSprite!';
}
+ // Validate asset path.
+ if (!Assets.exists('${path}/Animation.json'))
+ {
+ throw 'FlxAtlasSprite does not have an Animation.json file at the specified path (${path})';
+ }
+
super(x, y, path, settings);
- if (this.anim.curInstance == null)
+ if (this.anim.stageInstance == null)
{
throw 'FlxAtlasSprite not initialized properly. Are you sure the path (${path}) exists?';
}
- onAnimationFinish.add(cleanupAnimation);
+ onAnimationComplete.add(cleanupAnimation);
// This defaults the sprite to play the first animation in the atlas,
// then pauses it. This ensures symbols are intialized properly.
this.anim.play('');
this.anim.pause();
+
+ this.anim.onComplete.add(_onAnimationComplete);
+ this.anim.onFrame.add(_onAnimationFrame);
}
/**
@@ -62,9 +80,13 @@ class FlxAtlasSprite extends FlxAnimate
*/
public function listAnimations():Array
{
- if (this.anim == null) return [];
- return this.anim.getFrameLabels();
- // return [""];
+ var mainSymbol = this.anim.symbolDictionary[this.anim.stageInstance.symbol.name];
+ if (mainSymbol == null)
+ {
+ FlxG.log.error('FlxAtlasSprite does not have its main symbol!');
+ return [];
+ }
+ return mainSymbol.getFrameLabels().map(keyFrame -> keyFrame.name).filterNull();
}
/**
@@ -73,7 +95,7 @@ class FlxAtlasSprite extends FlxAnimate
*/
public function hasAnimation(id:String):Bool
{
- return getLabelIndex(id) != -1;
+ return getLabelIndex(id) != -1 || anim.symbolDictionary.exists(id);
}
/**
@@ -84,22 +106,13 @@ class FlxAtlasSprite extends FlxAnimate
return this.currentAnimation;
}
- /**
- * `anim.finished` always returns false on looping animations,
- * but this function will return true if we are on the last frame of the looping animation.
- */
- public function isLoopFinished():Bool
- {
- if (this.anim == null) return false;
- if (!this.anim.isPlaying) return false;
+ var _completeAnim:Bool = false;
- // Reverse animation finished.
- if (this.anim.reversed && this.anim.curFrame == 0) return true;
- // Forward animation finished.
- if (!this.anim.reversed && this.anim.curFrame >= (this.anim.length - 1)) return true;
+ var fr:FlxKeyFrame = null;
- return false;
- }
+ var looping:Bool = false;
+
+ public var ignoreExclusionPref:Array = [];
/**
* Plays an animation.
@@ -107,61 +120,86 @@ class FlxAtlasSprite extends FlxAnimate
* @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
+ * @param startFrame The frame to start the animation on
* NOTE: `loop` and `ignoreOther` are not compatible with each other!
*/
- public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false, ?loop:Bool = false):Void
+ public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false, loop:Bool = false, startFrame:Int = 0):Void
{
- if (loop == null) loop = false;
-
// Skip if not allowed to play animations.
- if ((!canPlayOtherAnims && !ignoreOther)) return;
+ if ((!canPlayOtherAnims))
+ {
+ if (this.currentAnimation == id && restart) {}
+ else if (ignoreExclusionPref != null && ignoreExclusionPref.length > 0)
+ {
+ var detected:Bool = false;
+ for (entry in ignoreExclusionPref)
+ {
+ if (StringTools.startsWith(id, entry))
+ {
+ detected = true;
+ break;
+ }
+ }
+ if (!detected) return;
+ }
+ else
+ return;
+ }
+
+ if (anim == null) return;
if (id == null || id == '') id = this.currentAnimation;
if (this.currentAnimation == id && !restart)
{
- if (anim.isPlaying)
- {
- // Skip if animation is already playing.
- return;
- }
- else
+ if (!anim.isPlaying)
{
+ if (fr != null) anim.curFrame = fr.index + startFrame;
+ else
+ anim.curFrame = startFrame;
+
// Resume animation if it's paused.
- anim.play('', false, false);
+ anim.resume();
}
- }
- // Skip if the animation doesn't exist
- if (!hasAnimation(id))
+ return;
+ }
+ else if (!hasAnimation(id))
{
+ // Skip if the animation doesn't exist
trace('Animation ' + id + ' not found');
return;
}
- anim.callback = function(_, frame:Int) {
- var offset = loop ? 0 : -1;
+ this.currentAnimation = id;
+ anim.onComplete.removeAll();
+ anim.onComplete.add(function() {
+ _onAnimationComplete();
+ });
- var frameLabel = anim.getFrameLabel(id);
- if (frame == (frameLabel.duration + offset) + frameLabel.index)
- {
- if (loop)
- {
- playAnimation(id, true, false, true);
- }
- else
- {
- onAnimationFinish.dispatch(id);
- }
- }
- };
+ looping = loop;
// Prevent other animations from playing if `ignoreOther` is true.
if (ignoreOther) canPlayOtherAnims = false;
// Move to the first frame of the animation.
- goToFrameLabel(id);
- this.currentAnimation = id;
+ // goToFrameLabel(id);
+ trace('Playing animation $id');
+ if ((id == null || id == "") || this.anim.symbolDictionary.exists(id) || (this.anim.getByName(id) != null))
+ {
+ this.anim.play(id, restart, false, startFrame);
+
+ this.currentAnimation = anim.curSymbol.name;
+
+ fr = null;
+ }
+ // Only call goToFrameLabel if there is a frame label with that name. This prevents annoying warnings!
+ if (getFrameLabelNames().indexOf(id) != -1)
+ {
+ goToFrameLabel(id);
+ fr = anim.getFrameLabel(id);
+ anim.curFrame += startFrame;
+ }
}
override public function update(elapsed:Float)
@@ -169,6 +207,29 @@ class FlxAtlasSprite extends FlxAnimate
super.update(elapsed);
}
+ /**
+ * Returns true if the animation has finished playing.
+ * Never true if animation is configured to loop.
+ */
+ public function isAnimationFinished():Bool
+ {
+ return this.anim.finished;
+ }
+
+ /**
+ * Returns true if the animation has reached the last frame.
+ * Can be true even if animation is configured to loop.
+ */
+ public function isLoopComplete():Bool
+ {
+ if (this.anim == null) return false;
+ if (!this.anim.isPlaying) return false;
+
+ if (fr != null) return (anim.reversed && anim.curFrame < fr.index || !anim.reversed && anim.curFrame >= (fr.index + fr.duration));
+
+ return (anim.reversed && anim.curFrame == 0 || !(anim.reversed) && (anim.curFrame) >= (anim.length - 1));
+ }
+
/**
* Stops the current animation.
*/
@@ -192,6 +253,18 @@ class FlxAtlasSprite extends FlxAnimate
this.anim.goToFrameLabel(label);
}
+ function getFrameLabelNames(?layer:haxe.extern.EitherType = null)
+ {
+ var labels = this.anim.getFrameLabels(layer);
+ var array = [];
+ for (label in labels)
+ {
+ array.push(label.name);
+ }
+
+ return array;
+ }
+
function getNextFrameLabel(label:String):String
{
return listAnimations()[(getLabelIndex(label) + 1) % listAnimations().length];
@@ -213,4 +286,95 @@ class FlxAtlasSprite extends FlxAnimate
// this.currentAnimation = null;
this.anim.pause();
}
+
+ function _onAnimationFrame(frame:Int):Void
+ {
+ if (currentAnimation != null)
+ {
+ onAnimationFrame.dispatch(currentAnimation, frame);
+
+ if (isLoopComplete())
+ {
+ anim.pause();
+ _onAnimationComplete();
+
+ if (looping)
+ {
+ anim.curFrame = (fr != null) ? fr.index : 0;
+ anim.resume();
+ }
+ else if (fr != null && anim.curFrame != anim.length - 1)
+ {
+ anim.curFrame--;
+ }
+ }
+ }
+ }
+
+ function _onAnimationComplete():Void
+ {
+ if (currentAnimation != null)
+ {
+ onAnimationComplete.dispatch(currentAnimation);
+ }
+ else
+ {
+ onAnimationComplete.dispatch('');
+ }
+ }
+
+ var prevFrames:Map = [];
+
+ public function replaceFrameGraphic(index:Int, ?graphic:FlxGraphicAsset):Void
+ {
+ if (graphic == null || !Assets.exists(graphic))
+ {
+ var prevFrame:Null = prevFrames.get(index);
+ if (prevFrame == null) return;
+
+ prevFrame.copyTo(frames.getByIndex(index));
+ return;
+ }
+
+ var prevFrame:FlxFrame = prevFrames.get(index) ?? frames.getByIndex(index).copyTo();
+ prevFrames.set(index, prevFrame);
+
+ var frame = FlxG.bitmap.add(graphic).imageFrame.frame;
+ frame.copyTo(frames.getByIndex(index));
+
+ // Additional sizing fix.
+ @:privateAccess
+ if (true)
+ {
+ var frame = frames.getByIndex(index);
+ frame.tileMatrix[0] = prevFrame.frame.width / frame.frame.width;
+ frame.tileMatrix[3] = prevFrame.frame.height / frame.frame.height;
+ }
+ }
+
+ public function getBasePosition():Null
+ {
+ // var stagePos = new FlxPoint(anim.stageInstance.matrix.tx, anim.stageInstance.matrix.ty);
+ var instancePos = new FlxPoint(anim.curInstance.matrix.tx, anim.curInstance.matrix.ty);
+ var firstElement = anim.curSymbol.timeline?.get(0)?.get(0)?.get(0);
+ if (firstElement == null) return instancePos;
+ var firstElementPos = new FlxPoint(firstElement.matrix.tx, firstElement.matrix.ty);
+
+ return instancePos + firstElementPos;
+ }
+
+ public function getPivotPosition():Null
+ {
+ return anim.curInstance.symbol.transformationPoint;
+ }
+
+ public override function destroy():Void
+ {
+ for (prevFrameId in prevFrames.keys())
+ {
+ replaceFrameGraphic(prevFrameId, null);
+ }
+
+ super.destroy();
+ }
}
diff --git a/source/funkin/graphics/shaders/AdjustColorShader.hx b/source/funkin/graphics/shaders/AdjustColorShader.hx
new file mode 100644
index 0000000000..2b0970eeb2
--- /dev/null
+++ b/source/funkin/graphics/shaders/AdjustColorShader.hx
@@ -0,0 +1,55 @@
+package funkin.graphics.shaders;
+
+import flixel.addons.display.FlxRuntimeShader;
+import funkin.Paths;
+import openfl.utils.Assets;
+
+class AdjustColorShader extends FlxRuntimeShader
+{
+ public var hue(default, set):Float;
+ public var saturation(default, set):Float;
+ public var brightness(default, set):Float;
+ public var contrast(default, set):Float;
+
+ public function new()
+ {
+ super(Assets.getText(Paths.frag('adjustColor')));
+ // FlxG.debugger.addTrackerProfile(new TrackerProfile(HSVShader, ['hue', 'saturation', 'brightness', 'contrast']));
+ hue = 0;
+ saturation = 0;
+ brightness = 0;
+ contrast = 0;
+ }
+
+ function set_hue(value:Float):Float
+ {
+ this.setFloat('hue', value);
+ this.hue = value;
+
+ return this.hue;
+ }
+
+ function set_saturation(value:Float):Float
+ {
+ this.setFloat('saturation', value);
+ this.saturation = value;
+
+ return this.saturation;
+ }
+
+ function set_brightness(value:Float):Float
+ {
+ this.setFloat('brightness', value);
+ this.brightness = value;
+
+ return this.brightness;
+ }
+
+ function set_contrast(value:Float):Float
+ {
+ this.setFloat('contrast', value);
+ this.contrast = value;
+
+ return this.contrast;
+ }
+}
diff --git a/source/funkin/graphics/shaders/AngleMask.hx b/source/funkin/graphics/shaders/AngleMask.hx
index 30e508a580..ce27311cdb 100644
--- a/source/funkin/graphics/shaders/AngleMask.hx
+++ b/source/funkin/graphics/shaders/AngleMask.hx
@@ -1,43 +1,96 @@
package funkin.graphics.shaders;
import flixel.system.FlxAssets.FlxShader;
+import flixel.util.FlxColor;
class AngleMask extends FlxShader
{
+ public var extraColor(default, set):FlxColor = 0xFFFFFFFF;
+
+ function set_extraColor(value:FlxColor):FlxColor
+ {
+ extraTint.value = [value.redFloat, value.greenFloat, value.blueFloat];
+ this.extraColor = value;
+
+ return this.extraColor;
+ }
+
@:glFragmentSource('
- #pragma header
- uniform vec2 endPosition;
- void main()
- {
- vec4 base = texture2D(bitmap, openfl_TextureCoordv);
+ #pragma header
+
+ uniform vec3 extraTint;
+
+ uniform vec2 endPosition;
+ vec2 hash22(vec2 p) {
+ vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973));
+ p3 += dot(p3, p3.yzx + 33.33);
+ return fract((p3.xx + p3.yz) * p3.zy);
+ }
+
+
+
+ // ====== GAMMA CORRECTION ====== //
+ // Helps with color mixing -- good to have by default in almost any shader
+ // See https://www.shadertoy.com/view/lscSzl
+ vec3 gamma(in vec3 color) {
+ return pow(color, vec3(1.0 / 2.2));
+ }
+
+ vec4 mainPass(vec2 fragCoord) {
+ vec4 base = texture2D(bitmap, fragCoord);
+
+ vec2 uv = fragCoord.xy;
+
+ vec2 start = vec2(0.0, 0.0);
+ vec2 end = vec2(endPosition.x / openfl_TextureSize.x, 1.0);
- vec2 uv = openfl_TextureCoordv.xy;
+ float dx = end.x - start.x;
+ float dy = end.y - start.y;
+ float angle = atan(dy, dx);
+ uv.x -= start.x;
+ uv.y -= start.y;
- vec2 start = vec2(0.0, 0.0);
- vec2 end = vec2(endPosition.x / openfl_TextureSize.x, 1.0);
+ float uvA = atan(uv.y, uv.x);
- float dx = end.x - start.x;
- float dy = end.y - start.y;
+ if (uvA < angle)
+ return base;
+ else
+ return vec4(0.0);
+ }
- float angle = atan(dy, dx);
+ vec4 antialias(vec2 fragCoord) {
- uv.x -= start.x;
- uv.y -= start.y;
+ const float AA_STAGES = 2.0;
- float uvA = atan(uv.y, uv.x);
+ const float AA_TOTAL_PASSES = AA_STAGES * AA_STAGES + 1.0;
+ const float AA_JITTER = 0.5;
- if (uvA < angle)
- gl_FragColor = base;
- else
- gl_FragColor = vec4(0.0);
+ // Run the shader multiple times with a random subpixel offset each time and average the results
+ vec4 color = mainPass(fragCoord);
+ for (float x = 0.0; x < AA_STAGES; x++)
+ {
+ for (float y = 0.0; y < AA_STAGES; y++)
+ {
+ vec2 offset = AA_JITTER * (2.0 * hash22(vec2(x, y)) - 1.0) / openfl_TextureSize.xy;
+ color += mainPass(fragCoord + offset);
+ }
+ }
+ return color / AA_TOTAL_PASSES;
+ }
- }')
+ void main() {
+ vec4 col = antialias(openfl_TextureCoordv);
+ col.xyz = col.xyz * extraTint.xyz;
+ // col.xyz = gamma(col.xyz);
+ gl_FragColor = col;
+ }')
public function new()
{
super();
endPosition.value = [90, 100]; // 100 AS DEFAULT WORKS NICELY FOR FREEPLAY?
+ extraTint.value = [1, 1, 1];
}
}
diff --git a/source/funkin/graphics/shaders/BlueFade.hx b/source/funkin/graphics/shaders/BlueFade.hx
new file mode 100644
index 0000000000..f57bcfbf1c
--- /dev/null
+++ b/source/funkin/graphics/shaders/BlueFade.hx
@@ -0,0 +1,51 @@
+package funkin.graphics.shaders;
+
+import flixel.system.FlxAssets.FlxShader;
+import flixel.tweens.FlxEase;
+import flixel.tweens.FlxTween;
+
+class BlueFade extends FlxShader
+{
+ public var fadeVal(default, set):Float;
+
+ function set_fadeVal(val:Float):Float
+ {
+ fadeAmt.value = [val];
+ fadeVal = val;
+ // trace(fadeVal);
+
+ return val;
+ }
+
+ public function fade(startAmt:Float = 0, targetAmt:Float = 1, duration:Float, _options:TweenOptions):Void
+ {
+ fadeVal = startAmt;
+ FlxTween.tween(this, {fadeVal: targetAmt}, duration, _options);
+ }
+
+ @:glFragmentSource('
+ #pragma header
+
+ // Value from (0, 1)
+ uniform float fadeAmt;
+
+ // fade the image to blue as it fades to black
+
+ void main()
+ {
+ vec4 tex = flixel_texture2D(bitmap, openfl_TextureCoordv);
+
+ vec4 finalColor = mix(vec4(vec4(0.0, 0.0, tex.b, tex.a) * fadeAmt), vec4(tex * fadeAmt), fadeAmt);
+
+ // Output to screen
+ gl_FragColor = finalColor;
+ }
+
+ ')
+ public function new()
+ {
+ super();
+
+ this.fadeVal = 1;
+ }
+}
diff --git a/source/funkin/graphics/shaders/MosaicEffect.hx b/source/funkin/graphics/shaders/MosaicEffect.hx
new file mode 100644
index 0000000000..fc3737afff
--- /dev/null
+++ b/source/funkin/graphics/shaders/MosaicEffect.hx
@@ -0,0 +1,23 @@
+package funkin.graphics.shaders;
+
+import flixel.addons.display.FlxRuntimeShader;
+import openfl.utils.Assets;
+import funkin.Paths;
+import flixel.math.FlxPoint;
+
+class MosaicEffect extends FlxRuntimeShader
+{
+ public var blockSize:FlxPoint = FlxPoint.get(1.0, 1.0);
+
+ public function new()
+ {
+ super(Assets.getText(Paths.frag('mosaic')));
+ setBlockSize(1.0, 1.0);
+ }
+
+ public function setBlockSize(w:Float, h:Float)
+ {
+ blockSize.set(w, h);
+ setFloatArray("uBlocksize", [w, h]);
+ }
+}
diff --git a/source/funkin/graphics/shaders/RuntimePostEffectShader.hx b/source/funkin/graphics/shaders/RuntimePostEffectShader.hx
index 9f49da0759..d39f57efe4 100644
--- a/source/funkin/graphics/shaders/RuntimePostEffectShader.hx
+++ b/source/funkin/graphics/shaders/RuntimePostEffectShader.hx
@@ -2,6 +2,7 @@ package funkin.graphics.shaders;
import flixel.FlxCamera;
import flixel.FlxG;
+import flixel.graphics.frames.FlxFrame;
import flixel.addons.display.FlxRuntimeShader;
import lime.graphics.opengl.GLProgram;
import lime.utils.Log;
@@ -32,6 +33,9 @@ class RuntimePostEffectShader extends FlxRuntimeShader
// equals (camera.viewLeft, camera.viewTop, camera.viewRight, camera.viewBottom)
uniform vec4 uCameraBounds;
+ // equals (frame.left, frame.top, frame.right, frame.bottom)
+ uniform vec4 uFrameBounds;
+
// screen coord -> world coord conversion
// returns world coord in px
vec2 screenToWorld(vec2 screenCoord) {
@@ -56,6 +60,25 @@ class RuntimePostEffectShader extends FlxRuntimeShader
return (worldCoord - offset) / scale;
}
+ // screen coord -> frame coord conversion
+ // returns normalized frame coord
+ vec2 screenToFrame(vec2 screenCoord) {
+ float left = uFrameBounds.x;
+ float top = uFrameBounds.y;
+ float right = uFrameBounds.z;
+ float bottom = uFrameBounds.w;
+ float width = right - left;
+ float height = bottom - top;
+
+ float clampedX = clamp(screenCoord.x, left, right);
+ float clampedY = clamp(screenCoord.y, top, bottom);
+
+ return vec2(
+ (clampedX - left) / (width),
+ (clampedY - top) / (height)
+ );
+ }
+
// internally used to get the maximum `openfl_TextureCoordv`
vec2 bitmapCoordScale() {
return openfl_TextureCoordv / screenCoord;
@@ -80,6 +103,8 @@ class RuntimePostEffectShader extends FlxRuntimeShader
{
super(fragmentSource, null, glVersion);
uScreenResolution.value = [FlxG.width, FlxG.height];
+ uCameraBounds.value = [0, 0, FlxG.width, FlxG.height];
+ uFrameBounds.value = [0, 0, FlxG.width, FlxG.height];
}
// basically `updateViewInfo(FlxG.width, FlxG.height, FlxG.camera)` is good
@@ -89,6 +114,12 @@ class RuntimePostEffectShader extends FlxRuntimeShader
uCameraBounds.value = [camera.viewLeft, camera.viewTop, camera.viewRight, camera.viewBottom];
}
+ public function updateFrameInfo(frame:FlxFrame)
+ {
+ // NOTE: uv.width is actually the right pos and uv.height is the bottom pos
+ uFrameBounds.value = [frame.uv.x, frame.uv.y, frame.uv.width, frame.uv.height];
+ }
+
override function __createGLProgram(vertexSource:String, fragmentSource:String):GLProgram
{
try
diff --git a/source/funkin/graphics/shaders/RuntimeRainShader.hx b/source/funkin/graphics/shaders/RuntimeRainShader.hx
index 239276bbef..d0c036623e 100644
--- a/source/funkin/graphics/shaders/RuntimeRainShader.hx
+++ b/source/funkin/graphics/shaders/RuntimeRainShader.hx
@@ -4,6 +4,7 @@ import flixel.system.FlxAssets.FlxShader;
import openfl.display.BitmapData;
import openfl.display.ShaderParameter;
import openfl.display.ShaderParameterType;
+import flixel.util.FlxColor;
import openfl.utils.Assets;
typedef Light =
@@ -32,6 +33,14 @@ class RuntimeRainShader extends RuntimePostEffectShader
return time = value;
}
+ public var spriteMode(default, set):Bool = false;
+
+ function set_spriteMode(value:Bool):Bool
+ {
+ this.setBool('uSpriteMode', value);
+ return spriteMode = value;
+ }
+
// The scale of the rain depends on the world coordinate system, so higher resolution makes
// the raindrops smaller. This parameter can be used to adjust the total scale of the scene.
// The size of the raindrops is proportional to the value of this parameter.
@@ -86,6 +95,14 @@ class RuntimeRainShader extends RuntimePostEffectShader
return mask = value;
}
+ public var rainColor(default, set):FlxColor;
+
+ function set_rainColor(color:FlxColor):FlxColor
+ {
+ this.setFloatArray("uRainColor", [color.red / 255, color.green / 255, color.blue / 255]);
+ return rainColor = color;
+ }
+
public var lightMap(default, set):BitmapData;
function set_lightMap(value:BitmapData):BitmapData
@@ -105,6 +122,7 @@ class RuntimeRainShader extends RuntimePostEffectShader
public function new()
{
super(Assets.getText(Paths.frag('rain')));
+ this.rainColor = 0xFF6680cc;
}
public function update(elapsed:Float):Void
diff --git a/source/funkin/graphics/shaders/TextureSwap.hx b/source/funkin/graphics/shaders/TextureSwap.hx
new file mode 100644
index 0000000000..65de87ea39
--- /dev/null
+++ b/source/funkin/graphics/shaders/TextureSwap.hx
@@ -0,0 +1,48 @@
+package funkin.graphics.shaders;
+
+import flixel.system.FlxAssets.FlxShader;
+import flixel.util.FlxColor;
+import openfl.display.BitmapData;
+
+class TextureSwap extends FlxShader
+{
+ public var swappedImage(default, set):BitmapData;
+ public var amount(default, set):Float;
+
+ function set_swappedImage(_bitmapData:BitmapData):BitmapData
+ {
+ image.input = _bitmapData;
+
+ return _bitmapData;
+ }
+
+ function set_amount(val:Float):Float
+ {
+ fadeAmount.value = [val];
+
+ return val;
+ }
+
+ @:glFragmentSource('
+ #pragma header
+
+ uniform sampler2D image;
+ uniform float fadeAmount;
+
+ void main()
+ {
+ vec4 tex = flixel_texture2D(bitmap, openfl_TextureCoordv);
+ vec4 tex2 = flixel_texture2D(image, openfl_TextureCoordv);
+
+ vec4 finalColor = mix(tex, vec4(tex2.rgb, tex.a), fadeAmount);
+
+ gl_FragColor = finalColor;
+ }
+ ')
+ public function new()
+ {
+ super();
+
+ this.amount = 1;
+ }
+}
diff --git a/source/funkin/import.hx b/source/funkin/import.hx
index 250de99cb2..c8431be33b 100644
--- a/source/funkin/import.hx
+++ b/source/funkin/import.hx
@@ -11,6 +11,7 @@ import flixel.system.debug.watch.Tracker;
// These are great.
using Lambda;
using StringTools;
+using thx.Arrays;
using funkin.util.tools.ArraySortTools;
using funkin.util.tools.ArrayTools;
using funkin.util.tools.FloatTools;
diff --git a/source/funkin/input/Controls.hx b/source/funkin/input/Controls.hx
index 548e4edfae..da5aaac58f 100644
--- a/source/funkin/input/Controls.hx
+++ b/source/funkin/input/Controls.hx
@@ -31,6 +31,7 @@ class Controls extends FlxActionSet
* Uses FlxActions to funnel various inputs to a single action.
*/
var _ui_up = new FunkinAction(Action.UI_UP);
+
var _ui_left = new FunkinAction(Action.UI_LEFT);
var _ui_right = new FunkinAction(Action.UI_RIGHT);
var _ui_down = new FunkinAction(Action.UI_DOWN);
@@ -58,7 +59,12 @@ class Controls extends FlxActionSet
var _back = new FunkinAction(Action.BACK);
var _pause = new FunkinAction(Action.PAUSE);
var _reset = new FunkinAction(Action.RESET);
- var _screenshot = new FunkinAction(Action.SCREENSHOT);
+ var _window_screenshot = new FunkinAction(Action.WINDOW_SCREENSHOT);
+ var _window_fullscreen = new FunkinAction(Action.WINDOW_FULLSCREEN);
+ var _freeplay_favorite = new FunkinAction(Action.FREEPLAY_FAVORITE);
+ var _freeplay_left = new FunkinAction(Action.FREEPLAY_LEFT);
+ var _freeplay_right = new FunkinAction(Action.FREEPLAY_RIGHT);
+ var _freeplay_char_select = new FunkinAction(Action.FREEPLAY_CHAR_SELECT);
var _cutscene_advance = new FunkinAction(Action.CUTSCENE_ADVANCE);
var _debug_menu = new FunkinAction(Action.DEBUG_MENU);
var _debug_chart = new FunkinAction(Action.DEBUG_CHART);
@@ -66,7 +72,6 @@ class Controls extends FlxActionSet
var _volume_up = new FunkinAction(Action.VOLUME_UP);
var _volume_down = new FunkinAction(Action.VOLUME_DOWN);
var _volume_mute = new FunkinAction(Action.VOLUME_MUTE);
- var _fullscreen = new FunkinAction(Action.FULLSCREEN);
var byName:Map = new Map();
@@ -233,10 +238,35 @@ class Controls extends FlxActionSet
inline function get_RESET()
return _reset.check();
- public var SCREENSHOT(get, never):Bool;
+ public var WINDOW_FULLSCREEN(get, never):Bool;
+
+ inline function get_WINDOW_FULLSCREEN()
+ return _window_fullscreen.check();
+
+ public var WINDOW_SCREENSHOT(get, never):Bool;
+
+ inline function get_WINDOW_SCREENSHOT()
+ return _window_screenshot.check();
+
+ public var FREEPLAY_FAVORITE(get, never):Bool;
+
+ inline function get_FREEPLAY_FAVORITE()
+ return _freeplay_favorite.check();
+
+ public var FREEPLAY_LEFT(get, never):Bool;
- inline function get_SCREENSHOT()
- return _screenshot.check();
+ inline function get_FREEPLAY_LEFT()
+ return _freeplay_left.check();
+
+ public var FREEPLAY_RIGHT(get, never):Bool;
+
+ inline function get_FREEPLAY_RIGHT()
+ return _freeplay_right.check();
+
+ public var FREEPLAY_CHAR_SELECT(get, never):Bool;
+
+ inline function get_FREEPLAY_CHAR_SELECT()
+ return _freeplay_char_select.check();
public var CUTSCENE_ADVANCE(get, never):Bool;
@@ -273,11 +303,6 @@ class Controls extends FlxActionSet
inline function get_VOLUME_MUTE()
return _volume_mute.check();
- public var FULLSCREEN(get, never):Bool;
-
- inline function get_FULLSCREEN()
- return _fullscreen.check();
-
public function new(name, scheme:KeyboardScheme = null)
{
super(name);
@@ -294,7 +319,12 @@ class Controls extends FlxActionSet
add(_back);
add(_pause);
add(_reset);
- add(_screenshot);
+ add(_window_screenshot);
+ add(_window_fullscreen);
+ add(_freeplay_favorite);
+ add(_freeplay_left);
+ add(_freeplay_right);
+ add(_freeplay_char_select);
add(_cutscene_advance);
add(_debug_menu);
add(_debug_chart);
@@ -302,21 +332,19 @@ class Controls extends FlxActionSet
add(_volume_up);
add(_volume_down);
add(_volume_mute);
- add(_fullscreen);
- for (action in digitalActions) {
- if (Std.isOfType(action, FunkinAction)) {
+ for (action in digitalActions)
+ {
+ if (Std.isOfType(action, FunkinAction))
+ {
var funkinAction:FunkinAction = cast action;
byName[funkinAction.name] = funkinAction;
- if (funkinAction.namePressed != null)
- byName[funkinAction.namePressed] = funkinAction;
- if (funkinAction.nameReleased != null)
- byName[funkinAction.nameReleased] = funkinAction;
+ if (funkinAction.namePressed != null) byName[funkinAction.namePressed] = funkinAction;
+ if (funkinAction.nameReleased != null) byName[funkinAction.nameReleased] = funkinAction;
}
}
- if (scheme == null)
- scheme = None;
+ if (scheme == null) scheme = None;
setKeyboardScheme(scheme, false);
}
@@ -328,63 +356,81 @@ class Controls extends FlxActionSet
public function check(name:Action, trigger:FlxInputState = JUST_PRESSED, gamepadOnly:Bool = false):Bool
{
- #if debug
- if (!byName.exists(name))
- throw 'Invalid name: $name';
+ #if FEATURE_DEBUG_FUNCTIONS
+ if (!byName.exists(name)) throw 'Invalid name: $name';
#end
+
var action = byName[name];
- if (gamepadOnly)
- return action.checkFiltered(trigger, GAMEPAD);
+ if (gamepadOnly) return action.checkFiltered(trigger, GAMEPAD);
else
return action.checkFiltered(trigger);
}
- public function getKeysForAction(name:Action):Array {
- #if debug
- if (!byName.exists(name))
- throw 'Invalid name: $name';
+ public function getKeysForAction(name:Action):Array
+ {
+ #if FEATURE_DEBUG_FUNCTIONS
+ if (!byName.exists(name)) throw 'Invalid name: $name';
#end
// TODO: Revert to `.map().filter()` once HashLink doesn't complain anymore.
var result:Array = [];
- for (input in byName[name].inputs) {
+ for (input in byName[name].inputs)
+ {
if (input.device == KEYBOARD) result.push(input.inputID);
}
return result;
}
- public function getButtonsForAction(name:Action):Array {
- #if debug
- if (!byName.exists(name))
- throw 'Invalid name: $name';
+ public function getButtonsForAction(name:Action):Array
+ {
+ #if FEATURE_DEBUG_FUNCTIONS
+ if (!byName.exists(name)) throw 'Invalid name: $name';
#end
var result:Array = [];
- for (input in byName[name].inputs) {
+ for (input in byName[name].inputs)
+ {
if (input.device == GAMEPAD) result.push(input.inputID);
}
return result;
}
- public function getDialogueName(action:FlxActionDigital):String
+ public function getDialogueName(action:FlxActionDigital, ?ignoreSurrounding:Bool = false):String
{
var input = action.inputs[0];
- return switch (input.device)
+ if (ignoreSurrounding == false)
{
- case KEYBOARD: return '[${(input.inputID : FlxKey)}]';
- case GAMEPAD: return '(${(input.inputID : FlxGamepadInputID)})';
- case device: throw 'unhandled device: $device';
+ return switch (input.device)
+ {
+ case KEYBOARD: return '[${(input.inputID : FlxKey)}]';
+ case GAMEPAD: return '(${(input.inputID : FlxGamepadInputID)})';
+ case device: throw 'unhandled device: $device';
+ }
}
+ else
+ {
+ return switch (input.device)
+ {
+ case KEYBOARD: return '${(input.inputID : FlxKey)}';
+ case GAMEPAD: return '${(input.inputID : FlxGamepadInputID)}';
+ case device: throw 'unhandled device: $device';
+ }
+ }
+ }
+
+ public function getDialogueNameFromToken(token:String, ?ignoreSurrounding:Bool = false):String
+ {
+ return getDialogueName(getActionFromControl(Control.createByName(token.toUpperCase())), ignoreSurrounding);
}
- public function getDialogueNameFromToken(token:String):String
+ public function getDialogueNameFromControl(control:Control, ?ignoreSurrounding:Bool = false):String
{
- return getDialogueName(getActionFromControl(Control.createByName(token.toUpperCase())));
+ return getDialogueName(getActionFromControl(control), ignoreSurrounding);
}
function getActionFromControl(control:Control):FlxActionDigital
{
- return switch(control)
+ return switch (control)
{
case UI_UP: _ui_up;
case UI_DOWN: _ui_down;
@@ -398,7 +444,12 @@ class Controls extends FlxActionSet
case BACK: _back;
case PAUSE: _pause;
case RESET: _reset;
- case SCREENSHOT: _screenshot;
+ case WINDOW_SCREENSHOT: _window_screenshot;
+ case WINDOW_FULLSCREEN: _window_fullscreen;
+ case FREEPLAY_FAVORITE: _freeplay_favorite;
+ case FREEPLAY_LEFT: _freeplay_left;
+ case FREEPLAY_RIGHT: _freeplay_right;
+ case FREEPLAY_CHAR_SELECT: _freeplay_char_select;
case CUTSCENE_ADVANCE: _cutscene_advance;
case DEBUG_MENU: _debug_menu;
case DEBUG_CHART: _debug_chart;
@@ -406,7 +457,6 @@ class Controls extends FlxActionSet
case VOLUME_UP: _volume_up;
case VOLUME_DOWN: _volume_down;
case VOLUME_MUTE: _volume_mute;
- case FULLSCREEN: _fullscreen;
}
}
@@ -424,7 +474,7 @@ class Controls extends FlxActionSet
*/
function forEachBound(control:Control, func:FlxActionDigital->FlxInputState->Void)
{
- switch(control)
+ switch (control)
{
case UI_UP:
func(_ui_up, PRESSED);
@@ -466,8 +516,18 @@ class Controls extends FlxActionSet
func(_pause, JUST_PRESSED);
case RESET:
func(_reset, JUST_PRESSED);
- case SCREENSHOT:
- func(_screenshot, JUST_PRESSED);
+ case WINDOW_SCREENSHOT:
+ func(_window_screenshot, JUST_PRESSED);
+ case WINDOW_FULLSCREEN:
+ func(_window_fullscreen, JUST_PRESSED);
+ case FREEPLAY_FAVORITE:
+ func(_freeplay_favorite, JUST_PRESSED);
+ case FREEPLAY_LEFT:
+ func(_freeplay_left, JUST_PRESSED);
+ case FREEPLAY_RIGHT:
+ func(_freeplay_right, JUST_PRESSED);
+ case FREEPLAY_CHAR_SELECT:
+ func(_freeplay_char_select, JUST_PRESSED);
case CUTSCENE_ADVANCE:
func(_cutscene_advance, JUST_PRESSED);
case DEBUG_MENU:
@@ -482,17 +542,14 @@ class Controls extends FlxActionSet
func(_volume_down, JUST_PRESSED);
case VOLUME_MUTE:
func(_volume_mute, JUST_PRESSED);
- case FULLSCREEN:
- func(_fullscreen, JUST_PRESSED);
}
}
public function replaceBinding(control:Control, device:Device, toAdd:Int, toRemove:Int)
{
- if (toAdd == toRemove)
- return;
+ if (toAdd == toRemove) return;
- switch(device)
+ switch (device)
{
case Keys:
forEachBound(control, function(action, state) replaceKey(action, toAdd, toRemove, state));
@@ -504,7 +561,8 @@ class Controls extends FlxActionSet
function replaceKey(action:FlxActionDigital, toAdd:FlxKey, toRemove:FlxKey, state:FlxInputState)
{
- if (action.inputs.length == 0) {
+ if (action.inputs.length == 0)
+ {
// Add the keybind, don't replace.
addKeys(action, [toAdd], state);
return;
@@ -518,34 +576,44 @@ class Controls extends FlxActionSet
if (input.device == KEYBOARD && input.inputID == toRemove)
{
- if (toAdd == FlxKey.NONE) {
+ if (toAdd == FlxKey.NONE)
+ {
// Remove the keybind, don't replace.
action.inputs.remove(input);
- } else {
+ }
+ else
+ {
// Replace the keybind.
@:privateAccess
action.inputs[i].inputID = toAdd;
}
hasReplaced = true;
- } else if (input.device == KEYBOARD && input.inputID == toAdd) {
+ }
+ else if (input.device == KEYBOARD && input.inputID == toAdd)
+ {
// This key is already bound!
- if (hasReplaced) {
+ if (hasReplaced)
+ {
// Remove the duplicate keybind, don't replace.
action.inputs.remove(input);
- } else {
+ }
+ else
+ {
hasReplaced = true;
}
}
}
- if (!hasReplaced) {
+ if (!hasReplaced)
+ {
addKeys(action, [toAdd], state);
}
}
function replaceButton(action:FlxActionDigital, deviceID:Int, toAdd:FlxGamepadInputID, toRemove:FlxGamepadInputID, state:FlxInputState)
{
- if (action.inputs.length == 0) {
+ if (action.inputs.length == 0)
+ {
addButtons(action, [toAdd], state, deviceID);
return;
}
@@ -564,7 +632,8 @@ class Controls extends FlxActionSet
}
}
- if (!hasReplaced) {
+ if (!hasReplaced)
+ {
addButtons(action, [toAdd], state, deviceID);
}
}
@@ -576,18 +645,16 @@ class Controls extends FlxActionSet
var action = controls.byName[name];
for (input in action.inputs)
{
- if (device == null || isDevice(input, device))
- byName[name].add(cast input);
+ if (device == null || isDevice(input, device)) byName[name].add(cast input);
}
}
- switch(device)
+ switch (device)
{
case null:
// add all
for (gamepad in controls.gamepadsAdded)
- if (gamepadsAdded.indexOf(gamepad) == -1)
- gamepadsAdded.push(gamepad);
+ if (gamepadsAdded.indexOf(gamepad) == -1) gamepadsAdded.push(gamepad);
mergeKeyboardScheme(controls.keyboardScheme);
@@ -607,7 +674,7 @@ class Controls extends FlxActionSet
{
if (scheme != None)
{
- switch(keyboardScheme)
+ switch (keyboardScheme)
{
case None:
keyboardScheme = scheme;
@@ -642,7 +709,8 @@ class Controls extends FlxActionSet
static function addKeys(action:FlxActionDigital, keys:Array, state:FlxInputState)
{
- for (key in keys) {
+ for (key in keys)
+ {
if (key == FlxKey.NONE) continue; // Ignore unbound keys.
action.addKey(key, state);
}
@@ -654,15 +722,13 @@ class Controls extends FlxActionSet
while (i-- > 0)
{
var input = action.inputs[i];
- if (input.device == KEYBOARD && keys.indexOf(cast input.inputID) != -1)
- action.remove(input);
+ if (input.device == KEYBOARD && keys.indexOf(cast input.inputID) != -1) action.remove(input);
}
}
public function setKeyboardScheme(scheme:KeyboardScheme, reset = true)
{
- if (reset)
- removeKeyboard();
+ if (reset) removeKeyboard();
keyboardScheme = scheme;
@@ -678,7 +744,12 @@ class Controls extends FlxActionSet
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.WINDOW_SCREENSHOT, getDefaultKeybinds(scheme, Control.WINDOW_SCREENSHOT));
+ bindKeys(Control.WINDOW_FULLSCREEN, getDefaultKeybinds(scheme, Control.WINDOW_FULLSCREEN));
+ bindKeys(Control.FREEPLAY_FAVORITE, getDefaultKeybinds(scheme, Control.FREEPLAY_FAVORITE));
+ bindKeys(Control.FREEPLAY_LEFT, getDefaultKeybinds(scheme, Control.FREEPLAY_LEFT));
+ bindKeys(Control.FREEPLAY_RIGHT, getDefaultKeybinds(scheme, Control.FREEPLAY_RIGHT));
+ bindKeys(Control.FREEPLAY_CHAR_SELECT, getDefaultKeybinds(scheme, Control.FREEPLAY_CHAR_SELECT));
bindKeys(Control.CUTSCENE_ADVANCE, getDefaultKeybinds(scheme, Control.CUTSCENE_ADVANCE));
bindKeys(Control.DEBUG_MENU, getDefaultKeybinds(scheme, Control.DEBUG_MENU));
bindKeys(Control.DEBUG_CHART, getDefaultKeybinds(scheme, Control.DEBUG_CHART));
@@ -686,15 +757,17 @@ class Controls extends FlxActionSet
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));
- bindKeys(Control.FULLSCREEN, getDefaultKeybinds(scheme, Control.FULLSCREEN));
bindMobileLol();
}
- function getDefaultKeybinds(scheme:KeyboardScheme, control:Control):Array {
- switch (scheme) {
+ function getDefaultKeybinds(scheme:KeyboardScheme, control:Control):Array
+ {
+ switch (scheme)
+ {
case Solo:
- switch (control) {
+ switch (control)
+ {
case Control.UI_UP: return [W, FlxKey.UP];
case Control.UI_DOWN: return [S, FlxKey.DOWN];
case Control.UI_LEFT: return [A, FlxKey.LEFT];
@@ -707,7 +780,12 @@ class Controls extends FlxActionSet
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.WINDOW_FULLSCREEN: return [F11]; // We use F for other things LOL.
+ case Control.WINDOW_SCREENSHOT: return [F3];
+ case Control.FREEPLAY_FAVORITE: return [F]; // Favorite a song on the menu
+ case Control.FREEPLAY_LEFT: return [Q]; // Switch tabs on the menu
+ case Control.FREEPLAY_RIGHT: return [E]; // Switch tabs on the menu
+ case Control.FREEPLAY_CHAR_SELECT: return [TAB];
case Control.CUTSCENE_ADVANCE: return [Z, ENTER];
case Control.DEBUG_MENU: return [GRAVEACCENT];
case Control.DEBUG_CHART: return [];
@@ -715,11 +793,10 @@ class Controls extends FlxActionSet
case Control.VOLUME_UP: return [PLUS, NUMPADPLUS];
case Control.VOLUME_DOWN: return [MINUS, NUMPADMINUS];
case Control.VOLUME_MUTE: return [ZERO, NUMPADZERO];
- case Control.FULLSCREEN: return [FlxKey.F];
-
}
case Duo(true):
- switch (control) {
+ switch (control)
+ {
case Control.UI_UP: return [W];
case Control.UI_DOWN: return [S];
case Control.UI_LEFT: return [A];
@@ -732,7 +809,12 @@ class Controls extends FlxActionSet
case Control.BACK: return [H, X];
case Control.PAUSE: return [ONE];
case Control.RESET: return [R];
- case Control.SCREENSHOT: return [PRINTSCREEN];
+ case Control.WINDOW_SCREENSHOT: return [F3];
+ case Control.WINDOW_FULLSCREEN: return [F11];
+ case Control.FREEPLAY_FAVORITE: return [F]; // Favorite a song on the menu
+ case Control.FREEPLAY_LEFT: return [Q]; // Switch tabs on the menu
+ case Control.FREEPLAY_RIGHT: return [E]; // Switch tabs on the menu
+ case Control.FREEPLAY_CHAR_SELECT: return [TAB];
case Control.CUTSCENE_ADVANCE: return [G, Z];
case Control.DEBUG_MENU: return [GRAVEACCENT];
case Control.DEBUG_CHART: return [];
@@ -740,11 +822,10 @@ class Controls extends FlxActionSet
case Control.VOLUME_UP: return [PLUS];
case Control.VOLUME_DOWN: return [MINUS];
case Control.VOLUME_MUTE: return [ZERO];
- case Control.FULLSCREEN: return [FlxKey.F];
-
}
case Duo(false):
- switch (control) {
+ switch (control)
+ {
case Control.UI_UP: return [FlxKey.UP];
case Control.UI_DOWN: return [FlxKey.DOWN];
case Control.UI_LEFT: return [FlxKey.LEFT];
@@ -757,16 +838,19 @@ class Controls extends FlxActionSet
case Control.BACK: return [ESCAPE];
case Control.PAUSE: return [ONE];
case Control.RESET: return [R];
- case Control.SCREENSHOT: return [PRINTSCREEN];
+ case Control.WINDOW_SCREENSHOT: return [];
+ case Control.WINDOW_FULLSCREEN: return [];
+ case Control.FREEPLAY_FAVORITE: return [];
+ case Control.FREEPLAY_LEFT: return [];
+ case Control.FREEPLAY_RIGHT: return [];
+ case Control.FREEPLAY_CHAR_SELECT: return [];
case Control.CUTSCENE_ADVANCE: return [ENTER];
- case Control.DEBUG_MENU: return [GRAVEACCENT];
+ case Control.DEBUG_MENU: return [];
case Control.DEBUG_CHART: return [];
case Control.DEBUG_STAGE: return [];
case Control.VOLUME_UP: return [NUMPADPLUS];
case Control.VOLUME_DOWN: return [NUMPADMINUS];
case Control.VOLUME_MUTE: return [NUMPADZERO];
- case Control.FULLSCREEN: return [];
-
}
default:
// Fallthrough.
@@ -793,8 +877,7 @@ class Controls extends FlxActionSet
#end
#if android
- forEachBound(Control.BACK, function(action, pres)
- {
+ forEachBound(Control.BACK, function(action, pres) {
action.add(new FlxActionInputDigitalAndroid(FlxAndroidKey.BACK, JUST_PRESSED));
});
#end
@@ -808,8 +891,7 @@ class Controls extends FlxActionSet
while (i-- > 0)
{
var input = action.inputs[i];
- if (input.device == KEYBOARD)
- action.remove(input);
+ if (input.device == KEYBOARD) action.remove(input);
}
}
}
@@ -821,11 +903,13 @@ class Controls extends FlxActionSet
fromSaveData(padData, Gamepad(id));
}
- public function getGamepadIds():Array {
+ public function getGamepadIds():Array
+ {
return gamepadsAdded;
}
- public function getGamepads():Array {
+ public function getGamepads():Array
+ {
return [for (id in gamepadsAdded) FlxG.gamepads.getByID(id)];
}
@@ -845,8 +929,7 @@ class Controls extends FlxActionSet
while (i-- > 0)
{
var input = action.inputs[i];
- if (isGamepad(input, deviceID))
- action.remove(input);
+ if (isGamepad(input, deviceID)) action.remove(input);
}
}
@@ -856,52 +939,85 @@ class Controls extends FlxActionSet
public function addDefaultGamepad(id):Void
{
addGamepadLiteral(id, [
-
Control.ACCEPT => getDefaultGamepadBinds(Control.ACCEPT),
Control.BACK => getDefaultGamepadBinds(Control.BACK),
Control.UI_UP => getDefaultGamepadBinds(Control.UI_UP),
Control.UI_DOWN => getDefaultGamepadBinds(Control.UI_DOWN),
Control.UI_LEFT => getDefaultGamepadBinds(Control.UI_LEFT),
Control.UI_RIGHT => getDefaultGamepadBinds(Control.UI_RIGHT),
- // don't swap A/B or X/Y for switch on these. A is always the bottom face button
Control.NOTE_UP => getDefaultGamepadBinds(Control.NOTE_UP),
Control.NOTE_DOWN => getDefaultGamepadBinds(Control.NOTE_DOWN),
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.WINDOW_FULLSCREEN => getDefaultGamepadBinds(Control.WINDOW_FULLSCREEN),
+ Control.WINDOW_SCREENSHOT => getDefaultGamepadBinds(Control.WINDOW_SCREENSHOT),
Control.CUTSCENE_ADVANCE => getDefaultGamepadBinds(Control.CUTSCENE_ADVANCE),
- // Control.DEBUG_MENU
- // Control.DEBUG_CHART
+ Control.FREEPLAY_FAVORITE => getDefaultGamepadBinds(Control.FREEPLAY_FAVORITE),
+ Control.FREEPLAY_LEFT => getDefaultGamepadBinds(Control.FREEPLAY_LEFT),
+ Control.FREEPLAY_RIGHT => getDefaultGamepadBinds(Control.FREEPLAY_RIGHT),
+ Control.VOLUME_UP => getDefaultGamepadBinds(Control.VOLUME_UP),
+ Control.VOLUME_DOWN => getDefaultGamepadBinds(Control.VOLUME_DOWN),
+ Control.VOLUME_MUTE => getDefaultGamepadBinds(Control.VOLUME_MUTE),
+ Control.DEBUG_MENU => getDefaultGamepadBinds(Control.DEBUG_MENU),
+ Control.DEBUG_CHART => getDefaultGamepadBinds(Control.DEBUG_CHART),
+ Control.DEBUG_STAGE => getDefaultGamepadBinds(Control.DEBUG_STAGE),
]);
}
- function getDefaultGamepadBinds(control:Control):Array {
- switch(control) {
- case Control.ACCEPT: return [#if switch B #else A #end];
- case Control.BACK: return [#if switch A #else B #end, FlxGamepadInputID.BACK];
- case Control.UI_UP: return [DPAD_UP, LEFT_STICK_DIGITAL_UP];
- case Control.UI_DOWN: return [DPAD_DOWN, LEFT_STICK_DIGITAL_DOWN];
- case Control.UI_LEFT: return [DPAD_LEFT, LEFT_STICK_DIGITAL_LEFT];
- case Control.UI_RIGHT: return [DPAD_RIGHT, LEFT_STICK_DIGITAL_RIGHT];
- case Control.NOTE_UP: return [DPAD_UP, Y, LEFT_STICK_DIGITAL_UP, RIGHT_STICK_DIGITAL_UP];
- case Control.NOTE_DOWN: return [DPAD_DOWN, A, LEFT_STICK_DIGITAL_DOWN, RIGHT_STICK_DIGITAL_DOWN];
- 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.DEBUG_MENU: return [];
- case Control.DEBUG_CHART: return [];
- case Control.FULLSCREEN: return [];
+ function getDefaultGamepadBinds(control:Control):Array
+ {
+ switch (control)
+ {
+ case Control.ACCEPT:
+ return [#if switch B #else A #end];
+ case Control.BACK:
+ return [#if switch A #else B #end];
+ case Control.UI_UP:
+ return [DPAD_UP, LEFT_STICK_DIGITAL_UP];
+ case Control.UI_DOWN:
+ return [DPAD_DOWN, LEFT_STICK_DIGITAL_DOWN];
+ case Control.UI_LEFT:
+ return [DPAD_LEFT, LEFT_STICK_DIGITAL_LEFT];
+ case Control.UI_RIGHT:
+ return [DPAD_RIGHT, LEFT_STICK_DIGITAL_RIGHT];
+ case Control.NOTE_UP:
+ return [DPAD_UP, Y, LEFT_STICK_DIGITAL_UP, RIGHT_STICK_DIGITAL_UP];
+ case Control.NOTE_DOWN:
+ return [DPAD_DOWN, A, LEFT_STICK_DIGITAL_DOWN, RIGHT_STICK_DIGITAL_DOWN];
+ 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 [FlxGamepadInputID.BACK]; // Back (i.e. Select)
+ case Control.WINDOW_FULLSCREEN:
+ [];
+ case Control.WINDOW_SCREENSHOT:
+ [];
+ case Control.CUTSCENE_ADVANCE:
+ return [A];
+ case Control.FREEPLAY_FAVORITE:
+ [FlxGamepadInputID.BACK]; // Back (i.e. Select)
+ case Control.FREEPLAY_LEFT:
+ [LEFT_SHOULDER];
+ case Control.FREEPLAY_RIGHT:
+ [RIGHT_SHOULDER];
+ case Control.VOLUME_UP:
+ [];
+ case Control.VOLUME_DOWN:
+ [];
+ case Control.VOLUME_MUTE:
+ [];
+ case Control.DEBUG_MENU:
+ [];
+ case Control.DEBUG_CHART:
+ [];
+ case Control.DEBUG_STAGE:
+ [];
default:
// Fallthrough.
}
@@ -919,8 +1035,7 @@ class Controls extends FlxActionSet
public function touchShit(control:Control, id)
{
- forEachBound(control, function(action, state)
- {
+ forEachBound(control, function(action, state) {
// action
});
}
@@ -936,7 +1051,8 @@ class Controls extends FlxActionSet
inline static function addButtons(action:FlxActionDigital, buttons:Array, state, id)
{
- for (button in buttons) {
+ for (button in buttons)
+ {
if (button == FlxGamepadInputID.NONE) continue; // Ignore unbound keys.
action.addGamepad(button, state, id);
}
@@ -948,29 +1064,25 @@ class Controls extends FlxActionSet
while (i-- > 0)
{
var input = action.inputs[i];
- if (isGamepad(input, gamepadID) && buttons.indexOf(cast input.inputID) != -1)
- action.remove(input);
+ if (isGamepad(input, gamepadID) && buttons.indexOf(cast input.inputID) != -1) action.remove(input);
}
}
public function getInputsFor(control:Control, device:Device, ?list:Array):Array
{
- if (list == null)
- list = [];
+ if (list == null) list = [];
- switch(device)
+ switch (device)
{
case Keys:
for (input in getActionFromControl(control).inputs)
{
- if (input.device == KEYBOARD)
- list.push(input.inputID);
+ if (input.device == KEYBOARD) list.push(input.inputID);
}
case Gamepad(id):
for (input in getActionFromControl(control).inputs)
{
- if (isGamepad(input, id))
- list.push(input.inputID);
+ if (isGamepad(input, id)) list.push(input.inputID);
}
}
return list;
@@ -978,7 +1090,7 @@ class Controls extends FlxActionSet
public function removeDevice(device:Device)
{
- switch(device)
+ switch (device)
{
case Keys:
setKeyboardScheme(None);
@@ -992,27 +1104,32 @@ class Controls extends FlxActionSet
* An EMPTY array means the control is uninitialized and needs to be reset to default.
* An array with a single FlxKey.NONE means the control was intentionally unbound by the user.
*/
- public function fromSaveData(data:Dynamic, device:Device)
+ public function fromSaveData(data:Dynamic, device:Device):Void
{
for (control in Control.createAll())
{
var inputs:Array = Reflect.field(data, control.getName());
- inputs = inputs.unique();
+ inputs = inputs?.distinct();
if (inputs != null)
{
- if (inputs.length == 0) {
+ if (inputs.length == 0)
+ {
trace('Control ${control} is missing bindings, resetting to default.');
- switch(device)
+ switch (device)
{
case Keys:
bindKeys(control, getDefaultKeybinds(Solo, control));
case Gamepad(id):
bindButtons(control, id, getDefaultGamepadBinds(control));
}
- } else if (inputs == [FlxKey.NONE]) {
+ }
+ else if (inputs == [FlxKey.NONE])
+ {
trace('Control ${control} is unbound, leaving it be.');
- } else {
- switch(device)
+ }
+ else
+ {
+ switch (device)
{
case Keys:
bindKeys(control, inputs.copy());
@@ -1020,9 +1137,11 @@ class Controls extends FlxActionSet
bindButtons(control, id, inputs.copy());
}
}
- } else {
+ }
+ else
+ {
trace('Control ${control} is missing bindings, resetting to default.');
- switch(device)
+ switch (device)
{
case Keys:
bindKeys(control, getDefaultKeybinds(Solo, control));
@@ -1047,10 +1166,13 @@ class Controls extends FlxActionSet
var inputs = getInputsFor(control, device);
isEmpty = isEmpty && inputs.length == 0;
- if (inputs.length == 0) {
+ if (inputs.length == 0)
+ {
inputs = [FlxKey.NONE];
- } else {
- inputs = inputs.unique();
+ }
+ else
+ {
+ inputs = inputs.distinct();
}
Reflect.setField(data, control.getName(), inputs);
@@ -1061,7 +1183,7 @@ class Controls extends FlxActionSet
static function isDevice(input:FlxActionInput, device:Device)
{
- return switch(device)
+ return switch (device)
{
case Keys: input.device == KEYBOARD;
case Gamepad(id): isGamepad(input, id);
@@ -1093,7 +1215,8 @@ typedef Swipes =
* - Combining `pressed` and `released` inputs into one action.
* - Filtering by input method (`KEYBOARD`, `MOUSE`, `GAMEPAD`, etc).
*/
-class FunkinAction extends FlxActionDigital {
+class FunkinAction extends FlxActionDigital
+{
public var namePressed(default, null):Null;
public var nameReleased(default, null):Null;
@@ -1110,83 +1233,102 @@ class FunkinAction extends FlxActionDigital {
/**
* Input checks default to whether the input was just pressed, on any input device.
*/
- public override function check():Bool {
+ public override function check():Bool
+ {
return checkFiltered(JUST_PRESSED);
}
/**
* Check whether the input is currently being held.
*/
- public function checkPressed():Bool {
+ public function checkPressed():Bool
+ {
return checkFiltered(PRESSED);
}
/**
* Check whether the input is currently being held, and was not held last frame.
*/
- public function checkJustPressed():Bool {
+ public function checkJustPressed():Bool
+ {
return checkFiltered(JUST_PRESSED);
}
/**
* Check whether the input is not currently being held.
*/
- public function checkReleased():Bool {
+ public function checkReleased():Bool
+ {
return checkFiltered(RELEASED);
}
/**
* Check whether the input is not currently being held, and was held last frame.
*/
- public function checkJustReleased():Bool {
+ public function checkJustReleased():Bool
+ {
return checkFiltered(JUST_RELEASED);
}
/**
* Check whether the input is currently being held by a gamepad device.
*/
- public function checkPressedGamepad():Bool {
+ public function checkPressedGamepad():Bool
+ {
return checkFiltered(PRESSED, GAMEPAD);
}
/**
* Check whether the input is currently being held by a gamepad device, and was not held last frame.
*/
- public function checkJustPressedGamepad():Bool {
+ public function checkJustPressedGamepad():Bool
+ {
return checkFiltered(JUST_PRESSED, GAMEPAD);
}
/**
* Check whether the input is not currently being held by a gamepad device.
*/
- public function checkReleasedGamepad():Bool {
+ public function checkReleasedGamepad():Bool
+ {
return checkFiltered(RELEASED, GAMEPAD);
}
/**
* Check whether the input is not currently being held by a gamepad device, and was held last frame.
*/
- public function checkJustReleasedGamepad():Bool {
+ public function checkJustReleasedGamepad():Bool
+ {
return checkFiltered(JUST_RELEASED, GAMEPAD);
}
- public function checkMultiFiltered(?filterTriggers:Array, ?filterDevices:Array):Bool {
- if (filterTriggers == null) {
+ public function checkMultiFiltered(?filterTriggers:Array, ?filterDevices:Array):Bool
+ {
+ if (filterTriggers == null)
+ {
filterTriggers = [PRESSED, JUST_PRESSED];
}
- if (filterDevices == null) {
+ if (filterDevices == null)
+ {
filterDevices = [];
}
// Perform checkFiltered for each combination.
- for (i in filterTriggers) {
- if (filterDevices.length == 0) {
- if (checkFiltered(i)) {
+ for (i in filterTriggers)
+ {
+ if (filterDevices.length == 0)
+ {
+ if (checkFiltered(i))
+ {
return true;
}
- } else {
- for (j in filterDevices) {
- if (checkFiltered(i, j)) {
+ }
+ else
+ {
+ for (j in filterDevices)
+ {
+ if (checkFiltered(i, j))
+ {
return true;
}
}
@@ -1201,52 +1343,56 @@ class FunkinAction extends FlxActionDigital {
* @param filterTrigger Optionally filter by trigger condition (`JUST_PRESSED`, `PRESSED`, `JUST_RELEASED`, `RELEASED`).
* @param filterDevice Optionally filter by device (`KEYBOARD`, `MOUSE`, `GAMEPAD`, `OTHER`).
*/
- public function checkFiltered(?filterTrigger:FlxInputState, ?filterDevice:FlxInputDevice):Bool {
+ public function checkFiltered(?filterTrigger:FlxInputState, ?filterDevice:FlxInputDevice):Bool
+ {
// The normal
// Make sure we only update the inputs once per frame.
var key = '${filterTrigger}:${filterDevice}';
var cacheEntry = cache.get(key);
- if (cacheEntry != null && cacheEntry.timestamp == FlxG.game.ticks) {
+ if (cacheEntry != null && cacheEntry.timestamp == FlxG.game.ticks)
+ {
return cacheEntry.value;
}
// Use a for loop instead so we can remove inputs while iterating.
// We don't return early because we need to call check() on ALL inputs.
var result = false;
- var len = inputs != null ? inputs.length : 0;
- for (i in 0...len)
- {
- var j = len - i - 1;
- var input = inputs[j];
+ var len = inputs != null ? inputs.length : 0;
+ for (i in 0...len)
+ {
+ var j = len - i - 1;
+ var input = inputs[j];
// Filter out dead inputs.
- if (input.destroyed)
- {
- inputs.splice(j, 1);
- continue;
- }
+ if (input.destroyed)
+ {
+ inputs.splice(j, 1);
+ continue;
+ }
// Update the input.
input.update();
// Check whether the input is the right trigger.
- if (filterTrigger != null && input.trigger != filterTrigger) {
+ if (filterTrigger != null && input.trigger != filterTrigger)
+ {
continue;
}
// Check whether the input is the right device.
- if (filterDevice != null && input.device != filterDevice) {
+ if (filterDevice != null && input.device != filterDevice)
+ {
continue;
}
// Check whether the input has triggered.
- if (input.check(this))
- {
- result = true;
- }
- }
+ if (input.check(this))
+ {
+ result = true;
+ }
+ }
// We need to cache this result.
cache.set(key, {timestamp: FlxG.game.ticks, value: result});
@@ -1343,12 +1489,12 @@ class FlxActionInputDigitalMobileSwipeGameplay extends FlxActionInputDigital
{
var degAngle = FlxAngle.asDegrees(swp.touchAngle);
- switch(trigger)
+ switch (trigger)
{
case JUST_PRESSED:
if (swp.touchLength >= activateLength)
{
- switch(inputID)
+ switch (inputID)
{
case FlxDirectionFlags.UP:
if (degAngle >= 45 && degAngle <= 90 + 45) return properTouch(swp);
@@ -1392,7 +1538,7 @@ class FlxActionInputDigitalAndroid extends FlxActionInputDigital
override public function check(Action:FlxAction):Bool
{
- returnswitch(trigger)
+ return switch (trigger)
{
#if android
case PRESSED: FlxG.android.checkStatus(inputID, PRESSED) || FlxG.android.checkStatus(inputID, PRESSED);
@@ -1425,14 +1571,19 @@ enum Control
UI_RIGHT;
UI_DOWN;
RESET;
- SCREENSHOT;
ACCEPT;
BACK;
PAUSE;
- FULLSCREEN;
// CUTSCENE
CUTSCENE_ADVANCE;
- // SCREENSHOT
+ // FREEPLAY
+ FREEPLAY_FAVORITE;
+ FREEPLAY_LEFT;
+ FREEPLAY_RIGHT;
+ FREEPLAY_CHAR_SELECT;
+ // WINDOW
+ WINDOW_SCREENSHOT;
+ WINDOW_FULLSCREEN;
// VOLUME
VOLUME_UP;
VOLUME_DOWN;
@@ -1475,11 +1626,16 @@ enum abstract Action(String) to String from String
var BACK = "back";
var PAUSE = "pause";
var RESET = "reset";
- var FULLSCREEN = "fullscreen";
- // SCREENSHOT
- var SCREENSHOT = "screenshot";
+ // WINDOW
+ var WINDOW_FULLSCREEN = "window_fullscreen";
+ var WINDOW_SCREENSHOT = "window_screenshot";
// CUTSCENE
var CUTSCENE_ADVANCE = "cutscene_advance";
+ // FREEPLAY
+ var FREEPLAY_FAVORITE = "freeplay_favorite";
+ var FREEPLAY_LEFT = "freeplay_left";
+ var FREEPLAY_RIGHT = "freeplay_right";
+ var FREEPLAY_CHAR_SELECT = "freeplay_char_select";
// VOLUME
var VOLUME_UP = "volume_up";
var VOLUME_DOWN = "volume_down";
diff --git a/source/funkin/modding/IScriptedClass.hx b/source/funkin/modding/IScriptedClass.hx
index 5f2ff2b9ee..14aa6b494c 100644
--- a/source/funkin/modding/IScriptedClass.hx
+++ b/source/funkin/modding/IScriptedClass.hx
@@ -73,6 +73,22 @@ interface INoteScriptedClass extends IScriptedClass
public function onNoteMiss(event:NoteScriptEvent):Void;
}
+/**
+ * Defines a set of callbacks available to scripted classes which represent sprites synced with the BPM.
+ */
+interface IBPMSyncedScriptedClass extends IScriptedClass
+{
+ /**
+ * Called once every step of the song.
+ */
+ public function onStepHit(event:SongTimeScriptEvent):Void;
+
+ /**
+ * Called once every beat of the song.
+ */
+ public function onBeatHit(event:SongTimeScriptEvent):Void;
+}
+
/**
* Developer note:
*
@@ -86,7 +102,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 INoteScriptedClass
+interface IPlayStateScriptedClass extends INoteScriptedClass extends IBPMSyncedScriptedClass
{
/**
* Called when the game is paused.
@@ -136,16 +152,6 @@ interface IPlayStateScriptedClass extends INoteScriptedClass
*/
public function onSongEvent(event:SongEventScriptEvent):Void;
- /**
- * Called once every step of the song.
- */
- public function onStepHit(event:SongTimeScriptEvent):Void;
-
- /**
- * Called once every beat of the song.
- */
- public function onBeatHit(event:SongTimeScriptEvent):Void;
-
/**
* Called when the countdown of the song starts.
*/
diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx
index ae754b780b..75c69e5065 100644
--- a/source/funkin/modding/PolymodHandler.hx
+++ b/source/funkin/modding/PolymodHandler.hx
@@ -7,7 +7,9 @@ import funkin.data.dialogue.speaker.SpeakerRegistry;
import funkin.data.event.SongEventRegistry;
import funkin.data.story.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
+import funkin.play.notes.notekind.NoteKindManager;
import funkin.data.song.SongRegistry;
+import funkin.data.freeplay.player.PlayerRegistry;
import funkin.data.stage.StageRegistry;
import funkin.data.freeplay.album.AlbumRegistry;
import funkin.modding.module.ModuleHandler;
@@ -26,11 +28,10 @@ class PolymodHandler
{
/**
* The API version that mods should comply with.
- * Format this with Semantic Versioning; ...
- * Bug fixes increment the patch version, new features increment the minor version.
- * Changes that break old mods increment the major version.
+ * Indicates which mods are compatible with this version of the game.
+ * Minor updates rarely impact mods but major versions often do.
*/
- static final API_VERSION:String = '0.1.0';
+ static final API_VERSION:String = "0.5.0"; // Constants.VERSION;
/**
* Where relative to the executable that mods are located.
@@ -176,7 +177,7 @@ class PolymodHandler
loadedModIds.push(mod.id);
}
- #if debug
+ #if FEATURE_DEBUG_FUNCTIONS
var fileList:Array = Polymod.listModFiles(PolymodAssetType.IMAGE);
trace('Installed mods have replaced ${fileList.length} images.');
for (item in fileList)
@@ -232,6 +233,12 @@ class PolymodHandler
// NOTE: Scripted classes are automatically aliased to their parent class.
Polymod.addImportAlias('flixel.math.FlxPoint', flixel.math.FlxPoint.FlxBasePoint);
+ Polymod.addImportAlias('funkin.data.event.SongEventSchema', funkin.data.event.SongEventSchema.SongEventSchemaRaw);
+
+ // `lime.utils.Assets` literally just has a private `resolveClass` function for some reason? so we replace it with our own.
+ Polymod.addImportAlias('lime.utils.Assets', funkin.Assets);
+ Polymod.addImportAlias('openfl.utils.Assets', funkin.Assets);
+
// Add blacklisting for prohibited classes and packages.
// `Sys`
@@ -250,8 +257,28 @@ class PolymodHandler
// Lib.load() can load malicious DLLs
Polymod.blacklistImport('cpp.Lib');
+ // `Unserializer`
+ // Unserializerr.DEFAULT_RESOLVER.resolveClass() can access blacklisted packages
+ Polymod.blacklistImport('Unserializer');
+
+ // `lime.system.CFFI`
+ // Can load and execute compiled binaries.
+ Polymod.blacklistImport('lime.system.CFFI');
+
+ // `lime.system.JNI`
+ // Can load and execute compiled binaries.
+ Polymod.blacklistImport('lime.system.JNI');
+
+ // `lime.system.System`
+ // System.load() can load malicious DLLs
+ Polymod.blacklistImport('lime.system.System');
+
+ // `openfl.desktop.NativeProcess`
+ // Can load native processes on the host operating system.
+ Polymod.blacklistImport('openfl.desktop.NativeProcess');
+
// `polymod.*`
- // You can probably unblacklist a module
+ // Contains functions which may allow for un-blacklisting other modules.
for (cls in ClassMacro.listClassesInPackage('polymod'))
{
if (cls == null) continue;
@@ -260,6 +287,7 @@ class PolymodHandler
}
// `sys.*`
+ // Access to system utilities such as the file system.
for (cls in ClassMacro.listClassesInPackage('sys'))
{
if (cls == null) continue;
@@ -369,16 +397,20 @@ class PolymodHandler
// These MUST be imported at the top of the file and not referred to by fully qualified name,
// to ensure build macros work properly.
+ SongEventRegistry.loadEventCache();
+
SongRegistry.instance.loadEntries();
LevelRegistry.instance.loadEntries();
NoteStyleRegistry.instance.loadEntries();
- SongEventRegistry.loadEventCache();
+ PlayerRegistry.instance.loadEntries();
ConversationRegistry.instance.loadEntries();
DialogueBoxRegistry.instance.loadEntries();
SpeakerRegistry.instance.loadEntries();
AlbumRegistry.instance.loadEntries();
StageRegistry.instance.loadEntries();
+
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
+ NoteKindManager.loadScripts();
ModuleHandler.loadModuleCache();
}
}
diff --git a/source/funkin/modding/base/ScriptedFlxUIState.hx b/source/funkin/modding/base/ScriptedFlxUIState.hx
deleted file mode 100644
index c58fc294fd..0000000000
--- a/source/funkin/modding/base/ScriptedFlxUIState.hx
+++ /dev/null
@@ -1,8 +0,0 @@
-package funkin.modding.base;
-
-/**
- * A script that can be tied to an FlxUIState.
- * Create a scripted class that extends FlxUIState to use this.
- */
-@:hscriptClass
-class ScriptedFlxUIState extends flixel.addons.ui.FlxUIState implements HScriptedClass {}
diff --git a/source/funkin/modding/events/ScriptEvent.hx b/source/funkin/modding/events/ScriptEvent.hx
index 8d625290d3..70055b2624 100644
--- a/source/funkin/modding/events/ScriptEvent.hx
+++ b/source/funkin/modding/events/ScriptEvent.hx
@@ -140,16 +140,37 @@ class HitNoteScriptEvent extends NoteScriptEvent
*/
public var score:Int;
- public function new(note:NoteSprite, healthChange:Float, score:Int, judgement:String, comboCount:Int = 0):Void
+ /**
+ * If the hit causes a combo break.
+ */
+ public var isComboBreak:Bool = false;
+
+ /**
+ * The time difference when the player hit the note
+ */
+ public var hitDiff:Float = 0;
+
+ /**
+ * Whether this note hit causes a note splash to display.
+ * Defaults to true only on "sick" notes.
+ */
+ public var doesNotesplash:Bool = false;
+
+ public function new(note:NoteSprite, healthChange:Float, score:Int, judgement:String, isComboBreak:Bool, comboCount:Int = 0, hitDiff:Float = 0,
+ doesNotesplash:Bool = false):Void
{
super(NOTE_HIT, note, healthChange, comboCount, true);
this.score = score;
this.judgement = judgement;
+ this.isComboBreak = isComboBreak;
+ this.doesNotesplash = doesNotesplash;
+ this.hitDiff = hitDiff;
}
public override function toString():String
{
- return 'HitNoteScriptEvent(note=' + note + ', comboCount=' + comboCount + ', judgement=' + judgement + ', score=' + score + ')';
+ return 'HitNoteScriptEvent(note=' + note + ', comboCount=' + comboCount + ', judgement=' + judgement + ', score=' + score + ', isComboBreak='
+ + isComboBreak + ', hitDiff=' + hitDiff + ', doesNotesplash=' + doesNotesplash + ')';
}
}
diff --git a/source/funkin/modding/events/ScriptEventDispatcher.hx b/source/funkin/modding/events/ScriptEventDispatcher.hx
index c262c311dc..7e19173c46 100644
--- a/source/funkin/modding/events/ScriptEventDispatcher.hx
+++ b/source/funkin/modding/events/ScriptEventDispatcher.hx
@@ -94,20 +94,29 @@ class ScriptEventDispatcher
}
}
- if (Std.isOfType(target, IPlayStateScriptedClass))
+ if (Std.isOfType(target, IBPMSyncedScriptedClass))
{
- var t:IPlayStateScriptedClass = cast(target, IPlayStateScriptedClass);
+ var t:IBPMSyncedScriptedClass = cast(target, IBPMSyncedScriptedClass);
switch (event.type)
{
- case NOTE_GHOST_MISS:
- t.onNoteGhostMiss(cast event);
- return;
case SONG_BEAT_HIT:
t.onBeatHit(cast event);
return;
case SONG_STEP_HIT:
t.onStepHit(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;
case SONG_START:
t.onSongStart(event);
return;
diff --git a/source/funkin/modding/events/ScriptEventType.hx b/source/funkin/modding/events/ScriptEventType.hx
index eeeb8ef299..6ac85649f9 100644
--- a/source/funkin/modding/events/ScriptEventType.hx
+++ b/source/funkin/modding/events/ScriptEventType.hx
@@ -20,7 +20,7 @@ enum abstract ScriptEventType(String) from String to String
var DESTROY = 'DESTROY';
/**
- * Called when the relevent object is added to the game state.
+ * Called when the relevant object is added to the game state.
* This assumes all data is loaded and ready to go.
*
* This event is not cancelable.
diff --git a/source/funkin/play/Countdown.hx b/source/funkin/play/Countdown.hx
index 10636afdf9..643883a43f 100644
--- a/source/funkin/play/Countdown.hx
+++ b/source/funkin/play/Countdown.hx
@@ -9,7 +9,11 @@ import funkin.modding.module.ModuleHandler;
import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
import flixel.util.FlxTimer;
+import funkin.util.EaseUtil;
import funkin.audio.FunkinSound;
+import openfl.utils.Assets;
+import funkin.data.notestyle.NoteStyleRegistry;
+import funkin.play.notes.notestyle.NoteStyle;
class Countdown
{
@@ -18,6 +22,24 @@ class Countdown
*/
public static var countdownStep(default, null):CountdownStep = BEFORE;
+ /**
+ * Which alternate graphic/sound on countdown to use.
+ * This is set via the current notestyle.
+ * For example, in Week 6 it is `pixel`.
+ */
+ public static var soundSuffix:String = '';
+
+ /**
+ * Which alternate graphic on countdown to use.
+ * You can set this via script.
+ * For example, in Week 6 it is `-pixel`.
+ */
+ public static var graphicSuffix:String = '';
+
+ static var noteStyle:NoteStyle;
+
+ static var fallbackNoteStyle:Null;
+
/**
* The currently running countdown. This will be null if there is no countdown running.
*/
@@ -29,7 +51,7 @@ class Countdown
* This will automatically stop and restart the countdown if it is already running.
* @returns `false` if the countdown was cancelled by a script.
*/
- public static function performCountdown(isPixelStyle:Bool):Bool
+ public static function performCountdown():Bool
{
countdownStep = BEFORE;
var cancelled:Bool = propagateCountdownEvent(countdownStep);
@@ -64,10 +86,10 @@ class Countdown
// PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0));
// Countdown graphic.
- showCountdownGraphic(countdownStep, isPixelStyle);
+ showCountdownGraphic(countdownStep);
// Countdown sound.
- playCountdownSound(countdownStep, isPixelStyle);
+ playCountdownSound(countdownStep);
// Event handling bullshit.
var cancelled:Bool = propagateCountdownEvent(countdownStep);
@@ -117,7 +139,7 @@ class Countdown
*
* If you want to call this from a module, it's better to use the event system and cancel the onCountdownStep event.
*/
- public static function pauseCountdown()
+ public static function pauseCountdown():Void
{
if (countdownTimer != null && !countdownTimer.finished)
{
@@ -130,7 +152,7 @@ class Countdown
*
* If you want to call this from a module, it's better to use the event system and cancel the onCountdownStep event.
*/
- public static function resumeCountdown()
+ public static function resumeCountdown():Void
{
if (countdownTimer != null && !countdownTimer.finished)
{
@@ -143,7 +165,7 @@ class Countdown
*
* If you want to call this from a module, it's better to use the event system and cancel the onCountdownStart event.
*/
- public static function stopCountdown()
+ public static function stopCountdown():Void
{
if (countdownTimer != null)
{
@@ -156,7 +178,7 @@ class Countdown
/**
* Stops the current countdown, then starts the song for you.
*/
- public static function skipCountdown()
+ public static function skipCountdown():Void
{
stopCountdown();
// This will trigger PlayState.startSong()
@@ -176,114 +198,69 @@ class Countdown
}
/**
- * Retrieves the graphic to use for this step of the countdown.
- * TODO: Make this less dumb. Unhardcode it? Use modules? Use notestyles?
- *
- * This is public so modules can do lol funny shit.
+ * Reset the countdown configuration to the default.
*/
- public static function showCountdownGraphic(index:CountdownStep, isPixelStyle:Bool):Void
+ public static function reset()
{
- var spritePath:String = null;
+ noteStyle = null;
+ }
- if (isPixelStyle)
- {
- switch (index)
- {
- case TWO:
- spritePath = 'weeb/pixelUI/ready-pixel';
- case ONE:
- spritePath = 'weeb/pixelUI/set-pixel';
- case GO:
- spritePath = 'weeb/pixelUI/date-pixel';
- default:
- // null
- }
- }
- else
- {
- switch (index)
- {
- case TWO:
- spritePath = 'ready';
- case ONE:
- spritePath = 'set';
- case GO:
- spritePath = 'go';
- default:
- // null
- }
- }
+ /**
+ * Retrieve the note style data (if we haven't already)
+ * @param noteStyleId The id of the note style to fetch. Defaults to the one used by the current PlayState.
+ * @param force Fetch the note style from the registry even if we've already fetched it.
+ */
+ static function fetchNoteStyle(?noteStyleId:String, force:Bool = false):Void
+ {
+ if (noteStyle != null && !force) return;
- if (spritePath == null) return;
+ if (noteStyleId == null) noteStyleId = PlayState.instance?.currentChart?.noteStyle;
- var countdownSprite:FunkinSprite = FunkinSprite.create(spritePath);
- countdownSprite.scrollFactor.set(0, 0);
+ noteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
+ if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
+ }
- if (isPixelStyle) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE));
+ /**
+ * Retrieves the graphic to use for this step of the countdown.
+ */
+ public static function showCountdownGraphic(index:CountdownStep):Void
+ {
+ fetchNoteStyle();
- countdownSprite.antialiasing = !isPixelStyle;
+ var countdownSprite = noteStyle.buildCountdownSprite(index);
+ if (countdownSprite == null) return;
- countdownSprite.updateHitbox();
- countdownSprite.screenCenter();
+ var fadeEase = FlxEase.cubeInOut;
+ if (noteStyle.isCountdownSpritePixel(index)) fadeEase = EaseUtil.stepped(8);
// Fade sprite in, then out, then destroy it.
- FlxTween.tween(countdownSprite, {y: countdownSprite.y += 100, alpha: 0}, Conductor.instance.beatLengthMs / 1000,
+ FlxTween.tween(countdownSprite, {alpha: 0}, Conductor.instance.beatLengthMs / 1000,
{
- ease: FlxEase.cubeInOut,
+ ease: fadeEase,
onComplete: function(twn:FlxTween) {
countdownSprite.destroy();
}
});
+ countdownSprite.cameras = [PlayState.instance.camHUD];
PlayState.instance.add(countdownSprite);
+ countdownSprite.screenCenter();
+
+ var offsets = noteStyle.getCountdownSpriteOffsets(index);
+ countdownSprite.x += offsets[0];
+ countdownSprite.y += offsets[1];
}
/**
* Retrieves the sound file to use for this step of the countdown.
- * TODO: Make this less dumb. Unhardcode it? Use modules? Use notestyles?
- *
- * This is public so modules can do lol funny shit.
*/
- public static function playCountdownSound(index:CountdownStep, isPixelStyle:Bool):Void
+ public static function playCountdownSound(step:CountdownStep):FunkinSound
{
- var soundPath:String = null;
-
- if (isPixelStyle)
- {
- switch (index)
- {
- case THREE:
- soundPath = 'intro3-pixel';
- case TWO:
- soundPath = 'intro2-pixel';
- case ONE:
- soundPath = 'intro1-pixel';
- case GO:
- soundPath = 'introGo-pixel';
- default:
- // null
- }
- }
- else
- {
- switch (index)
- {
- case THREE:
- soundPath = 'intro3';
- case TWO:
- soundPath = 'intro2';
- case ONE:
- soundPath = 'intro1';
- case GO:
- soundPath = 'introGo';
- default:
- // null
- }
- }
-
- if (soundPath == null) return;
+ fetchNoteStyle();
+ var path = noteStyle.getCountdownSoundPath(step);
+ if (path == null) return null;
- FunkinSound.playOnce(Paths.sound(soundPath), Constants.COUNTDOWN_VOLUME);
+ return FunkinSound.playOnce(path, Constants.COUNTDOWN_VOLUME);
}
public static function decrement(step:CountdownStep):CountdownStep
diff --git a/source/funkin/play/GameOverSubState.hx b/source/funkin/play/GameOverSubState.hx
index c3abbcf3e2..f6a7148f81 100644
--- a/source/funkin/play/GameOverSubState.hx
+++ b/source/funkin/play/GameOverSubState.hx
@@ -16,6 +16,8 @@ import funkin.ui.MusicBeatSubState;
import funkin.ui.story.StoryMenuState;
import funkin.util.MathUtil;
import openfl.utils.Assets;
+import funkin.effects.RetroCameraFade;
+import flixel.math.FlxPoint;
/**
* A substate which renders over the PlayState when the player dies.
@@ -71,7 +73,7 @@ class GameOverSubState extends MusicBeatSubState
var gameOverMusic:Null = null;
/**
- * Whether the player has confirmed and prepared to restart the level.
+ * Whether the player has confirmed and prepared to restart the level or to go back to the freeplay menu.
* This means the animation and transition have already started.
*/
var isEnding:Bool = false;
@@ -144,6 +146,7 @@ class GameOverSubState extends MusicBeatSubState
else
{
boyfriend = PlayState.instance.currentStage.getBoyfriend(true);
+ boyfriend.canPlayOtherAnims = true;
boyfriend.isDead = true;
add(boyfriend);
boyfriend.resetCharacter();
@@ -162,10 +165,12 @@ class GameOverSubState extends MusicBeatSubState
@:nullSafety(Off)
function setCameraTarget():Void
{
+ if (PlayState.instance.isMinimalMode || boyfriend == null) return;
+
// 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;
+ cameraFollowPoint.x = getMidPointOld(boyfriend).x;
+ cameraFollowPoint.y = getMidPointOld(boyfriend).y;
var offsets:Array = boyfriend.getDeathCameraOffsets();
cameraFollowPoint.x += offsets[0];
cameraFollowPoint.y += offsets[1];
@@ -176,6 +181,21 @@ class GameOverSubState extends MusicBeatSubState
targetCameraZoom = (PlayState?.instance?.currentStage?.camZoom ?? 1.0) * boyfriend.getDeathCameraZoom();
}
+ /**
+ * FlxSprite.getMidpoint(); calculations changed in this git commit
+ * https://github.com/HaxeFlixel/flixel/commit/1553b5af0871462fcefedc091b7885437d6c36d2
+ * https://github.com/HaxeFlixel/flixel/pull/3125
+ *
+ * So we use this to do the old math that gets the midpoint of our graphics
+ * Luckily, we don't use getGraphicMidpoint() much in the code, so it's fine being in GameoverSubState here.
+ * @return FlxPoint
+ */
+ function getMidPointOld(spr:FlxSprite, ?point:FlxPoint):FlxPoint
+ {
+ if (point == null) point = FlxPoint.get();
+ return point.set(spr.x + spr.frameWidth * 0.5 * spr.scale.x, spr.y + spr.frameHeight * 0.5 * spr.scale.y);
+ }
+
/**
* Forcibly reset the camera zoom level to that of the current stage.
* This prevents camera zoom events from adversely affecting the game over state.
@@ -235,15 +255,16 @@ class GameOverSubState extends MusicBeatSubState
}
// KEYBOARD ONLY: Restart the level when pressing the assigned key.
- if (controls.ACCEPT && blueballed)
+ if (controls.ACCEPT && blueballed && !mustNotExit)
{
blueballed = false;
confirmDeath();
}
// KEYBOARD ONLY: Return to the menu when pressing the assigned key.
- if (controls.BACK && !mustNotExit)
+ if (controls.BACK && !mustNotExit && !isEnding)
{
+ isEnding = true;
blueballed = false;
PlayState.instance.deathCounter = 0;
// PlayState.seenCutscene = false; // old thing...
@@ -254,6 +275,7 @@ class GameOverSubState extends MusicBeatSubState
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!
+ return;
}
else if (PlayStatePlaylist.isStoryMode)
{
@@ -327,9 +349,12 @@ class GameOverSubState extends MusicBeatSubState
// After the animation finishes...
new FlxTimer().start(0.7, function(tmr:FlxTimer) {
// ...fade out the graphics. Then after that happens...
- FlxG.camera.fade(FlxColor.BLACK, 2, false, function() {
+
+ var resetPlaying = function(pixel:Bool = false) {
// ...close the GameOverSubState.
- FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true);
+ if (pixel) RetroCameraFade.fadeBlack(FlxG.camera, 10, 1);
+ else
+ FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true);
PlayState.instance.needsReset = true;
if (PlayState.instance.isMinimalMode || boyfriend == null) {}
@@ -346,7 +371,22 @@ class GameOverSubState extends MusicBeatSubState
// Close the substate.
close();
- });
+ };
+
+ if (musicSuffix == '-pixel')
+ {
+ RetroCameraFade.fadeToBlack(FlxG.camera, 10, 2);
+ new FlxTimer().start(2, _ -> {
+ FlxG.camera.filters = [];
+ resetPlaying(true);
+ });
+ }
+ else
+ {
+ FlxG.camera.fade(FlxColor.BLACK, 2, false, function() {
+ resetPlaying();
+ });
+ }
});
}
}
diff --git a/source/funkin/play/PauseSubState.hx b/source/funkin/play/PauseSubState.hx
index fb9d9b4e29..9ff70bee57 100644
--- a/source/funkin/play/PauseSubState.hx
+++ b/source/funkin/play/PauseSubState.hx
@@ -101,6 +101,10 @@ class PauseSubState extends MusicBeatSubState
*/
static final MUSIC_FINAL_VOLUME:Float = 0.75;
+ static final CHARTER_FADE_DELAY:Float = 15.0;
+
+ static final CHARTER_FADE_DURATION:Float = 0.75;
+
/**
* Defines which pause music to use.
*/
@@ -163,6 +167,12 @@ class PauseSubState extends MusicBeatSubState
*/
var metadataDeaths:FlxText;
+ /**
+ * A text object which displays the current song's artist.
+ * Fades to the charter after a period before fading back.
+ */
+ var metadataArtist:FlxText;
+
/**
* The actual text objects for the menu entries.
*/
@@ -203,6 +213,8 @@ class PauseSubState extends MusicBeatSubState
regenerateMenu();
transitionIn();
+
+ startCharterTimer();
}
/**
@@ -222,6 +234,8 @@ class PauseSubState extends MusicBeatSubState
public override function destroy():Void
{
super.destroy();
+ charterFadeTween.cancel();
+ charterFadeTween = null;
pauseMusic.stop();
}
@@ -270,30 +284,39 @@ class PauseSubState extends MusicBeatSubState
metadata.scrollFactor.set(0, 0);
add(metadata);
- var metadataSong:FlxText = new FlxText(20, 15, FlxG.width - 40, 'Song Name - Artist');
+ var metadataSong:FlxText = new FlxText(20, 15, FlxG.width - 40, 'Song Name');
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.text = '${PlayState.instance.currentChart.songName}';
}
metadataSong.scrollFactor.set(0, 0);
metadata.add(metadataSong);
- var metadataDifficulty:FlxText = new FlxText(20, 15 + 32, FlxG.width - 40, 'Difficulty: ');
+ metadataArtist = new FlxText(20, metadataSong.y + 32, FlxG.width - 40, 'Artist: ${Constants.DEFAULT_ARTIST}');
+ metadataArtist.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
+ if (PlayState.instance?.currentChart != null)
+ {
+ metadataArtist.text = 'Artist: ${PlayState.instance.currentChart.songArtist}';
+ }
+ metadataArtist.scrollFactor.set(0, 0);
+ metadata.add(metadataArtist);
+
+ var metadataDifficulty:FlxText = new FlxText(20, metadataArtist.y + 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.text += PlayState.instance.currentDifficulty.replace('-', ' ').toTitleCase();
}
metadataDifficulty.scrollFactor.set(0, 0);
metadata.add(metadataDifficulty);
- metadataDeaths = new FlxText(20, 15 + 64, FlxG.width - 40, '${PlayState.instance?.deathCounter} Blue Balls');
+ metadataDeaths = new FlxText(20, metadataDifficulty.y + 32, 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 = new FlxText(20, metadataDeaths.y + 32, 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);
@@ -302,6 +325,62 @@ class PauseSubState extends MusicBeatSubState
updateMetadataText();
}
+ var charterFadeTween:Null = null;
+
+ function startCharterTimer():Void
+ {
+ charterFadeTween = FlxTween.tween(metadataArtist, {alpha: 0.0}, CHARTER_FADE_DURATION,
+ {
+ startDelay: CHARTER_FADE_DELAY,
+ ease: FlxEase.quartOut,
+ onComplete: (_) -> {
+ if (PlayState.instance?.currentChart != null)
+ {
+ metadataArtist.text = 'Charter: ${PlayState.instance.currentChart.charter ?? 'Unknown'}';
+ }
+ else
+ {
+ metadataArtist.text = 'Charter: ${Constants.DEFAULT_CHARTER}';
+ }
+
+ FlxTween.tween(metadataArtist, {alpha: 1.0}, CHARTER_FADE_DURATION,
+ {
+ ease: FlxEase.quartOut,
+ onComplete: (_) -> {
+ startArtistTimer();
+ }
+ });
+ }
+ });
+ }
+
+ function startArtistTimer():Void
+ {
+ charterFadeTween = FlxTween.tween(metadataArtist, {alpha: 0.0}, CHARTER_FADE_DURATION,
+ {
+ startDelay: CHARTER_FADE_DELAY,
+ ease: FlxEase.quartOut,
+ onComplete: (_) -> {
+ if (PlayState.instance?.currentChart != null)
+ {
+ metadataArtist.text = 'Artist: ${PlayState.instance.currentChart.songArtist}';
+ }
+ else
+ {
+ metadataArtist.text = 'Artist: ${Constants.DEFAULT_ARTIST}';
+ }
+
+ FlxTween.tween(metadataArtist, {alpha: 1.0}, CHARTER_FADE_DURATION,
+ {
+ ease: FlxEase.quartOut,
+ onComplete: (_) -> {
+ startCharterTimer();
+ }
+ });
+ }
+ });
+ }
+
/**
* Perform additional animations to transition the pause menu in when it is first displayed.
*/
@@ -351,7 +430,7 @@ class PauseSubState extends MusicBeatSubState
resume(this);
}
- #if (debug || FORCE_DEBUG_VERSION)
+ #if FEATURE_DEBUG_FUNCTIONS
// to pause the game and get screenshots easy, press H on pause menu!
if (FlxG.keys.justPressed.H)
{
@@ -370,13 +449,14 @@ class PauseSubState extends MusicBeatSubState
*/
function changeSelection(change:Int = 0):Void
{
- FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4);
-
+ var prevEntry:Int = currentEntry;
currentEntry += change;
if (currentEntry < 0) currentEntry = currentMenuEntries.length - 1;
if (currentEntry >= currentMenuEntries.length) currentEntry = 0;
+ if (currentEntry != prevEntry) FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4);
+
for (entryIndex in 0...currentMenuEntries.length)
{
var isCurrent:Bool = entryIndex == currentEntry;
diff --git a/source/funkin/play/PlayState.hx b/source/funkin/play/PlayState.hx
index a9ca09ce84..0b2b8846d3 100644
--- a/source/funkin/play/PlayState.hx
+++ b/source/funkin/play/PlayState.hx
@@ -16,6 +16,7 @@ import flixel.tweens.FlxTween;
import flixel.ui.FlxBar;
import flixel.util.FlxColor;
import flixel.util.FlxTimer;
+import flixel.util.FlxStringUtil;
import funkin.api.newgrounds.NGio;
import funkin.audio.FunkinSound;
import funkin.audio.VoicesGroup;
@@ -48,6 +49,7 @@ import funkin.play.notes.NoteSprite;
import funkin.play.notes.notestyle.NoteStyle;
import funkin.play.notes.Strumline;
import funkin.play.notes.SustainTrail;
+import funkin.play.notes.notekind.NoteKindManager;
import funkin.play.scoring.Scoring;
import funkin.play.song.Song;
import funkin.play.stage.Stage;
@@ -65,7 +67,7 @@ import lime.ui.Haptic;
import openfl.display.BitmapData;
import openfl.geom.Rectangle;
import openfl.Lib;
-#if discord_rpc
+#if FEATURE_DISCORD_RPC
import Discord.DiscordClient;
#end
@@ -175,6 +177,12 @@ class PlayState extends MusicBeatSubState
*/
public var currentVariation:String = Constants.DEFAULT_VARIATION;
+ /**
+ * The currently selected instrumental ID.
+ * @default `''`
+ */
+ public var currentInstrumental:String = '';
+
/**
* The currently active Stage. This is the object containing all the props.
*/
@@ -236,6 +244,11 @@ class PlayState extends MusicBeatSubState
*/
public var cameraZoomTween:FlxTween;
+ /**
+ * An FlxTween that changes the additive speed to the desired amount.
+ */
+ public var scrollSpeedTweens:Array = [];
+
/**
* The camera follow point from the last stage.
* Used to persist the position of the `cameraFollowPosition` between levels.
@@ -432,7 +445,7 @@ class PlayState extends MusicBeatSubState
*/
public var vocals:VoicesGroup;
- #if discord_rpc
+ #if FEATURE_DISCORD_RPC
// Discord RPC variables
var storyDifficultyText:String = '';
var iconRPC:String = '';
@@ -491,7 +504,13 @@ class PlayState extends MusicBeatSubState
public var camGame:FlxCamera;
/**
- * The camera which contains, and controls visibility of, a video cutscene.
+ * Simple helper debug variable, to be able to move the camera around for debug purposes
+ * without worrying about the camera tweening back to the follow point.
+ */
+ public var debugUnbindCameraZoom:Bool = false;
+
+ /**
+ * The camera which contains, and controls visibility of, a video cutscene, dialogue, pause menu and sticker transition.
*/
public var camCutscene:FlxCamera;
@@ -566,7 +585,8 @@ class PlayState extends MusicBeatSubState
// TODO: Refactor or document
var generatedMusic:Bool = false;
- var perfectMode:Bool = false;
+
+ var skipEndingTransition:Bool = false;
static final BACKGROUND_COLOR:FlxColor = FlxColor.BLACK;
@@ -598,6 +618,7 @@ class PlayState extends MusicBeatSubState
currentSong = params.targetSong;
if (params.targetDifficulty != null) currentDifficulty = params.targetDifficulty;
if (params.targetVariation != null) currentVariation = params.targetVariation;
+ if (params.targetInstrumental != null) currentInstrumental = params.targetInstrumental;
isPracticeMode = params.practiceMode ?? false;
isBotPlayMode = params.botPlayMode ?? false;
isMinimalMode = params.minimalMode ?? false;
@@ -653,7 +674,7 @@ class PlayState extends MusicBeatSubState
// Prepare the current song's instrumental and vocals to be played.
if (!overrideMusic && currentChart != null)
{
- currentChart.cacheInst();
+ currentChart.cacheInst(currentInstrumental);
currentChart.cacheVocals();
}
@@ -662,7 +683,7 @@ class PlayState extends MusicBeatSubState
if (currentChart.offsets != null)
{
- Conductor.instance.instrumentalOffset = currentChart.offsets.getInstrumentalOffset();
+ Conductor.instance.instrumentalOffset = currentChart.offsets.getInstrumentalOffset(currentInstrumental);
}
Conductor.instance.mapTimeChanges(currentChart.timeChanges);
@@ -681,14 +702,9 @@ class PlayState extends MusicBeatSubState
initMinimalMode();
}
initStrumlines();
+ initPopups();
- // Initialize the judgements and combo meter.
- comboPopUps = new PopUpStuff();
- comboPopUps.zIndex = 900;
- add(comboPopUps);
- comboPopUps.cameras = [camHUD];
-
- #if discord_rpc
+ #if FEATURE_DISCORD_RPC
// Initialize Discord Rich Presence.
initDiscord();
#end
@@ -728,7 +744,7 @@ class PlayState extends MusicBeatSubState
rightWatermarkText.cameras = [camHUD];
// Initialize some debug stuff.
- #if (debug || FORCE_DEBUG_VERSION)
+ #if FEATURE_DEBUG_FUNCTIONS
// Display the version number (and git commit hash) in the bottom right corner.
this.rightWatermarkText.text = Constants.VERSION;
@@ -772,19 +788,19 @@ class PlayState extends MusicBeatSubState
var message:String = 'There was a critical error. Click OK to return to the main menu.';
if (currentSong == null)
{
- message = 'The was a critical error loading this song\'s chart. Click OK to return to the main menu.';
+ message = 'There was a critical error loading this song\'s chart. Click OK to return to the main menu.';
}
else if (currentDifficulty == null)
{
- message = 'The was a critical error selecting a difficulty for this song. Click OK to return to the main menu.';
+ message = 'There was a critical error selecting a difficulty for this song. Click OK to return to the main menu.';
}
else if (currentChart == null)
{
- message = 'The was a critical error retrieving data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.';
+ message = 'There was a critical error retrieving data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.';
}
else if (currentChart.notes == null)
{
- message = 'The was a critical error retrieving note data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.';
+ message = 'There was a critical error retrieving note data for this song on "$currentDifficulty" difficulty with variation "$currentVariation". Click OK to return to the main menu.';
}
// Display a popup. This blocks the application until the user clicks OK.
@@ -822,6 +838,8 @@ class PlayState extends MusicBeatSubState
{
if (!assertChartExists()) return;
+ prevScrollTargets = [];
+
dispatchEvent(new ScriptEvent(SONG_RETRY));
resetCamera();
@@ -848,7 +866,7 @@ class PlayState extends MusicBeatSubState
{
// Stop the vocals if they already exist.
if (vocals != null) vocals.stop();
- vocals = currentChart.buildVocals();
+ vocals = currentChart.buildVocals(currentInstrumental);
if (vocals.members.length == 0)
{
@@ -885,7 +903,7 @@ class PlayState extends MusicBeatSubState
health = Constants.HEALTH_STARTING;
songScore = 0;
Highscore.tallies.combo = 0;
- Countdown.performCountdown(currentStageId.startsWith('school'));
+ Countdown.performCountdown();
needsReset = false;
}
@@ -960,12 +978,12 @@ class PlayState extends MusicBeatSubState
FlxTransitionableState.skipNextTransIn = true;
FlxTransitionableState.skipNextTransOut = true;
- pauseSubState.camera = camHUD;
+ pauseSubState.camera = camCutscene;
openSubState(pauseSubState);
// boyfriendPos.put(); // TODO: Why is this here?
}
- #if discord_rpc
+ #if FEATURE_DISCORD_RPC
DiscordClient.changePresence(detailsPausedText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC);
#end
}
@@ -980,7 +998,7 @@ class PlayState extends MusicBeatSubState
{
cameraBopMultiplier = FlxMath.lerp(1.0, cameraBopMultiplier, 0.95); // Lerp bop multiplier back to 1.0x
var zoomPlusBop = currentCameraZoom * cameraBopMultiplier; // Apply camera bop multiplier.
- FlxG.camera.zoom = zoomPlusBop; // Actually apply the zoom to the camera.
+ if (!debugUnbindCameraZoom) FlxG.camera.zoom = zoomPlusBop; // Actually apply the zoom to the camera.
camHUD.zoom = FlxMath.lerp(defaultHUDCameraZoom, camHUD.zoom, 0.95);
}
@@ -1024,7 +1042,7 @@ class PlayState extends MusicBeatSubState
// Disable updates, preventing animations in the background from playing.
persistentUpdate = false;
- #if (debug || FORCE_DEBUG_VERSION)
+ #if FEATURE_DEBUG_FUNCTIONS
if (FlxG.keys.pressed.THREE)
{
// TODO: Change the key or delete this?
@@ -1035,7 +1053,7 @@ class PlayState extends MusicBeatSubState
{
#end
persistentDraw = false;
- #if (debug || FORCE_DEBUG_VERSION)
+ #if FEATURE_DEBUG_FUNCTIONS
}
#end
@@ -1054,7 +1072,7 @@ class PlayState extends MusicBeatSubState
moveToGameOver();
}
- #if discord_rpc
+ #if FEATURE_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
@@ -1092,8 +1110,11 @@ class PlayState extends MusicBeatSubState
healthBar.value = healthLerp;
- iconP1.updatePosition();
- iconP2.updatePosition();
+ if (!isMinimalMode)
+ {
+ iconP1.updatePosition();
+ iconP2.updatePosition();
+ }
// Transition to the game over substate.
var gameOverSubState = new GameOverSubState(
@@ -1147,6 +1168,9 @@ class PlayState extends MusicBeatSubState
// super.dispatchEvent(event) dispatches event to module scripts.
super.dispatchEvent(event);
+ // Dispatch event to note kind scripts
+ NoteKindManager.callEvent(event);
+
// Dispatch event to stage script.
ScriptEventDispatcher.callEvent(currentStage, event);
@@ -1158,14 +1182,12 @@ class PlayState extends MusicBeatSubState
// Dispatch event to conversation script.
ScriptEventDispatcher.callEvent(currentConversation, event);
-
- // TODO: Dispatch event to note scripts
}
/**
- * Function called before opening a new substate.
- * @param subState The substate to open.
- */
+ * Function called before opening a new substate.
+ * @param subState The substate to open.
+ */
public override function openSubState(subState:FlxSubState):Void
{
// If there is a substate which requires the game to continue,
@@ -1201,6 +1223,18 @@ class PlayState extends MusicBeatSubState
cameraTweensPausedBySubState.add(cameraZoomTween);
}
+ // Pause camera follow
+ FlxG.camera.followLerp = 0;
+
+ for (tween in scrollSpeedTweens)
+ {
+ if (tween != null && tween.active)
+ {
+ tween.active = false;
+ cameraTweensPausedBySubState.add(tween);
+ }
+ }
+
// Pause the countdown.
Countdown.pauseCountdown();
}
@@ -1209,9 +1243,9 @@ class PlayState extends MusicBeatSubState
}
/**
- * Function called before closing the current substate.
- * @param subState
- */
+ * Function called before closing the current substate.
+ * @param subState
+ */
public override function closeSubState():Void
{
if (Std.isOfType(subState, PauseSubState))
@@ -1236,6 +1270,9 @@ class PlayState extends MusicBeatSubState
}
cameraTweensPausedBySubState.clear();
+ // Resume camera follow
+ FlxG.camera.followLerp = Constants.DEFAULT_CAMERA_FOLLOW_RATE;
+
if (currentConversation != null)
{
currentConversation.resumeMusic();
@@ -1247,7 +1284,7 @@ class PlayState extends MusicBeatSubState
// Resume the countdown.
Countdown.resumeCountdown();
- #if discord_rpc
+ #if FEATURE_DISCORD_RPC
if (startTimer.finished)
{
DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC, true,
@@ -1269,12 +1306,18 @@ class PlayState extends MusicBeatSubState
super.closeSubState();
}
- #if discord_rpc
/**
- * Function called when the game window gains focus.
- */
+ * Function called when the game window gains focus.
+ */
public override function onFocus():Void
{
+ if (VideoCutscene.isPlaying() && FlxG.autoPause && isGamePaused) VideoCutscene.pauseVideo();
+ #if html5
+ else
+ VideoCutscene.resumeVideo();
+ #end
+
+ #if FEATURE_DISCORD_RPC
if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause)
{
if (Conductor.instance.songPosition > 0.0) DiscordClient.changePresence(detailsText, currentSong.song
@@ -1286,81 +1329,36 @@ class PlayState extends MusicBeatSubState
else
DiscordClient.changePresence(detailsText, currentSong.song + ' (' + storyDifficultyText + ')', iconRPC);
}
+ #end
super.onFocus();
}
/**
- * Function called when the game window loses focus.
- */
+ * Function called when the game window loses focus.
+ */
public override function onFocusLost():Void
{
+ #if html5
+ if (FlxG.autoPause) VideoCutscene.pauseVideo();
+ #end
+
+ #if FEATURE_DISCORD_RPC
if (health > Constants.HEALTH_MIN && !paused && FlxG.autoPause) DiscordClient.changePresence(detailsPausedText,
currentSong.song + ' (' + storyDifficultyText + ')', iconRPC);
+ #end
super.onFocusLost();
}
- #end
/**
- * Removes any references to the current stage, then clears the stage cache,
- * then reloads all the stages.
- *
- * This is useful for when you want to edit a stage without reloading the whole game.
- * Reloading works on both the JSON and the HXC, if applicable.
- *
- * Call this by pressing F5 on a debug build.
- */
- override function debug_refreshModules():Void
+ * Call this by pressing F5 on a debug build.
+ */
+ override function reloadAssets():Void
{
- // Prevent further gameplay updates, which will try to reference dead objects.
- criticalFailure = true;
-
- // Remove the current stage. If the stage gets deleted while it's still in use,
- // it'll probably crash the game or something.
- if (this.currentStage != null)
- {
- remove(currentStage);
- var event:ScriptEvent = new ScriptEvent(DESTROY, false);
- ScriptEventDispatcher.callEvent(currentStage, event);
- currentStage = null;
- }
-
- if (!overrideMusic)
- {
- // Stop the instrumental.
- if (FlxG.sound.music != null)
- {
- FlxG.sound.music.destroy();
- FlxG.sound.music = null;
- }
-
- // Stop the vocals.
- if (vocals != null && vocals.exists)
- {
- vocals.destroy();
- vocals = null;
- }
- }
- else
- {
- // Stop the instrumental.
- if (FlxG.sound.music != null)
- {
- FlxG.sound.music.stop();
- }
-
- // Stop the vocals.
- if (vocals != null && vocals.exists)
- {
- vocals.stop();
- }
- }
-
- super.debug_refreshModules();
-
- var event:ScriptEvent = new ScriptEvent(CREATE, false);
- ScriptEventDispatcher.callEvent(currentSong, event);
+ funkin.modding.PolymodHandler.forceReloadAssets();
+ lastParams.targetSong = SongRegistry.instance.fetchEntry(currentSong.id);
+ LoadingState.loadPlayState(lastParams);
}
override function stepHit():Bool
@@ -1372,17 +1370,6 @@ class PlayState extends MusicBeatSubState
if (isGamePaused) return false;
- if (!startingSong
- && FlxG.sound.music != null
- && (Math.abs(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200
- || Math.abs(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 200))
- {
- trace("VOCALS NEED RESYNC");
- if (vocals != null) trace(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
- trace(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
- resyncVocals();
- }
-
if (iconP1 != null) iconP1.onStepHit(Std.int(Conductor.instance.currentStep));
if (iconP2 != null) iconP2.onStepHit(Std.int(Conductor.instance.currentStep));
@@ -1404,6 +1391,17 @@ class PlayState extends MusicBeatSubState
// activeNotes.sort(SortUtil.byStrumtime, FlxSort.DESCENDING);
}
+ if (!startingSong
+ && FlxG.sound.music != null
+ && (Math.abs(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 100
+ || Math.abs(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset)) > 100))
+ {
+ trace("VOCALS NEED RESYNC");
+ if (vocals != null) trace(vocals.checkSyncError(Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
+ trace(FlxG.sound.music.time - (Conductor.instance.songPosition + Conductor.instance.instrumentalOffset));
+ resyncVocals();
+ }
+
// Only bop camera if zoom level is below 135%
if (Preferences.zoomCamera
&& FlxG.camera.zoom < (1.35 * FlxCamera.defaultZoom)
@@ -1456,9 +1454,6 @@ class PlayState extends MusicBeatSubState
if (playerStrumline != null) playerStrumline.onBeatHit();
if (opponentStrumline != null) opponentStrumline.onBeatHit();
- // Make the characters dance on the beat
- danceOnBeat();
-
return true;
}
@@ -1469,29 +1464,16 @@ class PlayState extends MusicBeatSubState
super.destroy();
}
- /**
- * Handles characters dancing to the beat of the current song.
- *
- * TODO: Move some of this logic into `Bopper.hx`, or individual character scripts.
- */
- function danceOnBeat():Void
+ public override function initConsoleHelpers():Void
{
- if (currentStage == null) return;
-
- // TODO: Add HEY! song events to Tutorial.
- if (Conductor.instance.currentBeat % 16 == 15
- && currentStage.getDad().characterId == 'gf'
- && Conductor.instance.currentBeat > 16
- && Conductor.instance.currentBeat < 48)
- {
- currentStage.getBoyfriend().playAnimation('hey', true);
- currentStage.getDad().playAnimation('cheer', true);
- }
- }
+ FlxG.console.registerFunction("debugUnbindCameraZoom", () -> {
+ debugUnbindCameraZoom = !debugUnbindCameraZoom;
+ });
+ };
/**
- * Initializes the game and HUD cameras.
- */
+ * Initializes the game and HUD cameras.
+ */
function initCameras():Void
{
camGame = new FunkinCamera('playStateCamGame');
@@ -1515,8 +1497,8 @@ class PlayState extends MusicBeatSubState
}
/**
- * Initializes the health bar on the HUD.
- */
+ * Initializes the health bar on the HUD.
+ */
function initHealthBar():Void
{
var healthBarYPos:Float = Preferences.downscroll ? FlxG.height * 0.1 : FlxG.height * 0.9;
@@ -1547,8 +1529,8 @@ class PlayState extends MusicBeatSubState
}
/**
- * Generates the stage and all its props.
- */
+ * Generates the stage and all its props.
+ */
function initStage():Void
{
loadStage(currentStageId);
@@ -1568,10 +1550,10 @@ class PlayState extends MusicBeatSubState
}
/**
- * Loads stage data from cache, assembles the props,
- * and adds it to the state.
- * @param id
- */
+ * Loads stage data from cache, assembles the props,
+ * and adds it to the state.
+ * @param id
+ */
function loadStage(id:String):Void
{
currentStage = StageRegistry.instance.fetchEntry(id);
@@ -1589,7 +1571,7 @@ class PlayState extends MusicBeatSubState
// Add the stage to the scene.
this.add(currentStage);
- #if (debug || FORCE_DEBUG_VERSION)
+ #if FEATURE_DEBUG_FUNCTIONS
FlxG.console.registerObject('stage', currentStage);
#end
}
@@ -1612,8 +1594,8 @@ class PlayState extends MusicBeatSubState
}
/**
- * Generates the character sprites and adds them to the stage.
- */
+ * Generates the character sprites and adds them to the stage.
+ */
function initCharacters():Void
{
if (currentSong == null || currentChart == null)
@@ -1692,7 +1674,7 @@ class PlayState extends MusicBeatSubState
{
currentStage.addCharacter(girlfriend, GF);
- #if (debug || FORCE_DEBUG_VERSION)
+ #if FEATURE_DEBUG_FUNCTIONS
FlxG.console.registerObject('gf', girlfriend);
#end
}
@@ -1701,7 +1683,7 @@ class PlayState extends MusicBeatSubState
{
currentStage.addCharacter(boyfriend, BF);
- #if (debug || FORCE_DEBUG_VERSION)
+ #if FEATURE_DEBUG_FUNCTIONS
FlxG.console.registerObject('bf', boyfriend);
#end
}
@@ -1712,7 +1694,7 @@ class PlayState extends MusicBeatSubState
// Camera starts at dad.
cameraFollowPoint.setPosition(dad.cameraFocusPoint.x, dad.cameraFocusPoint.y);
- #if (debug || FORCE_DEBUG_VERSION)
+ #if FEATURE_DEBUG_FUNCTIONS
FlxG.console.registerObject('dad', dad);
#end
}
@@ -1723,16 +1705,11 @@ class PlayState extends MusicBeatSubState
}
/**
- * Constructs the strumlines for each player.
- */
+ * Constructs the strumlines for each player.
+ */
function initStrumlines():Void
{
- var noteStyleId:String = switch (currentStageId)
- {
- case 'school': 'pixel';
- case 'schoolEvil': 'pixel';
- default: Constants.DEFAULT_NOTE_STYLE;
- }
+ var noteStyleId:String = currentChart.noteStyle;
var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
@@ -1756,19 +1733,31 @@ class PlayState extends MusicBeatSubState
opponentStrumline.zIndex = 1000;
opponentStrumline.cameras = [camHUD];
- if (!PlayStatePlaylist.isStoryMode)
- {
- playerStrumline.fadeInArrows();
- opponentStrumline.fadeInArrows();
- }
+ playerStrumline.fadeInArrows();
+ opponentStrumline.fadeInArrows();
}
/**
- * Initializes the Discord Rich Presence.
- */
+ * Configures the judgement and combo popups.
+ */
+ function initPopups():Void
+ {
+ var noteStyleId:String = currentChart.noteStyle;
+ var noteStyle:NoteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
+ if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
+ // Initialize the judgements and combo meter.
+ comboPopUps = new PopUpStuff(noteStyle);
+ comboPopUps.zIndex = 900;
+ add(comboPopUps);
+ comboPopUps.cameras = [camHUD];
+ }
+
+ /**
+ * Initializes the Discord Rich Presence.
+ */
function initDiscord():Void
{
- #if discord_rpc
+ #if FEATURE_DISCORD_RPC
storyDifficultyText = difficultyString();
iconRPC = currentSong.player2;
@@ -1799,9 +1788,9 @@ class PlayState extends MusicBeatSubState
}
/**
- * Initializes the song (applying the chart, generating the notes, etc.)
- * Should be done before the countdown starts.
- */
+ * Initializes the song (applying the chart, generating the notes, etc.)
+ * Should be done before the countdown starts.
+ */
function generateSong():Void
{
if (currentChart == null)
@@ -1815,7 +1804,7 @@ class PlayState extends MusicBeatSubState
{
// Stop the vocals if they already exist.
if (vocals != null) vocals.stop();
- vocals = currentChart.buildVocals();
+ vocals = currentChart.buildVocals(currentInstrumental);
if (vocals.members.length == 0)
{
@@ -1832,8 +1821,8 @@ class PlayState extends MusicBeatSubState
}
/**
- * Read note data from the chart and generate the notes.
- */
+ * Read note data from the chart and generate the notes.
+ */
function regenNoteData(startTime:Float = 0):Void
{
Highscore.tallies.combo = 0;
@@ -1886,27 +1875,26 @@ class PlayState extends MusicBeatSubState
}
/**
- * Prepares to start the countdown.
- * Ends any running cutscenes, creates the strumlines, and starts the countdown.
- * This is public so that scripts can call it.
- */
+ * Prepares to start the countdown.
+ * Ends any running cutscenes, creates the strumlines, and starts the countdown.
+ * This is public so that scripts can call it.
+ */
public function startCountdown():Void
{
// If Countdown.performCountdown returns false, then the countdown was canceled by a script.
- var result:Bool = Countdown.performCountdown(currentStageId.startsWith('school'));
+ var result:Bool = Countdown.performCountdown();
if (!result) return;
isInCutscene = false;
- camCutscene.visible = false;
// TODO: Maybe tween in the camera after any cutscenes.
camHUD.visible = true;
}
/**
- * Displays a dialogue cutscene with the given ID.
- * This is used by song scripts to display dialogue.
- */
+ * Displays a dialogue cutscene with the given ID.
+ * This is used by song scripts to display dialogue.
+ */
public function startConversation(conversationId:String):Void
{
isInCutscene = true;
@@ -1926,8 +1914,8 @@ class PlayState extends MusicBeatSubState
}
/**
- * Handler function called when a conversation ends.
- */
+ * Handler function called when a conversation ends.
+ */
function onConversationComplete():Void
{
isInCutscene = false;
@@ -1946,15 +1934,15 @@ class PlayState extends MusicBeatSubState
}
/**
- * Starts playing the song after the countdown has completed.
- */
+ * Starts playing the song after the countdown has completed.
+ */
function startSong():Void
{
startingSong = false;
if (!overrideMusic && !isGamePaused && currentChart != null)
{
- currentChart.playInst(1.0, false);
+ currentChart.playInst(1.0, currentInstrumental, false);
}
if (FlxG.sound.music == null)
@@ -1963,7 +1951,9 @@ class PlayState extends MusicBeatSubState
return;
}
- FlxG.sound.music.onComplete = endSong.bind(false);
+ FlxG.sound.music.onComplete = function() {
+ endSong(skipEndingTransition);
+ };
// 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.play(true, startTimestamp - Conductor.instance.instrumentalOffset);
@@ -1980,7 +1970,7 @@ class PlayState extends MusicBeatSubState
vocals.pitch = playbackRate;
resyncVocals();
- #if discord_rpc
+ #if FEATURE_DISCORD_RPC
// Updating Discord Rich Presence (with Time Left)
DiscordClient.changePresence(detailsText, '${currentChart.songName} ($storyDifficultyText)', iconRPC, true, currentSongLengthMs);
#end
@@ -1995,26 +1985,28 @@ class PlayState extends MusicBeatSubState
}
/**
- * Resyncronize the vocal tracks if they have become offset from the instrumental.
- */
+ * Resyncronize the vocal tracks if they have become offset from the instrumental.
+ */
function resyncVocals():Void
{
if (vocals == null) return;
// Skip this if the music is paused (GameOver, Pause menu, start-of-song offset, etc.)
- if (!FlxG.sound.music.playing) return;
-
+ if (!(FlxG.sound.music?.playing ?? false)) return;
+ var timeToPlayAt:Float = Conductor.instance.songPosition - Conductor.instance.instrumentalOffset;
+ FlxG.sound.music.pause();
vocals.pause();
- FlxG.sound.music.play(FlxG.sound.music.time);
+ FlxG.sound.music.time = timeToPlayAt;
+ FlxG.sound.music.play(false, timeToPlayAt);
- vocals.time = FlxG.sound.music.time;
- vocals.play(false, FlxG.sound.music.time);
+ vocals.time = timeToPlayAt;
+ vocals.play(false, timeToPlayAt);
}
/**
- * Updates the position and contents of the score display.
- */
+ * Updates the position and contents of the score display.
+ */
function updateScoreText():Void
{
// TODO: Add functionality for modules to update the score text.
@@ -2024,13 +2016,15 @@ class PlayState extends MusicBeatSubState
}
else
{
- scoreText.text = 'Score:' + songScore;
+ // TODO: Add an option for this maybe?
+ var commaSeparated:Bool = true;
+ scoreText.text = 'Score: ${FlxStringUtil.formatMoney(songScore, false, commaSeparated)}';
}
}
/**
- * Updates the values of the health bar.
- */
+ * Updates the values of the health bar.
+ */
function updateHealthBar():Void
{
if (isBotPlayMode)
@@ -2044,8 +2038,8 @@ class PlayState extends MusicBeatSubState
}
/**
- * Callback executed when one of the note keys is pressed.
- */
+ * Callback executed when one of the note keys is pressed.
+ */
function onKeyPress(event:PreciseInputEvent):Void
{
if (isGamePaused) return;
@@ -2055,8 +2049,8 @@ class PlayState extends MusicBeatSubState
}
/**
- * Callback executed when one of the note keys is released.
- */
+ * Callback executed when one of the note keys is released.
+ */
function onKeyRelease(event:PreciseInputEvent):Void
{
if (isGamePaused) return;
@@ -2066,8 +2060,8 @@ class PlayState extends MusicBeatSubState
}
/**
- * Handles opponent note hits and player note misses.
- */
+ * Handles opponent note hits and player note misses.
+ */
function processNotes(elapsed:Float):Void
{
if (playerStrumline?.notes?.members == null || opponentStrumline?.notes?.members == null) return;
@@ -2101,7 +2095,8 @@ 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 HitNoteScriptEvent(note, 0.0, 0, 'perfect', 0);
+
+ var event:NoteScriptEvent = new HitNoteScriptEvent(note, 0.0, 0, 'perfect', false, 0);
dispatchEvent(event);
// Calling event.cancelEvent() skips all the other logic! Neat!
@@ -2197,7 +2192,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 HitNoteScriptEvent(note, 0.0, 0, 'perfect', 0);
+ var event:NoteScriptEvent = new HitNoteScriptEvent(note, 0.0, 0, 'perfect', false, 0);
dispatchEvent(event);
// Calling event.cancelEvent() skips all the other logic! Neat!
@@ -2239,10 +2234,14 @@ class PlayState extends MusicBeatSubState
// Calling event.cancelEvent() skips all the other logic! Neat!
if (event.eventCanceled) continue;
- // Judge the miss.
- // NOTE: This is what handles the scoring.
- trace('Missed note! ${note.noteData}');
- onNoteMiss(note, event.playSound, event.healthChange);
+ // Skip handling the miss in botplay!
+ if (!isBotPlayMode)
+ {
+ // Judge the miss.
+ // NOTE: This is what handles the scoring.
+ trace('Missed note! ${note.noteData}');
+ onNoteMiss(note, event.playSound, event.healthChange);
+ }
note.handledMiss = true;
}
@@ -2255,11 +2254,20 @@ 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 (!isBotPlayMode && holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0)
+ if (holdNote.hitNote && !holdNote.missedNote && holdNote.sustainLength > 0)
{
// Grant the player health.
- health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed;
- songScore += Std.int(Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed);
+ if (!isBotPlayMode)
+ {
+ health += Constants.HEALTH_HOLD_BONUS_PER_SECOND * elapsed;
+ songScore += Std.int(Constants.SCORE_HOLD_BONUS_PER_SECOND * elapsed);
+ }
+
+ // Make sure the player keeps singing while the note is held by the bot.
+ if (isBotPlayMode && currentStage != null && currentStage.getBoyfriend() != null && currentStage.getBoyfriend().isSinging())
+ {
+ currentStage.getBoyfriend().holdTimer = 0;
+ }
}
if (holdNote.missedNote && !holdNote.handledMiss)
@@ -2275,8 +2283,8 @@ class PlayState extends MusicBeatSubState
}
/**
- * Spitting out the input for ravy 🙇♂️!!
- */
+ * Spitting out the input for ravy 🙇♂️!!
+ */
var inputSpitter:Array = [];
function handleSkippedNotes():Void
@@ -2300,9 +2308,9 @@ class PlayState extends MusicBeatSubState
}
/**
- * PreciseInputEvents are put into a queue between update() calls,
- * and then processed here.
- */
+ * PreciseInputEvents are put into a queue between update() calls,
+ * and then processed here.
+ */
function processInputQueue():Void
{
if (inputPressQueue.length + inputReleaseQueue.length == 0) return;
@@ -2319,8 +2327,6 @@ class PlayState extends MusicBeatSubState
var notesInRange:Array = playerStrumline.getNotesMayHit();
var holdNotesInRange:Array = playerStrumline.getHoldNotesHitOrMissed();
- // If there are notes in range, pressing a key will cause a ghost miss.
-
var notesByDirection:Array> = [[], [], [], []];
for (note in notesInRange)
@@ -2332,9 +2338,16 @@ class PlayState extends MusicBeatSubState
playerStrumline.pressKey(input.noteDirection);
+ // Don't credit or penalize inputs in Bot Play.
+ if (isBotPlayMode) continue;
+
var notesInDirection:Array = notesByDirection[input.noteDirection];
- if (!Constants.GHOST_TAPPING && notesInDirection.length == 0)
+ #if FEATURE_GHOST_TAPPING
+ if ((!playerStrumline.mayGhostTap()) && notesInDirection.length == 0)
+ #else
+ if (notesInDirection.length == 0)
+ #end
{
// Pressed a wrong key with no notes nearby.
// Perform a ghost miss (anti-spam).
@@ -2342,37 +2355,33 @@ class PlayState extends MusicBeatSubState
// Play the strumline animation.
playerStrumline.playPress(input.noteDirection);
+ trace('PENALTY Score: ${songScore}');
}
- else if (Constants.GHOST_TAPPING && (holdNotesInRange.length + notesInRange.length > 0) && notesInDirection.length == 0)
- {
- // Pressed a wrong key with no notes nearby AND with notes in a different direction available.
- // Perform a ghost miss (anti-spam).
- ghostNoteMiss(input.noteDirection, notesInRange.length > 0);
+ else if (notesInDirection.length == 0)
+ {
+ // Press a key with no penalty.
- // Play the strumline animation.
- playerStrumline.playPress(input.noteDirection);
- }
- else if (notesInDirection.length > 0)
- {
- // Choose the first note, deprioritizing low priority notes.
- var targetNote:Null = notesInDirection.find((note) -> !note.lowPriority);
- if (targetNote == null) targetNote = notesInDirection[0];
- if (targetNote == null) continue;
+ // Play the strumline animation.
+ playerStrumline.playPress(input.noteDirection);
+ trace('NO PENALTY Score: ${songScore}');
+ }
+ else
+ {
+ // Choose the first note, deprioritizing low priority notes.
+ var targetNote:Null = notesInDirection.find((note) -> !note.lowPriority);
+ if (targetNote == null) targetNote = notesInDirection[0];
+ if (targetNote == null) continue;
- // Judge and hit the note.
- trace('Hit note! ${targetNote.noteData}');
- goodNoteHit(targetNote, input);
+ // Judge and hit the note.
+ trace('Hit note! ${targetNote.noteData}');
+ goodNoteHit(targetNote, input);
+ trace('Score: ${songScore}');
- notesInDirection.remove(targetNote);
+ notesInDirection.remove(targetNote);
- // Play the strumline animation.
- playerStrumline.playConfirm(input.noteDirection);
- }
- else
- {
- // Play the strumline animation.
- playerStrumline.playPress(input.noteDirection);
- }
+ // Play the strumline animation.
+ playerStrumline.playConfirm(input.noteDirection);
+ }
}
while (inputReleaseQueue.length > 0)
@@ -2402,40 +2411,51 @@ class PlayState extends MusicBeatSubState
var daRating = Scoring.judgeNote(noteDiff, PBOT1);
var healthChange = 0.0;
+ var isComboBreak = false;
switch (daRating)
{
case 'sick':
healthChange = Constants.HEALTH_SICK_BONUS;
+ isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK;
case 'good':
healthChange = Constants.HEALTH_GOOD_BONUS;
+ isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK;
case 'bad':
healthChange = Constants.HEALTH_BAD_BONUS;
+ isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK;
case 'shit':
+ isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK;
healthChange = Constants.HEALTH_SHIT_BONUS;
}
// Send the note hit event.
- var event:HitNoteScriptEvent = new HitNoteScriptEvent(note, healthChange, score, daRating, Highscore.tallies.combo + 1);
+ var event:HitNoteScriptEvent = new HitNoteScriptEvent(note, healthChange, score, daRating, isComboBreak, Highscore.tallies.combo + 1, noteDiff,
+ daRating == 'sick');
dispatchEvent(event);
// Calling event.cancelEvent() skips all the other logic! Neat!
if (event.eventCanceled) return;
+ Highscore.tallies.totalNotesHit++;
+ // Display the hit on the strums
+ playerStrumline.hitNote(note, !isComboBreak);
+ if (event.doesNotesplash) playerStrumline.playNoteSplash(note.noteData.getDirection());
+ if (note.isHoldNote && note.holdNoteSprite != null) playerStrumline.playNoteHoldCover(note.holdNoteSprite);
+ vocals.playerVolume = 1;
+
// Display the combo meter and add the calculation to the score.
- popUpScore(note, event.score, event.judgement, event.healthChange);
+ applyScore(event.score, event.judgement, event.healthChange, event.isComboBreak);
+ popUpScore(event.judgement);
}
/**
- * Called when a note leaves the screen and is considered missed by the player.
- * @param note
- */
+ * Called when a note leaves the screen and is considered missed by the player.
+ * @param note
+ */
function onNoteMiss(note:NoteSprite, playSound:Bool = false, healthChange:Float):Void
{
// If we are here, we already CALLED the onNoteMiss script hook!
- health += healthChange;
- songScore -= 10;
-
if (!isPracticeMode)
{
// messy copy paste rn lol
@@ -2475,14 +2495,9 @@ class PlayState extends MusicBeatSubState
}
vocals.playerVolume = 0;
- Highscore.tallies.missed++;
+ if (Highscore.tallies.combo != 0) if (Highscore.tallies.combo >= 10) comboPopUps.displayCombo(0);
- if (Highscore.tallies.combo != 0)
- {
- // Break the combo.
- if (Highscore.tallies.combo >= 10) comboPopUps.displayCombo(0);
- Highscore.tallies.combo = 0;
- }
+ applyScore(-10, 'miss', healthChange, true);
if (playSound)
{
@@ -2492,13 +2507,13 @@ class PlayState extends MusicBeatSubState
}
/**
- * Called when a player presses a key with no note present.
- * Scripts can modify the amount of health/score lost, whether player animations or sounds are used,
- * or even cancel the event entirely.
- *
- * @param direction
- * @param hasPossibleNotes
- */
+ * Called when a player presses a key with no note present.
+ * Scripts can modify the amount of health/score lost, whether player animations or sounds are used,
+ * or even cancel the event entirely.
+ *
+ * @param direction
+ * @param hasPossibleNotes
+ */
function ghostNoteMiss(direction:NoteDirection, hasPossibleNotes:Bool = true):Void
{
var event:GhostMissNoteScriptEvent = new GhostMissNoteScriptEvent(direction, // Direction missed in.
@@ -2547,17 +2562,11 @@ class PlayState extends MusicBeatSubState
}
/**
- * Debug keys. Disabled while in cutscenes.
- */
+ * Debug keys. Disabled while in cutscenes.
+ */
function debugKeyShit():Void
{
- #if !debug
- perfectMode = false;
- #else
- if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible;
- #end
-
- #if CHART_EDITOR_SUPPORTED
+ #if FEATURE_CHART_EDITOR
// Open the stage editor overlaying the current state.
if (controls.DEBUG_STAGE)
{
@@ -2570,15 +2579,15 @@ class PlayState extends MusicBeatSubState
// Redirect to the chart editor playing the current song.
if (controls.DEBUG_CHART)
{
+ disableKeys = true;
+ persistentUpdate = false;
if (isChartingMode)
{
- if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position!
- this.close(); // This only works because PlayState is a substate!
+ FlxG.sound.music?.pause();
+ this.close();
}
else
{
- disableKeys = true;
- persistentUpdate = false;
FlxG.switchState(() -> new ChartEditorState(
{
targetSongId: currentSong.id,
@@ -2587,7 +2596,10 @@ class PlayState extends MusicBeatSubState
}
#end
- #if (debug || FORCE_DEBUG_VERSION)
+ #if FEATURE_DEBUG_FUNCTIONS
+ // H: Hide the HUD.
+ if (FlxG.keys.justPressed.H) camHUD.visible = !camHUD.visible;
+
// 1: End the song immediately.
if (FlxG.keys.justPressed.ONE) endSong(true);
@@ -2601,7 +2613,7 @@ class PlayState extends MusicBeatSubState
// 9: Toggle the old icon.
if (FlxG.keys.justPressed.NINE) iconP1.toggleOldIcon();
- #if (debug || FORCE_DEBUG_VERSION)
+ #if FEATURE_DEBUG_FUNCTIONS
// PAGEUP: Skip forward two sections.
// SHIFT+PAGEUP: Skip forward twenty sections.
if (FlxG.keys.justPressed.PAGEUP) changeSection(FlxG.keys.pressed.SHIFT ? 20 : 2);
@@ -2614,46 +2626,24 @@ class PlayState extends MusicBeatSubState
}
/**
- * Handles health, score, and rating popups when a note is hit.
- */
- function popUpScore(daNote:NoteSprite, score:Int, daRating:String, healthChange:Float):Void
+ * Handles applying health, score, and ratings.
+ */
+ function applyScore(score:Int, daRating:String, healthChange:Float, isComboBreak:Bool)
{
- if (daRating == 'miss')
- {
- // If daRating is 'miss', that means we made a mistake and should not continue.
- FlxG.log.warn('popUpScore judged a note as a miss!');
- // TODO: Remove this.
- // comboPopUps.displayRating('miss');
- return;
- }
-
- vocals.playerVolume = 1;
-
- var isComboBreak = false;
switch (daRating)
{
case 'sick':
Highscore.tallies.sick += 1;
- Highscore.tallies.totalNotesHit++;
- isComboBreak = Constants.JUDGEMENT_SICK_COMBO_BREAK;
case 'good':
Highscore.tallies.good += 1;
- Highscore.tallies.totalNotesHit++;
- isComboBreak = Constants.JUDGEMENT_GOOD_COMBO_BREAK;
case 'bad':
Highscore.tallies.bad += 1;
- Highscore.tallies.totalNotesHit++;
- isComboBreak = Constants.JUDGEMENT_BAD_COMBO_BREAK;
case 'shit':
Highscore.tallies.shit += 1;
- Highscore.tallies.totalNotesHit++;
- isComboBreak = Constants.JUDGEMENT_SHIT_COMBO_BREAK;
- default:
- FlxG.log.error('Wuh? Buh? Guh? Note hit judgement was $daRating!');
+ case 'miss':
+ Highscore.tallies.missed += 1;
}
-
health += healthChange;
-
if (isComboBreak)
{
// Break the combo, but don't increment tallies.misses.
@@ -2665,15 +2655,23 @@ class PlayState extends MusicBeatSubState
Highscore.tallies.combo++;
if (Highscore.tallies.combo > Highscore.tallies.maxCombo) Highscore.tallies.maxCombo = Highscore.tallies.combo;
}
+ songScore += score;
+ }
- playerStrumline.hitNote(daNote, !isComboBreak);
-
- if (daRating == 'sick')
+ /**
+ * Handles rating popups when a note is hit.
+ */
+ function popUpScore(daRating:String, ?combo:Int):Void
+ {
+ if (daRating == 'miss')
{
- playerStrumline.playNoteSplash(daNote.noteData.getDirection());
+ // If daRating is 'miss', that means we made a mistake and should not continue.
+ FlxG.log.warn('popUpScore judged a note as a miss!');
+ // TODO: Remove this.
+ // comboPopUps.displayRating('miss');
+ return;
}
-
- songScore += score;
+ if (combo == null) combo = Highscore.tallies.combo;
if (!isPracticeMode)
{
@@ -2713,21 +2711,16 @@ class PlayState extends MusicBeatSubState
}
}
comboPopUps.displayRating(daRating);
- if (Highscore.tallies.combo >= 10 || Highscore.tallies.combo == 0) comboPopUps.displayCombo(Highscore.tallies.combo);
-
- if (daNote.isHoldNote && daNote.holdNoteSprite != null)
- {
- playerStrumline.playNoteHoldCover(daNote.holdNoteSprite);
- }
+ if (combo >= 10 || combo == 0) comboPopUps.displayCombo(combo);
vocals.playerVolume = 1;
}
/**
- * Handle keyboard inputs during cutscenes.
- * This includes advancing conversations and skipping videos.
- * @param elapsed Time elapsed since last game update.
- */
+ * Handle keyboard inputs during cutscenes.
+ * This includes advancing conversations and skipping videos.
+ * @param elapsed Time elapsed since last game update.
+ */
function handleCutsceneKeys(elapsed:Float):Void
{
if (isGamePaused) return;
@@ -2771,20 +2764,20 @@ class PlayState extends MusicBeatSubState
}
/**
- * Handle logic for actually skipping a video cutscene after it has been held.
- */
+ * Handle logic for actually skipping a video cutscene after it has been held.
+ */
function skipVideoCutscene():Void
{
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.
- */
+ * 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.
+ */
public function endSong(rightGoddamnNow:Bool = false):Void
{
if (FlxG.sound.music != null) FlxG.sound.music.volume = 0;
@@ -2805,7 +2798,13 @@ class PlayState extends MusicBeatSubState
deathCounter = 0;
+ // TODO: This line of code makes me sad, but you can't really fix it without a breaking migration.
+ // `easy`, `erect`, `normal-pico`, etc.
+ var suffixedDifficulty = (currentVariation != Constants.DEFAULT_VARIATION
+ && currentVariation != 'erect') ? '$currentDifficulty-${currentVariation}' : currentDifficulty;
+
var isNewHighscore = false;
+ var prevScoreData:Null = Save.instance.getSongScore(currentSong.id, suffixedDifficulty);
if (currentSong != null && currentSong.validScore)
{
@@ -2825,19 +2824,26 @@ class PlayState extends MusicBeatSubState
totalNotesHit: Highscore.tallies.totalNotesHit,
totalNotes: Highscore.tallies.totalNotes,
},
- accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
};
// adds current song data into the tallies for the level (story levels)
Highscore.talliesLevel = Highscore.combineTallies(Highscore.tallies, Highscore.talliesLevel);
- if (!isPracticeMode && !isBotPlayMode && Save.instance.isSongHighScore(currentSong.id, currentDifficulty, data))
+ if (!isPracticeMode && !isBotPlayMode)
{
- Save.instance.setSongScore(currentSong.id, currentDifficulty, data);
- #if newgrounds
- NGio.postScore(score, currentSong.id);
- #end
- isNewHighscore = true;
+ isNewHighscore = Save.instance.isSongHighScore(currentSong.id, suffixedDifficulty, data);
+
+ // If no high score is present, save both score and rank.
+ // If score or rank are better, save the highest one.
+ // If neither are higher, nothing will change.
+ Save.instance.applySongRank(currentSong.id, suffixedDifficulty, data);
+
+ if (isNewHighscore)
+ {
+ #if newgrounds
+ NGio.postScore(score, currentSong.id);
+ #end
+ }
}
}
@@ -2862,7 +2868,7 @@ class PlayState extends MusicBeatSubState
score: PlayStatePlaylist.campaignScore,
tallies:
{
- // TODO: Sum up the values for the whole level!
+ // TODO: Sum up the values for the whole week!
sick: 0,
good: 0,
bad: 0,
@@ -2873,7 +2879,6 @@ class PlayState extends MusicBeatSubState
totalNotesHit: 0,
totalNotes: 0,
},
- accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
};
if (Save.instance.isLevelHighScore(PlayStatePlaylist.campaignId, PlayStatePlaylist.campaignDifficulty, data))
@@ -2927,11 +2932,16 @@ class PlayState extends MusicBeatSubState
FunkinSound.playOnce(Paths.sound('Lights_Shut_off'), function() {
// no camFollow so it centers on horror tree
var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId);
+ var targetVariation:String = currentVariation;
+ if (!targetSong.hasDifficulty(PlayStatePlaylist.campaignDifficulty, currentVariation))
+ {
+ targetVariation = targetSong.getFirstValidVariation(PlayStatePlaylist.campaignDifficulty) ?? Constants.DEFAULT_VARIATION;
+ }
LoadingState.loadPlayState(
{
targetSong: targetSong,
targetDifficulty: PlayStatePlaylist.campaignDifficulty,
- targetVariation: currentVariation,
+ targetVariation: targetVariation,
cameraFollowPoint: cameraFollowPoint.getPosition(),
});
});
@@ -2939,11 +2949,16 @@ class PlayState extends MusicBeatSubState
else
{
var targetSong:Song = SongRegistry.instance.fetchEntry(targetSongId);
+ var targetVariation:String = currentVariation;
+ if (!targetSong.hasDifficulty(PlayStatePlaylist.campaignDifficulty, currentVariation))
+ {
+ targetVariation = targetSong.getFirstValidVariation(PlayStatePlaylist.campaignDifficulty) ?? Constants.DEFAULT_VARIATION;
+ }
LoadingState.loadPlayState(
{
targetSong: targetSong,
targetDifficulty: PlayStatePlaylist.campaignDifficulty,
- targetVariation: currentVariation,
+ targetVariation: targetVariation,
cameraFollowPoint: cameraFollowPoint.getPosition(),
});
}
@@ -2959,11 +2974,11 @@ class PlayState extends MusicBeatSubState
{
if (rightGoddamnNow)
{
- moveToResultsScreen(isNewHighscore);
+ moveToResultsScreen(isNewHighscore, prevScoreData);
}
else
{
- zoomIntoResultsScreen(isNewHighscore);
+ zoomIntoResultsScreen(isNewHighscore, prevScoreData);
}
}
}
@@ -2977,8 +2992,8 @@ class PlayState extends MusicBeatSubState
}
/**
- * Perform necessary cleanup before leaving the PlayState.
- */
+ * Perform necessary cleanup before leaving the PlayState.
+ */
function performCleanup():Void
{
// If the camera is being tweened, stop it.
@@ -3029,23 +3044,25 @@ class PlayState extends MusicBeatSubState
GameOverSubState.reset();
PauseSubState.reset();
+ Countdown.reset();
// Clear the static reference to this state.
instance = null;
}
/**
- * Play the camera zoom animation and then move to the results screen once it's done.
- */
- function zoomIntoResultsScreen(isNewHighscore:Bool):Void
+ * Play the camera zoom animation and then move to the results screen once it's done.
+ */
+ function zoomIntoResultsScreen(isNewHighscore:Bool, ?prevScoreData:SaveScoreData):Void
{
trace('WENT TO RESULTS SCREEN!');
// Stop camera zooming on beat.
cameraZoomRate = 0;
- // Cancel camera tweening if it's active.
+ // Cancel camera and scroll tweening if it's active.
cancelAllCameraTweens();
+ cancelScrollSpeedTweens();
// If the opponent is GF, zoom in on the opponent.
// Else, if there is no GF, zoom in on BF.
@@ -3072,12 +3089,12 @@ class PlayState extends MusicBeatSubState
FlxG.camera.targetOffset.x += 20;
// Replace zoom animation with a fade out for now.
- camGame.fade(FlxColor.BLACK, 0.6);
+ FlxG.camera.fade(FlxColor.BLACK, 0.6);
FlxTween.tween(camHUD, {alpha: 0}, 0.6,
{
onComplete: function(_) {
- moveToResultsScreen(isNewHighscore);
+ moveToResultsScreen(isNewHighscore, prevScoreData);
}
});
@@ -3099,18 +3116,18 @@ class PlayState extends MusicBeatSubState
// Zoom over to the Results screen.
// TODO: Re-enable this.
/*
- FlxTween.tween(FlxG.camera, {zoom: 1200}, 1.1,
- {
- ease: FlxEase.expoIn,
- });
- */
+ FlxTween.tween(FlxG.camera, {zoom: 1200}, 1.1,
+ {
+ ease: FlxEase.expoIn,
+ });
+ */
});
}
/**
- * Move to the results screen right goddamn now.
- */
- function moveToResultsScreen(isNewHighscore:Bool):Void
+ * Move to the results screen right goddamn now.
+ */
+ function moveToResultsScreen(isNewHighscore:Bool, ?prevScoreData:SaveScoreData):Void
{
persistentUpdate = false;
vocals.stop();
@@ -3121,7 +3138,11 @@ class PlayState extends MusicBeatSubState
var res:ResultState = new ResultState(
{
storyMode: PlayStatePlaylist.isStoryMode,
+ songId: currentChart.song.id,
+ difficultyId: currentDifficulty,
+ characterId: currentChart.characters.player,
title: PlayStatePlaylist.isStoryMode ? ('${PlayStatePlaylist.campaignTitle}') : ('${currentChart.songName} by ${currentChart.songArtist}'),
+ prevScoreData: prevScoreData,
scoreData:
{
score: PlayStatePlaylist.isStoryMode ? PlayStatePlaylist.campaignScore : songScore,
@@ -3137,17 +3158,16 @@ class PlayState extends MusicBeatSubState
totalNotesHit: talliesToUse.totalNotesHit,
totalNotes: talliesToUse.totalNotes,
},
- accuracy: Highscore.tallies.totalNotesHit / Highscore.tallies.totalNotes,
},
isNewHighscore: isNewHighscore
});
- res.camera = camHUD;
+ this.persistentDraw = false;
openSubState(res);
}
/**
- * Pauses music and vocals easily.
- */
+ * Pauses music and vocals easily.
+ */
public function pauseMusic():Void
{
if (FlxG.sound.music != null) FlxG.sound.music.pause();
@@ -3155,8 +3175,8 @@ class PlayState extends MusicBeatSubState
}
/**
- * Resets the camera's zoom level and focus point.
- */
+ * Resets the camera's zoom level and focus point.
+ */
public function resetCamera(?resetZoom:Bool = true, ?cancelTweens:Bool = true):Void
{
// Cancel camera tweens if any are active.
@@ -3165,7 +3185,7 @@ class PlayState extends MusicBeatSubState
cancelAllCameraTweens();
}
- FlxG.camera.follow(cameraFollowPoint, LOCKON, 0.04);
+ FlxG.camera.follow(cameraFollowPoint, LOCKON, Constants.DEFAULT_CAMERA_FOLLOW_RATE);
FlxG.camera.targetOffset.set();
if (resetZoom)
@@ -3178,8 +3198,8 @@ class PlayState extends MusicBeatSubState
}
/**
- * Sets the camera follow point's position and tweens the camera there.
- */
+ * Sets the camera follow point's position and tweens the camera there.
+ */
public function tweenCameraToPosition(?x:Float, ?y:Float, ?duration:Float, ?ease:NullFloat>):Void
{
cameraFollowPoint.setPosition(x, y);
@@ -3187,8 +3207,8 @@ class PlayState extends MusicBeatSubState
}
/**
- * Disables camera following and tweens the camera to the follow point manually.
- */
+ * 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.
@@ -3225,8 +3245,8 @@ class PlayState extends MusicBeatSubState
}
/**
- * Tweens the camera zoom to the desired amount.
- */
+ * Tweens the camera zoom to the desired amount.
+ */
public function tweenCameraZoom(?zoom:Float, ?duration:Float, ?direct:Bool, ?ease:NullFloat>):Void
{
// Cancel the current tween if it's active.
@@ -3257,20 +3277,74 @@ class PlayState extends MusicBeatSubState
}
/**
- * Cancel all active camera tweens simultaneously.
- */
+ * Cancel all active camera tweens simultaneously.
+ */
public function cancelAllCameraTweens()
{
cancelCameraFollowTween();
cancelCameraZoomTween();
}
- #if (debug || FORCE_DEBUG_VERSION)
+ var prevScrollTargets:Array = []; // used to snap scroll speed when things go unruely
+
/**
- * Jumps forward or backward a number of sections in the song.
- * Accounts for BPM changes, does not prevent death from skipped notes.
- * @param sections The number of sections to jump, negative to go backwards.
- */
+ * The magical function that shall tween the scroll speed.
+ */
+ public function tweenScrollSpeed(?speed:Float, ?duration:Float, ?ease:NullFloat>, strumlines:Array):Void
+ {
+ // Cancel the current tween if it's active.
+ cancelScrollSpeedTweens();
+
+ // Snap to previous event value to prevent the tween breaking when another event cancels the previous tween.
+ for (i in prevScrollTargets)
+ {
+ var value:Float = i[0];
+ var strum:Strumline = Reflect.getProperty(this, i[1]);
+ strum.scrollSpeed = value;
+ }
+
+ // for next event, clean array.
+ prevScrollTargets = [];
+
+ for (i in strumlines)
+ {
+ var value:Float = speed;
+ var strum:Strumline = Reflect.getProperty(this, i);
+
+ if (duration == 0)
+ {
+ strum.scrollSpeed = value;
+ }
+ else
+ {
+ scrollSpeedTweens.push(FlxTween.tween(strum,
+ {
+ 'scrollSpeed': value
+ }, duration, {ease: ease}));
+ }
+ // make sure charts dont break if the charter is dumb and stupid
+ prevScrollTargets.push([value, i]);
+ }
+ }
+
+ public function cancelScrollSpeedTweens()
+ {
+ for (tween in scrollSpeedTweens)
+ {
+ if (tween != null)
+ {
+ tween.cancel();
+ }
+ }
+ scrollSpeedTweens = [];
+ }
+
+ #if FEATURE_DEBUG_FUNCTIONS
+ /**
+ * Jumps forward or backward a number of sections in the song.
+ * Accounts for BPM changes, does not prevent death from skipped notes.
+ * @param sections The number of sections to jump, negative to go backwards.
+ */
function changeSection(sections:Int):Void
{
// FlxG.sound.music.pause();
diff --git a/source/funkin/play/ResultScore.hx b/source/funkin/play/ResultScore.hx
index d5d5a65670..23e6c8d326 100644
--- a/source/funkin/play/ResultScore.hx
+++ b/source/funkin/play/ResultScore.hx
@@ -2,11 +2,16 @@ package funkin.play;
import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
+import flixel.tweens.FlxTween;
+import flixel.util.FlxTimer;
+import flixel.tweens.FlxEase;
class ResultScore extends FlxTypedSpriteGroup
{
public var scoreShit(default, set):Int = 0;
+ public var scoreStart:Int = 0;
+
function set_scoreShit(val):Int
{
if (group == null || group.members == null) return val;
@@ -16,7 +21,8 @@ class ResultScore extends FlxTypedSpriteGroup
while (dumbNumb > 0)
{
- group.members[loopNum].digit = dumbNumb % 10;
+ scoreStart += 1;
+ group.members[loopNum].finalDigit = dumbNumb % 10;
// var funnyNum = group.members[loopNum];
// prevNum = group.members[loopNum + 1];
@@ -44,9 +50,15 @@ class ResultScore extends FlxTypedSpriteGroup
public function animateNumbers():Void
{
- for (i in group.members)
+ for (i in group.members.length-scoreStart...group.members.length)
{
- i.playAnim();
+ // if(i.finalDigit == 10) continue;
+
+ new FlxTimer().start((i-1)/24, _ -> {
+ group.members[i].finalDelay = scoreStart - (i-1);
+ group.members[i].playAnim();
+ group.members[i].shuffle();
+ });
}
}
@@ -71,12 +83,26 @@ class ResultScore extends FlxTypedSpriteGroup
class ScoreNum extends FlxSprite
{
public var digit(default, set):Int = 10;
+ public var finalDigit(default, set):Int = 10;
+ public var glow:Bool = true;
+
+ function set_finalDigit(val):Int
+ {
+ animation.play('GONE', true, false, 0);
+
+ return finalDigit = val;
+ }
function set_digit(val):Int
{
if (val >= 0 && animation.curAnim != null && animation.curAnim.name != numToString[val])
{
- animation.play(numToString[val], true, false, 0);
+ if(glow){
+ animation.play(numToString[val], true, false, 0);
+ glow = false;
+ }else{
+ animation.play(numToString[val], true, false, 4);
+ }
updateHitbox();
switch (val)
@@ -107,6 +133,10 @@ class ScoreNum extends FlxSprite
animation.play(numToString[digit], true, false, 0);
}
+ public var shuffleTimer:FlxTimer;
+ public var finalTween:FlxTween;
+ public var finalDelay:Float = 0;
+
public var baseY:Float = 0;
public var baseX:Float = 0;
@@ -114,6 +144,47 @@ class ScoreNum extends FlxSprite
"ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", "EIGHT", "NINE", "DISABLED"
];
+ function finishShuffleTween():Void{
+
+ var tweenFunction = function(x) {
+ var digitRounded = Math.floor(x);
+ //if(digitRounded == finalDigit) glow = true;
+ digit = digitRounded;
+ };
+
+ finalTween = FlxTween.num(0.0, finalDigit, 23/24, {
+ ease: FlxEase.quadOut,
+ onComplete: function (input) {
+ new FlxTimer().start((finalDelay)/24, _ -> {
+ animation.play(animation.curAnim.name, true, false, 0);
+ });
+ // fuck
+ }
+ }, tweenFunction);
+ }
+
+
+ function shuffleProgress(shuffleTimer:FlxTimer):Void
+ {
+ var tempDigit:Int = digit;
+ tempDigit += 1;
+ if(tempDigit > 9) tempDigit = 0;
+ if(tempDigit < 0) tempDigit = 0;
+ digit = tempDigit;
+
+ if (shuffleTimer.loops > 0 && shuffleTimer.loopsLeft == 0)
+ {
+ //digit = finalDigit;
+ finishShuffleTween();
+ }
+ }
+
+ public function shuffle():Void{
+ var duration:Float = 41/24;
+ var interval:Float = 1/24;
+ shuffleTimer = new FlxTimer().start(interval, shuffleProgress, Std.int(duration / interval));
+ }
+
public function new(x:Float, y:Float)
{
super(x, y);
@@ -130,6 +201,7 @@ class ScoreNum extends FlxSprite
}
animation.addByPrefix('DISABLED', 'DISABLED', 24, false);
+ animation.addByPrefix('GONE', 'GONE', 24, false);
this.digit = 10;
diff --git a/source/funkin/play/ResultState.hx b/source/funkin/play/ResultState.hx
index 56dd1e80fc..739df167d8 100644
--- a/source/funkin/play/ResultState.hx
+++ b/source/funkin/play/ResultState.hx
@@ -4,7 +4,10 @@ import funkin.util.MathUtil;
import funkin.ui.story.StoryMenuState;
import funkin.graphics.adobeanimate.FlxAtlasSprite;
import flixel.FlxSprite;
+import flixel.FlxState;
+import flixel.FlxSubState;
import funkin.graphics.FunkinSprite;
+import flixel.effects.FlxFlicker;
import flixel.graphics.frames.FlxBitmapFont;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.math.FlxPoint;
@@ -12,163 +15,263 @@ import funkin.ui.MusicBeatSubState;
import flixel.math.FlxRect;
import flixel.text.FlxBitmapText;
import funkin.ui.freeplay.FreeplayScore;
+import flixel.text.FlxText;
+import funkin.data.freeplay.player.PlayerRegistry;
+import funkin.data.freeplay.player.PlayerData;
+import funkin.ui.freeplay.charselect.PlayableCharacter;
+import flixel.util.FlxColor;
import flixel.tweens.FlxEase;
+import funkin.graphics.FunkinCamera;
import funkin.ui.freeplay.FreeplayState;
import flixel.tweens.FlxTween;
+import flixel.addons.display.FlxBackdrop;
import funkin.audio.FunkinSound;
import flixel.util.FlxGradient;
import flixel.util.FlxTimer;
import funkin.save.Save;
+import funkin.play.scoring.Scoring;
import funkin.save.Save.SaveScoreData;
import funkin.graphics.shaders.LeftMaskShader;
import funkin.play.components.TallyCounter;
+import funkin.play.components.ClearPercentCounter;
/**
* The state for the results screen after a song or week is finished.
*/
+@:nullSafety
class ResultState extends MusicBeatSubState
{
final params:ResultsStateParams;
- var resultsVariation:ResultVariations;
- var songName:FlxBitmapText;
- var difficulty:FlxSprite;
+ final rank:ScoringRank;
+ final songName:FlxBitmapText;
+ final difficulty:FlxSprite;
+ final clearPercentSmall:ClearPercentCounter;
- var maskShaderSongName:LeftMaskShader = new LeftMaskShader();
- var maskShaderDifficulty:LeftMaskShader = new LeftMaskShader();
+ final maskShaderSongName:LeftMaskShader = new LeftMaskShader();
+ final maskShaderDifficulty:LeftMaskShader = new LeftMaskShader();
+
+ final resultsAnim:FunkinSprite;
+ final ratingsPopin:FunkinSprite;
+ final scorePopin:FunkinSprite;
+
+ final bgFlash:FlxSprite;
+
+ final highscoreNew:FlxSprite;
+ final score:ResultScore;
+
+ var characterAtlasAnimations:Array<
+ {
+ sprite:FlxAtlasSprite,
+ delay:Float,
+ forceLoop:Bool
+ }> = [];
+ var characterSparrowAnimations:Array<
+ {
+ sprite:FunkinSprite,
+ delay:Float
+ }> = [];
+
+ var playerCharacterId:Null;
+
+ var rankBg:FunkinSprite;
+ final cameraBG:FunkinCamera;
+ final cameraScroll:FunkinCamera;
+ final cameraEverything:FunkinCamera;
public function new(params:ResultsStateParams)
{
super();
this.params = params;
+
+ rank = Scoring.calculateRank(params.scoreData) ?? SHIT;
+
+ cameraBG = new FunkinCamera('resultsBG', 0, 0, FlxG.width, FlxG.height);
+ cameraScroll = new FunkinCamera('resultsScroll', 0, 0, FlxG.width, FlxG.height);
+ cameraEverything = new FunkinCamera('resultsEverything', 0, 0, FlxG.width, FlxG.height);
+
+ // We build a lot of this stuff in the constructor, then place it in create().
+ // This prevents having to do `null` checks everywhere.
+
+ var fontLetters:String = "AaBbCcDdEeFfGgHhiIJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:1234567890";
+ songName = new FlxBitmapText(FlxBitmapFont.fromMonospace(Paths.image("resultScreen/tardlingSpritesheet"), fontLetters, FlxPoint.get(49, 62)));
+ songName.text = params.title;
+ songName.letterSpacing = -15;
+ songName.angle = -4.4;
+ songName.zIndex = 1000;
+
+ difficulty = new FlxSprite(555);
+ difficulty.zIndex = 1000;
+
+ clearPercentSmall = new ClearPercentCounter(FlxG.width / 2 + 300, FlxG.height / 2 - 100, 100, true);
+ clearPercentSmall.zIndex = 1000;
+ clearPercentSmall.visible = false;
+
+ bgFlash = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFFF1A6, 0xFFFFF1BE], 90);
+
+ resultsAnim = FunkinSprite.createSparrow(-200, -10, "resultScreen/results");
+
+ ratingsPopin = FunkinSprite.createSparrow(-135, 135, "resultScreen/ratingsPopin");
+
+ scorePopin = FunkinSprite.createSparrow(-180, 515, "resultScreen/scorePopin");
+
+ highscoreNew = new FlxSprite(44, 557);
+
+ score = new ResultScore(35, 305, 10, params.scoreData.score);
+
+ rankBg = new FunkinSprite(0, 0);
}
override function create():Void
{
- /*
- if (params.scoreData.sick == params.scoreData.totalNotesHit
- && params.scoreData.maxCombo == params.scoreData.totalNotesHit) resultsVariation = PERFECT;
- else if (params.scoreData.missed + params.scoreData.bad + params.scoreData.shit >= params.scoreData.totalNotes * 0.50)
- resultsVariation = SHIT; // if more than half of your song was missed, bad, or shit notes, you get shit ending!
- else
- resultsVariation = NORMAL;
- */
- resultsVariation = NORMAL;
+ if (FlxG.sound.music != null) FlxG.sound.music.stop();
- FunkinSound.playMusic('results$resultsVariation',
- {
- startingVolume: 1.0,
- overrideExisting: true,
- restartTrack: true,
- loop: resultsVariation != SHIT
- });
+ // We need multiple cameras so we can put one at an angle.
+ cameraScroll.angle = -3.8;
- // Reset the camera zoom on the results screen.
- FlxG.camera.zoom = 1.0;
+ cameraBG.bgColor = FlxColor.MAGENTA;
+ cameraScroll.bgColor = FlxColor.TRANSPARENT;
+ cameraEverything.bgColor = FlxColor.TRANSPARENT;
+
+ FlxG.cameras.add(cameraBG, false);
+ FlxG.cameras.add(cameraScroll, false);
+ FlxG.cameras.add(cameraEverything, false);
- // TEMP-ish, just used to sorta "cache" the 3000x3000 image!
- var cacheBullShit:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/soundSystem"));
- add(cacheBullShit);
+ FlxG.cameras.setDefaultDrawTarget(cameraEverything, true);
+ this.camera = cameraEverything;
- var dumb:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/scorePopin"));
- add(dumb);
+ // Reset the camera zoom on the results screen.
+ FlxG.camera.zoom = 1.0;
var bg:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFECC5C, 0xFFFDC05C], 90);
bg.scrollFactor.set();
+ bg.zIndex = 10;
+ bg.cameras = [cameraBG];
add(bg);
- var bgFlash:FlxSprite = FlxGradient.createGradientFlxSprite(FlxG.width, FlxG.height, [0xFFFFEB69, 0xFFFFE66A], 90);
bgFlash.scrollFactor.set();
bgFlash.visible = false;
+ bgFlash.zIndex = 20;
+ // bgFlash.cameras = [cameraBG];
add(bgFlash);
- // var bfGfExcellent:FlxAtlasSprite = new FlxAtlasSprite(380, -170, Paths.animateAtlas("resultScreen/resultsBoyfriendExcellent", "shared"));
- // bfGfExcellent.visible = false;
- // add(bfGfExcellent);
- //
- // var bfPerfect:FlxAtlasSprite = new FlxAtlasSprite(370, -180, Paths.animateAtlas("resultScreen/resultsBoyfriendPerfect", "shared"));
- // bfPerfect.visible = false;
- // add(bfPerfect);
- //
- // var bfSHIT:FlxAtlasSprite = new FlxAtlasSprite(0, 20, Paths.animateAtlas("resultScreen/resultsBoyfriendSHIT", "shared"));
- // bfSHIT.visible = false;
- // add(bfSHIT);
- //
- // bfGfExcellent.anim.onComplete = () -> {
- // bfGfExcellent.anim.curFrame = 28;
- // bfGfExcellent.anim.play(); // unpauses this anim, since it's on PlayOnce!
- // };
- //
- // bfPerfect.anim.onComplete = () -> {
- // bfPerfect.anim.curFrame = 136;
- // bfPerfect.anim.play(); // unpauses this anim, since it's on PlayOnce!
- // };
- //
- // bfSHIT.anim.onComplete = () -> {
- // bfSHIT.anim.curFrame = 150;
- // bfSHIT.anim.play(); // unpauses this anim, since it's on PlayOnce!
- // };
-
- var gf:FlxSprite = FunkinSprite.createSparrow(625, 325, 'resultScreen/resultGirlfriendGOOD');
- gf.animation.addByPrefix("clap", "Girlfriend Good Anim", 24, false);
- gf.visible = false;
- gf.animation.finishCallback = _ -> {
- gf.animation.play('clap', true, false, 9);
- };
- add(gf);
-
- var boyfriend:FlxSprite = FunkinSprite.createSparrow(640, -200, 'resultScreen/resultBoyfriendGOOD');
- boyfriend.animation.addByPrefix("fall", "Boyfriend Good Anim0", 24, false);
- boyfriend.visible = false;
- boyfriend.animation.finishCallback = function(_) {
- boyfriend.animation.play('fall', true, false, 14);
- };
-
- add(boyfriend);
-
+ // The sound system which falls into place behind the score text. Plays every time!
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, _ -> {
+ new FlxTimer().start(8 / 24, _ -> {
soundSystem.animation.play("idle");
soundSystem.visible = true;
});
+ soundSystem.zIndex = 1100;
add(soundSystem);
- difficulty = new FlxSprite(555);
+ // Fetch playable character data. Default to BF on the results screen if we can't find it.
+ playerCharacterId = PlayerRegistry.instance.getCharacterOwnerId(params.characterId);
+ var playerCharacter:Null = PlayerRegistry.instance.fetchEntry(playerCharacterId ?? 'bf');
- var diffSpr:String = switch (PlayState.instance.currentDifficulty)
+ trace('Got playable character: ${playerCharacter?.getName()}');
+ // Query JSON data based on the rank, then use that to build the animation(s) the player sees.
+ var playerAnimationDatas:Array = playerCharacter != null ? playerCharacter.getResultsAnimationDatas(rank) : [];
+
+ for (animData in playerAnimationDatas)
{
- case 'easy':
- 'difEasy';
- case 'normal':
- 'difNormal';
- case 'hard':
- 'difHard';
- case 'erect':
- 'difErect';
- case 'nightmare':
- 'difNightmare';
- case _:
- 'difNormal';
+ if (animData == null) continue;
+
+ var animPath:String = Paths.stripLibrary(animData.assetPath);
+ var animLibrary:String = Paths.getLibrary(animData.assetPath);
+ var offsets = animData.offsets ?? [0, 0];
+ switch (animData.renderType)
+ {
+ case 'animateatlas':
+ var animation:FlxAtlasSprite = new FlxAtlasSprite(offsets[0], offsets[1], Paths.animateAtlas(animPath, animLibrary));
+ animation.zIndex = animData.zIndex ?? 500;
+
+ animation.scale.set(animData.scale ?? 1.0, animData.scale ?? 1.0);
+
+ if (!(animData.looped ?? true))
+ {
+ // Animation is not looped.
+ animation.onAnimationComplete.add((_name:String) -> {
+ trace("AHAHAH 2");
+ if (animation != null)
+ {
+ animation.anim.pause();
+ }
+ });
+ }
+ else if (animData.loopFrameLabel != null)
+ {
+ animation.onAnimationComplete.add((_name:String) -> {
+ trace("AHAHAH 2");
+ if (animation != null)
+ {
+ animation.playAnimation(animData.loopFrameLabel ?? '', true, false, true); // unpauses this anim, since it's on PlayOnce!
+ }
+ });
+ }
+ else if (animData.loopFrame != null)
+ {
+ animation.onAnimationComplete.add((_name:String) -> {
+ if (animation != null)
+ {
+ trace("AHAHAH");
+ animation.anim.curFrame = animData.loopFrame ?? 0;
+ animation.anim.play(); // unpauses this anim, since it's on PlayOnce!
+ }
+ });
+ }
+
+ // Hide until ready to play.
+ animation.visible = false;
+ // Queue to play.
+ characterAtlasAnimations.push(
+ {
+ sprite: animation,
+ delay: animData.delay ?? 0.0,
+ forceLoop: (animData.loopFrame ?? -1) == 0
+ });
+ // Add to the scene.
+ add(animation);
+ case 'sparrow':
+ var animation:FunkinSprite = FunkinSprite.createSparrow(offsets[0], offsets[1], animPath);
+ animation.animation.addByPrefix('idle', '', 24, false, false, false);
+
+ if (animData.loopFrame != null)
+ {
+ animation.animation.finishCallback = (_name:String) -> {
+ if (animation != null)
+ {
+ animation.animation.play('idle', true, false, animData.loopFrame ?? 0);
+ }
+ }
+ }
+
+ // Hide until ready to play.
+ animation.visible = false;
+ // Queue to play.
+ characterSparrowAnimations.push(
+ {
+ sprite: animation,
+ delay: animData.delay ?? 0.0
+ });
+ // Add to the scene.
+ add(animation);
+ }
}
+ var diffSpr:String = 'diff_${params?.difficultyId ?? 'Normal'}';
difficulty.loadGraphic(Paths.image("resultScreen/" + diffSpr));
add(difficulty);
- var fontLetters:String = "AaBbCcDdEeFfGgHhiIJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz:1234567890";
- songName = new FlxBitmapText(FlxBitmapFont.fromMonospace(Paths.image("resultScreen/tardlingSpritesheet"), fontLetters, FlxPoint.get(49, 62)));
- songName.text = params.title;
- songName.letterSpacing = -15;
- songName.angle = -4.4;
add(songName);
var angleRad = songName.angle * Math.PI / 180;
speedOfTween.x = -1.0 * Math.cos(angleRad);
speedOfTween.y = -1.0 * Math.sin(angleRad);
- timerThenSongName();
+ timerThenSongName(1.0, false);
songName.shader = maskShaderSongName;
difficulty.shader = maskShaderDifficulty;
@@ -178,35 +281,77 @@ class ResultState extends MusicBeatSubState
var blackTopBar:FlxSprite = new FlxSprite().loadGraphic(Paths.image("resultScreen/topBarBlack"));
blackTopBar.y = -blackTopBar.height;
- FlxTween.tween(blackTopBar, {y: 0}, 0.4, {ease: FlxEase.quartOut, startDelay: 0.5});
+ FlxTween.tween(blackTopBar, {y: 0}, 7 / 24, {ease: FlxEase.quartOut, startDelay: 3 / 24});
+ blackTopBar.zIndex = 1010;
add(blackTopBar);
- var resultsAnim:FunkinSprite = FunkinSprite.createSparrow(-200, -10, "resultScreen/results");
resultsAnim.animation.addByPrefix("result", "results instance 1", 24, false);
- resultsAnim.animation.play("result");
+ resultsAnim.visible = false;
+ resultsAnim.zIndex = 1200;
add(resultsAnim);
+ new FlxTimer().start(6 / 24, _ -> {
+ resultsAnim.visible = true;
+ resultsAnim.animation.play("result");
+ });
- var ratingsPopin:FunkinSprite = FunkinSprite.createSparrow(-150, 120, "resultScreen/ratingsPopin");
ratingsPopin.animation.addByPrefix("idle", "Categories", 24, false);
ratingsPopin.visible = false;
+ ratingsPopin.zIndex = 1200;
add(ratingsPopin);
+ new FlxTimer().start(21 / 24, _ -> {
+ ratingsPopin.visible = true;
+ ratingsPopin.animation.play("idle");
+ });
- var scorePopin:FunkinSprite = FunkinSprite.createSparrow(-180, 520, "resultScreen/scorePopin");
scorePopin.animation.addByPrefix("score", "tally score", 24, false);
scorePopin.visible = false;
+ scorePopin.zIndex = 1200;
add(scorePopin);
+ new FlxTimer().start(36 / 24, _ -> {
+ scorePopin.visible = true;
+ scorePopin.animation.play("score");
+ scorePopin.animation.finishCallback = anim -> {};
+ });
+
+ new FlxTimer().start(37 / 24, _ -> {
+ score.visible = true;
+ score.animateNumbers();
+ startRankTallySequence();
+ });
+
+ new FlxTimer().start(rank.getBFDelay(), _ -> {
+ afterRankTallySequence();
+ });
+
+ new FlxTimer().start(rank.getFlashDelay(), _ -> {
+ displayRankText();
+ });
- var highscoreNew:FlxSprite = new FlxSprite(310, 570);
highscoreNew.frames = Paths.getSparrowAtlas("resultScreen/highscoreNew");
- highscoreNew.animation.addByPrefix("new", "NEW HIGHSCORE", 24);
+ highscoreNew.animation.addByPrefix("new", "highscoreAnim0", 24, false);
highscoreNew.visible = false;
- highscoreNew.setGraphicSize(Std.int(highscoreNew.width * 0.8));
+ // highscoreNew.setGraphicSize(Std.int(highscoreNew.width * 0.8));
highscoreNew.updateHitbox();
+ highscoreNew.zIndex = 1200;
add(highscoreNew);
+ new FlxTimer().start(rank.getHighscoreDelay(), _ -> {
+ if (params.isNewHighscore ?? false)
+ {
+ highscoreNew.visible = true;
+ highscoreNew.animation.play("new");
+ highscoreNew.animation.finishCallback = _ -> highscoreNew.animation.play("new", true, false, 16);
+ }
+ else
+ {
+ highscoreNew.visible = false;
+ }
+ });
+
var hStuf:Int = 50;
var ratingGrp:FlxTypedGroup = new FlxTypedGroup();
+ ratingGrp.zIndex = 1200;
add(ratingGrp);
/**
@@ -220,7 +365,10 @@ class ResultState extends MusicBeatSubState
ratingGrp.add(maxCombo);
hStuf += 2;
- var extraYOffset:Float = 5;
+ var extraYOffset:Float = 7;
+
+ hStuf += 2;
+
var tallySick:TallyCounter = new TallyCounter(230, (hStuf * 5) + extraYOffset, params.scoreData.tallies.sick, 0xFF89E59E);
ratingGrp.add(tallySick);
@@ -236,83 +384,222 @@ class ResultState extends MusicBeatSubState
var tallyMissed:TallyCounter = new TallyCounter(260, (hStuf * 9) + extraYOffset, params.scoreData.tallies.missed, 0xFFC68AE6);
ratingGrp.add(tallyMissed);
- var score:ResultScore = new ResultScore(35, 305, 10, params.scoreData.score);
score.visible = false;
+ score.zIndex = 1200;
add(score);
for (ind => rating in ratingGrp.members)
{
rating.visible = false;
- new FlxTimer().start((0.3 * ind) + 0.55, _ -> {
+ new FlxTimer().start((0.3 * ind) + 1.20, _ -> {
rating.visible = true;
FlxTween.tween(rating, {curNumber: rating.neededNumber}, 0.5, {ease: FlxEase.quartOut});
});
}
- new FlxTimer().start(0.5, _ -> {
- ratingsPopin.animation.play("idle");
- ratingsPopin.visible = true;
+ // if (params.isNewHighscore ?? false)
+ // {
+ // highscoreNew.visible = true;
+ // highscoreNew.animation.play("new");
+ // //FlxTween.tween(highscoreNew, {y: highscoreNew.y + 10}, 0.8, {ease: FlxEase.quartOut});
+ // }
+ // else
+ // {
+ // highscoreNew.visible = false;
+ // }
+
+ new FlxTimer().start(rank.getMusicDelay(), _ -> {
+ var introMusic:String = Paths.music(getMusicPath(playerCharacter, rank) + '/' + getMusicPath(playerCharacter, rank) + '-intro');
+ if (Assets.exists(introMusic))
+ {
+ // Play the intro music.
+ FunkinSound.load(introMusic, 1.0, false, true, true, () -> {
+ FunkinSound.playMusic(getMusicPath(playerCharacter, rank),
+ {
+ startingVolume: 1.0,
+ overrideExisting: true,
+ restartTrack: true
+ });
+ });
+ }
+ else
+ {
+ FunkinSound.playMusic(getMusicPath(playerCharacter, rank),
+ {
+ startingVolume: 1.0,
+ overrideExisting: true,
+ restartTrack: true
+ });
+ }
+ });
+
+ rankBg.makeSolidColor(FlxG.width, FlxG.height, 0xFF000000);
+ rankBg.zIndex = 99999;
+ add(rankBg);
+
+ rankBg.alpha = 0;
+
+ refresh();
+
+ super.create();
+ }
+
+ function getMusicPath(playerCharacter:Null, rank:ScoringRank):String
+ {
+ return playerCharacter?.getResultsMusicPath(rank) ?? 'resultsNORMAL';
+ }
+
+ var rankTallyTimer:Null