Merge branch 'develop' into docs/compiling-guide

This commit is contained in:
Eric 2024-09-16 18:12:49 -04:00 committed by GitHub
commit 06485518db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
213 changed files with 16800 additions and 3603 deletions

2
.gitattributes vendored
View file

@ -1 +1,3 @@
* text=auto eol=lf * text=auto eol=lf
*.hxc linguist-language=Haxe
*.hxp linguist-language=Haxe

View file

@ -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

62
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View file

@ -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.)

View file

@ -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.

1
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1 @@
blank_issues_enabled: false

70
.github/ISSUE_TEMPLATE/crash.yml vendored Normal file
View file

@ -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

View file

@ -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?

15
.github/ISSUE_TEMPLATE/enhancement.yml vendored Normal file
View file

@ -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?

View file

@ -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?

View file

@ -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.

View file

@ -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?

View file

@ -44,7 +44,7 @@ runs:
g++ \ g++ \
libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \ libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \
libgl-dev libgl1-mesa-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 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 - name: Install linux-specific dependencies
if: ${{ runner.os == 'Linux' && contains(inputs.targets, 'linux') }} if: ${{ runner.os == 'Linux' && contains(inputs.targets, 'linux') }}
@ -56,12 +56,17 @@ runs:
shell: bash shell: bash
run: | run: |
echo "TIMER_HAXELIB=$(date +%s)" >> "$GITHUB_ENV" echo "TIMER_HAXELIB=$(date +%s)" >> "$GITHUB_ENV"
haxelib --debug --never install haxelib 4.1.0 --global haxelib fixrepo --global || true
haxelib --debug --never deleterepo || 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 --debug --never newrepo
haxelib version
echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV" 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" echo "TIMER_DEPS=$(date +%s)" >> "$GITHUB_ENV"
- name: Restore cached dependencies - name: Restore cached dependencies
@ -75,7 +80,12 @@ runs:
name: Prep git for dependency install name: Prep git for dependency install
uses: gacts/run-and-post-run@v1 uses: gacts/run-and-post-run@v1
with: 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' post: git config --global --unset 'url.https://x-access-token:${{ inputs.gh-token }}@github.com/.insteadOf'
- if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }} - if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }}

12
.github/changed-lines-count-labeler.yml vendored Normal file
View file

@ -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

11
.github/labeler.yml vendored Normal file
View file

@ -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'

6
.github/pull_request_template.md vendored Normal file
View file

@ -0,0 +1,6 @@
<!-- Please check for duplicates or similar PRs before submitting this PR. -->
## Does this PR close any issues? If so, link them below.
## Briefly describe the issue(s) fixed.
## Include any relevant screenshots or videos.

View file

@ -45,7 +45,11 @@ jobs:
uses: ./.github/actions/setup-haxe uses: ./.github/actions/setup-haxe
with: with:
gh-token: ${{ steps.app_token.outputs.token }} 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 - name: Build game
if: ${{ matrix.target == 'windows' }} if: ${{ matrix.target == 'windows' }}
run: | run: |
@ -107,7 +111,9 @@ jobs:
name: Install dependencies name: Install dependencies
run: | run: |
git config --global 'url.https://x-access-token:${{ steps.app_token.outputs.token }}@github.com/.insteadOf' https://github.com/ 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 haxelib --global run hmm install -q
cd .haxelib/hxcpp/git/tools/hxcpp && haxe compile.hxml
- if: ${{ matrix.target != 'html5' }} - if: ${{ matrix.target != 'html5' }}
name: Restore hxcpp cache name: Restore hxcpp cache

27
.github/workflows/labeler.yml vendored Normal file
View file

@ -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

1
.gitignore vendored
View file

@ -13,3 +13,4 @@ shitAudio/
node_modules/ node_modules/
package.json package.json
package-lock.json package-lock.json
.aider*

9
.vscode/launch.json vendored
View file

@ -3,10 +3,17 @@
"configurations": [ "configurations": [
{ {
// Launch in native/CPP on Windows/OSX/Linux // Launch in native/CPP on Windows/OSX/Linux
"name": "Lime", "name": "Lime Build+Debug",
"type": "lime", "type": "lime",
"request": "launch" "request": "launch"
}, },
{
// Launch in native/CPP on Windows/OSX/Linux
"name": "Lime Debug (No Build)",
"type": "lime",
"request": "launch",
"preLaunchTask": null
},
{ {
// Launch in browser // Launch in browser
"name": "HTML5 Debug", "name": "HTML5 Debug",

35
.vscode/settings.json vendored
View file

@ -94,12 +94,12 @@
{ {
"label": "Windows / Debug", "label": "Windows / Debug",
"target": "windows", "target": "windows",
"args": ["-debug", "-DFORCE_DEBUG_VERSION"] "args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
}, },
{ {
"label": "Linux / Debug", "label": "Linux / Debug",
"target": "linux", "target": "linux",
"args": ["-debug", "-DFORCE_DEBUG_VERSION"] "args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
}, },
{ {
"label": "HashLink / Debug", "label": "HashLink / Debug",
@ -109,7 +109,7 @@
{ {
"label": "Windows / Debug (FlxAnimate Test)", "label": "Windows / Debug (FlxAnimate Test)",
"target": "windows", "target": "windows",
"args": ["-debug", "-DANIMATE", "-DFORCE_DEBUG_VERSION"] "args": ["-debug", "-DANIMATE", "-DFEATURE_DEBUG_FUNCTIONS"]
}, },
{ {
"label": "HashLink / Debug (FlxAnimate Test)", "label": "HashLink / Debug (FlxAnimate Test)",
@ -119,7 +119,7 @@
{ {
"label": "Windows / Debug (Straight to Freeplay)", "label": "Windows / Debug (Straight to Freeplay)",
"target": "windows", "target": "windows",
"args": ["-debug", "-DFREEPLAY", "-DFORCE_DEBUG_VERSION"] "args": ["-debug", "-DFREEPLAY", "-DFEATURE_DEBUG_FUNCTIONS"]
}, },
{ {
"label": "HashLink / Debug (Straight to Freeplay)", "label": "HashLink / Debug (Straight to Freeplay)",
@ -132,13 +132,13 @@
"args": [ "args": [
"-debug", "-debug",
"-DSONG=bopeebo -DDIFFICULTY=normal", "-DSONG=bopeebo -DDIFFICULTY=normal",
"-DFORCE_DEBUG_VERSION" "-DFEATURE_DEBUG_FUNCTIONS"
] ]
}, },
{ {
"label": "Windows / Debug (Straight to Play - 2hot)", "label": "Windows / Debug (Straight to Play - 2hot)",
"target": "windows", "target": "windows",
"args": ["-debug", "-DSONG=2hot", "-DFORCE_DEBUG_VERSION"] "args": ["-debug", "-DSONG=2hot", "-DFEATURE_DEBUG_FUNCTIONS"]
}, },
{ {
"label": "HashLink / Debug (Straight to Play - Bopeebo Normal)", "label": "HashLink / Debug (Straight to Play - Bopeebo Normal)",
@ -148,17 +148,22 @@
{ {
"label": "Windows / Debug (Conversation Test)", "label": "Windows / Debug (Conversation Test)",
"target": "windows", "target": "windows",
"args": ["-debug", "-DDIALOGUE", "-DFORCE_DEBUG_VERSION"] "args": ["-debug", "-DDIALOGUE", "-DFEATURE_DEBUG_FUNCTIONS"]
}, },
{ {
"label": "HashLink / Debug (Conversation Test)", "label": "HashLink / Debug (Conversation Test)",
"target": "hl", "target": "hl",
"args": ["-debug", "-DDIALOGUE"] "args": ["-debug", "-DDIALOGUE"]
}, },
{
"label": "Windows / Debug (Results Screen Test)",
"target": "windows",
"args": ["-debug", "-DRESULTS"]
},
{ {
"label": "Windows / Debug (Straight to Chart Editor)", "label": "Windows / Debug (Straight to Chart Editor)",
"target": "windows", "target": "windows",
"args": ["-debug", "-DCHARTING", "-DFORCE_DEBUG_VERSION"] "args": ["-debug", "-DCHARTING", "-DFEATURE_DEBUG_FUNCTIONS"]
}, },
{ {
"label": "HashLink / Debug (Straight to Chart Editor)", "label": "HashLink / Debug (Straight to Chart Editor)",
@ -168,12 +173,12 @@
{ {
"label": "Windows / Debug (Straight to Animation Editor)", "label": "Windows / Debug (Straight to Animation Editor)",
"target": "windows", "target": "windows",
"args": ["-debug", "-DANIMDEBUG", "-DFORCE_DEBUG_VERSION"] "args": ["-debug", "-DANIMDEBUG", "-DFEATURE_DEBUG_FUNCTIONS"]
}, },
{ {
"label": "Windows / Debug (Debug hxCodec)", "label": "Windows / Debug (Debug hxCodec)",
"target": "windows", "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)", "label": "HashLink / Debug (Straight to Animation Editor)",
@ -183,7 +188,7 @@
{ {
"label": "Windows / Debug (Latency Test)", "label": "Windows / Debug (Latency Test)",
"target": "windows", "target": "windows",
"args": ["-debug", "-DLATENCY", "-DFORCE_DEBUG_VERSION"] "args": ["-debug", "-DLATENCY", "-DFEATURE_DEBUG_FUNCTIONS"]
}, },
{ {
"label": "HashLink / Debug (Latency Test)", "label": "HashLink / Debug (Latency Test)",
@ -193,7 +198,7 @@
{ {
"label": "Windows / Debug (Waveform Test)", "label": "Windows / Debug (Waveform Test)",
"target": "windows", "target": "windows",
"args": ["-debug", "-DWAVEFORM", "-DFORCE_DEBUG_VERSION"] "args": ["-debug", "-DWAVEFORM", "-DFEATURE_DEBUG_FUNCTIONS"]
}, },
{ {
"label": "Windows / Release", "label": "Windows / Release",
@ -213,17 +218,17 @@
{ {
"label": "HTML5 / Debug", "label": "HTML5 / Debug",
"target": "html5", "target": "html5",
"args": ["-debug", "-DFORCE_DEBUG_VERSION"] "args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
}, },
{ {
"label": "HTML5 / Debug (Watch)", "label": "HTML5 / Debug (Watch)",
"target": "html5", "target": "html5",
"args": ["-debug", "-watch", "-DFORCE_DEBUG_VERSION"] "args": ["-debug", "-watch", "-DFEATURE_DEBUG_FUNCTIONS"]
}, },
{ {
"label": "macOS / Debug", "label": "macOS / Debug",
"target": "mac", "target": "mac",
"args": ["-debug", "-DFORCE_DEBUG_VERSION"] "args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
}, },
{ {
"label": "macOS / Release", "label": "macOS / Release",

View file

@ -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/), 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). 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 ## [0.3.3] - 2024-05-14
### Changed ### Changed
- Cleaned up some code in `PlayAnimationSongEvent.hx` (thanks BurgerBalls!) - Cleaned up some code in `PlayAnimationSongEvent.hx` (thanks BurgerBalls!)
### Fixed ### 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!) - Don't allow any more inputs when exiting freeplay (thanks gamerbros!)
- Fixed using mouse wheel to scroll on freeplay (thanks JugieNoob!) - 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!) - 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 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!) - 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 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 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 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!) - 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 ## [0.3.2] - 2024-05-03
### Added ### Added

View file

@ -1,265 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<project>
<!-- _________________________ Application Settings _________________________ -->
<app title="Friday Night Funkin'" file="Funkin" packageName="com.funkin.fnf" package="com.funkin.fnf" main="Main" version="0.3.3" company="ninjamuffin99" />
<!--Switch Export with Unique ApplicationID and Icon-->
<set name="APP_ID" value="0x0100f6c013bbc000" />
<!--
Define the OpenFL sprite which displays the preloader.
You can't replace the preloader's logic here, sadly, but you can extend it.
Basic preloading logic is done by `openfl.display.Preloader`.
-->
<app preloader="funkin.ui.transition.preload.FunkinPreloader" />
<!--Minimum without FLX_NO_GAMEPAD: 11.8, without FLX_NO_NATIVE_CURSOR: 11.2-->
<set name="SWF_VERSION" value="11.8" />
<!-- ____________________________ Window Settings ___________________________ -->
<!--These window settings apply to all targets-->
<window width="1280" height="720" fps="60" background="#000000" hardware="true" vsync="false" />
<!--HTML5-specific-->
<window if="html5" resizable="true" />
<!--Desktop-specific-->
<window if="desktop" orientation="landscape" fullscreen="false" resizable="true" vsync="false" />
<!--Mobile-specific-->
<window if="mobile" orientation="landscape" fullscreen="true" width="0" height="0" resizable="false" />
<!-- _____________________________ Path Settings ____________________________ -->
<set name="BUILD_DIR" value="export/debug" if="debug" />
<set name="BUILD_DIR" value="export/release" unless="debug" />
<set name="BUILD_DIR" value="export/32bit" if="32bit" />
<classpath name="source" />
<assets path="assets/preload" rename="assets" exclude="*.ogg|*.wav" if="web" />
<assets path="assets/preload" rename="assets" exclude="*.mp3|*.wav" unless="web" />
<define name="PRELOAD_ALL" unless="web" />
<define name="NO_PRELOAD_ALL" unless="PRELOAD_ALL" />
<section if="PRELOAD_ALL">
<library name="songs" preload="true" />
<library name="shared" preload="true" />
<library name="tutorial" preload="true" />
<library name="week1" preload="true" />
<library name="week2" preload="true" />
<library name="week3" preload="true" />
<library name="week4" preload="true" />
<library name="week5" preload="true" />
<library name="week6" preload="true" />
<library name="week7" preload="true" />
<library name="weekend1" preload="true" />
<library name="videos" preload="true" />
</section>
<section if="NO_PRELOAD_ALL">
<library name="songs" preload="false" />
<library name="shared" preload="false" />
<library name="tutorial" preload="false" />
<library name="week1" preload="false" />
<library name="week2" preload="false" />
<library name="week3" preload="false" />
<library name="week4" preload="false" />
<library name="week5" preload="false" />
<library name="week6" preload="false" />
<library name="week7" preload="false" />
<library name="weekend1" preload="false" />
<library name="videos" preload="false" />
</section>
<library name="art" preload="false" />
<assets path="assets/songs" library="songs" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/songs" library="songs" exclude="*.fla|*.mp3|*.wav" unless="web" />
<!-- Videos go in their own library because web never needs to preload them, they can just be streamed. -->
<assets path="assets/videos" library="videos" />
<assets path="assets/shared" library="shared" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/shared" library="shared" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/tutorial" library="tutorial" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week1" library="week1" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week1" library="week1" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week2" library="week2" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week2" library="week2" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week3" library="week3" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week3" library="week3" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week4" library="week4" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week4" library="week4" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week5" library="week5" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week5" library="week5" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week6" library="week6" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week6" library="week6" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/week7" library="week7" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/week7" library="week7" exclude="*.fla|*.mp3|*.wav" unless="web" />
<assets path="assets/weekend1" library="weekend1" exclude="*.fla|*.ogg|*.wav" if="web" />
<assets path="assets/weekend1" library="weekend1" exclude="*.fla|*.mp3|*.wav" unless="web" />
<!-- <assets path='example_mods' rename='mods' embed='false'/> -->
<!--
AUTOMATICALLY MOVING EXAMPLE MODS INTO THE BUILD CAUSES ISSUES
Currently, this line will add the mod files to the library manifest,
which causes issues if the mod is not enabled.
If we can exclude the `mods` folder from the manifest, we can re-enable this line.
<assets path='example_mods' rename='mods' embed='false' exclude="*.md" />
-->
<assets path="art/readme.txt" rename="do NOT readme.txt" library="art"/>
<assets path="CHANGELOG.md" rename="changelog.txt" library="art"/>
<!-- NOTE FOR FUTURE SELF SINCE FONTS ARE ALWAYS FUCKY
TO FIX ONE OF THEM, I CONVERTED IT TO OTF. DUNNO IF YOU NEED TO
THEN UHHH I USED THE NAME OF THE FONT WITH SETFORMAT() ON THE TEXT!!!
NOT USING A DIRECT THING TO THE ASSET!!!
-->
<assets path="assets/fonts" embed="true" />
<!-- If compiled via github actions, show debug version number. -->
<define name="FORCE_DEBUG_VERSION" if="GITHUB_BUILD" />
<define name="NO_REDIRECT_ASSETS_FOLDER" if="GITHUB_BUILD" />
<define name="TOUCH_HERE_TO_PLAY" if="web" />
<!-- _______________________________ Libraries ______________________________ -->
<haxelib name="lime" /> <!-- Game engine backend -->
<haxelib name="openfl" /> <!-- Game engine backend -->
<haxelib name="flixel" /> <!-- Game engine -->
<haxedev set="webgl" />
<haxelib name="flixel-addons" /> <!-- Additional utilities for Flixel -->
<haxelib name="hscript" /> <!-- Scripting -->
<haxelib name="flixel-ui" /> <!-- UI framework (DEPRECATED) -->
<haxelib name="haxeui-core" /> <!-- UI framework -->
<haxelib name="haxeui-flixel" /> <!-- Integrate HaxeUI with Flixel -->
<haxelib name="flixel-text-input" /> <!-- Improved text field rendering for HaxeUI -->
<haxelib name="polymod" /> <!-- Modding framework -->
<haxelib name="flxanimate" /> <!-- Texture atlas rendering -->
<haxelib name="hxCodec" if="desktop" unless="hl" /> <!-- Video playback -->
<haxelib name="funkin.vis"/>
<haxelib name="json2object" /> <!-- JSON parsing -->
<haxelib name="thx.semver" /> <!-- Version string handling -->
<haxelib name="hxcpp-debug-server" if="desktop debug" /> <!-- VSCode debug support -->
<!--Disable the Flixel core focus lost screen-->
<haxedef name="FLX_NO_FOCUS_LOST_SCREEN" />
<!--Disable the Flixel core debugger. Automatically gets set whenever you compile in release mode!-->
<haxedef name="FLX_NO_DEBUG" unless="debug || FORCE_DEBUG_VERSION" />
<!--Enable this for Nape release builds for a serious peformance improvement-->
<haxedef name="NAPE_RELEASE_BUILD" unless="debug" />
<!--
Hide deprecation warnings until they're fixed.
TODO: REMOVE THIS!!!!
<haxeflag name="-w" value="-WDeprecated" />
-->
<!-- Haxe 4.3.0+: Enable pretty syntax errors and stuff. -->
<haxedef name="message.reporting" value="pretty" />
<!-- _________________________________ Custom _______________________________ -->
<!-- Disable trace() calls in release builds to bump up performance.
<haxeflag name="- -no-traces" unless="debug" />-->
<!-- HScript relies heavily on Reflection, which means we can't use DCE. -->
<haxeflag name="-dce no" />
<!-- Ensure all Funkin' classes are available at runtime. -->
<haxeflag name="--macro" value="include('funkin')" />
<!-- Ensure all UI components are available at runtime. -->
<haxeflag name="--macro" value="include('haxe.ui.backend.flixel.components')" />
<haxeflag name="--macro" value="include('haxe.ui.containers.dialogs')" />
<haxeflag name="--macro" value="include('haxe.ui.containers.menus')" />
<haxeflag name="--macro" value="include('haxe.ui.containers.properties')" />
<haxeflag name="--macro" value="include('haxe.ui.core')" />
<haxeflag name="--macro" value="include('haxe.ui.components')" />
<haxeflag name="--macro" value="include('haxe.ui.containers')" />
<!--
Ensure additional class packages are available at runtime (some only really used by scripts).
Ignore packages we can't include.
-->
<haxeflag name="--macro" value="include('flixel', true, [ 'flixel.addons.editors.spine.*', 'flixel.addons.nape.*', 'flixel.system.macros.*' ])" />
<!-- Necessary to provide stack traces for HScript. -->
<haxedef name="hscriptPos" />
<haxedef name="safeMode"/>
<haxedef name="HXCPP_CHECK_POINTER" />
<haxedef name="HXCPP_STACK_LINE" />
<haxedef name="HXCPP_STACK_TRACE" />
<!-- This macro allows addition of new functionality to existing Flixel. -->
<haxeflag name="--macro" value="addMetadata('@:build(funkin.util.macro.FlxMacro.buildFlxBasic())', 'flixel.FlxBasic')" />
<!--Place custom nodes like icons here (higher priority to override the HaxeFlixel icon)-->
<icon path="art/icon16.png" size="16" />
<icon path="art/icon32.png" size="32" />
<icon path="art/icon64.png" size="64" />
<icon path="art/iconOG.png" />
<haxedef name="CAN_OPEN_LINKS" unless="switch" />
<haxedef name="CAN_CHEAT" if="switch debug" />
<!-- I don't remember what this is for. -->
<haxedef name="haxeui_no_mouse_reset" />
<!-- Clicking outside a dialog should deselect the current focused component. -->
<haxedef name="haxeui_focus_out_on_click" />
<!-- Required to use haxe.ui.backend.flixel.UIState with build macros. -->
<haxedef name="haxeui_dont_impose_base_class" />
<haxedef name="HARDCODED_CREDITS" />
<!-- Skip the Intro -->
<section if="debug">
<!-- Starts the game at the specified week, at the first song -->
<!-- <haxedef name="week" value="1" if="debug"/> -->
<!-- Starts the game at the specified song -->
<!-- <haxedef name="song" value="bopeebo" if="debug"/> -->
<!-- Difficulty, only used for week or song, defaults to 1 -->
<!-- <haxedef name="dif" value="2" if="debug"/> -->
</section>
<section if="newgrounds">
<!-- Enables Ng.core.verbose -->
<!-- <haxedef name="NG_VERBOSE" /> -->
<!-- Enables a NG debug session, so medals don't permently unlock -->
<!-- <haxedef name="NG_DEBUG" /> -->
<!-- pretends that the saved session Id was expired, forcing the reconnect prompt -->
<!-- <haxedef name="NG_FORCE_EXPIRED_SESSION" if="debug" /> -->
</section>
<!-- Uncomment this to wipe your input settings. -->
<!-- <haxedef name="CLEAR_INPUT_SAVE"/> -->
<section if="debug" unless="NO_REDIRECT_ASSETS_FOLDER || html5 || GITHUB_BUILD">
<!--
Use the parent assets folder rather than the exported one
No more will we accidentally undo our changes!
-->
<haxedef name="REDIRECT_ASSETS_FOLDER" />
</section>
<section>
<!--
This flag enables the popup/crashlog error handler.
However, it also messes with breakpoints on some platforms.
-->
<haxedef name="openfl-enable-handle-error" />
</section>
<!-- Run a script before and after building. -->
<prebuild haxe="source/Prebuild.hx"/> -->
<postbuild haxe="source/Postbuild.hx"/> -->
<!-- Enable this on platforms which do not support dropping files onto the window. -->
<haxedef name="FILE_DROP_UNSUPPORTED" if="mac" />
<section unless="FILE_DROP_UNSUPPORTED">
<haxedef name="FILE_DROP_SUPPORTED" />
</section>
<!-- Enable this on platforms which do not support the edsior views. -->
<haxedef name="CHART_EDITOR_UNSUPPORTED" if="web" />
<haxedef name="CHART_EDITOR_SUPPORTED" unless="web"/>
<!-- Options for Polymod -->
<section if="polymod">
<!-- Turns on additional debug logging. -->
<haxedef name="POLYMOD_DEBUG" value="true" if="debug" />
<!-- The file extension to use for script files. -->
<haxedef name="POLYMOD_SCRIPT_EXT" value=".hscript" />
<!-- Which asset library to use for scripts. -->
<haxedef name="POLYMOD_SCRIPT_LIBRARY" value="scripts" />
<!-- The base path from which scripts should be accessed. -->
<haxedef name="POLYMOD_ROOT_PATH" value="scripts/" />
<!-- Determines the subdirectory of the mod folder used for file appending. -->
<haxedef name="POLYMOD_APPEND_FOLDER" value="_append" />
<!-- Determines the subdirectory of the mod folder used for file merges. -->
<haxedef name="POLYMOD_MERGE_FOLDER" value="_merge" />
<!-- Determines the file in the mod folder used for metadata. -->
<haxedef name="POLYMOD_MOD_METADATA_FILE" value="_polymod_meta.json" />
<!-- Determines the file in the mod folder used for the icon. -->
<haxedef name="POLYMOD_MOD_ICON_FILE" value="_polymod_icon.png" />
</section>
</project>

View file

@ -1,8 +1,8 @@
# Friday Night Funkin' # 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) - [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) - [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 ## Programming
- [ninjamuffin99](https://twitter.com/ninja_muffin99) - Lead Programmer - [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 - [MtH](https://twitter.com/emmnyaa) - Charting and Additional Programming
- [GeoKureli](https://twitter.com/Geokureli/) - Additional Programming - [GeoKureli](https://twitter.com/Geokureli/) - Additional Programming
- Our contributors on GitHub - Our contributors on GitHub

2
art

@ -1 +1 @@
Subproject commit 66572f85d826ce2ec1d45468c12733b161237ffa Subproject commit bfca2ea98d11a0f4dee4a27b9390951fbc5701ea

2
assets

@ -1 +1 @@
Subproject commit 783f22e741c85223da7f3f815b28fc4c6f240cbc Subproject commit bc7009b4242691faa5c4552f7ca8a2f28e8cb1d2

View file

@ -83,7 +83,7 @@ apt-fast install -y --no-install-recommends \
libc6-dev libffi-dev \ libc6-dev libffi-dev \
libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \ libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \
libgl-dev libgl1-mesa-dev \ libgl-dev libgl1-mesa-dev \
libasound2-dev \ libasound2-dev libpulse-dev \
libvlc-dev libvlccore-dev libvlc-dev libvlccore-dev
EOF EOF
@ -137,8 +137,8 @@ ENV PATH="$HAXEPATH:$PATH"
RUN <<EOF RUN <<EOF
HOME=/etc haxelib setup "$HAXEPATH/lib" HOME=/etc haxelib setup "$HAXEPATH/lib"
haxelib --global --never install haxelib $haxelib_version haxelib --global --never install haxelib $haxelib_version
haxelib --global --never git haxelib https://github.com/HaxeFoundation/haxelib.git master haxelib --global --never git haxelib https://github.com/FunkinCrew/haxelib.git funkin-patches --skip-dependencies
haxelib --global --never install hmm haxelib --global --never git hmm https://github.com/FunkinCrew/hmm funkin-patches
EOF EOF
# hxcpp # hxcpp

View file

@ -79,7 +79,7 @@
{ {
"props": { "props": {
"ignoreExtern": true, "ignoreExtern": true,
"format": "^[a-z][A-Z][A-Z0-9]*(_[A-Z0-9_]+)*$", "format": "^[a-zA-Z0-9]+(?:_[a-zA-Z0-9]+)*$",
"tokens": ["INLINE", "NOTINLINE"] "tokens": ["INLINE", "NOTINLINE"]
}, },
"type": "ConstantName" "type": "ConstantName"
@ -327,7 +327,8 @@
"INLINE", "INLINE",
"DYNAMIC", "DYNAMIC",
"FINAL" "FINAL"
] ],
"severity": "IGNORE"
}, },
"type": "ModifierOrder" "type": "ModifierOrder"
}, },

View file

@ -2,13 +2,19 @@
0. Setup 0. Setup
- Download Haxe from [Haxe.org](https://haxe.org) - Download Haxe from [Haxe.org](https://haxe.org)
1. Cloning the Repository: Make sure when you clone, you clone the submodules to get the assets repo: - Download Git from [git-scm.com](https://www.git-scm.com)
- `git clone --recurse-submodules https://github.com/FunkinCrew/funkin.git` - Do NOT download the repository using the Download ZIP button on GitHub or you may run into errors!
- If you accidentally cloned without the `assets` submodule (aka didn't follow the step above), you can run `git submodule update --init --recursive` to get the assets in a foolproof way. - Instead, open a command prompt and do the following steps...
2. Install `hmm` (run `haxelib --global install hmm` and then `haxelib --global run hmm setup`) 1. Run `cd the\directory\you\want\the\source\code\in` to specify which folder the command prompt is working in.
3. Install all haxelibs of the current branch by running `hmm install` - For example, `cd C:\Users\YOURNAME\Documents` would instruct the command prompt to perform the next steps in your Documents folder.
4. Setup lime: `haxelib run lime setup` 2. Run `git clone https://github.com/FunkinCrew/funkin.git` to clone the base repository.
5. Platform setup 3. Run `cd funkin` to enter the cloned repository's directory.
4. Run `git submodule update --init --recursive` to download the game's assets.
- NOTE: By performing this operation, you are downloading Content which is proprietary and protected by national and international copyright and trademark laws. See [the LICENSE.md file for the Funkin.assets](https://github.com/FunkinCrew/funkin.assets/blob/main/LICENSE.md) repo for more information.
5. Run `haxelib --global install hmm` and then `haxelib --global run hmm setup` to install hmm.json
6. Run `hmm install` to install all haxelibs of the current branch
7. Run `haxelib run lime setup` to set up lime
8. Perform additional platform setup
- For Windows, download the [Visual Studio Build Tools](https://aka.ms/vs/17/release/vs_BuildTools.exe) - For Windows, download the [Visual Studio Build Tools](https://aka.ms/vs/17/release/vs_BuildTools.exe)
- When prompted, select "Individual Components" and make sure to download the following: - When prompted, select "Individual Components" and make sure to download the following:
- MSVC v143 VS 2022 C++ x64/x86 build tools - MSVC v143 VS 2022 C++ x64/x86 build tools
@ -16,69 +22,25 @@
- Mac: [`lime setup mac` Documentation](https://lime.openfl.org/docs/advanced-setup/macos/) - Mac: [`lime setup mac` Documentation](https://lime.openfl.org/docs/advanced-setup/macos/)
- Linux: [`lime setup linux` Documentation](https://lime.openfl.org/docs/advanced-setup/linux/) - Linux: [`lime setup linux` Documentation](https://lime.openfl.org/docs/advanced-setup/linux/)
- HTML5: Compiles without any extra setup - HTML5: Compiles without any extra setup
6. If you are targeting for native, you may need to run `lime rebuild PLATFORM` and `lime rebuild PLATFORM -debug` 9. If you are targeting for native, you may need to run `lime rebuild <PLATFORM>` and `lime rebuild <PLATFORM> -debug`
7. `lime test PLATFORM` ! Add `-debug` to enable several debug features such as time travel (`PgUp`/`PgDn` in Play State). 10. `lime test <PLATFORM>` to build and launch the game for your platform (for example, `lime test windows`)
## Build Flags
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:
- `-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.
# Troubleshooting # Troubleshooting
While performing the process of compilation, you may experience one of the following issues: 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.
## 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
```
## PolymodHandler: Couldn't find a match for this asset library: (vlc)
```
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
```
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:
```
git config --global http.postBuffer 4096M
```

View file

@ -3,7 +3,7 @@
"description": "An introductory mod.", "description": "An introductory mod.",
"contributors": [ "contributors": [
{ {
"name": "MasterEric" "name": "EliteMasterEric"
} }
], ],
"api_version": "0.1.0", "api_version": "0.1.0",

View file

@ -3,7 +3,7 @@
"description": "Newgrounds? More like OLDGROUNDS lol.", "description": "Newgrounds? More like OLDGROUNDS lol.",
"contributors": [ "contributors": [
{ {
"name": "MasterEric" "name": "EliteMasterEric"
} }
], ],
"api_version": "0.1.0", "api_version": "0.1.0",

View file

@ -1,44 +1,53 @@
{ {
"dependencies": [ "dependencies": [
{
"name": "FlxPartialSound",
"type": "git",
"dir": null,
"ref": "a1eab7b9bf507b87200a3341719054fe427f3b15",
"url": "https://github.com/FunkinCrew/FlxPartialSound.git"
},
{ {
"name": "discord_rpc", "name": "discord_rpc",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "2d83fa863ef0c1eace5f1cf67c3ac315d1a3a8a5", "ref": "2d83fa863ef0c1eace5f1cf67c3ac315d1a3a8a5",
"url": "https://github.com/Aidan63/linc_discord-rpc" "url": "https://github.com/FunkinCrew/linc_discord-rpc"
}, },
{ {
"name": "flixel", "name": "flixel",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "a7d8e3bad89a0a3506a4714121f73d8e34522c49", "ref": "f2b090d6c608471e730b051c8ee22b8b378964b1",
"url": "https://github.com/FunkinCrew/flixel" "url": "https://github.com/FunkinCrew/flixel"
}, },
{ {
"name": "flixel-addons", "name": "flixel-addons",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "a523c3b56622f0640933944171efed46929e360e", "ref": "9c6fb47968e894eb36bf10e94725cd7640c49281",
"url": "https://github.com/FunkinCrew/flixel-addons" "url": "https://github.com/FunkinCrew/flixel-addons"
}, },
{ {
"name": "flixel-text-input", "name": "flixel-text-input",
"type": "haxelib", "type": "git",
"version": "1.1.0" "dir": null,
"ref": "951a0103a17bfa55eed86703ce50b4fb0d7590bc",
"url": "https://github.com/FunkinCrew/flixel-text-input"
}, },
{ {
"name": "flixel-ui", "name": "flixel-ui",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "719b4f10d94186ed55f6fef1b6618d32abec8c15", "ref": "27f1ba626f80a6282fa8a187115e79a4a2133dc2",
"url": "https://github.com/HaxeFlixel/flixel-ui" "url": "https://github.com/HaxeFlixel/flixel-ui"
}, },
{ {
"name": "flxanimate", "name": "flxanimate",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "17e0d59fdbc2b6283a5c0e4df41f1c7f27b71c49", "ref": "0654797e5eb7cd7de0c1b2dbaa1efe5a1e1d9412",
"url": "https://github.com/FunkinCrew/flxanimate" "url": "https://github.com/Dot-Stuff/flxanimate"
}, },
{ {
"name": "format", "name": "format",
@ -49,9 +58,16 @@
"name": "funkin.vis", "name": "funkin.vis",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "2aa654b974507ab51ab1724d2d97e75726fd7d78", "ref": "22b1ce089dd924f15cdc4632397ef3504d464e90",
"url": "https://github.com/FunkinCrew/funkVis" "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", "name": "hamcrest",
"type": "haxelib", "type": "haxelib",
@ -61,20 +77,22 @@
"name": "haxeui-core", "name": "haxeui-core",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "0212d8fdfcafeb5f0d5a41e1ddba8ff21d0e183b", "ref": "22f7c5a8ffca90d4677cffd6e570f53761709fbc",
"url": "https://github.com/haxeui/haxeui-core" "url": "https://github.com/haxeui/haxeui-core"
}, },
{ {
"name": "haxeui-flixel", "name": "haxeui-flixel",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "63a906a6148958dbfde8c7b48d90b0693767fd95", "ref": "28bb710d0ae5d94b5108787593052165be43b980",
"url": "https://github.com/haxeui/haxeui-flixel" "url": "https://github.com/haxeui/haxeui-flixel"
}, },
{ {
"name": "hscript", "name": "hscript",
"type": "haxelib", "type": "git",
"version": "2.5.0" "dir": null,
"ref": "12785398e2f07082f05034cb580682e5671442a2",
"url": "https://github.com/FunkinCrew/hscript"
}, },
{ {
"name": "hxCodec", "name": "hxCodec",
@ -85,8 +103,10 @@
}, },
{ {
"name": "hxcpp", "name": "hxcpp",
"type": "haxelib", "type": "git",
"version": "4.3.2" "dir": null,
"ref": "904ea40643b050a5a154c5e4c33a83fd2aec18b1",
"url": "https://github.com/HaxeFoundation/hxcpp"
}, },
{ {
"name": "hxcpp-debug-server", "name": "hxcpp-debug-server",
@ -95,10 +115,17 @@
"ref": "147294123f983e35f50a966741474438069a7a8f", "ref": "147294123f983e35f50a966741474438069a7a8f",
"url": "https://github.com/FunkinCrew/hxcpp-debugger" "url": "https://github.com/FunkinCrew/hxcpp-debugger"
}, },
{
"name": "hxjsonast",
"type": "git",
"dir": null,
"ref": "20e72cc68c823496359775ac1f06500e67f189d5",
"url": "https://github.com/nadako/hxjsonast/"
},
{ {
"name": "hxp", "name": "hxp",
"type": "haxelib", "type": "haxelib",
"version": "1.2.2" "version": "1.3.0"
}, },
{ {
"name": "json2object", "name": "json2object",
@ -107,11 +134,25 @@
"ref": "a8c26f18463c98da32f744c214fe02273e1823fa", "ref": "a8c26f18463c98da32f744c214fe02273e1823fa",
"url": "https://github.com/FunkinCrew/json2object" "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", "name": "lime",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "872ff6db2f2d27c0243d4ff76802121ded550dd7", "ref": "fe3368f611a84a19afc03011353945ae4da8fffd",
"url": "https://github.com/FunkinCrew/lime" "url": "https://github.com/FunkinCrew/lime"
}, },
{ {
@ -146,29 +187,29 @@
"name": "openfl", "name": "openfl",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "228c1b5063911e2ad75cef6e3168ef0a4b9f9134", "ref": "8306425c497766739510ab29e876059c96f77bd2",
"url": "https://github.com/FunkinCrew/openfl" "url": "https://github.com/FunkinCrew/openfl"
}, },
{ {
"name": "polymod", "name": "polymod",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "8553b800965f225bb14c7ab8f04bfa9cdec362ac", "ref": "0fbdf27fe124549730accd540cec8a183f8652c0",
"url": "https://github.com/larsiusprime/polymod" "url": "https://github.com/larsiusprime/polymod"
}, },
{ {
"name": "thx.core", "name": "thx.core",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "22605ff44f01971d599641790d6bae4869f7d9f4", "ref": "76d87418fadd92eb8e1b61f004cff27d656e53dd",
"url": "https://github.com/FunkinCrew/thx.core" "url": "https://github.com/fponticelli/thx.core"
}, },
{ {
"name": "thx.semver", "name": "thx.semver",
"type": "git", "type": "git",
"dir": null, "dir": null,
"ref": "cf8d213589a2c7ce4a59b0fdba9e8ff36bc029fa", "ref": "bdb191fe7cf745c02a980749906dbf22719e200b",
"url": "https://github.com/FunkinCrew/thx.semver" "url": "https://github.com/fponticelli/thx.semver"
} }
] ]
} }

1109
project.hxp Normal file

File diff suppressed because it is too large Load diff

View file

@ -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 gameHeight:Int = 720; // Height of the game in pixels (might be less / more in actual pixels depending on your zoom).
var initialState:Class<FlxState> = funkin.InitState; // The FlxState the game starts with. var initialState:Class<FlxState> = funkin.InitState; // The FlxState the game starts with.
var zoom:Float = -1; // If -1, zoom is automatically calculated to fit the window dimensions. 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. var framerate:Int = 60; // How many frames per second the game should run at.
#else #else
// TODO: This should probably be in the options menu? // TODO: This should probably be in the options menu?
@ -66,6 +66,12 @@ class Main extends Sprite
function init(?event:Event):Void 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)) if (hasEventListener(Event.ADDED_TO_STAGE))
{ {
removeEventListener(Event.ADDED_TO_STAGE, init); removeEventListener(Event.ADDED_TO_STAGE, init);
@ -113,7 +119,7 @@ class Main extends Sprite
addChild(game); addChild(game);
#if debug #if FEATURE_DEBUG_FUNCTIONS
game.debugger.interaction.addTool(new funkin.util.TrackerToolButtonUtil()); game.debugger.interaction.addTool(new funkin.util.TrackerToolButtonUtil());
#end #end

38
source/funkin/Assets.hx Normal file
View file

@ -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<String>
{
return openfl.utils.Assets.list(type);
}
}

View file

@ -430,7 +430,7 @@ class Conductor
else if (currentTimeChange != null && this.songPosition > 0.0) else if (currentTimeChange != null && this.songPosition > 0.0)
{ {
// roundDecimal prevents representing 8 as 7.9999999 // 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.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
this.currentMeasureTime = currentStepTime / stepsPerMeasure; this.currentMeasureTime = currentStepTime / stepsPerMeasure;
this.currentStep = Math.floor(currentStepTime); this.currentStep = Math.floor(currentStepTime);
@ -564,7 +564,7 @@ class Conductor
if (ms >= timeChange.timeStamp) if (ms >= timeChange.timeStamp)
{ {
lastTimeChange = timeChange; lastTimeChange = timeChange;
resultStep = lastTimeChange.beatTime * 4; resultStep = lastTimeChange.beatTime * Constants.STEPS_PER_BEAT;
} }
else else
{ {
@ -600,7 +600,7 @@ class Conductor
var lastTimeChange:SongTimeChange = timeChanges[0]; var lastTimeChange:SongTimeChange = timeChanges[0];
for (timeChange in timeChanges) for (timeChange in timeChanges)
{ {
if (stepTime >= timeChange.beatTime * 4) if (stepTime >= timeChange.beatTime * Constants.STEPS_PER_BEAT)
{ {
lastTimeChange = timeChange; lastTimeChange = timeChange;
resultMs = lastTimeChange.timeStamp; resultMs = lastTimeChange.timeStamp;
@ -613,7 +613,7 @@ class Conductor
} }
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator; 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; return resultMs;
} }

View file

@ -1,5 +1,6 @@
package funkin; package funkin;
import funkin.data.freeplay.player.PlayerRegistry;
import funkin.ui.debug.charting.ChartEditorState; import funkin.ui.debug.charting.ChartEditorState;
import funkin.ui.transition.LoadingState; import funkin.ui.transition.LoadingState;
import flixel.FlxState; import flixel.FlxState;
@ -18,6 +19,7 @@ import funkin.play.PlayStatePlaylist;
import openfl.display.BitmapData; import openfl.display.BitmapData;
import funkin.data.story.level.LevelRegistry; import funkin.data.story.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.freeplay.style.FreeplayStyleRegistry;
import funkin.data.event.SongEventRegistry; import funkin.data.event.SongEventRegistry;
import funkin.data.stage.StageRegistry; import funkin.data.stage.StageRegistry;
import funkin.data.dialogue.conversation.ConversationRegistry; 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.freeplay.album.AlbumRegistry;
import funkin.data.song.SongRegistry; import funkin.data.song.SongRegistry;
import funkin.play.character.CharacterData.CharacterDataParser; import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.notes.notekind.NoteKindManager;
import funkin.modding.module.ModuleHandler; import funkin.modding.module.ModuleHandler;
import funkin.ui.title.TitleState; import funkin.ui.title.TitleState;
import funkin.util.CLIUtil; import funkin.util.CLIUtil;
import funkin.util.CLIUtil.CLIParams; import funkin.util.CLIUtil.CLIParams;
import funkin.util.TimerUtil; import funkin.util.TimerUtil;
import funkin.util.TrackerUtil; import funkin.util.TrackerUtil;
#if discord_rpc #if FEATURE_DISCORD_RPC
import Discord.DiscordClient; import Discord.DiscordClient;
#end #end
@ -121,7 +124,7 @@ class InitState extends FlxState
// //
// DISCORD API SETUP // DISCORD API SETUP
// //
#if discord_rpc #if FEATURE_DISCORD_RPC
DiscordClient.initialize(); DiscordClient.initialize();
Application.current.onExit.add(function(exitCode) { Application.current.onExit.add(function(exitCode) {
@ -142,7 +145,7 @@ class InitState extends FlxState
// Plugins provide a useful interface for globally active Flixel objects, // Plugins provide a useful interface for globally active Flixel objects,
// that receive update events regardless of the current state. // that receive update events regardless of the current state.
// TODO: Move scripted Module behavior to a Flixel plugin. // TODO: Move scripted Module behavior to a Flixel plugin.
#if debug #if FEATURE_DEBUG_FUNCTIONS
funkin.util.plugins.MemoryGCPlugin.initialize(); funkin.util.plugins.MemoryGCPlugin.initialize();
#end #end
funkin.util.plugins.EvacuateDebugPlugin.initialize(); funkin.util.plugins.EvacuateDebugPlugin.initialize();
@ -164,9 +167,11 @@ class InitState extends FlxState
SongRegistry.instance.loadEntries(); SongRegistry.instance.loadEntries();
LevelRegistry.instance.loadEntries(); LevelRegistry.instance.loadEntries();
NoteStyleRegistry.instance.loadEntries(); NoteStyleRegistry.instance.loadEntries();
PlayerRegistry.instance.loadEntries();
ConversationRegistry.instance.loadEntries(); ConversationRegistry.instance.loadEntries();
DialogueBoxRegistry.instance.loadEntries(); DialogueBoxRegistry.instance.loadEntries();
SpeakerRegistry.instance.loadEntries(); SpeakerRegistry.instance.loadEntries();
FreeplayStyleRegistry.instance.loadEntries();
AlbumRegistry.instance.loadEntries(); AlbumRegistry.instance.loadEntries();
StageRegistry.instance.loadEntries(); StageRegistry.instance.loadEntries();
@ -174,6 +179,8 @@ class InitState extends FlxState
// Move it to use a BaseRegistry. // Move it to use a BaseRegistry.
CharacterDataParser.loadCharacterCache(); CharacterDataParser.loadCharacterCache();
NoteKindManager.loadScripts();
ModuleHandler.buildModuleCallbacks(); ModuleHandler.buildModuleCallbacks();
ModuleHandler.loadModuleCache(); ModuleHandler.loadModuleCache();
ModuleHandler.callOnCreate(); ModuleHandler.callOnCreate();
@ -214,6 +221,38 @@ class InitState extends FlxState
#elseif STAGEBUILD #elseif STAGEBUILD
// -DSTAGEBUILD // -DSTAGEBUILD
FlxG.switchState(() -> new funkin.ui.debug.stage.StageBuilderState()); 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 #elseif ANIMDEBUG
// -DANIMDEBUG // -DANIMDEBUG
FlxG.switchState(() -> new funkin.ui.debug.anim.DebugBoundingState()); FlxG.switchState(() -> new funkin.ui.debug.anim.DebugBoundingState());
@ -337,11 +376,16 @@ class InitState extends FlxState
// //
// FLIXEL DEBUG SETUP // FLIXEL DEBUG SETUP
// //
#if (debug || FORCE_DEBUG_VERSION) #if FEATURE_DEBUG_FUNCTIONS
// Make errors and warnings less annoying. trace('Initializing Flixel debugger...');
// Forcing this always since I have never been happy to have the debugger to pop up
#if !debug
// Make errors less annoying on release builds.
LogStyle.ERROR.openConsole = false; LogStyle.ERROR.openConsole = false;
LogStyle.ERROR.errorSound = null; LogStyle.ERROR.errorSound = null;
#end
// Make errors and warnings less annoying.
LogStyle.WARNING.openConsole = false; LogStyle.WARNING.openConsole = false;
LogStyle.WARNING.errorSound = null; LogStyle.WARNING.errorSound = null;

View file

@ -11,10 +11,17 @@ class Paths
{ {
static var currentLevel:Null<String> = null; static var currentLevel:Null<String> = null;
public static function setCurrentLevel(name:String):Void public static function setCurrentLevel(name:Null<String>):Void
{
if (name == null)
{
currentLevel = null;
}
else
{ {
currentLevel = name.toLowerCase(); currentLevel = name.toLowerCase();
} }
}
public static function stripLibrary(path:String):String public static function stripLibrary(path:String):String
{ {
@ -123,9 +130,17 @@ class Paths
return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.${Constants.EXT_SOUND}'; 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 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)); return FlxAtlasFrames.fromSpriteSheetPacker(image(key, library), file('images/$key.txt', library));
} }
} }
enum abstract PathsFunction(String)
{
var MUSIC;
var INST;
var VOICES;
var SOUND;
}

View file

@ -128,6 +128,48 @@ class Preferences
return value; 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. * Loads the user's preferences from the save data and apply them.
*/ */
@ -137,6 +179,17 @@ class Preferences
FlxG.autoPause = Preferences.autoPause; FlxG.autoPause = Preferences.autoPause;
// Apply the debugDisplay setting (enables the FPS and RAM display). // Apply the debugDisplay setting (enables the FPS and RAM display).
toggleDebugDisplay(Preferences.debugDisplay); 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 static function toggleDebugDisplay(show:Bool):Void

View file

@ -1,13 +1,13 @@
package funkin.api.discord; package funkin.api.discord;
import Sys.sleep; import Sys.sleep;
#if discord_rpc #if FEATURE_DISCORD_RPC
import discord_rpc.DiscordRpc; import discord_rpc.DiscordRpc;
#end #end
class DiscordClient class DiscordClient
{ {
#if discord_rpc #if FEATURE_DISCORD_RPC
public function new() public function new()
{ {
trace("Discord Client starting..."); trace("Discord Client starting...");

View file

@ -1,9 +1,5 @@
package funkin.api.newgrounds; package funkin.api.newgrounds;
import flixel.util.FlxSignal;
import flixel.util.FlxTimer;
import lime.app.Application;
import openfl.display.Stage;
#if newgrounds #if newgrounds
import io.newgrounds.NG; import io.newgrounds.NG;
import io.newgrounds.NGLite; import io.newgrounds.NGLite;
@ -28,7 +24,7 @@ class NGUnsafe
NG.core.calls.event.logEvent(event).send(); NG.core.calls.event.logEvent(event).send();
trace('should have logged: ' + event); trace('should have logged: ' + event);
#else #else
#if debug #if FEATURE_DEBUG_FUNCTIONS
trace('event:$event - not logged, missing NG.io lib'); trace('event:$event - not logged, missing NG.io lib');
#end #end
#end #end
@ -43,7 +39,7 @@ class NGUnsafe
if (!medal.unlocked) medal.sendUnlock(); if (!medal.unlocked) medal.sendUnlock();
} }
#else #else
#if debug #if FEATURE_DEBUG_FUNCTIONS
trace('medal:$id - not unlocked, missing NG.io lib'); trace('medal:$id - not unlocked, missing NG.io lib');
#end #end
#end #end
@ -67,7 +63,7 @@ class NGUnsafe
} }
} }
#else #else
#if debug #if FEATURE_DEBUG_FUNCTIONS
trace('Song:$song, Score:$score - not posted, missing NG.io lib'); trace('Song:$song, Score:$score - not posted, missing NG.io lib');
#end #end
#end #end

View file

@ -2,19 +2,11 @@ package funkin.api.newgrounds;
#if newgrounds #if newgrounds
import flixel.util.FlxSignal; import flixel.util.FlxSignal;
import flixel.util.FlxTimer;
import io.newgrounds.NG; import io.newgrounds.NG;
import io.newgrounds.NGLite; import io.newgrounds.NGLite;
import io.newgrounds.components.ScoreBoardComponent.Period;
import io.newgrounds.objects.Error; import io.newgrounds.objects.Error;
import io.newgrounds.objects.Medal;
import io.newgrounds.objects.Score; 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 lime.app.Application;
import openfl.display.Stage;
#end #end
/** /**
@ -247,7 +239,7 @@ class NGio
NG.core.calls.event.logEvent(event).send(); NG.core.calls.event.logEvent(event).send();
trace('should have logged: ' + event); trace('should have logged: ' + event);
#else #else
#if debug #if FEATURE_DEBUG_FUNCTIONS
trace('event:$event - not logged, missing NG.io lib'); trace('event:$event - not logged, missing NG.io lib');
#end #end
#end #end
@ -262,7 +254,7 @@ class NGio
if (!medal.unlocked) medal.sendUnlock(); if (!medal.unlocked) medal.sendUnlock();
} }
#else #else
#if debug #if FEATURE_DEBUG_FUNCTIONS
trace('medal:$id - not unlocked, missing NG.io lib'); trace('medal:$id - not unlocked, missing NG.io lib');
#end #end
#end #end
@ -286,7 +278,7 @@ class NGio
} }
} }
#else #else
#if debug #if FEATURE_DEBUG_FUNCTIONS
trace('Song:$song, Score:$score - not posted, missing NG.io lib'); trace('Song:$song, Score:$score - not posted, missing NG.io lib');
#end #end
#end #end

View file

@ -11,10 +11,14 @@ import funkin.audio.waveform.WaveformDataParser;
import funkin.data.song.SongData.SongMusicData; import funkin.data.song.SongData.SongMusicData;
import funkin.data.song.SongRegistry; import funkin.data.song.SongRegistry;
import funkin.util.tools.ICloneable; import funkin.util.tools.ICloneable;
import funkin.util.flixel.sound.FlxPartialSound;
import funkin.Paths.PathsFunction;
import openfl.Assets; import openfl.Assets;
import lime.app.Future;
import lime.app.Promise;
import openfl.media.SoundMixer; import openfl.media.SoundMixer;
#if (openfl >= "8.0.0") #if (openfl >= "8.0.0")
import openfl.utils.AssetType;
#end #end
/** /**
@ -223,12 +227,12 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
// already paused before we lost focus. // already paused before we lost focus.
if (_lostFocus && !_alreadyPaused) if (_lostFocus && !_alreadyPaused)
{ {
trace('Resuming audio (${this._label}) on focus!'); // trace('Resuming audio (${this._label}) on focus!');
resume(); resume();
} }
else else
{ {
trace('Not resuming audio (${this._label}) on focus!'); // trace('Not resuming audio (${this._label}) on focus!');
} }
_lostFocus = false; _lostFocus = false;
} }
@ -238,7 +242,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
*/ */
override function onFocusLost():Void override function onFocusLost():Void
{ {
trace('Focus lost, pausing audio!'); // trace('Focus lost, pausing audio!');
_lostFocus = true; _lostFocus = true;
_alreadyPaused = _paused; _alreadyPaused = _paused;
pause(); pause();
@ -336,21 +340,47 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
if (songMusicData != null) if (songMusicData != null)
{ {
Conductor.instance.mapTimeChanges(songMusicData.timeChanges); Conductor.instance.mapTimeChanges(songMusicData.timeChanges);
if (songMusicData.looped != null && params.loop == null) params.loop = songMusicData.looped;
} }
else else
{ {
FlxG.log.warn('Tried and failed to find music metadata for $key'); 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 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)
{
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);
var music = FunkinSound.load(Paths.music('$key/$key'), params?.startingVolume ?? 1.0, params.loop ?? true, false, true);
if (music != null) if (music != null)
{ {
FlxG.sound.music = music; partialQueue.push(music);
// Prevent repeat update() and onFocus() calls. @:nullSafety(Off)
music.future.onComplete(function(partialMusic:Null<FunkinSound>) {
FlxG.sound.music = partialMusic;
FlxG.sound.list.remove(FlxG.sound.music); FlxG.sound.list.remove(FlxG.sound.music);
if (FlxG.sound.music != null && params.onLoad != null) params.onLoad();
});
return true; return true;
} }
else else
@ -358,6 +388,37 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
return false; return false;
} }
} }
else
{
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<Promise<Null<FunkinSound>>> = [];
/** /**
* Creates a new `FunkinSound` object synchronously. * Creates a new `FunkinSound` object synchronously.
@ -415,6 +476,51 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
return sound; 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<Null<FunkinSound>>
{
var promise:lime.app.Promise<Null<FunkinSound>> = new lime.app.Promise<Null<FunkinSound>>();
// 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) @:nullSafety(Off)
public override function destroy():Void public override function destroy():Void
{ {
@ -433,11 +539,12 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
* Play a sound effect once, then destroy it. * Play a sound effect once, then destroy it.
* @param key * @param key
* @param volume * @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<FunkinSound>
{ {
var result = FunkinSound.load(key, volume, false, true, true, onComplete, onLoad); var result = FunkinSound.load(key, volume, false, true, true, onComplete, onLoad);
return result;
} }
/** /**
@ -462,6 +569,14 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
return sound; 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; 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. * Whether to override music if a different track is already playing.
* @default `false` * @default `false`
@ -498,4 +619,22 @@ typedef FunkinSoundPlayMusicParams =
* @default `true` * @default `true`
*/ */
var ?mapTimeChanges:Bool; 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;
} }

View file

@ -113,6 +113,11 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
public function play(forceRestart:Bool = false, startTime:Float = 0.0, ?endTime:Float) public function play(forceRestart:Bool = false, startTime:Float = 0.0, ?endTime:Float)
{ {
forEachAlive(function(sound:FunkinSound) { forEachAlive(function(sound:FunkinSound) {
if (sound.length < startTime)
{
// trace('Queuing sound (${sound.toString()} past its length! Skipping...)');
return;
}
sound.play(forceRestart, startTime, endTime); sound.play(forceRestart, startTime, endTime);
}); });
} }

View file

@ -1,9 +1,7 @@
package funkin.audio; package funkin.audio;
import funkin.audio.FunkinSound;
import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxGroup.FlxTypedGroup;
import funkin.audio.waveform.WaveformData; import funkin.audio.waveform.WaveformData;
import funkin.audio.waveform.WaveformDataParser;
class VoicesGroup extends SoundGroup class VoicesGroup extends SoundGroup
{ {

View file

@ -1,13 +1,9 @@
package funkin.audio.visualize; package funkin.audio.visualize;
import funkin.audio.visualize.dsp.FFT;
import flixel.FlxSprite; import flixel.FlxSprite;
import flixel.addons.plugin.taskManager.FlxTask;
import flixel.graphics.frames.FlxAtlasFrames; import flixel.graphics.frames.FlxAtlasFrames;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxMath;
import flixel.sound.FlxSound; import flixel.sound.FlxSound;
import funkin.util.MathUtil;
import funkin.vis.dsp.SpectralAnalyzer; import funkin.vis.dsp.SpectralAnalyzer;
import funkin.vis.audioclip.frontends.LimeAudioClip; import funkin.vis.audioclip.frontends.LimeAudioClip;
@ -58,8 +54,15 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
public function initAnalyzer() public function initAnalyzer()
{ {
@:privateAccess @:privateAccess
analyzer = new SpectralAnalyzer(7, new LimeAudioClip(cast snd._channel.__source), 0.01, 30); analyzer = new SpectralAnalyzer(snd._channel.__audioSource, 7, 0.1, 40);
analyzer.maxDb = -35;
#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; // analyzer.fftN = 2048;
} }
@ -83,9 +86,7 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
override function draw() override function draw()
{ {
#if web
if (analyzer != null) drawFFT(); if (analyzer != null) drawFFT();
#end
super.draw(); super.draw();
} }
@ -94,12 +95,18 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
*/ */
function drawFFT():Void function drawFFT():Void
{ {
var levels = analyzer.getLevels(false); var levels = analyzer.getLevels();
for (i in 0...min(group.members.length, levels.length)) for (i in 0...min(group.members.length, levels.length))
{ {
var animFrame:Int = Math.round(levels[i].value * 5); 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.min(5, animFrame));
animFrame = Math.floor(Math.max(0, animFrame)); animFrame = Math.floor(Math.max(0, animFrame));

View file

@ -1,6 +1,5 @@
package funkin.audio.visualize; package funkin.audio.visualize;
import funkin.audio.visualize.PolygonSpectogram;
import flixel.group.FlxGroup.FlxTypedGroup; import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.sound.FlxSound; import flixel.sound.FlxSound;

View file

@ -8,8 +8,6 @@ import flixel.sound.FlxSound;
import flixel.util.FlxColor; import flixel.util.FlxColor;
import funkin.audio.visualize.PolygonSpectogram.VISTYPE; import funkin.audio.visualize.PolygonSpectogram.VISTYPE;
import funkin.audio.visualize.VisShit.CurAudioInfo; import funkin.audio.visualize.VisShit.CurAudioInfo;
import funkin.audio.visualize.dsp.FFT;
import lime.system.ThreadPool;
import lime.utils.Int16Array; import lime.utils.Int16Array;
using Lambda; using Lambda;
@ -38,8 +36,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
lengthOfShit = amnt; lengthOfShit = amnt;
regenLineShit(); regenLineShit();
// makeGraphic(200, 200, FlxColor.BLACK);
} }
public function regenLineShit():Void public function regenLineShit():Void
@ -89,8 +85,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
{ {
checkAndSetBuffer(); checkAndSetBuffer();
// vis.checkAndSetBuffer();
if (setBuffer) if (setBuffer)
{ {
var samplesToGen:Int = Std.int(sampleRate * seconds); var samplesToGen:Int = Std.int(sampleRate * seconds);
@ -191,7 +185,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
// a value between 10hz and 100Khz // a value between 10hz and 100Khz
var hzPicker:Float = Math.pow(10, powedShit); 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)); var remappedFreq:Int = Std.int(FlxMath.remapToRange(hzPicker, 0, 10000, 0, freqShit[0].length - 1));
group.members[i].x = prevLine.x; group.members[i].x = prevLine.x;
@ -211,8 +204,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
var line = FlxPoint.get(prevLine.x - group.members[i].x, prevLine.y - group.members[i].y); 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! // 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<FlxSprite>
group.members[Std.int(remappedSample)].x = prevLine.x; group.members[Std.int(remappedSample)].x = prevLine.x;
group.members[Std.int(remappedSample)].y = prevLine.y; 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.x = (curAud.balanced * swagheight / 2 + swagheight / 2) + x;
prevLine.y = (Std.int(remappedSample) / lengthOfShit * daHeight) + y; prevLine.y = (Std.int(remappedSample) / lengthOfShit * daHeight) + y;

View file

@ -3,7 +3,6 @@ package funkin.audio.visualize;
import flixel.math.FlxMath; import flixel.math.FlxMath;
import flixel.sound.FlxSound; import flixel.sound.FlxSound;
import funkin.audio.visualize.dsp.FFT; import funkin.audio.visualize.dsp.FFT;
import lime.system.ThreadPool;
import lime.utils.Int16Array; import lime.utils.Int16Array;
import funkin.util.MathUtil; import funkin.util.MathUtil;
@ -73,9 +72,6 @@ class VisShit
freqOutput.push([]); 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 // find spectral peaks and their instantaneous frequencies
for (k => s in freqs) for (k => s in freqs)
{ {
@ -91,7 +87,6 @@ class VisShit
if (freq < maxFreq) freqOutput[indexOfArray].push(power); if (freq < maxFreq) freqOutput[indexOfArray].push(power);
// //
} }
// haxe.Log.trace("", null);
indexOfArray++; indexOfArray++;
// move to next (overlapping) chunk // move to next (overlapping) chunk
@ -122,7 +117,7 @@ class VisShit
{ {
// Math.pow3 // Math.pow3
@:privateAccess @:privateAccess
var buf = snd._channel.__source.buffer; var buf = snd._channel.__audioSource.buffer;
// @:privateAccess // @:privateAccess
audioData = cast buf.data; // jank and hacky lol! kinda busted on HTML5 also!! audioData = cast buf.data; // jank and hacky lol! kinda busted on HTML5 also!!

View file

@ -1,7 +1,5 @@
package funkin.audio.visualize.dsp; package funkin.audio.visualize.dsp;
import funkin.audio.visualize.dsp.Complex;
using funkin.audio.visualize.dsp.OffsetArray; using funkin.audio.visualize.dsp.OffsetArray;
using funkin.audio.visualize.dsp.Signal; using funkin.audio.visualize.dsp.Signal;

View file

@ -1,7 +1,5 @@
package funkin.audio.waveform; package funkin.audio.waveform;
import funkin.util.MathUtil;
@:nullSafety @:nullSafety
class WaveformData class WaveformData
{ {

View file

@ -16,7 +16,7 @@ class WaveformDataParser
// Method 1. This only works if the sound has been played before. // Method 1. This only works if the sound has been played before.
@:privateAccess @:privateAccess
var soundBuffer:Null<lime.media.AudioBuffer> = sound?._channel?.__source?.buffer; var soundBuffer:Null<lime.media.AudioBuffer> = sound?._channel?.__audioSource?.buffer;
if (soundBuffer == null) if (soundBuffer == null)
{ {

View file

@ -1,7 +1,5 @@
package funkin.audio.waveform; package funkin.audio.waveform;
import funkin.audio.waveform.WaveformData;
import funkin.audio.waveform.WaveformDataParser;
import funkin.graphics.rendering.MeshRender; import funkin.graphics.rendering.MeshRender;
import flixel.util.FlxColor; import flixel.util.FlxColor;

View file

@ -263,7 +263,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
* @param version The entry's version (use `fetchEntryVersion(id)`). * @param version The entry's version (use `fetchEntryVersion(id)`).
* @return The created entry. * @return The created entry.
*/ */
public function parseEntryDataWithMigration(id:String, version:thx.semver.Version):Null<J> public function parseEntryDataWithMigration(id:String, version:Null<thx.semver.Version>):Null<J>
{ {
if (version == null) if (version == null)
{ {

View file

@ -1,7 +1,5 @@
package funkin.data.dialogue.conversation; package funkin.data.dialogue.conversation;
import funkin.data.animation.AnimationData;
/** /**
* A type definition for the data for a specific conversation. * 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. * It includes things like what dialogue boxes to use, what text to display, and what animations to play.

View file

@ -1,7 +1,6 @@
package funkin.data.dialogue.conversation; package funkin.data.dialogue.conversation;
import funkin.play.cutscene.dialogue.Conversation; import funkin.play.cutscene.dialogue.Conversation;
import funkin.data.dialogue.conversation.ConversationData;
import funkin.play.cutscene.dialogue.ScriptedConversation; import funkin.play.cutscene.dialogue.ScriptedConversation;
class ConversationRegistry extends BaseRegistry<Conversation, ConversationData> class ConversationRegistry extends BaseRegistry<Conversation, ConversationData>

View file

@ -46,7 +46,7 @@ class SongEventRegistry
if (event != null) if (event != null)
{ {
trace(' Loaded built-in song event: (${event.id})'); trace(' Loaded built-in song event: ${event.id}');
eventCache.set(event.id, event); eventCache.set(event.id, event);
} }
else else
@ -59,9 +59,9 @@ class SongEventRegistry
static function registerScriptedEvents() static function registerScriptedEvents()
{ {
var scriptedEventClassNames:Array<String> = ScriptedSongEvent.listScriptClasses(); var scriptedEventClassNames:Array<String> = ScriptedSongEvent.listScriptClasses();
trace('Instantiating ${scriptedEventClassNames.length} scripted song events...');
if (scriptedEventClassNames == null || scriptedEventClassNames.length == 0) return; if (scriptedEventClassNames == null || scriptedEventClassNames.length == 0) return;
trace('Instantiating ${scriptedEventClassNames.length} scripted song events...');
for (eventCls in scriptedEventClassNames) for (eventCls in scriptedEventClassNames)
{ {
var event:SongEvent = ScriptedSongEvent.init(eventCls, "UKNOWN"); var event:SongEvent = ScriptedSongEvent.init(eventCls, "UKNOWN");

View file

@ -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.

View file

@ -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<String> = [];
/**
* 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<PlayerFreeplayDJData> = null;
/**
* Data for displaying this character in the Character Select menu.
* If null, exclude from Character Select.
*/
@:optional
public var charSelect:Null<PlayerCharSelectData> = null;
/**
* Data for displaying this character in the results screen.
*/
@:optional
public var results:Null<PlayerResultsData> = null;
/**
* 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<PlayerData>();
return writer.write(this, pretty ? ' ' : null);
}
public function updateVersionToLatest():Void
{
this.version = PlayerRegistry.PLAYER_DATA_VERSION;
}
}
class PlayerFreeplayDJData
{
var assetPath:String;
var animations:Array<AnimationData>;
@: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<String, AnimationData>;
@:jignored
var prefixToOffsetsMap:Map<String, Array<Float>>;
@:optional
var charSelect:Null<PlayerFreeplayDJCharSelectData>;
@:optional
var cartoon:Null<PlayerFreeplayDJCartoonData>;
@:optional
var fistPump:Null<PlayerFreeplayDJFistPumpData>;
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<String>
{
if (animationMap.size() == 0) mapAnimations();
var anim = animationMap.get(name);
if (anim == null) return null;
return anim.prefix;
}
public function getAnimationOffsetsByPrefix(?prefix:String):Array<Float>
{
if (prefixToOffsetsMap.size() == 0) mapAnimations();
if (prefix == null) return [0, 0];
return prefixToOffsetsMap.get(prefix);
}
public function getAnimationOffsets(name:String):Array<Float>
{
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<Int>;
}
typedef PlayerResultsData =
{
var music:PlayerResultsMusicData;
var perfect:Array<PlayerResultsAnimationData>;
var excellent:Array<PlayerResultsAnimationData>;
var great:Array<PlayerResultsAnimationData>;
var good:Array<PlayerResultsAnimationData>;
var loss:Array<PlayerResultsAnimationData>;
};
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<Float>;
@: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<String>;
@:optional
@:default(true)
var looped:Bool;
@:optional
var loopFrame:Null<Int>;
@:optional
var loopFrameLabel:Null<String>;
};
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;
};

View file

@ -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<PlayableCharacter, PlayerData>
{
/**
* 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<PlayerRegistry> = 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<String, String> = [];
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<String>
{
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<String>):Null<String>
{
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<PlayerData>
{
// JsonParser does not take type parameters,
// otherwise this function would be in BaseRegistry.
var parser = new json2object.JsonParser<PlayerData>();
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<PlayerData>
{
var parser = new json2object.JsonParser<PlayerData>();
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<String>
{
return ScriptedPlayableCharacter.listScriptClasses();
}
/**
* A list of all the playable characters from the base game, in order.
*/
public function listBaseGamePlayerIds():Array<String>
{
return ["bf", "pico"];
}
/**
* A list of all installed playable characters that are not from the base game.
*/
public function listModdedPlayerIds():Array<String>
{
return listEntryIds().filter(function(id:String):Bool {
return listBaseGamePlayerIds().indexOf(id) == -1;
});
}
}

View file

@ -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.

View file

@ -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<String>;
/**
* Delay time after confirming a song selection, before entering PlayState.
* Useful for letting longer animations play out.
*/
public var startDelay:Float;
}

View file

@ -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<FreeplayStyle, FreeplayStyleData>
{
/**
* 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<FreeplayStyleData>
{
// JsonParser does not take type parameters,
// otherwise this function would be in BaseRegistry.
var parser:json2object.JsonParser<FreeplayStyleData> = new json2object.JsonParser<FreeplayStyleData>();
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<FreeplayStyleData>
{
var parser:json2object.JsonParser<FreeplayStyleData> = new json2object.JsonParser<FreeplayStyleData>();
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<String>
{
return ScriptedFreeplayStyle.listScriptClasses();
}
}

View file

@ -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.

View file

@ -74,6 +74,84 @@ typedef NoteStyleAssetsData =
*/ */
@:optional @:optional
var holdNoteCover:NoteStyleAssetData<NoteStyleData_HoldNoteCover>; var holdNoteCover:NoteStyleAssetData<NoteStyleData_HoldNoteCover>;
/**
* The THREE sound (and an optional pre-READY graphic).
*/
@:optional
var countdownThree:NoteStyleAssetData<NoteStyleData_Countdown>;
/**
* The TWO sound and READY graphic.
*/
@:optional
var countdownTwo:NoteStyleAssetData<NoteStyleData_Countdown>;
/**
* The ONE sound and SET graphic.
*/
@:optional
var countdownOne:NoteStyleAssetData<NoteStyleData_Countdown>;
/**
* The GO sound and GO! graphic.
*/
@:optional
var countdownGo:NoteStyleAssetData<NoteStyleData_Countdown>;
/**
* The SICK! judgement.
*/
@:optional
var judgementSick:NoteStyleAssetData<NoteStyleData_Judgement>;
/**
* The GOOD! judgement.
*/
@:optional
var judgementGood:NoteStyleAssetData<NoteStyleData_Judgement>;
/**
* The BAD! judgement.
*/
@:optional
var judgementBad:NoteStyleAssetData<NoteStyleData_Judgement>;
/**
* The SHIT! judgement.
*/
@:optional
var judgementShit:NoteStyleAssetData<NoteStyleData_Judgement>;
@:optional
var comboNumber0:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber1:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber2:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber3:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber4:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber5:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber6:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber7:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber8:NoteStyleAssetData<NoteStyleData_ComboNum>;
@:optional
var comboNumber9:NoteStyleAssetData<NoteStyleData_ComboNum>;
} }
/** /**
@ -109,10 +187,19 @@ typedef NoteStyleAssetData<T> =
@:optional @:optional
var isPixel:Bool; 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. * The structure of this data depends on the asset.
*/ */
var data:T; @:optional
var data:Null<T>;
} }
typedef NoteStyleData_Note = typedef NoteStyleData_Note =
@ -123,7 +210,14 @@ typedef NoteStyleData_Note =
var right:UnnamedAnimationData; var right:UnnamedAnimationData;
} }
typedef NoteStyleData_Countdown =
{
var audioPath:String;
}
typedef NoteStyleData_HoldNote = {} typedef NoteStyleData_HoldNote = {}
typedef NoteStyleData_Judgement = {}
typedef NoteStyleData_ComboNum = {}
/** /**
* Data on animations for each direction of the strumline. * Data on animations for each direction of the strumline.

View file

@ -11,9 +11,9 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
* Handle breaking changes by incrementing this value * Handle breaking changes by incrementing this value
* and adding migration to the `migrateNoteStyleData()` function. * 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; public static var instance(get, never):NoteStyleRegistry;
static var _instance:Null<NoteStyleRegistry> = null; static var _instance:Null<NoteStyleRegistry> = null;

View file

@ -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/), 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). 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] ## [2.2.2]
### Added ### 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. - 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.

View file

@ -30,6 +30,9 @@ class SongMetadata implements ICloneable<SongMetadata>
@:default("Unknown") @:default("Unknown")
public var artist:String; public var artist:String;
@:optional
public var charter:Null<String> = null;
@:optional @:optional
@:default(96) @:default(96)
public var divisions:Null<Int>; // Optional field public var divisions:Null<Int>; // Optional field
@ -53,6 +56,8 @@ class SongMetadata implements ICloneable<SongMetadata>
@:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY) @:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY)
public var generatedBy:String; public var generatedBy:String;
@:optional
@:default(funkin.data.song.SongData.SongTimeFormat.MILLISECONDS)
public var timeFormat:SongTimeFormat; public var timeFormat:SongTimeFormat;
public var timeChanges:Array<SongTimeChange>; public var timeChanges:Array<SongTimeChange>;
@ -112,14 +117,23 @@ class SongMetadata implements ICloneable<SongMetadata>
*/ */
public function serialize(pretty:Bool = true):String public function serialize(pretty:Bool = true):String
{ {
// Update generatedBy and version before writing.
updateVersionToLatest();
var ignoreNullOptionals = true; var ignoreNullOptionals = true;
var writer = new json2object.JsonWriter<SongMetadata>(ignoreNullOptionals); var writer = new json2object.JsonWriter<SongMetadata>(ignoreNullOptionals);
// I believe @:jignored should be iggnored by the writer? // I believe @:jignored should be ignored by the writer?
// var output = this.clone(); // var output = this.clone();
// output.variation = null; // Not sure how to make a field optional on the reader and ignored on the writer. // 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); 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. * Produces a string representation suitable for debugging.
*/ */
@ -243,18 +257,27 @@ class SongOffsets implements ICloneable<SongOffsets>
public var altInstrumentals:Map<String, Float>; public var altInstrumentals:Map<String, Float>;
/** /**
* 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. * These are applied ON TOP OF the instrumental offset.
*/ */
@:optional @:optional
@:default([]) @:default([])
public var vocals:Map<String, Float>; public var vocals:Map<String, Float>;
public function new(instrumental:Float = 0.0, ?altInstrumentals:Map<String, Float>, ?vocals:Map<String, Float>) /**
* 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<String, Map<String, Float>>;
public function new(instrumental:Float = 0.0, ?altInstrumentals:Map<String, Float>, ?vocals:Map<String, Float>, ?altVocals:Map<String, Map<String, Float>>)
{ {
this.instrumental = instrumental; this.instrumental = instrumental;
this.altInstrumentals = altInstrumentals == null ? new Map<String, Float>() : altInstrumentals; this.altInstrumentals = altInstrumentals == null ? new Map<String, Float>() : altInstrumentals;
this.vocals = vocals == null ? new Map<String, Float>() : vocals; this.vocals = vocals == null ? new Map<String, Float>() : vocals;
this.altVocals = altVocals == null ? new Map<String, Map<String, Float>>() : altVocals;
} }
public function getInstrumentalOffset(?instrumental:String):Float public function getInstrumentalOffset(?instrumental:String):Float
@ -279,12 +302,20 @@ class SongOffsets implements ICloneable<SongOffsets>
return value; return value;
} }
public function getVocalOffset(charId:String):Float public function getVocalOffset(charId:String, ?instrumental:String):Float
{
if (instrumental == null)
{ {
if (!this.vocals.exists(charId)) return 0.0; if (!this.vocals.exists(charId)) return 0.0;
return this.vocals.get(charId); 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 public function setVocalOffset(charId:String, value:Float):Float
{ {
@ -306,7 +337,7 @@ class SongOffsets implements ICloneable<SongOffsets>
*/ */
public function toString():String 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<SongMusicData>
this.variation = variation == null ? Constants.DEFAULT_VARIATION : variation; 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 public function clone():SongMusicData
{ {
var result:SongMusicData = new SongMusicData(this.songName, this.artist, this.variation); var result:SongMusicData = new SongMusicData(this.songName, this.artist, this.variation);
@ -509,12 +546,26 @@ class SongCharacterData implements ICloneable<SongCharacterData>
@:default([]) @:default([])
public var altInstrumentals:Array<String> = []; public var altInstrumentals:Array<String> = [];
public function new(player:String = '', girlfriend:String = '', opponent:String = '', instrumental:String = '') @:optional
public var opponentVocals:Null<Array<String>> = null;
@:optional
public var playerVocals:Null<Array<String>> = null;
public function new(player:String = '', girlfriend:String = '', opponent:String = '', instrumental:String = '', ?altInstrumentals:Array<String>,
?opponentVocals:Array<String>, ?playerVocals:Array<String>)
{ {
this.player = player; this.player = player;
this.girlfriend = girlfriend; this.girlfriend = girlfriend;
this.opponent = opponent; this.opponent = opponent;
this.instrumental = instrumental; 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 public function clone():SongCharacterData
@ -600,11 +651,20 @@ class SongChartData implements ICloneable<SongChartData>
*/ */
public function serialize(pretty:Bool = true):String public function serialize(pretty:Bool = true):String
{ {
// Update generatedBy and version before writing.
updateVersionToLatest();
var ignoreNullOptionals = true; var ignoreNullOptionals = true;
var writer = new json2object.JsonWriter<SongChartData>(ignoreNullOptionals); var writer = new json2object.JsonWriter<SongChartData>(ignoreNullOptionals);
return writer.write(this, pretty ? ' ' : null); 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 public function clone():SongChartData
{ {
// We have to manually perform the deep clone here because Map.deepClone() doesn't work. // We have to manually perform the deep clone here because Map.deepClone() doesn't work.
@ -693,18 +753,6 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
{ {
return new SongEventDataRaw(this.time, this.eventKind, this.value); 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 public function valueAsStruct(?defaultKey:String = "key"):Dynamic
{ {
@ -728,27 +776,27 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
} }
} }
public inline function getHandler():Null<SongEvent> public function getHandler():Null<SongEvent>
{ {
return SongEventRegistry.getEvent(this.eventKind); return SongEventRegistry.getEvent(this.eventKind);
} }
public inline function getSchema():Null<SongEventSchema> public function getSchema():Null<SongEventSchema>
{ {
return SongEventRegistry.getEventSchema(this.eventKind); return SongEventRegistry.getEventSchema(this.eventKind);
} }
public inline function getDynamic(key:String):Null<Dynamic> public function getDynamic(key:String):Null<Dynamic>
{ {
return this.value == null ? null : Reflect.field(this.value, key); return this.value == null ? null : Reflect.field(this.value, key);
} }
public inline function getBool(key:String):Null<Bool> public function getBool(key:String):Null<Bool>
{ {
return this.value == null ? null : cast Reflect.field(this.value, key); return this.value == null ? null : cast Reflect.field(this.value, key);
} }
public inline function getInt(key:String):Null<Int> public function getInt(key:String):Null<Int>
{ {
if (this.value == null) return null; if (this.value == null) return null;
var result = Reflect.field(this.value, key); var result = Reflect.field(this.value, key);
@ -758,7 +806,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return cast result; return cast result;
} }
public inline function getFloat(key:String):Null<Float> public function getFloat(key:String):Null<Float>
{ {
if (this.value == null) return null; if (this.value == null) return null;
var result = Reflect.field(this.value, key); var result = Reflect.field(this.value, key);
@ -768,17 +816,17 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return cast result; 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); return this.value == null ? null : cast Reflect.field(this.value, key);
} }
public inline function getArray(key:String):Array<Dynamic> public function getArray(key:String):Array<Dynamic>
{ {
return this.value == null ? null : cast Reflect.field(this.value, key); return this.value == null ? null : cast Reflect.field(this.value, key);
} }
public inline function getBoolArray(key:String):Array<Bool> public function getBoolArray(key:String):Array<Bool>
{ {
return this.value == null ? null : cast Reflect.field(this.value, key); return this.value == null ? null : cast Reflect.field(this.value, key);
} }
@ -810,6 +858,19 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return result; 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 public function clone():SongEventData
{ {
@ -922,12 +983,18 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
return this.kind = value; return this.kind = value;
} }
public function new(time:Float, data:Int, length:Float = 0, kind:String = '') @:alias("p")
@:default([])
@:optional
public var params:Array<NoteParamData>;
public function new(time:Float, data:Int, length:Float = 0, kind:String = '', ?params:Array<NoteParamData>)
{ {
this.time = time; this.time = time;
this.data = data; this.data = data;
this.length = length; this.length = length;
this.kind = kind; this.kind = kind;
this.params = params ?? [];
} }
/** /**
@ -1022,9 +1089,19 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
_stepLength = null; _stepLength = null;
} }
public function cloneParams():Array<NoteParamData>
{
var params:Array<NoteParamData> = [];
for (param in this.params)
{
params.push(param.clone());
}
return params;
}
public function clone():SongNoteDataRaw 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 public function toString():String
@ -1040,9 +1117,9 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
@:forward @:forward
abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw 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<NoteParamData>)
{ {
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 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; 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) @:op(A != B)
@ -1105,7 +1182,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
if (other.kind == '') return true; 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) @:op(A > B)
@ -1142,7 +1219,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
public function clone():SongNoteData 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}])' : ')'); + (this.kind != '' ? ' [kind: ${this.kind}])' : ')');
} }
} }
class NoteParamData implements ICloneable<NoteParamData>
{
@: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})';
}
}

View file

@ -20,7 +20,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
* Handle breaking changes by incrementing this value * Handle breaking changes by incrementing this value
* and adding migration to the `migrateStageData()` function. * 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"; public static final SONG_METADATA_VERSION_RULE:thx.semver.VersionRule = "2.2.x";

View file

@ -61,10 +61,18 @@ class ChartManifestData
*/ */
public function serialize(pretty:Bool = true):String public function serialize(pretty:Bool = true):String
{ {
// Update generatedBy and version before writing.
updateVersionToLatest();
var writer = new json2object.JsonWriter<ChartManifestData>(); var writer = new json2object.JsonWriter<ChartManifestData>();
return writer.write(this, pretty ? ' ' : null); return writer.write(this, pretty ? ' ' : null);
} }
public function updateVersionToLatest():Void
{
this.version = CHART_MANIFEST_DATA_VERSION;
}
public static function deserialize(contents:String):Null<ChartManifestData> public static function deserialize(contents:String):Null<ChartManifestData>
{ {
var parser = new json2object.JsonParser<ChartManifestData>(); var parser = new json2object.JsonParser<ChartManifestData>();

View file

@ -36,7 +36,7 @@ class FNFLegacyImporter
{ {
trace('Migrating song metadata from FNF Legacy.'); 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; var hadError:Bool = false;
@ -65,7 +65,7 @@ class FNFLegacyImporter
songMetadata.timeChanges = rebuildTimeChanges(songData); 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; return songMetadata;
} }
@ -199,6 +199,8 @@ class FNFLegacyImporter
{ {
// Handle the dumb logic for mustHitSection. // Handle the dumb logic for mustHitSection.
var noteData = note.data; 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). // Flip notes if mustHitSection is FALSE (not true lol).
if (!mustHitSection) if (!mustHitSection)

View file

@ -58,9 +58,17 @@ class StageData
*/ */
public function serialize(pretty:Bool = true):String public function serialize(pretty:Bool = true):String
{ {
// Update generatedBy and version before writing.
updateVersionToLatest();
var writer = new json2object.JsonWriter<StageData>(); var writer = new json2object.JsonWriter<StageData>();
return writer.write(this, pretty ? ' ' : null); return writer.write(this, pretty ? ' ' : null);
} }
public function updateVersionToLatest():Void
{
this.version = StageRegistry.STAGE_DATA_VERSION;
}
} }
typedef StageDataCharacters = typedef StageDataCharacters =
@ -132,12 +140,12 @@ typedef StageDataProp =
* If not zero, this prop will play an animation every X beats of the song. * 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, * This requires animations to be defined. If `danceLeft` and `danceRight` are defined,
* they will alternated between, otherwise the `idle` animation will be used. * they will alternated between, otherwise the `idle` animation will be used.
* * Supports up to 0.25 precision.
* @default 0 * @default 0.0
*/ */
@:default(0) @:default(0.0)
@:optional @:optional
var danceEvery:Int; var danceEvery:Float;
/** /**
* How much the prop scrolls relative to the camera. Used to create a parallax effect. * How much the prop scrolls relative to the camera. Used to create a parallax effect.

View file

@ -93,8 +93,8 @@ class StageRegistry extends BaseRegistry<Stage, StageData>
public function listBaseGameStageIds():Array<String> public function listBaseGameStageIds():Array<String>
{ {
return [ return [
"mainStage", "spookyMansion", "phillyTrain", "limoRide", "mallXmas", "mallEvil", "school", "schoolEvil", "tankmanBattlefield", "phillyStreets", "mainStage", "mainStageErect", "spookyMansion", "phillyTrain", "phillyTrainErect", "limoRide", "limoRideErect", "mallXmas", "mallXmasErect", "mallEvil",
"phillyBlazin", "school", "schoolEvil", "tankmanBattlefield", "phillyStreets", "phillyStreetsErect", "phillyBlazin",
]; ];
} }

View file

@ -91,11 +91,13 @@ typedef LevelPropData =
/** /**
* The frequency to bop at, in beats. * 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 @:optional
var danceEvery:Int; var danceEvery:Float;
/** /**
* The offset on the position to render the prop at. * The offset on the position to render the prop at.

View file

@ -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<IntervalShake> = new FlxPool<IntervalShake>(IntervalShake.new);
/**
* Internal map for looking up which objects are currently shaking and getting their shake data.
*/
static var _boundObjects:Map<FlxObject, IntervalShake> = new Map<FlxObject, IntervalShake>();
/**
* 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() {}
}

View file

@ -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);
}
}

View file

@ -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<BitmapFilter>;
/**
* 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<BitmapFilter>)
{
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<BitmapFilter>,
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;
}
}

View file

@ -7,6 +7,10 @@ import flixel.tweens.FlxTween;
import openfl.display3D.textures.TextureBase; import openfl.display3D.textures.TextureBase;
import funkin.graphics.framebuffer.FixedBitmapData; import funkin.graphics.framebuffer.FixedBitmapData;
import openfl.display.BitmapData; 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. * An FlxSprite with additional functionality.
@ -269,6 +273,103 @@ class FunkinSprite extends FlxSprite
return result; 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 public override function destroy():Void
{ {
frames = null; frames = null;

View file

@ -4,8 +4,12 @@ import flixel.util.FlxSignal.FlxTypedSignal;
import flxanimate.FlxAnimate; import flxanimate.FlxAnimate;
import flxanimate.FlxAnimate.Settings; import flxanimate.FlxAnimate.Settings;
import flxanimate.frames.FlxAnimateFrames; import flxanimate.frames.FlxAnimateFrames;
import flixel.graphics.frames.FlxFrame;
import flixel.system.FlxAssets.FlxGraphicAsset;
import openfl.display.BitmapData; import openfl.display.BitmapData;
import openfl.utils.Assets; 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. * A sprite which provides convenience functions for rendering a texture atlas with animations.
@ -18,16 +22,21 @@ class FlxAtlasSprite extends FlxAnimate
FrameRate: 24.0, FrameRate: 24.0,
Reversed: false, Reversed: false,
// ?OnComplete:Void -> Void, // ?OnComplete:Void -> Void,
ShowPivot: #if debug false #else false #end, ShowPivot: false,
Antialiasing: true, Antialiasing: true,
ScrollFactor: null, ScrollFactor: null,
// Offset: new FlxPoint(0, 0), // This is just FlxSprite.offset // 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:FlxTypedSignal<String->Void> = new FlxTypedSignal<String->Void>(); public var onAnimationFrame:FlxTypedSignal<String->Int->Void> = new FlxTypedSignal();
/**
* Signal dispatched when a non-looping animation finishes playing.
*/
public var onAnimationComplete:FlxTypedSignal<String->Void> = new FlxTypedSignal();
var currentAnimation:String; var currentAnimation:String;
@ -42,19 +51,28 @@ class FlxAtlasSprite extends FlxAnimate
throw 'Null path specified for FlxAtlasSprite!'; 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); 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?'; 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, // This defaults the sprite to play the first animation in the atlas,
// then pauses it. This ensures symbols are intialized properly. // then pauses it. This ensures symbols are intialized properly.
this.anim.play(''); this.anim.play('');
this.anim.pause(); 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<String> public function listAnimations():Array<String>
{ {
if (this.anim == null) return []; var mainSymbol = this.anim.symbolDictionary[this.anim.stageInstance.symbol.name];
return this.anim.getFrameLabels(); if (mainSymbol == null)
// return [""]; {
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 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; return this.currentAnimation;
} }
/** var _completeAnim:Bool = false;
* `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;
// Reverse animation finished. var fr:FlxKeyFrame = null;
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;
return false; var looping:Bool = false;
}
public var ignoreExclusionPref:Array<String> = [];
/** /**
* Plays an animation. * Plays an animation.
@ -107,61 +120,86 @@ class FlxAtlasSprite extends FlxAnimate
* @param restart Whether to restart the animation if it is already playing. * @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 ignoreOther Whether to ignore all other animation inputs, until this one is done playing
* @param loop Whether to loop the animation * @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! * 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. // 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 (id == null || id == '') id = this.currentAnimation;
if (this.currentAnimation == id && !restart) if (this.currentAnimation == id && !restart)
{ {
if (anim.isPlaying) if (!anim.isPlaying)
{ {
// Skip if animation is already playing. if (fr != null) anim.curFrame = fr.index + startFrame;
return;
}
else else
{ anim.curFrame = startFrame;
// Resume animation if it's paused. // Resume animation if it's paused.
anim.play('', false, false); anim.resume();
}
} }
// Skip if the animation doesn't exist return;
if (!hasAnimation(id)) }
else if (!hasAnimation(id))
{ {
// Skip if the animation doesn't exist
trace('Animation ' + id + ' not found'); trace('Animation ' + id + ' not found');
return; return;
} }
anim.callback = function(_, frame:Int) { this.currentAnimation = id;
var offset = loop ? 0 : -1; anim.onComplete.removeAll();
anim.onComplete.add(function() {
_onAnimationComplete();
});
var frameLabel = anim.getFrameLabel(id); looping = loop;
if (frame == (frameLabel.duration + offset) + frameLabel.index)
{
if (loop)
{
playAnimation(id, true, false, true);
}
else
{
onAnimationFinish.dispatch(id);
}
}
};
// Prevent other animations from playing if `ignoreOther` is true. // Prevent other animations from playing if `ignoreOther` is true.
if (ignoreOther) canPlayOtherAnims = false; if (ignoreOther) canPlayOtherAnims = false;
// Move to the first frame of the animation. // Move to the first frame of the animation.
// 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); goToFrameLabel(id);
this.currentAnimation = id; fr = anim.getFrameLabel(id);
anim.curFrame += startFrame;
}
} }
override public function update(elapsed:Float) override public function update(elapsed:Float)
@ -169,6 +207,29 @@ class FlxAtlasSprite extends FlxAnimate
super.update(elapsed); 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. * Stops the current animation.
*/ */
@ -192,6 +253,18 @@ class FlxAtlasSprite extends FlxAnimate
this.anim.goToFrameLabel(label); this.anim.goToFrameLabel(label);
} }
function getFrameLabelNames(?layer:haxe.extern.EitherType<Int, String> = null)
{
var labels = this.anim.getFrameLabels(layer);
var array = [];
for (label in labels)
{
array.push(label.name);
}
return array;
}
function getNextFrameLabel(label:String):String function getNextFrameLabel(label:String):String
{ {
return listAnimations()[(getLabelIndex(label) + 1) % listAnimations().length]; return listAnimations()[(getLabelIndex(label) + 1) % listAnimations().length];
@ -213,4 +286,95 @@ class FlxAtlasSprite extends FlxAnimate
// this.currentAnimation = null; // this.currentAnimation = null;
this.anim.pause(); 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<Int, FlxFrame> = [];
public function replaceFrameGraphic(index:Int, ?graphic:FlxGraphicAsset):Void
{
if (graphic == null || !Assets.exists(graphic))
{
var prevFrame:Null<FlxFrame> = 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<FlxPoint>
{
// 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<FlxPoint>
{
return anim.curInstance.symbol.transformationPoint;
}
public override function destroy():Void
{
for (prevFrameId in prevFrames.keys())
{
replaceFrameGraphic(prevFrameId, null);
}
super.destroy();
}
} }

View file

@ -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;
}
}

View file

@ -1,20 +1,46 @@
package funkin.graphics.shaders; package funkin.graphics.shaders;
import flixel.system.FlxAssets.FlxShader; import flixel.system.FlxAssets.FlxShader;
import flixel.util.FlxColor;
class AngleMask extends FlxShader 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(' @:glFragmentSource('
#pragma header #pragma header
uniform vec3 extraTint;
uniform vec2 endPosition; uniform vec2 endPosition;
void main() vec2 hash22(vec2 p) {
{ vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973));
vec4 base = texture2D(bitmap, openfl_TextureCoordv); p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.xx + p3.yz) * p3.zy);
vec2 uv = openfl_TextureCoordv.xy; }
// ====== 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 start = vec2(0.0, 0.0);
vec2 end = vec2(endPosition.x / openfl_TextureSize.x, 1.0); vec2 end = vec2(endPosition.x / openfl_TextureSize.x, 1.0);
@ -29,15 +55,42 @@ class AngleMask extends FlxShader
float uvA = atan(uv.y, uv.x); float uvA = atan(uv.y, uv.x);
if (uvA < angle) if (uvA < angle)
gl_FragColor = base; return base;
else else
gl_FragColor = vec4(0.0); return vec4(0.0);
}
vec4 antialias(vec2 fragCoord) {
const float AA_STAGES = 2.0;
const float AA_TOTAL_PASSES = AA_STAGES * AA_STAGES + 1.0;
const float AA_JITTER = 0.5;
// 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() public function new()
{ {
super(); super();
endPosition.value = [90, 100]; // 100 AS DEFAULT WORKS NICELY FOR FREEPLAY? endPosition.value = [90, 100]; // 100 AS DEFAULT WORKS NICELY FOR FREEPLAY?
extraTint.value = [1, 1, 1];
} }
} }

View file

@ -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;
}
}

View file

@ -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]);
}
}

View file

@ -2,6 +2,7 @@ package funkin.graphics.shaders;
import flixel.FlxCamera; import flixel.FlxCamera;
import flixel.FlxG; import flixel.FlxG;
import flixel.graphics.frames.FlxFrame;
import flixel.addons.display.FlxRuntimeShader; import flixel.addons.display.FlxRuntimeShader;
import lime.graphics.opengl.GLProgram; import lime.graphics.opengl.GLProgram;
import lime.utils.Log; import lime.utils.Log;
@ -32,6 +33,9 @@ class RuntimePostEffectShader extends FlxRuntimeShader
// equals (camera.viewLeft, camera.viewTop, camera.viewRight, camera.viewBottom) // equals (camera.viewLeft, camera.viewTop, camera.viewRight, camera.viewBottom)
uniform vec4 uCameraBounds; uniform vec4 uCameraBounds;
// equals (frame.left, frame.top, frame.right, frame.bottom)
uniform vec4 uFrameBounds;
// screen coord -> world coord conversion // screen coord -> world coord conversion
// returns world coord in px // returns world coord in px
vec2 screenToWorld(vec2 screenCoord) { vec2 screenToWorld(vec2 screenCoord) {
@ -56,6 +60,25 @@ class RuntimePostEffectShader extends FlxRuntimeShader
return (worldCoord - offset) / scale; 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` // internally used to get the maximum `openfl_TextureCoordv`
vec2 bitmapCoordScale() { vec2 bitmapCoordScale() {
return openfl_TextureCoordv / screenCoord; return openfl_TextureCoordv / screenCoord;
@ -80,6 +103,8 @@ class RuntimePostEffectShader extends FlxRuntimeShader
{ {
super(fragmentSource, null, glVersion); super(fragmentSource, null, glVersion);
uScreenResolution.value = [FlxG.width, FlxG.height]; 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 // 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]; 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 override function __createGLProgram(vertexSource:String, fragmentSource:String):GLProgram
{ {
try try

View file

@ -4,6 +4,7 @@ import flixel.system.FlxAssets.FlxShader;
import openfl.display.BitmapData; import openfl.display.BitmapData;
import openfl.display.ShaderParameter; import openfl.display.ShaderParameter;
import openfl.display.ShaderParameterType; import openfl.display.ShaderParameterType;
import flixel.util.FlxColor;
import openfl.utils.Assets; import openfl.utils.Assets;
typedef Light = typedef Light =
@ -32,6 +33,14 @@ class RuntimeRainShader extends RuntimePostEffectShader
return time = value; 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 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 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. // The size of the raindrops is proportional to the value of this parameter.
@ -86,6 +95,14 @@ class RuntimeRainShader extends RuntimePostEffectShader
return mask = value; 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; public var lightMap(default, set):BitmapData;
function set_lightMap(value:BitmapData):BitmapData function set_lightMap(value:BitmapData):BitmapData
@ -105,6 +122,7 @@ class RuntimeRainShader extends RuntimePostEffectShader
public function new() public function new()
{ {
super(Assets.getText(Paths.frag('rain'))); super(Assets.getText(Paths.frag('rain')));
this.rainColor = 0xFF6680cc;
} }
public function update(elapsed:Float):Void public function update(elapsed:Float):Void

View file

@ -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;
}
}

View file

@ -11,6 +11,7 @@ import flixel.system.debug.watch.Tracker;
// These are great. // These are great.
using Lambda; using Lambda;
using StringTools; using StringTools;
using thx.Arrays;
using funkin.util.tools.ArraySortTools; using funkin.util.tools.ArraySortTools;
using funkin.util.tools.ArrayTools; using funkin.util.tools.ArrayTools;
using funkin.util.tools.FloatTools; using funkin.util.tools.FloatTools;

View file

@ -31,6 +31,7 @@ class Controls extends FlxActionSet
* Uses FlxActions to funnel various inputs to a single action. * Uses FlxActions to funnel various inputs to a single action.
*/ */
var _ui_up = new FunkinAction(Action.UI_UP); var _ui_up = new FunkinAction(Action.UI_UP);
var _ui_left = new FunkinAction(Action.UI_LEFT); var _ui_left = new FunkinAction(Action.UI_LEFT);
var _ui_right = new FunkinAction(Action.UI_RIGHT); var _ui_right = new FunkinAction(Action.UI_RIGHT);
var _ui_down = new FunkinAction(Action.UI_DOWN); var _ui_down = new FunkinAction(Action.UI_DOWN);
@ -58,7 +59,12 @@ class Controls extends FlxActionSet
var _back = new FunkinAction(Action.BACK); var _back = new FunkinAction(Action.BACK);
var _pause = new FunkinAction(Action.PAUSE); var _pause = new FunkinAction(Action.PAUSE);
var _reset = new FunkinAction(Action.RESET); 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 _cutscene_advance = new FunkinAction(Action.CUTSCENE_ADVANCE);
var _debug_menu = new FunkinAction(Action.DEBUG_MENU); var _debug_menu = new FunkinAction(Action.DEBUG_MENU);
var _debug_chart = new FunkinAction(Action.DEBUG_CHART); 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_up = new FunkinAction(Action.VOLUME_UP);
var _volume_down = new FunkinAction(Action.VOLUME_DOWN); var _volume_down = new FunkinAction(Action.VOLUME_DOWN);
var _volume_mute = new FunkinAction(Action.VOLUME_MUTE); var _volume_mute = new FunkinAction(Action.VOLUME_MUTE);
var _fullscreen = new FunkinAction(Action.FULLSCREEN);
var byName:Map<String, FunkinAction> = new Map<String, FunkinAction>(); var byName:Map<String, FunkinAction> = new Map<String, FunkinAction>();
@ -233,10 +238,35 @@ class Controls extends FlxActionSet
inline function get_RESET() inline function get_RESET()
return _reset.check(); return _reset.check();
public var SCREENSHOT(get, never):Bool; public var WINDOW_FULLSCREEN(get, never):Bool;
inline function get_SCREENSHOT() inline function get_WINDOW_FULLSCREEN()
return _screenshot.check(); 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_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; public var CUTSCENE_ADVANCE(get, never):Bool;
@ -273,11 +303,6 @@ class Controls extends FlxActionSet
inline function get_VOLUME_MUTE() inline function get_VOLUME_MUTE()
return _volume_mute.check(); return _volume_mute.check();
public var FULLSCREEN(get, never):Bool;
inline function get_FULLSCREEN()
return _fullscreen.check();
public function new(name, scheme:KeyboardScheme = null) public function new(name, scheme:KeyboardScheme = null)
{ {
super(name); super(name);
@ -294,7 +319,12 @@ class Controls extends FlxActionSet
add(_back); add(_back);
add(_pause); add(_pause);
add(_reset); 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(_cutscene_advance);
add(_debug_menu); add(_debug_menu);
add(_debug_chart); add(_debug_chart);
@ -302,21 +332,19 @@ class Controls extends FlxActionSet
add(_volume_up); add(_volume_up);
add(_volume_down); add(_volume_down);
add(_volume_mute); add(_volume_mute);
add(_fullscreen);
for (action in digitalActions) { for (action in digitalActions)
if (Std.isOfType(action, FunkinAction)) { {
if (Std.isOfType(action, FunkinAction))
{
var funkinAction:FunkinAction = cast action; var funkinAction:FunkinAction = cast action;
byName[funkinAction.name] = funkinAction; byName[funkinAction.name] = funkinAction;
if (funkinAction.namePressed != null) if (funkinAction.namePressed != null) byName[funkinAction.namePressed] = funkinAction;
byName[funkinAction.namePressed] = funkinAction; if (funkinAction.nameReleased != null) byName[funkinAction.nameReleased] = funkinAction;
if (funkinAction.nameReleased != null)
byName[funkinAction.nameReleased] = funkinAction;
} }
} }
if (scheme == null) if (scheme == null) scheme = None;
scheme = None;
setKeyboardScheme(scheme, false); setKeyboardScheme(scheme, false);
} }
@ -328,47 +356,50 @@ class Controls extends FlxActionSet
public function check(name:Action, trigger:FlxInputState = JUST_PRESSED, gamepadOnly:Bool = false):Bool public function check(name:Action, trigger:FlxInputState = JUST_PRESSED, gamepadOnly:Bool = false):Bool
{ {
#if debug #if FEATURE_DEBUG_FUNCTIONS
if (!byName.exists(name)) if (!byName.exists(name)) throw 'Invalid name: $name';
throw 'Invalid name: $name';
#end #end
var action = byName[name]; var action = byName[name];
if (gamepadOnly) if (gamepadOnly) return action.checkFiltered(trigger, GAMEPAD);
return action.checkFiltered(trigger, GAMEPAD);
else else
return action.checkFiltered(trigger); return action.checkFiltered(trigger);
} }
public function getKeysForAction(name:Action):Array<FlxKey> { public function getKeysForAction(name:Action):Array<FlxKey>
#if debug {
if (!byName.exists(name)) #if FEATURE_DEBUG_FUNCTIONS
throw 'Invalid name: $name'; if (!byName.exists(name)) throw 'Invalid name: $name';
#end #end
// TODO: Revert to `.map().filter()` once HashLink doesn't complain anymore. // TODO: Revert to `.map().filter()` once HashLink doesn't complain anymore.
var result:Array<FlxKey> = []; var result:Array<FlxKey> = [];
for (input in byName[name].inputs) { for (input in byName[name].inputs)
{
if (input.device == KEYBOARD) result.push(input.inputID); if (input.device == KEYBOARD) result.push(input.inputID);
} }
return result; return result;
} }
public function getButtonsForAction(name:Action):Array<FlxGamepadInputID> { public function getButtonsForAction(name:Action):Array<FlxGamepadInputID>
#if debug {
if (!byName.exists(name)) #if FEATURE_DEBUG_FUNCTIONS
throw 'Invalid name: $name'; if (!byName.exists(name)) throw 'Invalid name: $name';
#end #end
var result:Array<FlxGamepadInputID> = []; var result:Array<FlxGamepadInputID> = [];
for (input in byName[name].inputs) { for (input in byName[name].inputs)
{
if (input.device == GAMEPAD) result.push(input.inputID); if (input.device == GAMEPAD) result.push(input.inputID);
} }
return result; return result;
} }
public function getDialogueName(action:FlxActionDigital):String public function getDialogueName(action:FlxActionDigital, ?ignoreSurrounding:Bool = false):String
{ {
var input = action.inputs[0]; var input = action.inputs[0];
if (ignoreSurrounding == false)
{
return switch (input.device) return switch (input.device)
{ {
case KEYBOARD: return '[${(input.inputID : FlxKey)}]'; case KEYBOARD: return '[${(input.inputID : FlxKey)}]';
@ -376,10 +407,25 @@ class Controls extends FlxActionSet
case device: throw 'unhandled device: $device'; case device: throw 'unhandled device: $device';
} }
} }
else
public function getDialogueNameFromToken(token:String):String
{ {
return getDialogueName(getActionFromControl(Control.createByName(token.toUpperCase()))); 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 getDialogueNameFromControl(control:Control, ?ignoreSurrounding:Bool = false):String
{
return getDialogueName(getActionFromControl(control), ignoreSurrounding);
} }
function getActionFromControl(control:Control):FlxActionDigital function getActionFromControl(control:Control):FlxActionDigital
@ -398,7 +444,12 @@ class Controls extends FlxActionSet
case BACK: _back; case BACK: _back;
case PAUSE: _pause; case PAUSE: _pause;
case RESET: _reset; 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 CUTSCENE_ADVANCE: _cutscene_advance;
case DEBUG_MENU: _debug_menu; case DEBUG_MENU: _debug_menu;
case DEBUG_CHART: _debug_chart; case DEBUG_CHART: _debug_chart;
@ -406,7 +457,6 @@ class Controls extends FlxActionSet
case VOLUME_UP: _volume_up; case VOLUME_UP: _volume_up;
case VOLUME_DOWN: _volume_down; case VOLUME_DOWN: _volume_down;
case VOLUME_MUTE: _volume_mute; case VOLUME_MUTE: _volume_mute;
case FULLSCREEN: _fullscreen;
} }
} }
@ -466,8 +516,18 @@ class Controls extends FlxActionSet
func(_pause, JUST_PRESSED); func(_pause, JUST_PRESSED);
case RESET: case RESET:
func(_reset, JUST_PRESSED); func(_reset, JUST_PRESSED);
case SCREENSHOT: case WINDOW_SCREENSHOT:
func(_screenshot, JUST_PRESSED); 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: case CUTSCENE_ADVANCE:
func(_cutscene_advance, JUST_PRESSED); func(_cutscene_advance, JUST_PRESSED);
case DEBUG_MENU: case DEBUG_MENU:
@ -482,15 +542,12 @@ class Controls extends FlxActionSet
func(_volume_down, JUST_PRESSED); func(_volume_down, JUST_PRESSED);
case VOLUME_MUTE: case VOLUME_MUTE:
func(_volume_mute, JUST_PRESSED); func(_volume_mute, JUST_PRESSED);
case FULLSCREEN:
func(_fullscreen, JUST_PRESSED);
} }
} }
public function replaceBinding(control:Control, device:Device, toAdd:Int, toRemove:Int) public function replaceBinding(control:Control, device:Device, toAdd:Int, toRemove:Int)
{ {
if (toAdd == toRemove) if (toAdd == toRemove) return;
return;
switch (device) switch (device)
{ {
@ -504,7 +561,8 @@ class Controls extends FlxActionSet
function replaceKey(action:FlxActionDigital, toAdd:FlxKey, toRemove:FlxKey, state:FlxInputState) 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. // Add the keybind, don't replace.
addKeys(action, [toAdd], state); addKeys(action, [toAdd], state);
return; return;
@ -518,34 +576,44 @@ class Controls extends FlxActionSet
if (input.device == KEYBOARD && input.inputID == toRemove) if (input.device == KEYBOARD && input.inputID == toRemove)
{ {
if (toAdd == FlxKey.NONE) { if (toAdd == FlxKey.NONE)
{
// Remove the keybind, don't replace. // Remove the keybind, don't replace.
action.inputs.remove(input); action.inputs.remove(input);
} else { }
else
{
// Replace the keybind. // Replace the keybind.
@:privateAccess @:privateAccess
action.inputs[i].inputID = toAdd; action.inputs[i].inputID = toAdd;
} }
hasReplaced = true; hasReplaced = true;
} else if (input.device == KEYBOARD && input.inputID == toAdd) { }
else if (input.device == KEYBOARD && input.inputID == toAdd)
{
// This key is already bound! // This key is already bound!
if (hasReplaced) { if (hasReplaced)
{
// Remove the duplicate keybind, don't replace. // Remove the duplicate keybind, don't replace.
action.inputs.remove(input); action.inputs.remove(input);
} else { }
else
{
hasReplaced = true; hasReplaced = true;
} }
} }
} }
if (!hasReplaced) { if (!hasReplaced)
{
addKeys(action, [toAdd], state); addKeys(action, [toAdd], state);
} }
} }
function replaceButton(action:FlxActionDigital, deviceID:Int, toAdd:FlxGamepadInputID, toRemove:FlxGamepadInputID, state:FlxInputState) 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); addButtons(action, [toAdd], state, deviceID);
return; return;
} }
@ -564,7 +632,8 @@ class Controls extends FlxActionSet
} }
} }
if (!hasReplaced) { if (!hasReplaced)
{
addButtons(action, [toAdd], state, deviceID); addButtons(action, [toAdd], state, deviceID);
} }
} }
@ -576,8 +645,7 @@ class Controls extends FlxActionSet
var action = controls.byName[name]; var action = controls.byName[name];
for (input in action.inputs) for (input in action.inputs)
{ {
if (device == null || isDevice(input, device)) if (device == null || isDevice(input, device)) byName[name].add(cast input);
byName[name].add(cast input);
} }
} }
@ -586,8 +654,7 @@ class Controls extends FlxActionSet
case null: case null:
// add all // add all
for (gamepad in controls.gamepadsAdded) for (gamepad in controls.gamepadsAdded)
if (gamepadsAdded.indexOf(gamepad) == -1) if (gamepadsAdded.indexOf(gamepad) == -1) gamepadsAdded.push(gamepad);
gamepadsAdded.push(gamepad);
mergeKeyboardScheme(controls.keyboardScheme); mergeKeyboardScheme(controls.keyboardScheme);
@ -642,7 +709,8 @@ class Controls extends FlxActionSet
static function addKeys(action:FlxActionDigital, keys:Array<FlxKey>, state:FlxInputState) static function addKeys(action:FlxActionDigital, keys:Array<FlxKey>, state:FlxInputState)
{ {
for (key in keys) { for (key in keys)
{
if (key == FlxKey.NONE) continue; // Ignore unbound keys. if (key == FlxKey.NONE) continue; // Ignore unbound keys.
action.addKey(key, state); action.addKey(key, state);
} }
@ -654,15 +722,13 @@ class Controls extends FlxActionSet
while (i-- > 0) while (i-- > 0)
{ {
var input = action.inputs[i]; var input = action.inputs[i];
if (input.device == KEYBOARD && keys.indexOf(cast input.inputID) != -1) if (input.device == KEYBOARD && keys.indexOf(cast input.inputID) != -1) action.remove(input);
action.remove(input);
} }
} }
public function setKeyboardScheme(scheme:KeyboardScheme, reset = true) public function setKeyboardScheme(scheme:KeyboardScheme, reset = true)
{ {
if (reset) if (reset) removeKeyboard();
removeKeyboard();
keyboardScheme = scheme; keyboardScheme = scheme;
@ -678,7 +744,12 @@ class Controls extends FlxActionSet
bindKeys(Control.BACK, getDefaultKeybinds(scheme, Control.BACK)); bindKeys(Control.BACK, getDefaultKeybinds(scheme, Control.BACK));
bindKeys(Control.PAUSE, getDefaultKeybinds(scheme, Control.PAUSE)); bindKeys(Control.PAUSE, getDefaultKeybinds(scheme, Control.PAUSE));
bindKeys(Control.RESET, getDefaultKeybinds(scheme, Control.RESET)); 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.CUTSCENE_ADVANCE, getDefaultKeybinds(scheme, Control.CUTSCENE_ADVANCE));
bindKeys(Control.DEBUG_MENU, getDefaultKeybinds(scheme, Control.DEBUG_MENU)); bindKeys(Control.DEBUG_MENU, getDefaultKeybinds(scheme, Control.DEBUG_MENU));
bindKeys(Control.DEBUG_CHART, getDefaultKeybinds(scheme, Control.DEBUG_CHART)); 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_UP, getDefaultKeybinds(scheme, Control.VOLUME_UP));
bindKeys(Control.VOLUME_DOWN, getDefaultKeybinds(scheme, Control.VOLUME_DOWN)); bindKeys(Control.VOLUME_DOWN, getDefaultKeybinds(scheme, Control.VOLUME_DOWN));
bindKeys(Control.VOLUME_MUTE, getDefaultKeybinds(scheme, Control.VOLUME_MUTE)); bindKeys(Control.VOLUME_MUTE, getDefaultKeybinds(scheme, Control.VOLUME_MUTE));
bindKeys(Control.FULLSCREEN, getDefaultKeybinds(scheme, Control.FULLSCREEN));
bindMobileLol(); bindMobileLol();
} }
function getDefaultKeybinds(scheme:KeyboardScheme, control:Control):Array<FlxKey> { function getDefaultKeybinds(scheme:KeyboardScheme, control:Control):Array<FlxKey>
switch (scheme) { {
switch (scheme)
{
case Solo: case Solo:
switch (control) { switch (control)
{
case Control.UI_UP: return [W, FlxKey.UP]; case Control.UI_UP: return [W, FlxKey.UP];
case Control.UI_DOWN: return [S, FlxKey.DOWN]; case Control.UI_DOWN: return [S, FlxKey.DOWN];
case Control.UI_LEFT: return [A, FlxKey.LEFT]; 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.BACK: return [X, BACKSPACE, ESCAPE];
case Control.PAUSE: return [P, ENTER, ESCAPE]; case Control.PAUSE: return [P, ENTER, ESCAPE];
case Control.RESET: return [R]; 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.CUTSCENE_ADVANCE: return [Z, ENTER];
case Control.DEBUG_MENU: return [GRAVEACCENT]; case Control.DEBUG_MENU: return [GRAVEACCENT];
case Control.DEBUG_CHART: return []; case Control.DEBUG_CHART: return [];
@ -715,11 +793,10 @@ class Controls extends FlxActionSet
case Control.VOLUME_UP: return [PLUS, NUMPADPLUS]; case Control.VOLUME_UP: return [PLUS, NUMPADPLUS];
case Control.VOLUME_DOWN: return [MINUS, NUMPADMINUS]; case Control.VOLUME_DOWN: return [MINUS, NUMPADMINUS];
case Control.VOLUME_MUTE: return [ZERO, NUMPADZERO]; case Control.VOLUME_MUTE: return [ZERO, NUMPADZERO];
case Control.FULLSCREEN: return [FlxKey.F];
} }
case Duo(true): case Duo(true):
switch (control) { switch (control)
{
case Control.UI_UP: return [W]; case Control.UI_UP: return [W];
case Control.UI_DOWN: return [S]; case Control.UI_DOWN: return [S];
case Control.UI_LEFT: return [A]; case Control.UI_LEFT: return [A];
@ -732,7 +809,12 @@ class Controls extends FlxActionSet
case Control.BACK: return [H, X]; case Control.BACK: return [H, X];
case Control.PAUSE: return [ONE]; case Control.PAUSE: return [ONE];
case Control.RESET: return [R]; 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.CUTSCENE_ADVANCE: return [G, Z];
case Control.DEBUG_MENU: return [GRAVEACCENT]; case Control.DEBUG_MENU: return [GRAVEACCENT];
case Control.DEBUG_CHART: return []; case Control.DEBUG_CHART: return [];
@ -740,11 +822,10 @@ class Controls extends FlxActionSet
case Control.VOLUME_UP: return [PLUS]; case Control.VOLUME_UP: return [PLUS];
case Control.VOLUME_DOWN: return [MINUS]; case Control.VOLUME_DOWN: return [MINUS];
case Control.VOLUME_MUTE: return [ZERO]; case Control.VOLUME_MUTE: return [ZERO];
case Control.FULLSCREEN: return [FlxKey.F];
} }
case Duo(false): case Duo(false):
switch (control) { switch (control)
{
case Control.UI_UP: return [FlxKey.UP]; case Control.UI_UP: return [FlxKey.UP];
case Control.UI_DOWN: return [FlxKey.DOWN]; case Control.UI_DOWN: return [FlxKey.DOWN];
case Control.UI_LEFT: return [FlxKey.LEFT]; case Control.UI_LEFT: return [FlxKey.LEFT];
@ -757,16 +838,19 @@ class Controls extends FlxActionSet
case Control.BACK: return [ESCAPE]; case Control.BACK: return [ESCAPE];
case Control.PAUSE: return [ONE]; case Control.PAUSE: return [ONE];
case Control.RESET: return [R]; 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.CUTSCENE_ADVANCE: return [ENTER];
case Control.DEBUG_MENU: return [GRAVEACCENT]; case Control.DEBUG_MENU: return [];
case Control.DEBUG_CHART: return []; case Control.DEBUG_CHART: return [];
case Control.DEBUG_STAGE: return []; case Control.DEBUG_STAGE: return [];
case Control.VOLUME_UP: return [NUMPADPLUS]; case Control.VOLUME_UP: return [NUMPADPLUS];
case Control.VOLUME_DOWN: return [NUMPADMINUS]; case Control.VOLUME_DOWN: return [NUMPADMINUS];
case Control.VOLUME_MUTE: return [NUMPADZERO]; case Control.VOLUME_MUTE: return [NUMPADZERO];
case Control.FULLSCREEN: return [];
} }
default: default:
// Fallthrough. // Fallthrough.
@ -793,8 +877,7 @@ class Controls extends FlxActionSet
#end #end
#if android #if android
forEachBound(Control.BACK, function(action, pres) forEachBound(Control.BACK, function(action, pres) {
{
action.add(new FlxActionInputDigitalAndroid(FlxAndroidKey.BACK, JUST_PRESSED)); action.add(new FlxActionInputDigitalAndroid(FlxAndroidKey.BACK, JUST_PRESSED));
}); });
#end #end
@ -808,8 +891,7 @@ class Controls extends FlxActionSet
while (i-- > 0) while (i-- > 0)
{ {
var input = action.inputs[i]; var input = action.inputs[i];
if (input.device == KEYBOARD) if (input.device == KEYBOARD) action.remove(input);
action.remove(input);
} }
} }
} }
@ -821,11 +903,13 @@ class Controls extends FlxActionSet
fromSaveData(padData, Gamepad(id)); fromSaveData(padData, Gamepad(id));
} }
public function getGamepadIds():Array<Int> { public function getGamepadIds():Array<Int>
{
return gamepadsAdded; return gamepadsAdded;
} }
public function getGamepads():Array<FlxGamepad> { public function getGamepads():Array<FlxGamepad>
{
return [for (id in gamepadsAdded) FlxG.gamepads.getByID(id)]; return [for (id in gamepadsAdded) FlxG.gamepads.getByID(id)];
} }
@ -845,8 +929,7 @@ class Controls extends FlxActionSet
while (i-- > 0) while (i-- > 0)
{ {
var input = action.inputs[i]; var input = action.inputs[i];
if (isGamepad(input, deviceID)) if (isGamepad(input, deviceID)) action.remove(input);
action.remove(input);
} }
} }
@ -856,52 +939,85 @@ class Controls extends FlxActionSet
public function addDefaultGamepad(id):Void public function addDefaultGamepad(id):Void
{ {
addGamepadLiteral(id, [ addGamepadLiteral(id, [
Control.ACCEPT => getDefaultGamepadBinds(Control.ACCEPT), Control.ACCEPT => getDefaultGamepadBinds(Control.ACCEPT),
Control.BACK => getDefaultGamepadBinds(Control.BACK), Control.BACK => getDefaultGamepadBinds(Control.BACK),
Control.UI_UP => getDefaultGamepadBinds(Control.UI_UP), Control.UI_UP => getDefaultGamepadBinds(Control.UI_UP),
Control.UI_DOWN => getDefaultGamepadBinds(Control.UI_DOWN), Control.UI_DOWN => getDefaultGamepadBinds(Control.UI_DOWN),
Control.UI_LEFT => getDefaultGamepadBinds(Control.UI_LEFT), Control.UI_LEFT => getDefaultGamepadBinds(Control.UI_LEFT),
Control.UI_RIGHT => getDefaultGamepadBinds(Control.UI_RIGHT), 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_UP => getDefaultGamepadBinds(Control.NOTE_UP),
Control.NOTE_DOWN => getDefaultGamepadBinds(Control.NOTE_DOWN), Control.NOTE_DOWN => getDefaultGamepadBinds(Control.NOTE_DOWN),
Control.NOTE_LEFT => getDefaultGamepadBinds(Control.NOTE_LEFT), Control.NOTE_LEFT => getDefaultGamepadBinds(Control.NOTE_LEFT),
Control.NOTE_RIGHT => getDefaultGamepadBinds(Control.NOTE_RIGHT), Control.NOTE_RIGHT => getDefaultGamepadBinds(Control.NOTE_RIGHT),
Control.PAUSE => getDefaultGamepadBinds(Control.PAUSE), Control.PAUSE => getDefaultGamepadBinds(Control.PAUSE),
Control.RESET => getDefaultGamepadBinds(Control.RESET), Control.RESET => getDefaultGamepadBinds(Control.RESET),
// Control.SCREENSHOT => [], Control.WINDOW_FULLSCREEN => getDefaultGamepadBinds(Control.WINDOW_FULLSCREEN),
// Control.VOLUME_UP => [RIGHT_SHOULDER], Control.WINDOW_SCREENSHOT => getDefaultGamepadBinds(Control.WINDOW_SCREENSHOT),
// Control.VOLUME_DOWN => [LEFT_SHOULDER],
// Control.VOLUME_MUTE => [RIGHT_TRIGGER],
Control.CUTSCENE_ADVANCE => getDefaultGamepadBinds(Control.CUTSCENE_ADVANCE), Control.CUTSCENE_ADVANCE => getDefaultGamepadBinds(Control.CUTSCENE_ADVANCE),
// Control.DEBUG_MENU Control.FREEPLAY_FAVORITE => getDefaultGamepadBinds(Control.FREEPLAY_FAVORITE),
// Control.DEBUG_CHART 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<FlxGamepadInputID> { function getDefaultGamepadBinds(control:Control):Array<FlxGamepadInputID>
switch(control) { {
case Control.ACCEPT: return [#if switch B #else A #end]; switch (control)
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.ACCEPT:
case Control.UI_DOWN: return [DPAD_DOWN, LEFT_STICK_DIGITAL_DOWN]; return [#if switch B #else A #end];
case Control.UI_LEFT: return [DPAD_LEFT, LEFT_STICK_DIGITAL_LEFT]; case Control.BACK:
case Control.UI_RIGHT: return [DPAD_RIGHT, LEFT_STICK_DIGITAL_RIGHT]; return [#if switch A #else B #end];
case Control.NOTE_UP: return [DPAD_UP, Y, LEFT_STICK_DIGITAL_UP, RIGHT_STICK_DIGITAL_UP]; case Control.UI_UP:
case Control.NOTE_DOWN: return [DPAD_DOWN, A, LEFT_STICK_DIGITAL_DOWN, RIGHT_STICK_DIGITAL_DOWN]; return [DPAD_UP, LEFT_STICK_DIGITAL_UP];
case Control.NOTE_LEFT: return [DPAD_LEFT, X, LEFT_STICK_DIGITAL_LEFT, RIGHT_STICK_DIGITAL_LEFT]; case Control.UI_DOWN:
case Control.NOTE_RIGHT: return [DPAD_RIGHT, B, LEFT_STICK_DIGITAL_RIGHT, RIGHT_STICK_DIGITAL_RIGHT]; return [DPAD_DOWN, LEFT_STICK_DIGITAL_DOWN];
case Control.PAUSE: return [START]; case Control.UI_LEFT:
case Control.RESET: return [RIGHT_SHOULDER]; return [DPAD_LEFT, LEFT_STICK_DIGITAL_LEFT];
case Control.SCREENSHOT: return []; case Control.UI_RIGHT:
case Control.VOLUME_UP: return []; return [DPAD_RIGHT, LEFT_STICK_DIGITAL_RIGHT];
case Control.VOLUME_DOWN: return []; case Control.NOTE_UP:
case Control.VOLUME_MUTE: return []; return [DPAD_UP, Y, LEFT_STICK_DIGITAL_UP, RIGHT_STICK_DIGITAL_UP];
case Control.CUTSCENE_ADVANCE: return [A]; case Control.NOTE_DOWN:
case Control.DEBUG_MENU: return []; return [DPAD_DOWN, A, LEFT_STICK_DIGITAL_DOWN, RIGHT_STICK_DIGITAL_DOWN];
case Control.DEBUG_CHART: return []; case Control.NOTE_LEFT:
case Control.FULLSCREEN: return []; 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: default:
// Fallthrough. // Fallthrough.
} }
@ -919,8 +1035,7 @@ class Controls extends FlxActionSet
public function touchShit(control:Control, id) public function touchShit(control:Control, id)
{ {
forEachBound(control, function(action, state) forEachBound(control, function(action, state) {
{
// action // action
}); });
} }
@ -936,7 +1051,8 @@ class Controls extends FlxActionSet
inline static function addButtons(action:FlxActionDigital, buttons:Array<FlxGamepadInputID>, state, id) inline static function addButtons(action:FlxActionDigital, buttons:Array<FlxGamepadInputID>, state, id)
{ {
for (button in buttons) { for (button in buttons)
{
if (button == FlxGamepadInputID.NONE) continue; // Ignore unbound keys. if (button == FlxGamepadInputID.NONE) continue; // Ignore unbound keys.
action.addGamepad(button, state, id); action.addGamepad(button, state, id);
} }
@ -948,29 +1064,25 @@ class Controls extends FlxActionSet
while (i-- > 0) while (i-- > 0)
{ {
var input = action.inputs[i]; var input = action.inputs[i];
if (isGamepad(input, gamepadID) && buttons.indexOf(cast input.inputID) != -1) if (isGamepad(input, gamepadID) && buttons.indexOf(cast input.inputID) != -1) action.remove(input);
action.remove(input);
} }
} }
public function getInputsFor(control:Control, device:Device, ?list:Array<Int>):Array<Int> public function getInputsFor(control:Control, device:Device, ?list:Array<Int>):Array<Int>
{ {
if (list == null) if (list == null) list = [];
list = [];
switch (device) switch (device)
{ {
case Keys: case Keys:
for (input in getActionFromControl(control).inputs) for (input in getActionFromControl(control).inputs)
{ {
if (input.device == KEYBOARD) if (input.device == KEYBOARD) list.push(input.inputID);
list.push(input.inputID);
} }
case Gamepad(id): case Gamepad(id):
for (input in getActionFromControl(control).inputs) for (input in getActionFromControl(control).inputs)
{ {
if (isGamepad(input, id)) if (isGamepad(input, id)) list.push(input.inputID);
list.push(input.inputID);
} }
} }
return list; return list;
@ -992,15 +1104,16 @@ class Controls extends FlxActionSet
* An EMPTY array means the control is uninitialized and needs to be reset to default. * 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. * 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()) for (control in Control.createAll())
{ {
var inputs:Array<Int> = Reflect.field(data, control.getName()); var inputs:Array<Int> = Reflect.field(data, control.getName());
inputs = inputs.unique(); inputs = inputs?.distinct();
if (inputs != null) if (inputs != null)
{ {
if (inputs.length == 0) { if (inputs.length == 0)
{
trace('Control ${control} is missing bindings, resetting to default.'); trace('Control ${control} is missing bindings, resetting to default.');
switch (device) switch (device)
{ {
@ -1009,9 +1122,13 @@ class Controls extends FlxActionSet
case Gamepad(id): case Gamepad(id):
bindButtons(control, id, getDefaultGamepadBinds(control)); bindButtons(control, id, getDefaultGamepadBinds(control));
} }
} else if (inputs == [FlxKey.NONE]) { }
else if (inputs == [FlxKey.NONE])
{
trace('Control ${control} is unbound, leaving it be.'); trace('Control ${control} is unbound, leaving it be.');
} else { }
else
{
switch (device) switch (device)
{ {
case Keys: case Keys:
@ -1020,7 +1137,9 @@ class Controls extends FlxActionSet
bindButtons(control, id, inputs.copy()); bindButtons(control, id, inputs.copy());
} }
} }
} else { }
else
{
trace('Control ${control} is missing bindings, resetting to default.'); trace('Control ${control} is missing bindings, resetting to default.');
switch (device) switch (device)
{ {
@ -1047,10 +1166,13 @@ class Controls extends FlxActionSet
var inputs = getInputsFor(control, device); var inputs = getInputsFor(control, device);
isEmpty = isEmpty && inputs.length == 0; isEmpty = isEmpty && inputs.length == 0;
if (inputs.length == 0) { if (inputs.length == 0)
{
inputs = [FlxKey.NONE]; inputs = [FlxKey.NONE];
} else { }
inputs = inputs.unique(); else
{
inputs = inputs.distinct();
} }
Reflect.setField(data, control.getName(), inputs); Reflect.setField(data, control.getName(), inputs);
@ -1093,7 +1215,8 @@ typedef Swipes =
* - Combining `pressed` and `released` inputs into one action. * - Combining `pressed` and `released` inputs into one action.
* - Filtering by input method (`KEYBOARD`, `MOUSE`, `GAMEPAD`, etc). * - Filtering by input method (`KEYBOARD`, `MOUSE`, `GAMEPAD`, etc).
*/ */
class FunkinAction extends FlxActionDigital { class FunkinAction extends FlxActionDigital
{
public var namePressed(default, null):Null<String>; public var namePressed(default, null):Null<String>;
public var nameReleased(default, null):Null<String>; public var nameReleased(default, null):Null<String>;
@ -1110,83 +1233,102 @@ class FunkinAction extends FlxActionDigital {
/** /**
* Input checks default to whether the input was just pressed, on any input device. * 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); return checkFiltered(JUST_PRESSED);
} }
/** /**
* Check whether the input is currently being held. * Check whether the input is currently being held.
*/ */
public function checkPressed():Bool { public function checkPressed():Bool
{
return checkFiltered(PRESSED); return checkFiltered(PRESSED);
} }
/** /**
* Check whether the input is currently being held, and was not held last frame. * 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); return checkFiltered(JUST_PRESSED);
} }
/** /**
* Check whether the input is not currently being held. * Check whether the input is not currently being held.
*/ */
public function checkReleased():Bool { public function checkReleased():Bool
{
return checkFiltered(RELEASED); return checkFiltered(RELEASED);
} }
/** /**
* Check whether the input is not currently being held, and was held last frame. * 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); return checkFiltered(JUST_RELEASED);
} }
/** /**
* Check whether the input is currently being held by a gamepad device. * Check whether the input is currently being held by a gamepad device.
*/ */
public function checkPressedGamepad():Bool { public function checkPressedGamepad():Bool
{
return checkFiltered(PRESSED, GAMEPAD); return checkFiltered(PRESSED, GAMEPAD);
} }
/** /**
* Check whether the input is currently being held by a gamepad device, and was not held last frame. * 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); return checkFiltered(JUST_PRESSED, GAMEPAD);
} }
/** /**
* Check whether the input is not currently being held by a gamepad device. * 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); return checkFiltered(RELEASED, GAMEPAD);
} }
/** /**
* Check whether the input is not currently being held by a gamepad device, and was held last frame. * 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); return checkFiltered(JUST_RELEASED, GAMEPAD);
} }
public function checkMultiFiltered(?filterTriggers:Array<FlxInputState>, ?filterDevices:Array<FlxInputDevice>):Bool { public function checkMultiFiltered(?filterTriggers:Array<FlxInputState>, ?filterDevices:Array<FlxInputDevice>):Bool
if (filterTriggers == null) { {
if (filterTriggers == null)
{
filterTriggers = [PRESSED, JUST_PRESSED]; filterTriggers = [PRESSED, JUST_PRESSED];
} }
if (filterDevices == null) { if (filterDevices == null)
{
filterDevices = []; filterDevices = [];
} }
// Perform checkFiltered for each combination. // Perform checkFiltered for each combination.
for (i in filterTriggers) { for (i in filterTriggers)
if (filterDevices.length == 0) { {
if (checkFiltered(i)) { if (filterDevices.length == 0)
{
if (checkFiltered(i))
{
return true; return true;
} }
} else { }
for (j in filterDevices) { else
if (checkFiltered(i, j)) { {
for (j in filterDevices)
{
if (checkFiltered(i, j))
{
return true; return true;
} }
} }
@ -1201,14 +1343,16 @@ class FunkinAction extends FlxActionDigital {
* @param filterTrigger Optionally filter by trigger condition (`JUST_PRESSED`, `PRESSED`, `JUST_RELEASED`, `RELEASED`). * @param filterTrigger Optionally filter by trigger condition (`JUST_PRESSED`, `PRESSED`, `JUST_RELEASED`, `RELEASED`).
* @param filterDevice Optionally filter by device (`KEYBOARD`, `MOUSE`, `GAMEPAD`, `OTHER`). * @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 // The normal
// Make sure we only update the inputs once per frame. // Make sure we only update the inputs once per frame.
var key = '${filterTrigger}:${filterDevice}'; var key = '${filterTrigger}:${filterDevice}';
var cacheEntry = cache.get(key); var cacheEntry = cache.get(key);
if (cacheEntry != null && cacheEntry.timestamp == FlxG.game.ticks) { if (cacheEntry != null && cacheEntry.timestamp == FlxG.game.ticks)
{
return cacheEntry.value; return cacheEntry.value;
} }
// Use a for loop instead so we can remove inputs while iterating. // Use a for loop instead so we can remove inputs while iterating.
@ -1232,12 +1376,14 @@ class FunkinAction extends FlxActionDigital {
input.update(); input.update();
// Check whether the input is the right trigger. // Check whether the input is the right trigger.
if (filterTrigger != null && input.trigger != filterTrigger) { if (filterTrigger != null && input.trigger != filterTrigger)
{
continue; continue;
} }
// Check whether the input is the right device. // Check whether the input is the right device.
if (filterDevice != null && input.device != filterDevice) { if (filterDevice != null && input.device != filterDevice)
{
continue; continue;
} }
@ -1425,14 +1571,19 @@ enum Control
UI_RIGHT; UI_RIGHT;
UI_DOWN; UI_DOWN;
RESET; RESET;
SCREENSHOT;
ACCEPT; ACCEPT;
BACK; BACK;
PAUSE; PAUSE;
FULLSCREEN;
// CUTSCENE // CUTSCENE
CUTSCENE_ADVANCE; CUTSCENE_ADVANCE;
// SCREENSHOT // FREEPLAY
FREEPLAY_FAVORITE;
FREEPLAY_LEFT;
FREEPLAY_RIGHT;
FREEPLAY_CHAR_SELECT;
// WINDOW
WINDOW_SCREENSHOT;
WINDOW_FULLSCREEN;
// VOLUME // VOLUME
VOLUME_UP; VOLUME_UP;
VOLUME_DOWN; VOLUME_DOWN;
@ -1475,11 +1626,16 @@ enum abstract Action(String) to String from String
var BACK = "back"; var BACK = "back";
var PAUSE = "pause"; var PAUSE = "pause";
var RESET = "reset"; var RESET = "reset";
var FULLSCREEN = "fullscreen"; // WINDOW
// SCREENSHOT var WINDOW_FULLSCREEN = "window_fullscreen";
var SCREENSHOT = "screenshot"; var WINDOW_SCREENSHOT = "window_screenshot";
// CUTSCENE // CUTSCENE
var CUTSCENE_ADVANCE = "cutscene_advance"; 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 // VOLUME
var VOLUME_UP = "volume_up"; var VOLUME_UP = "volume_up";
var VOLUME_DOWN = "volume_down"; var VOLUME_DOWN = "volume_down";

View file

@ -73,6 +73,22 @@ interface INoteScriptedClass extends IScriptedClass
public function onNoteMiss(event:NoteScriptEvent):Void; 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: * 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. * 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. * Called when the game is paused.
@ -136,16 +152,6 @@ interface IPlayStateScriptedClass extends INoteScriptedClass
*/ */
public function onSongEvent(event:SongEventScriptEvent):Void; 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. * Called when the countdown of the song starts.
*/ */

View file

@ -7,7 +7,9 @@ import funkin.data.dialogue.speaker.SpeakerRegistry;
import funkin.data.event.SongEventRegistry; import funkin.data.event.SongEventRegistry;
import funkin.data.story.level.LevelRegistry; import funkin.data.story.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notekind.NoteKindManager;
import funkin.data.song.SongRegistry; import funkin.data.song.SongRegistry;
import funkin.data.freeplay.player.PlayerRegistry;
import funkin.data.stage.StageRegistry; import funkin.data.stage.StageRegistry;
import funkin.data.freeplay.album.AlbumRegistry; import funkin.data.freeplay.album.AlbumRegistry;
import funkin.modding.module.ModuleHandler; import funkin.modding.module.ModuleHandler;
@ -26,11 +28,10 @@ class PolymodHandler
{ {
/** /**
* The API version that mods should comply with. * The API version that mods should comply with.
* Format this with Semantic Versioning; <MAJOR>.<MINOR>.<PATCH>. * Indicates which mods are compatible with this version of the game.
* Bug fixes increment the patch version, new features increment the minor version. * Minor updates rarely impact mods but major versions often do.
* Changes that break old mods increment the major version.
*/ */
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. * Where relative to the executable that mods are located.
@ -176,7 +177,7 @@ class PolymodHandler
loadedModIds.push(mod.id); loadedModIds.push(mod.id);
} }
#if debug #if FEATURE_DEBUG_FUNCTIONS
var fileList:Array<String> = Polymod.listModFiles(PolymodAssetType.IMAGE); var fileList:Array<String> = Polymod.listModFiles(PolymodAssetType.IMAGE);
trace('Installed mods have replaced ${fileList.length} images.'); trace('Installed mods have replaced ${fileList.length} images.');
for (item in fileList) for (item in fileList)
@ -232,6 +233,12 @@ class PolymodHandler
// NOTE: Scripted classes are automatically aliased to their parent class. // NOTE: Scripted classes are automatically aliased to their parent class.
Polymod.addImportAlias('flixel.math.FlxPoint', flixel.math.FlxPoint.FlxBasePoint); 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. // Add blacklisting for prohibited classes and packages.
// `Sys` // `Sys`
@ -250,8 +257,28 @@ class PolymodHandler
// Lib.load() can load malicious DLLs // Lib.load() can load malicious DLLs
Polymod.blacklistImport('cpp.Lib'); 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.*` // `polymod.*`
// You can probably unblacklist a module // Contains functions which may allow for un-blacklisting other modules.
for (cls in ClassMacro.listClassesInPackage('polymod')) for (cls in ClassMacro.listClassesInPackage('polymod'))
{ {
if (cls == null) continue; if (cls == null) continue;
@ -260,6 +287,7 @@ class PolymodHandler
} }
// `sys.*` // `sys.*`
// Access to system utilities such as the file system.
for (cls in ClassMacro.listClassesInPackage('sys')) for (cls in ClassMacro.listClassesInPackage('sys'))
{ {
if (cls == null) continue; 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, // These MUST be imported at the top of the file and not referred to by fully qualified name,
// to ensure build macros work properly. // to ensure build macros work properly.
SongEventRegistry.loadEventCache();
SongRegistry.instance.loadEntries(); SongRegistry.instance.loadEntries();
LevelRegistry.instance.loadEntries(); LevelRegistry.instance.loadEntries();
NoteStyleRegistry.instance.loadEntries(); NoteStyleRegistry.instance.loadEntries();
SongEventRegistry.loadEventCache(); PlayerRegistry.instance.loadEntries();
ConversationRegistry.instance.loadEntries(); ConversationRegistry.instance.loadEntries();
DialogueBoxRegistry.instance.loadEntries(); DialogueBoxRegistry.instance.loadEntries();
SpeakerRegistry.instance.loadEntries(); SpeakerRegistry.instance.loadEntries();
AlbumRegistry.instance.loadEntries(); AlbumRegistry.instance.loadEntries();
StageRegistry.instance.loadEntries(); StageRegistry.instance.loadEntries();
CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry. CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
NoteKindManager.loadScripts();
ModuleHandler.loadModuleCache(); ModuleHandler.loadModuleCache();
} }
} }

View file

@ -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 {}

View file

@ -140,16 +140,37 @@ class HitNoteScriptEvent extends NoteScriptEvent
*/ */
public var score:Int; 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); super(NOTE_HIT, note, healthChange, comboCount, true);
this.score = score; this.score = score;
this.judgement = judgement; this.judgement = judgement;
this.isComboBreak = isComboBreak;
this.doesNotesplash = doesNotesplash;
this.hitDiff = hitDiff;
} }
public override function toString():String 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 + ')';
} }
} }

View file

@ -94,6 +94,21 @@ class ScriptEventDispatcher
} }
} }
if (Std.isOfType(target, IBPMSyncedScriptedClass))
{
var t:IBPMSyncedScriptedClass = cast(target, IBPMSyncedScriptedClass);
switch (event.type)
{
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)) if (Std.isOfType(target, IPlayStateScriptedClass))
{ {
var t:IPlayStateScriptedClass = cast(target, IPlayStateScriptedClass); var t:IPlayStateScriptedClass = cast(target, IPlayStateScriptedClass);
@ -102,12 +117,6 @@ class ScriptEventDispatcher
case NOTE_GHOST_MISS: case NOTE_GHOST_MISS:
t.onNoteGhostMiss(cast event); t.onNoteGhostMiss(cast event);
return; return;
case SONG_BEAT_HIT:
t.onBeatHit(cast event);
return;
case SONG_STEP_HIT:
t.onStepHit(cast event);
return;
case SONG_START: case SONG_START:
t.onSongStart(event); t.onSongStart(event);
return; return;

View file

@ -20,7 +20,7 @@ enum abstract ScriptEventType(String) from String to String
var DESTROY = 'DESTROY'; 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 assumes all data is loaded and ready to go.
* *
* This event is not cancelable. * This event is not cancelable.

View file

@ -9,7 +9,11 @@ import funkin.modding.module.ModuleHandler;
import funkin.modding.events.ScriptEvent; import funkin.modding.events.ScriptEvent;
import funkin.modding.events.ScriptEvent.CountdownScriptEvent; import funkin.modding.events.ScriptEvent.CountdownScriptEvent;
import flixel.util.FlxTimer; import flixel.util.FlxTimer;
import funkin.util.EaseUtil;
import funkin.audio.FunkinSound; import funkin.audio.FunkinSound;
import openfl.utils.Assets;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.play.notes.notestyle.NoteStyle;
class Countdown class Countdown
{ {
@ -18,6 +22,24 @@ class Countdown
*/ */
public static var countdownStep(default, null):CountdownStep = BEFORE; 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<NoteStyle>;
/** /**
* The currently running countdown. This will be null if there is no countdown running. * 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. * This will automatically stop and restart the countdown if it is already running.
* @returns `false` if the countdown was cancelled by a script. * @returns `false` if the countdown was cancelled by a script.
*/ */
public static function performCountdown(isPixelStyle:Bool):Bool public static function performCountdown():Bool
{ {
countdownStep = BEFORE; countdownStep = BEFORE;
var cancelled:Bool = propagateCountdownEvent(countdownStep); var cancelled:Bool = propagateCountdownEvent(countdownStep);
@ -64,10 +86,10 @@ class Countdown
// PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0)); // PlayState.instance.dispatchEvent(new SongTimeScriptEvent(SONG_BEAT_HIT, 0, 0));
// Countdown graphic. // Countdown graphic.
showCountdownGraphic(countdownStep, isPixelStyle); showCountdownGraphic(countdownStep);
// Countdown sound. // Countdown sound.
playCountdownSound(countdownStep, isPixelStyle); playCountdownSound(countdownStep);
// Event handling bullshit. // Event handling bullshit.
var cancelled:Bool = propagateCountdownEvent(countdownStep); 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. * 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) 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. * 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) 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. * 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) if (countdownTimer != null)
{ {
@ -156,7 +178,7 @@ class Countdown
/** /**
* Stops the current countdown, then starts the song for you. * Stops the current countdown, then starts the song for you.
*/ */
public static function skipCountdown() public static function skipCountdown():Void
{ {
stopCountdown(); stopCountdown();
// This will trigger PlayState.startSong() // This will trigger PlayState.startSong()
@ -176,114 +198,69 @@ class Countdown
} }
/** /**
* Retrieves the graphic to use for this step of the countdown. * Reset the countdown configuration to the default.
* TODO: Make this less dumb. Unhardcode it? Use modules? Use notestyles?
*
* This is public so modules can do lol funny shit.
*/ */
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
}
} }
if (spritePath == null) return; /**
* 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;
var countdownSprite:FunkinSprite = FunkinSprite.create(spritePath); if (noteStyleId == null) noteStyleId = PlayState.instance?.currentChart?.noteStyle;
countdownSprite.scrollFactor.set(0, 0);
if (isPixelStyle) countdownSprite.setGraphicSize(Std.int(countdownSprite.width * Constants.PIXEL_ART_SCALE)); noteStyle = NoteStyleRegistry.instance.fetchEntry(noteStyleId);
if (noteStyle == null) noteStyle = NoteStyleRegistry.instance.fetchDefault();
}
countdownSprite.antialiasing = !isPixelStyle; /**
* Retrieves the graphic to use for this step of the countdown.
*/
public static function showCountdownGraphic(index:CountdownStep):Void
{
fetchNoteStyle();
countdownSprite.updateHitbox(); var countdownSprite = noteStyle.buildCountdownSprite(index);
countdownSprite.screenCenter(); if (countdownSprite == null) return;
var fadeEase = FlxEase.cubeInOut;
if (noteStyle.isCountdownSpritePixel(index)) fadeEase = EaseUtil.stepped(8);
// Fade sprite in, then out, then destroy it. // 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) { onComplete: function(twn:FlxTween) {
countdownSprite.destroy(); countdownSprite.destroy();
} }
}); });
countdownSprite.cameras = [PlayState.instance.camHUD];
PlayState.instance.add(countdownSprite); 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. * 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; fetchNoteStyle();
var path = noteStyle.getCountdownSoundPath(step);
if (path == null) return null;
if (isPixelStyle) return FunkinSound.playOnce(path, Constants.COUNTDOWN_VOLUME);
{
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;
FunkinSound.playOnce(Paths.sound(soundPath), Constants.COUNTDOWN_VOLUME);
} }
public static function decrement(step:CountdownStep):CountdownStep public static function decrement(step:CountdownStep):CountdownStep

View file

@ -16,6 +16,8 @@ import funkin.ui.MusicBeatSubState;
import funkin.ui.story.StoryMenuState; import funkin.ui.story.StoryMenuState;
import funkin.util.MathUtil; import funkin.util.MathUtil;
import openfl.utils.Assets; import openfl.utils.Assets;
import funkin.effects.RetroCameraFade;
import flixel.math.FlxPoint;
/** /**
* A substate which renders over the PlayState when the player dies. * A substate which renders over the PlayState when the player dies.
@ -71,7 +73,7 @@ class GameOverSubState extends MusicBeatSubState
var gameOverMusic:Null<FunkinSound> = null; var gameOverMusic:Null<FunkinSound> = 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. * This means the animation and transition have already started.
*/ */
var isEnding:Bool = false; var isEnding:Bool = false;
@ -144,6 +146,7 @@ class GameOverSubState extends MusicBeatSubState
else else
{ {
boyfriend = PlayState.instance.currentStage.getBoyfriend(true); boyfriend = PlayState.instance.currentStage.getBoyfriend(true);
boyfriend.canPlayOtherAnims = true;
boyfriend.isDead = true; boyfriend.isDead = true;
add(boyfriend); add(boyfriend);
boyfriend.resetCharacter(); boyfriend.resetCharacter();
@ -162,10 +165,12 @@ class GameOverSubState extends MusicBeatSubState
@:nullSafety(Off) @:nullSafety(Off)
function setCameraTarget():Void function setCameraTarget():Void
{ {
if (PlayState.instance.isMinimalMode || boyfriend == null) return;
// Assign a camera follow point to the boyfriend's position. // Assign a camera follow point to the boyfriend's position.
cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1); cameraFollowPoint = new FlxObject(PlayState.instance.cameraFollowPoint.x, PlayState.instance.cameraFollowPoint.y, 1, 1);
cameraFollowPoint.x = boyfriend.getGraphicMidpoint().x; cameraFollowPoint.x = getMidPointOld(boyfriend).x;
cameraFollowPoint.y = boyfriend.getGraphicMidpoint().y; cameraFollowPoint.y = getMidPointOld(boyfriend).y;
var offsets:Array<Float> = boyfriend.getDeathCameraOffsets(); var offsets:Array<Float> = boyfriend.getDeathCameraOffsets();
cameraFollowPoint.x += offsets[0]; cameraFollowPoint.x += offsets[0];
cameraFollowPoint.y += offsets[1]; cameraFollowPoint.y += offsets[1];
@ -176,6 +181,21 @@ class GameOverSubState extends MusicBeatSubState
targetCameraZoom = (PlayState?.instance?.currentStage?.camZoom ?? 1.0) * boyfriend.getDeathCameraZoom(); 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. * Forcibly reset the camera zoom level to that of the current stage.
* This prevents camera zoom events from adversely affecting the game over state. * 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. // KEYBOARD ONLY: Restart the level when pressing the assigned key.
if (controls.ACCEPT && blueballed) if (controls.ACCEPT && blueballed && !mustNotExit)
{ {
blueballed = false; blueballed = false;
confirmDeath(); confirmDeath();
} }
// KEYBOARD ONLY: Return to the menu when pressing the assigned key. // KEYBOARD ONLY: Return to the menu when pressing the assigned key.
if (controls.BACK && !mustNotExit) if (controls.BACK && !mustNotExit && !isEnding)
{ {
isEnding = true;
blueballed = false; blueballed = false;
PlayState.instance.deathCounter = 0; PlayState.instance.deathCounter = 0;
// PlayState.seenCutscene = false; // old thing... // PlayState.seenCutscene = false; // old thing...
@ -254,6 +275,7 @@ class GameOverSubState extends MusicBeatSubState
this.close(); this.close();
if (FlxG.sound.music != null) FlxG.sound.music.pause(); // Don't reset song position! 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! PlayState.instance.close(); // This only works because PlayState is a substate!
return;
} }
else if (PlayStatePlaylist.isStoryMode) else if (PlayStatePlaylist.isStoryMode)
{ {
@ -327,8 +349,11 @@ class GameOverSubState extends MusicBeatSubState
// After the animation finishes... // After the animation finishes...
new FlxTimer().start(0.7, function(tmr:FlxTimer) { new FlxTimer().start(0.7, function(tmr:FlxTimer) {
// ...fade out the graphics. Then after that happens... // ...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. // ...close the GameOverSubState.
if (pixel) RetroCameraFade.fadeBlack(FlxG.camera, 10, 1);
else
FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true); FlxG.camera.fade(FlxColor.BLACK, 1, true, null, true);
PlayState.instance.needsReset = true; PlayState.instance.needsReset = true;
@ -346,7 +371,22 @@ class GameOverSubState extends MusicBeatSubState
// Close the substate. // Close the substate.
close(); 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();
});
}
}); });
} }
} }

View file

@ -101,6 +101,10 @@ class PauseSubState extends MusicBeatSubState
*/ */
static final MUSIC_FINAL_VOLUME:Float = 0.75; 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. * Defines which pause music to use.
*/ */
@ -163,6 +167,12 @@ class PauseSubState extends MusicBeatSubState
*/ */
var metadataDeaths:FlxText; 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. * The actual text objects for the menu entries.
*/ */
@ -203,6 +213,8 @@ class PauseSubState extends MusicBeatSubState
regenerateMenu(); regenerateMenu();
transitionIn(); transitionIn();
startCharterTimer();
} }
/** /**
@ -222,6 +234,8 @@ class PauseSubState extends MusicBeatSubState
public override function destroy():Void public override function destroy():Void
{ {
super.destroy(); super.destroy();
charterFadeTween.cancel();
charterFadeTween = null;
pauseMusic.stop(); pauseMusic.stop();
} }
@ -270,30 +284,39 @@ class PauseSubState extends MusicBeatSubState
metadata.scrollFactor.set(0, 0); metadata.scrollFactor.set(0, 0);
add(metadata); 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); metadataSong.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
if (PlayState.instance?.currentChart != null) 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); metadataSong.scrollFactor.set(0, 0);
metadata.add(metadataSong); 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); metadataDifficulty.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
if (PlayState.instance?.currentDifficulty != null) if (PlayState.instance?.currentDifficulty != null)
{ {
metadataDifficulty.text += PlayState.instance.currentDifficulty.toTitleCase(); metadataDifficulty.text += PlayState.instance.currentDifficulty.replace('-', ' ').toTitleCase();
} }
metadataDifficulty.scrollFactor.set(0, 0); metadataDifficulty.scrollFactor.set(0, 0);
metadata.add(metadataDifficulty); 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.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
metadataDeaths.scrollFactor.set(0, 0); metadataDeaths.scrollFactor.set(0, 0);
metadata.add(metadataDeaths); 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.setFormat(Paths.font('vcr.ttf'), 32, FlxColor.WHITE, FlxTextAlign.RIGHT);
metadataPractice.visible = PlayState.instance?.isPracticeMode ?? false; metadataPractice.visible = PlayState.instance?.isPracticeMode ?? false;
metadataPractice.scrollFactor.set(0, 0); metadataPractice.scrollFactor.set(0, 0);
@ -302,6 +325,62 @@ class PauseSubState extends MusicBeatSubState
updateMetadataText(); updateMetadataText();
} }
var charterFadeTween:Null<FlxTween> = 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. * Perform additional animations to transition the pause menu in when it is first displayed.
*/ */
@ -351,7 +430,7 @@ class PauseSubState extends MusicBeatSubState
resume(this); resume(this);
} }
#if (debug || FORCE_DEBUG_VERSION) #if FEATURE_DEBUG_FUNCTIONS
// to pause the game and get screenshots easy, press H on pause menu! // to pause the game and get screenshots easy, press H on pause menu!
if (FlxG.keys.justPressed.H) if (FlxG.keys.justPressed.H)
{ {
@ -370,13 +449,14 @@ class PauseSubState extends MusicBeatSubState
*/ */
function changeSelection(change:Int = 0):Void function changeSelection(change:Int = 0):Void
{ {
FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4); var prevEntry:Int = currentEntry;
currentEntry += change; currentEntry += change;
if (currentEntry < 0) currentEntry = currentMenuEntries.length - 1; if (currentEntry < 0) currentEntry = currentMenuEntries.length - 1;
if (currentEntry >= currentMenuEntries.length) currentEntry = 0; if (currentEntry >= currentMenuEntries.length) currentEntry = 0;
if (currentEntry != prevEntry) FunkinSound.playOnce(Paths.sound('scrollMenu'), 0.4);
for (entryIndex in 0...currentMenuEntries.length) for (entryIndex in 0...currentMenuEntries.length)
{ {
var isCurrent:Bool = entryIndex == currentEntry; var isCurrent:Bool = entryIndex == currentEntry;

File diff suppressed because it is too large Load diff

View file

@ -2,11 +2,16 @@ package funkin.play;
import flixel.FlxSprite; import flixel.FlxSprite;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup; import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.tweens.FlxTween;
import flixel.util.FlxTimer;
import flixel.tweens.FlxEase;
class ResultScore extends FlxTypedSpriteGroup<ScoreNum> class ResultScore extends FlxTypedSpriteGroup<ScoreNum>
{ {
public var scoreShit(default, set):Int = 0; public var scoreShit(default, set):Int = 0;
public var scoreStart:Int = 0;
function set_scoreShit(val):Int function set_scoreShit(val):Int
{ {
if (group == null || group.members == null) return val; if (group == null || group.members == null) return val;
@ -16,7 +21,8 @@ class ResultScore extends FlxTypedSpriteGroup<ScoreNum>
while (dumbNumb > 0) while (dumbNumb > 0)
{ {
group.members[loopNum].digit = dumbNumb % 10; scoreStart += 1;
group.members[loopNum].finalDigit = dumbNumb % 10;
// var funnyNum = group.members[loopNum]; // var funnyNum = group.members[loopNum];
// prevNum = group.members[loopNum + 1]; // prevNum = group.members[loopNum + 1];
@ -44,9 +50,15 @@ class ResultScore extends FlxTypedSpriteGroup<ScoreNum>
public function animateNumbers():Void 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<ScoreNum>
class ScoreNum extends FlxSprite class ScoreNum extends FlxSprite
{ {
public var digit(default, set):Int = 10; 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 function set_digit(val):Int
{ {
if (val >= 0 && animation.curAnim != null && animation.curAnim.name != numToString[val]) if (val >= 0 && animation.curAnim != null && animation.curAnim.name != numToString[val])
{ {
if(glow){
animation.play(numToString[val], true, false, 0); animation.play(numToString[val], true, false, 0);
glow = false;
}else{
animation.play(numToString[val], true, false, 4);
}
updateHitbox(); updateHitbox();
switch (val) switch (val)
@ -107,6 +133,10 @@ class ScoreNum extends FlxSprite
animation.play(numToString[digit], true, false, 0); 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 baseY:Float = 0;
public var baseX: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" "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) public function new(x:Float, y:Float)
{ {
super(x, y); super(x, y);
@ -130,6 +201,7 @@ class ScoreNum extends FlxSprite
} }
animation.addByPrefix('DISABLED', 'DISABLED', 24, false); animation.addByPrefix('DISABLED', 'DISABLED', 24, false);
animation.addByPrefix('GONE', 'GONE', 24, false);
this.digit = 10; this.digit = 10;

File diff suppressed because it is too large Load diff

View file

@ -109,8 +109,6 @@ class AnimateAtlasCharacter extends BaseCharacter
var loop:Bool = animData.looped; var loop:Bool = animData.looped;
this.mainSprite.playAnimation(prefix, restart, ignoreOther, loop); this.mainSprite.playAnimation(prefix, restart, ignoreOther, loop);
animFinished = false;
} }
public override function hasAnimation(name:String):Bool public override function hasAnimation(name:String):Bool
@ -124,17 +122,21 @@ class AnimateAtlasCharacter extends BaseCharacter
*/ */
public override function isAnimationFinished():Bool public override function isAnimationFinished():Bool
{ {
return animFinished; return mainSprite?.isAnimationFinished() ?? false;
} }
function loadAtlasSprite():FlxAtlasSprite function loadAtlasSprite():FlxAtlasSprite
{ {
trace('[ATLASCHAR] Loading sprite atlas for ${characterId}.'); trace('[ATLASCHAR] Loading sprite atlas for ${characterId}.');
var sprite:FlxAtlasSprite = new FlxAtlasSprite(0, 0, Paths.animateAtlas(_data.assetPath, 'shared')); var animLibrary:String = Paths.getLibrary(_data.assetPath);
var animPath:String = Paths.stripLibrary(_data.assetPath);
var assetPath:String = Paths.animateAtlas(animPath, animLibrary);
sprite.onAnimationFinish.removeAll(); var sprite:FlxAtlasSprite = new FlxAtlasSprite(0, 0, assetPath);
sprite.onAnimationFinish.add(this.onAnimationFinished);
// sprite.onAnimationComplete.removeAll();
sprite.onAnimationComplete.add(this.onAnimationFinished);
return sprite; return sprite;
} }
@ -152,7 +154,6 @@ class AnimateAtlasCharacter extends BaseCharacter
// Make the game hold on the last frame. // Make the game hold on the last frame.
this.mainSprite.cleanupAnimation(prefix); this.mainSprite.cleanupAnimation(prefix);
// currentAnimName = null; // currentAnimName = null;
animFinished = true;
// Fallback to idle! // Fallback to idle!
// playAnimation('idle', true, false); // playAnimation('idle', true, false);
@ -165,6 +166,13 @@ class AnimateAtlasCharacter extends BaseCharacter
this.mainSprite = sprite; this.mainSprite = sprite;
mainSprite.ignoreExclusionPref = ["sing"];
// This forces the atlas to recalcuate its width and height
this.mainSprite.alpha = 0.0001;
this.mainSprite.draw();
this.mainSprite.alpha = 1.0;
var feetPos:FlxPoint = feetPosition; var feetPos:FlxPoint = feetPosition;
this.updateHitbox(); this.updateHitbox();

Some files were not shown because too many files have changed in this diff Show more