Merge branch 'rewrite/master' into nebulazorua/classic-camera-fix

This commit is contained in:
EliteMasterEric
2024-09-25 08:48:20 -04:00
237 changed files with 17740 additions and 3778 deletions
+3 -1
View File
@@ -1 +1,3 @@
* text=auto eol=lf
* text=auto eol=lf
*.hxc linguist-language=Haxe
*.hxp linguist-language=Haxe
-44
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
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.)
-25
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
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+70
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
-8
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
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?
-12
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?
-10
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.
@@ -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?
+16 -6
View File
@@ -44,7 +44,7 @@ runs:
g++ \
libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \
libgl-dev libgl1-mesa-dev \
libasound2-dev
libasound2-dev libpulse-dev
ln -s /usr/lib/x86_64-linux-gnu/libffi.so.8 /usr/lib/x86_64-linux-gnu/libffi.so.6 || true
- name: Install linux-specific dependencies
if: ${{ runner.os == 'Linux' && contains(inputs.targets, 'linux') }}
@@ -56,12 +56,17 @@ runs:
shell: bash
run: |
echo "TIMER_HAXELIB=$(date +%s)" >> "$GITHUB_ENV"
haxelib --debug --never install haxelib 4.1.0 --global
haxelib --debug --never deleterepo || true
haxelib fixrepo --global || true
haxelib --debug --never --global install haxelib 4.1.0
haxelib --debug --global set haxelib 4.1.0
haxelib --global remove haxelib git || true
haxelib --global remove hmm || true
rm -rf .haxelib
haxelib --debug --never --global git haxelib https://github.com/FunkinCrew/haxelib.git funkin-patches --skip-dependencies
haxelib --debug --never --global git hmm https://github.com/FunkinCrew/hmm funkin-patches
haxelib --debug --never newrepo
haxelib version
echo "HAXEPATH=$(haxelib config)" >> "$GITHUB_ENV"
haxelib --debug --never git haxelib https://github.com/HaxeFoundation/haxelib.git master
haxelib --debug --global install hmm
echo "TIMER_DEPS=$(date +%s)" >> "$GITHUB_ENV"
- name: Restore cached dependencies
@@ -75,7 +80,12 @@ runs:
name: Prep git for dependency install
uses: gacts/run-and-post-run@v1
with:
run: git config --global 'url.https://x-access-token:${{ inputs.gh-token }}@github.com/.insteadOf' https://github.com/
run: |
git config --global --name-only --get-regexp 'url\.https\:\/\/x-access-token:.+@github\.com\/\.insteadOf' \
| xargs -I {} git config --global --unset {}
git config -l --show-scope --show-origin
git config --global 'url.https://x-access-token:${{ inputs.gh-token }}@github.com/.insteadOf' https://github.com/
post: git config --global --unset 'url.https://x-access-token:${{ inputs.gh-token }}@github.com/.insteadOf'
- if: ${{ steps.cache-hmm.outputs.cache-hit != 'true' }}
+12
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
+44
View File
@@ -0,0 +1,44 @@
# Configuration for Label Actions - https://github.com/dessant/label-actions
# Automatically close issues and pull requests when the `status: duplicate` label is applied
'status: duplicate':
issues:
# Post a comment
comment: >
This issue is a duplicate. Please direct all discussion to the original issue.
# Close the issue
close: true
# Remove other status labels
unlabel:
- 'status: pending triage'
# Set a close reason
close-reason: 'not planned'
prs:
# Post a comment
comment: >
This pull request is a duplicate. Please direct all discussion to the original pull request.
# Remove other status labels
unlabel:
- 'status: pending triage'
# Close the pull request
close: true
# Set a close reason
close-reason: 'not planned'
'status: rejected':
issues:
# Close the issue
close: true
# Remove other status labels
unlabel:
- 'status: pending triage'
# Set a close reason
close-reason: 'not planned'
prs:
# Close the pull request
close: true
# Remove other status labels
unlabel:
- 'status: pending triage'
# Set a close reason
close-reason: 'not planned'
+11
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
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.
+7 -1
View File
@@ -45,7 +45,11 @@ jobs:
uses: ./.github/actions/setup-haxe
with:
gh-token: ${{ steps.app_token.outputs.token }}
- name: Setup HXCPP dev commit
run: |
cd .haxelib/hxcpp/git/tools/hxcpp
haxe compile.hxml
cd ../../../../..
- name: Build game
if: ${{ matrix.target == 'windows' }}
run: |
@@ -107,7 +111,9 @@ jobs:
name: Install dependencies
run: |
git config --global 'url.https://x-access-token:${{ steps.app_token.outputs.token }}@github.com/.insteadOf' https://github.com/
git config --global advice.detachedHead false
haxelib --global run hmm install -q
cd .haxelib/hxcpp/git/tools/hxcpp && haxe compile.hxml
- if: ${{ matrix.target != 'html5' }}
name: Restore hxcpp cache
+29
View File
@@ -0,0 +1,29 @@
# Perform actions when labels are applied to issues, discussions, or pull requests
# See .github/label-actions.yml
name: 'Label Actions'
on:
issues:
types:
- labeled
- unlabeled
pull_request_target:
types:
- labeled
- unlabeled
discussion:
types:
- labeled
- unlabeled
permissions:
contents: read
issues: write
pull-requests: write
discussions: write
jobs:
action:
runs-on: ubuntu-latest
steps:
- uses: dessant/label-actions@v4
+52
View File
@@ -0,0 +1,52 @@
name: "Issue Labeler"
on:
issues:
types:
- opened
- reopened
- edited
jobs:
# When an issue is opened, perform a similarity check for potential duplicates.
# If some are found, add a label and comment listing the potential duplicate issues.
potential-duplicate:
name: Detect potential duplicate issues
runs-on: ubuntu-latest
steps:
- uses: wow-actions/potential-duplicates@v1
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Issue title filter work with anymatch https://www.npmjs.com/package/anymatch.
# Any matched issue will stop detection immediately.
# You can specify multi filters in each line.
filter: ''
# Exclude keywords in title before detecting.
exclude: ''
# Label to set, when potential duplicates are detected.
label: 'potential duplicate'
# Get issues with state to compare. Supported state: 'all', 'closed', 'open'.
state: all
# If similarity is higher than this threshold([0,1]), issue will be marked as duplicate.
# Turn this up if the detection is too sensitive
threshold: 0.6
# Reactions to be add to comment when potential duplicates are detected.
# Available reactions: "-1", "+1", "confused", "laugh", "heart", "hooray", "rocket", "eyes"
# reactions: '-1'
# Comment to post when potential duplicates are detected.
comment: >
Potential duplicates: {{#issues}}
- [#{{ number }}] {{ title }} ({{ accuracy }}%)
{{/issues}}
# When an issue is opened, detect if it has an empty body or incomplete issue form.
# If it does, close the issue immediately.
empty-issues:
name: Close empty issues
runs-on: ubuntu-latest
steps:
- name: Run empty issues closer action
uses: rickstaa/empty-issues-closer-action@v1
env:
github_token: ${{ secrets.GITHUB_TOKEN }}
with:
close_comment: Closing this issue because it appears to be empty. Please update the issue for it to be reopened.
open_comment: Reopening this issue because the author provided more information.
+29
View File
@@ -0,0 +1,29 @@
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
# Apply labels to pull requests based on which files were edited
labeler:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: Set basic labels
uses: actions/labeler@v5
with:
sync-labels: true
# Apply labels to pull requests based on how many lines were edited
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
View File
@@ -13,3 +13,4 @@ shitAudio/
node_modules/
package.json
package-lock.json
.aider*
+2 -2
View File
@@ -1,6 +1,6 @@
[submodule "assets"]
path = assets
url = https://github.com/FunkinCrew/funkin.assets
url = https://github.com/FunkinCrew/Funkin-assets-secret
[submodule "art"]
path = art
url = https://github.com/FunkinCrew/funkin.art
url = https://github.com/FunkinCrew/Funkin-art-secret
+8 -1
View File
@@ -3,10 +3,17 @@
"configurations": [
{
// Launch in native/CPP on Windows/OSX/Linux
"name": "Lime",
"name": "Lime Build+Debug",
"type": "lime",
"request": "launch"
},
{
// Launch in native/CPP on Windows/OSX/Linux
"name": "Lime Debug (No Build)",
"type": "lime",
"request": "launch",
"preLaunchTask": null
},
{
// Launch in browser
"name": "HTML5 Debug",
+25 -15
View File
@@ -94,12 +94,17 @@
{
"label": "Windows / Debug",
"target": "windows",
"args": ["-debug", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Windows / Debug (Tracy)",
"target": "windows",
"args": ["-debug", "-DFEATURE_DEBUG_TRACY", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Linux / Debug",
"target": "linux",
"args": ["-debug", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug",
@@ -109,7 +114,7 @@
{
"label": "Windows / Debug (FlxAnimate Test)",
"target": "windows",
"args": ["-debug", "-DANIMATE", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DANIMATE", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (FlxAnimate Test)",
@@ -119,7 +124,7 @@
{
"label": "Windows / Debug (Straight to Freeplay)",
"target": "windows",
"args": ["-debug", "-DFREEPLAY", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DFREEPLAY", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Freeplay)",
@@ -132,13 +137,13 @@
"args": [
"-debug",
"-DSONG=bopeebo -DDIFFICULTY=normal",
"-DFORCE_DEBUG_VERSION"
"-DFEATURE_DEBUG_FUNCTIONS"
]
},
{
"label": "Windows / Debug (Straight to Play - 2hot)",
"target": "windows",
"args": ["-debug", "-DSONG=2hot", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DSONG=2hot", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Play - Bopeebo Normal)",
@@ -148,17 +153,22 @@
{
"label": "Windows / Debug (Conversation Test)",
"target": "windows",
"args": ["-debug", "-DDIALOGUE", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DDIALOGUE", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Conversation Test)",
"target": "hl",
"args": ["-debug", "-DDIALOGUE"]
},
{
"label": "Windows / Debug (Results Screen Test)",
"target": "windows",
"args": ["-debug", "-DRESULTS"]
},
{
"label": "Windows / Debug (Straight to Chart Editor)",
"target": "windows",
"args": ["-debug", "-DCHARTING", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DCHARTING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Chart Editor)",
@@ -168,12 +178,12 @@
{
"label": "Windows / Debug (Straight to Animation Editor)",
"target": "windows",
"args": ["-debug", "-DANIMDEBUG", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DANIMDEBUG", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Windows / Debug (Debug hxCodec)",
"target": "windows",
"args": ["-debug", "-DHXC_LIBVLC_LOGGING", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DHXC_LIBVLC_LOGGING", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Straight to Animation Editor)",
@@ -183,7 +193,7 @@
{
"label": "Windows / Debug (Latency Test)",
"target": "windows",
"args": ["-debug", "-DLATENCY", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DLATENCY", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HashLink / Debug (Latency Test)",
@@ -193,7 +203,7 @@
{
"label": "Windows / Debug (Waveform Test)",
"target": "windows",
"args": ["-debug", "-DWAVEFORM", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DWAVEFORM", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "Windows / Release",
@@ -213,17 +223,17 @@
{
"label": "HTML5 / Debug",
"target": "html5",
"args": ["-debug", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "HTML5 / Debug (Watch)",
"target": "html5",
"args": ["-debug", "-watch", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-watch", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "macOS / Debug",
"target": "mac",
"args": ["-debug", "-DFORCE_DEBUG_VERSION"]
"args": ["-debug", "-DFEATURE_DEBUG_FUNCTIONS"]
},
{
"label": "macOS / Release",
+182
View File
@@ -4,6 +4,188 @@ All notable changes will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.5.0] - 2024-09-12
### Added
- Added a new Character Select screen to switch between playable characters in Freeplay
- Modding isn't 100% there but we're working on it!
- Added Pico as a playable character! Unlock him by completing Weekend 1 (if you haven't already done that)
- The songs from Weekend 1 have moved; you must now switch to Pico via Character Select screen in Freeplay to access them
- Added 11 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)
- Pico (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 completing Weekend 1 as Pico and then selecting Boyfriend 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)
- Implemented new animations for Tankman and Pico
## Changed
- Girlfriend and Nene now perform previously unused animations when you achieve a large combo, or drop a large combo.
- The pixel character icons in the Freeplay menu now display an animation!
- Altered how Week 6 displays sprites to make things look more retro.
- Character offsets are now independent of the character's scale.
- This should resolve issues with offsets when porting characters from older mods.
- Pixel character offsets have been modified to compensate.
- Note style data can now specify custom combo count graphics, judgement graphics, countdown graphics, and countdown audio. (community feature by anysad)
- These were previously using hardcoded values based on whether the stage was `school` or `schoolEvil`.
- The `danceEvery` property of characters and stage props can now use values with a precision of `0.25`, to play their idle animation up to four times per beat.
- Reworked the JSON merging system in Polymod; you can now include JSONPatch files under `_merge` in your mod folder to add, modify, or remove values in a JSON without replacing it entirely!
- Cutscenes now automatically pause when tabbing out (community fix by AbnormalPoof)
- Characters will now respect the `danceEvery` property (community fix by gamerbross)
- The F5 function now reloads the current song's chart data from disc (community feature by gamerbross)
- Refactored the compilation guide and added common troubleshooting steps (community fix by Hundrec)
- Made several layout improvements and fixes to the Animation Offsets editor in the Debug menu (community fix by gamerbross)
- Fixed a bug where the Back sound would be not played when leaving the Story menu and Options menu (community fix by AppleHair)
- Animation offsets no longer directly modify the `x` and `y` position of props, which makes props work better with tweens (community fix by Sword352)
- The YEAH! events in Tutorial now use chart events rather than being hard-coded (community fix by anysad)
- The player's Score now displays commas in it (community fix by loggo)
## Fixed
- Fixed an issue where songs with no notes would crash on the Results screen.
- Fixed an issue where the old icon easter egg would not work properly on pixel levels.
- Fixed an issue where you could play notes during the Thorns cutscene.
- Fixed an issue where the Heart icon when favoriting a song in Freeplay would be malformed.
- Fixed an issue where Pico's death animation displays a faint blue background (community fix by doggogit)
- Fixed an issue where mod songs would not play a preview in the Freeplay menu (community fix by KarimAkra)
- Fixed an issue where the Memory Usage counter could overflow and display a negative number (community fix by KarimAkra)
- Fixed an issue where pressing the Chart Editor keybind while playtesting a chart would reset the chart editor (community fix by gamerbross)
- Fixed a crash bug when pressing F5 after seeing the sticker transition (community fix by gamerbross)
- Fixed an issue where the Story Mode menu couldn't be scrolled with a mouse (community fix by JVNpixels)
- Fixed an issue causing the song to majorly desync sometimes (community fix by Burgerballs)
- Fixed an issue where the Freeplay song preview would not respect the instrumental ID specified in the song metadata (community fix by AppleHair)
- Fixed an issue where Tankman's icon wouldn't display in the Chart Editor (community fix by hundrec)
- Fixed an issue where pausing the game during a camera zoom would zoom the pause menu. (community fix by gamerbros)
- Fixed an issue where certain UI elements would not flash at a consistent rate (community fix by cyn0x8)
- Fixed an issue where the game would not use the placeholder health icon as a fallback (community fix by gamerbross)
- Fixed an issue where the chart editor could get stuck creating a hold note when using Live Inputs (community fix by gamerbross)
- Fixed an issue where character graphics could not be placed in week folders (community fix by 7oltan)
- Fixed a crash issue when a Freeplay song has no `Normal` difficulty (community fix by Applehair and gamerbross)
- Fixed an issue in Story Mode where a song that isn't valid for the current variation could be selected (community fix by Applehair)
## [0.4.1] - 2024-06-12
### Added
- Pressing ESCAPE on the title screen on desktop now exits the game, allowing you to exit the game while in fullscreen on desktop
- Freeplay menu controls (favoriting and switching categories) are now rebindable from the Options menu, and now have default binds on controllers.
### Changed
- Highscores and ranks are now saved separately, which fixes the issue where people would overwrite their saves with higher scores,
which would remove their rank if they had a lower one.
- A-Bot speaker now reacts to the user's volume preference on desktop (thanks to [M7theguy for the issue report/suggestion](https://github.com/FunkinCrew/Funkin/issues/2744)!)
- On Freeplay, heart icons are shifted to the right when you favorite a song that has no rank on it.
- Only play `scrollMenu` sound effect when there's a real change on the freeplay menu ([thanks gamerbross for the PR!](https://github.com/FunkinCrew/Funkin/pull/2741))
- Gave antialiasing to the edge of the dad graphic on Freeplay
- Rearranged some controls in the controls menu
- Made several chart revisions
- Re-enabled custom camera events in Roses (Erect/Nightmare)
- Tweaked the chart for Lit Up (Hard)
- Corrected the difficulty ratings for M.I.L.F. (Easy/Normal/Hard)
### Fixed
- Fixed an issue in the controls menu where some control binds would overlap their names
- Fixed crash when attempting to exit the gameover screen when also attempting to retry the song ([thanks DMMaster636 for the PR!](https://github.com/FunkinCrew/Funkin/pull/2709))
- Fix botplay sustain release bug ([thanks Hundrec!](Fix botplay sustain release bug #2683))
- Fix for the camera not pausing during a gameplay pause ([thanks gamerbross!](https://github.com/FunkinCrew/Funkin/pull/2684))
- Fixed issue where Pico's gameplay sprite would unintentionally appear on the gameover screen when dying on 2Hot from an explosion
- Freeplay previews properly fade volume during the BF idle animation
- Fixed bug where Dadbattle incorrectly appeared as Dadbattle Erect when returning to freeplay on Hard
- Fixed 2Hot not appearing under the "#" category in Freeplay menu
- Fixed a bug where the Chart Editor would crash when attempting to select an event with the Event toolbox open
- Improved offsets for Pico and Tankman opponents so they don't slide around as much.
- Fixed the black "temp" graphic on freeplay from being incorrectly sized / masked, now it's identical to the dad freeplay graphic
## [0.4.0] - 2024-06-06
### Added
- 2 new Erect remixes, Eggnog and Satin Panties. Check them out from the Freeplay menu!
- Major visual improvements to the Results screen, with additional animations and audio based on your performance.
- Major visual improvements to the Freeplay screen, with song difficulty ratings and player rank displays.
- Freeplay now plays a preview of songs when you hover over them.
- Added a Charter field to the chart format, to allow for crediting the creator of a level's chart.
- You can see who charted a song from the Pause menu.
- Added a new Scroll Speed chart event to change the note speed mid-song (thanks burgerballs!)
### Changed
- Tweaked the charts for several songs:
- Tutorial (increased the note speed slightly)
- Spookeez
- Monster
- Winter Horrorland
- M.I.L.F.
- Senpai (increased the note speed)
- Roses
- Thorns (increased the note speed slightly)
- Ugh
- Stress
- Lit Up
- Favorite songs marked in Freeplay are now stored between sessions.
- The Freeplay easter eggs are now easier to see.
- In the event that the game cannot load your save data, it will now perform a backup before clearing it, so that we can try to repair it in the future.
- Custom note styles are now properly supported for songs; add new notestyles via JSON, then select it for use from the Chart Editor Metadata toolbox. (thanks Keoiki!)
- Health icons now support a Winning frame without requiring a spritesheet, simply include a third frame in the icon file. (thanks gamerbross!)
- Remember that for more complex behaviors such as animations or transitions, you should use an XML file to define each frame.
- Improved the Event Toolbox in the Chart Editor; dropdowns are now bigger, include search field, and display elements in alphabetical order rather than a random order.
### Fixed
- Fixed an issue where Nene's visualizer would not play on Desktop builds
- Fixed a bug where the game would silently fail to load saves on HTML5
- Fixed some bugs with the props on the Story Menu not bopping properly
- Additional fixes to the Loading bar on HTML5 (thanks lemz1!)
- Fixed several bugs with the TitleState, including missing music when returning from the Main Menu (thanks gamerbross!)
- Fixed a camera bug in the Main Menu (thanks richTrash21!)
- Fixed a bug where changing difficulties in Story mode wouldn't update the score (thanks sectorA!)
- Fixed a crash in Freeplay caused by a level referencing an invalid song (thanks gamerbross!)
- Fixed a bug where pressing the volume keys would stop the Toy commercial (thanks gamerbross!)
- Fixed a bug where the Chart Editor Playtest would crash when losing (thanks gamerbross!)
- Fixed a bug where hold notes would display improperly in the Chart Editor when downscroll was enabled for gameplay (thanks gamerbross!)
- Fixed a bug where hold notes would be positioned wrong on downscroll (thanks MaybeMaru!)
- Removed a large number of unused imports to optimize builds (thanks Ethan-makes-music!)
- Improved debug logging for unscripted stages (thanks gamerbross!)
- Made improvements to compiling documentation (thanks gedehari!)
- Fixed a crash on Linux caused by an old version of hxCodec (thanks Noobz4Life!)
- Optimized animation handling for characters (thanks richTrash21!)
- Made improvements to compiling documentation (thanks gedehari!)
- Fixed an issue where the Chart Editor would use an incorrect instrumental on imported Legacy songs (thanks gamerbross!)
- Fixed a camera bug in the Main Menu (thanks richTrash21!)
- Fixed a bug where opening the game from the command line would crash the preloader (thanks NotHyper474!)
- Fixed a bug where characters would sometimes use the wrong scale value (thanks PurSnake!)
- Additional bug fixes and optimizations.
## [0.3.3] - 2024-05-14
### Changed
- Cleaned up some code in `PlayAnimationSongEvent.hx` (thanks BurgerBalls!)
### Fixed
- Fixes to the Loading bar on HTML5 (thanks lemz1!)
- Don't allow any more inputs when exiting freeplay (thanks gamerbros!)
- Fixed using mouse wheel to scroll on freeplay (thanks JugieNoob!)
- Fixed the reset's of the health icons, score, and notes when re-entering gameplay from gameover (thanks ImCodist!)
- Fixed the chart editor character selector's hitbox width (thanks MadBear422!)
- Fixed camera stutter once a wipe transition to the Main Menu completes (thanks ImCodist!)
- Fixed an issue where hold note would be invisible for a single frame (thanks ImCodist!)
- Fix tween accumulation on title screen when pressing Y multiple times (thanks TheGaloXx!)
- Fix a crash when querying FlxG.state in the crash handler
- Fix for a game over easter egg so you don't accidentally exit it when viewing
- Fix an issue where the Freeplay menu never displays 100% clear
- Fix an issue where Weekend 1 Pico attempted to retrieve a missing asset.
- Fix an issue where duplicate keybinds would be stoed, potentially causing a crash
- Chart debug key now properly returns you to the previous chart editor session if you were playtesting a chart (thanks nebulazorua!)
- Fix a crash on Freeplay found on AMD graphics cards
## [0.3.2] - 2024-05-03
### Added
- Added `,` and `.` keybinds to the Chart Editor. These place Focus Camera events at the playhead, for the opponent and player respectively.
-265
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.2" 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>
+3 -3
View File
@@ -1,8 +1,8 @@
# Friday Night Funkin'
Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for Ludem Dare 47.
Friday Night Funkin' is a rhythm game. Built using HaxeFlixel for Ludum Dare 47.
This game was made with love to Newgrounds and it's community. Extra love to Tom Fulp.
This game was made with love to Newgrounds and its community. Extra love to Tom Fulp.
- [Playable web demo on Newgrounds!](https://www.newgrounds.com/portal/view/770371)
- [Demo download builds for Windows, Mac, and Linux from Itch.io!](https://ninja-muffin24.itch.io/funkin)
@@ -23,7 +23,7 @@ Full credits can be found in-game, or wherever the credits.json file is.
## Programming
- [ninjamuffin99](https://twitter.com/ninja_muffin99) - Lead Programmer
- [MasterEric](https://twitter.com/EliteMasterEric) - Programmer
- [EliteMasterEric](https://twitter.com/EliteMasterEric) - Programmer
- [MtH](https://twitter.com/emmnyaa) - Charting and Additional Programming
- [GeoKureli](https://twitter.com/Geokureli/) - Additional Programming
- Our contributors on GitHub
+1 -1
Submodule art updated: 66572f85d8...1f64f3e740
+1 -1
Submodule assets updated: 962130b224...8140f8255c
+3 -3
View File
@@ -83,7 +83,7 @@ apt-fast install -y --no-install-recommends \
libc6-dev libffi-dev \
libx11-dev libxi-dev libxext-dev libxinerama-dev libxrandr-dev \
libgl-dev libgl1-mesa-dev \
libasound2-dev \
libasound2-dev libpulse-dev \
libvlc-dev libvlccore-dev
EOF
@@ -137,8 +137,8 @@ ENV PATH="$HAXEPATH:$PATH"
RUN <<EOF
HOME=/etc haxelib setup "$HAXEPATH/lib"
haxelib --global --never install haxelib $haxelib_version
haxelib --global --never git haxelib https://github.com/HaxeFoundation/haxelib.git master
haxelib --global --never install hmm
haxelib --global --never git haxelib https://github.com/FunkinCrew/haxelib.git funkin-patches --skip-dependencies
haxelib --global --never git hmm https://github.com/FunkinCrew/hmm funkin-patches
EOF
# hxcpp
+3 -2
View File
@@ -79,7 +79,7 @@
{
"props": {
"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"]
},
"type": "ConstantName"
@@ -327,7 +327,8 @@
"INLINE",
"DYNAMIC",
"FINAL"
]
],
"severity": "IGNORE"
},
"type": "ModifierOrder"
},
+35 -9
View File
@@ -2,13 +2,19 @@
0. Setup
- 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:
- `git clone --recurse-submodules https://github.com/FunkinCrew/funkin.git`
- 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.
2. Install `hmm` (run `haxelib --global install hmm` and then `haxelib --global run hmm setup`)
3. Install all haxelibs of the current branch by running `hmm install`
4. Setup lime: `haxelib run lime setup`
5. Platform setup
- Download Git from [git-scm.com](https://www.git-scm.com)
- Do NOT download the repository using the Download ZIP button on GitHub or you may run into errors!
- Instead, open a command prompt and do the following steps...
1. Run `cd the\directory\you\want\the\source\code\in` to specify which folder the command prompt is working in.
- For example, `cd C:\Users\YOURNAME\Documents` would instruct the command prompt to perform the next steps in your Documents folder.
2. Run `git clone https://github.com/FunkinCrew/funkin.git` to clone the base repository.
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)
- When prompted, select "Individual Components" and make sure to download the following:
- MSVC v143 VS 2022 C++ x64/x86 build tools
@@ -16,5 +22,25 @@
- 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/)
- 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`
7. `lime test PLATFORM` ! Add `-debug` to enable several debug features such as time travel (`PgUp`/`PgDn` in Play State).
9. If you are targeting for native, you may need to run `lime rebuild <PLATFORM>` and `lime rebuild <PLATFORM> -debug`
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
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.
+24 -24
View File
@@ -1,31 +1,31 @@
# Funkin' Debug Hotkeys
`F4` (EVERYWHERE) - Leave Current State and move to Main Menu
`F5` (EVERYWHERE) - Hot Reload Data Files
Most of this functionality is only available on debug builds of the game!
`Y` (Title Screen) - WOAH
## Any State
- `F2`: ***OVERLAY***: Enables the Flixel debug overlay, which has partial support for scripting.
- `F3`: ***SCREENSHOT***: Takes a screenshot of the game and saves it to the local `screenshots` directory. Works outside of debug builds too!
- `F4`: ***EJECT***: Forcibly switch state to the Main Menu (with no extra transition). Useful if you're stuck in a level and you need to get out!
- `F5`: ***HOT RELOAD***: Forcibly reload the game's scripts and data files, then restart the current state. If any files in the `assets` folder have been modified, the game should process the changes for you! NOTE: Known bug, this does not reset song charts or song scripts, but it should reset everything else (such as stage layout data and character animation data).
- `CTRL-SHIFT-L`: ***FORCE CRASH***: Immediately crash the game with a detailed crash log and a stack trace.
`~` (Main Menu) - Access Debug Menu
## **Play State**
- `H`: ***HIDE UI***: Makes the user interface invisible. Works in Pause Menu, great for screenshots.
- `1`: ***END SONG***: Immediately ends the song and moves to Results Screen on Freeplay, or next song on Story Mode.
- `2`: ***GAIN HEALTH***: Debug function, add 10% to the player's health.
- `3`: ***LOSE HEALTH***: Debug function, subtract 5% to the player's health.
- `9`: NEATO!
- `PAGEUP` (MacOS: `Fn-Up`): ***FORWARDS TIME TRAVEL****: Move forward by 2 sections. Hold SHIFT to move forward by 20 sections instead.
- `PAGEDOWN` (MacOS: `Fn-Down`): ***BACKWARDS TIME TRAVEL****: Move backward by 2 sections. Hold SHIFT to move backward by 20 sections instead.
`U` (Play) - Open Stage Editor State
`H` (Play) - Show/Hide HUD
`1` (Play) - End Song
`2` (Play) - Add 10% Health
`3` (Play) - Subtract 5% Health
`7` (Play) - (NOT WORKING) Open Chart Editor
`8` (Play) - Open Animation Editor
`9` (Play) - (Easter Egg) Classic Health Icon
`PGUP`/`Fn+Up` (Play) - Skip Forward In Time
`PGDN`/`Fn+Down` (Play) - 🦃 That's right, we're going to go BACK IN TIME
## **Freeplay State**
- `F` (Freeplay Menu) - Move to Favorites
- `Q` (Freeplay Menu) - Back one category
- `E` (Freeplay Menu) - Forward one category
`F` (Freeplay Menu) - Move to Favorites
`P` (Freeplay Menu) - Switch to Pico (probably doesn't work)
`T` (Freeplay Menu) - Start typing in search bar
`Q` (Freeplay Menu) - Back one letter
`E` (Freeplay Menu) - Forward one letter
## **Title State**
- `Y` - WOAH
`Arrows` (Stage Editor) - Move Prop
`Ctrl-Z` (Stage Editor) - Undo
`Y` (Stage Editor) - Leave Stage Editor
`H` (Pause Menu) - Hide the Pause Menu UI (good for screenshots!)
## **Main Menu**
- `~`: ***DEBUG****: Opens a menu to access the Chart Editor and other work-in-progress editors. Rebindable in the options menu.
- `CTRL-ALT-SHIFT-W`: ***ALL ACCESS***: Unlocks all songs in Freeplay. Only available on debug builds.
+9 -1
View File
@@ -1,4 +1,4 @@
# Troubleshooting Common Issues
# Troubleshooting Common Compilation Issues
- Weird macro error with a very tall call stack: Restart Visual Studio Code
- NOTE: This is caused by Polymod somewhere, and seems to only occur when there is another compile error somewhere in the program. There is a bounty up for it.
@@ -13,3 +13,11 @@
- `LINK : fatal error LNK1201: error writing to program database ''; check for insufficient disk space, invalid path, or insufficient privilege`
- This error occurs if the PDB file located in your `export` folder is in use or exceeds 4 GB. Try deleting the `export` folder and building again from scratch.
- `error: RPC failed; curl 92 HTTP/2 stream 0 was not closed cleanly: PROTOCOL_ERROR (err 1)`
- This error can happen during cloning as a result of poor network connectivity. A common fix is to run ` git config --global http.postBuffer 4096M` in your terminal.
- Repository is missing an `assets` folder, or `assets` folder is empty.
- You did not clone the repository correctly! Copy the path to your `funkin` folder and run `cd the\path\you\copied`. Then follow the compilation guide starting from **Step 4**.
- Other compilation issues may be caused by installing bad library versions. Try deleting the `.haxelib` folder and following the guide starting from **Step 5**.
+2 -2
View File
@@ -3,10 +3,10 @@
"description": "An introductory mod.",
"contributors": [
{
"name": "MasterEric"
"name": "EliteMasterEric"
}
],
"api_version": "0.1.0",
"api_version": "0.5.0",
"mod_version": "1.0.0",
"license": "Apache-2.0"
}
+1 -1
View File
@@ -3,7 +3,7 @@
"description": "Newgrounds? More like OLDGROUNDS lol.",
"contributors": [
{
"name": "MasterEric"
"name": "EliteMasterEric"
}
],
"api_version": "0.1.0",
+63 -29
View File
@@ -1,44 +1,46 @@
{
"dependencies": [
{
"name": "FlxPartialSound",
"type": "git",
"dir": null,
"ref": "a1eab7b9bf507b87200a3341719054fe427f3b15",
"url": "https://github.com/FunkinCrew/FlxPartialSound.git"
},
{
"name": "discord_rpc",
"type": "git",
"dir": null,
"ref": "2d83fa863ef0c1eace5f1cf67c3ac315d1a3a8a5",
"url": "https://github.com/Aidan63/linc_discord-rpc"
"url": "https://github.com/FunkinCrew/linc_discord-rpc"
},
{
"name": "flixel",
"type": "git",
"dir": null,
"ref": "a7d8e3bad89a0a3506a4714121f73d8e34522c49",
"ref": "f2b090d6c608471e730b051c8ee22b8b378964b1",
"url": "https://github.com/FunkinCrew/flixel"
},
{
"name": "flixel-addons",
"type": "git",
"dir": null,
"ref": "a523c3b56622f0640933944171efed46929e360e",
"ref": "7424db4e9164ff46f224a7c47de1b732d2542dd7",
"url": "https://github.com/FunkinCrew/flixel-addons"
},
{
"name": "flixel-text-input",
"type": "haxelib",
"version": "1.1.0"
},
{
"name": "flixel-ui",
"type": "git",
"dir": null,
"ref": "719b4f10d94186ed55f6fef1b6618d32abec8c15",
"url": "https://github.com/HaxeFlixel/flixel-ui"
"ref": "951a0103a17bfa55eed86703ce50b4fb0d7590bc",
"url": "https://github.com/FunkinCrew/flixel-text-input"
},
{
"name": "flxanimate",
"type": "git",
"dir": null,
"ref": "17e0d59fdbc2b6283a5c0e4df41f1c7f27b71c49",
"url": "https://github.com/FunkinCrew/flxanimate"
"ref": "0654797e5eb7cd7de0c1b2dbaa1efe5a1e1d9412",
"url": "https://github.com/Dot-Stuff/flxanimate"
},
{
"name": "format",
@@ -49,9 +51,16 @@
"name": "funkin.vis",
"type": "git",
"dir": null,
"ref": "98c9db09f0bbfedfe67a84538a5814aaef80bdea",
"ref": "22b1ce089dd924f15cdc4632397ef3504d464e90",
"url": "https://github.com/FunkinCrew/funkVis"
},
{
"name": "grig.audio",
"type": "git",
"dir": "src",
"ref": "57f5d47f2533fd0c3dcd025a86cb86c0dfa0b6d2",
"url": "https://gitlab.com/haxe-grig/grig.audio.git"
},
{
"name": "hamcrest",
"type": "haxelib",
@@ -61,32 +70,36 @@
"name": "haxeui-core",
"type": "git",
"dir": null,
"ref": "0212d8fdfcafeb5f0d5a41e1ddba8ff21d0e183b",
"ref": "c9d96b168ea2a19274ad7c766ab1a34b57baa793",
"url": "https://github.com/haxeui/haxeui-core"
},
{
"name": "haxeui-flixel",
"type": "git",
"dir": null,
"ref": "63a906a6148958dbfde8c7b48d90b0693767fd95",
"ref": "013b9d4e56bfe9a034e028a8d685f0b274cb73c4",
"url": "https://github.com/haxeui/haxeui-flixel"
},
{
"name": "hscript",
"type": "haxelib",
"version": "2.5.0"
"type": "git",
"dir": null,
"ref": "12785398e2f07082f05034cb580682e5671442a2",
"url": "https://github.com/FunkinCrew/hscript"
},
{
"name": "hxCodec",
"type": "git",
"dir": null,
"ref": "c0c7f2680cc190c932a549c2e2fdd9b0ba2bd10e",
"ref": "61b98a7a353b7f529a8fec84ed9afc919a2dffdd",
"url": "https://github.com/FunkinCrew/hxCodec"
},
{
"name": "hxcpp",
"type": "haxelib",
"version": "4.3.2"
"type": "git",
"dir": null,
"ref": "c6bac3d6c7d683f25104296b2f4c50f8c90b8349",
"url": "https://github.com/cortex-engine/hxcpp"
},
{
"name": "hxcpp-debug-server",
@@ -95,10 +108,17 @@
"ref": "147294123f983e35f50a966741474438069a7a8f",
"url": "https://github.com/FunkinCrew/hxcpp-debugger"
},
{
"name": "hxjsonast",
"type": "git",
"dir": null,
"ref": "20e72cc68c823496359775ac1f06500e67f189d5",
"url": "https://github.com/nadako/hxjsonast/"
},
{
"name": "hxp",
"type": "haxelib",
"version": "1.2.2"
"version": "1.3.0"
},
{
"name": "json2object",
@@ -107,11 +127,25 @@
"ref": "a8c26f18463c98da32f744c214fe02273e1823fa",
"url": "https://github.com/FunkinCrew/json2object"
},
{
"name": "jsonpatch",
"type": "git",
"dir": null,
"ref": "f9b83215acd586dc28754b4ae7f69d4c06c3b4d3",
"url": "https://github.com/EliteMasterEric/jsonpatch"
},
{
"name": "jsonpath",
"type": "git",
"dir": null,
"ref": "7a24193717b36393458c15c0435bb7c4470ecdda",
"url": "https://github.com/EliteMasterEric/jsonpath"
},
{
"name": "lime",
"type": "git",
"dir": null,
"ref": "872ff6db2f2d27c0243d4ff76802121ded550dd7",
"ref": "fe3368f611a84a19afc03011353945ae4da8fffd",
"url": "https://github.com/FunkinCrew/lime"
},
{
@@ -146,29 +180,29 @@
"name": "openfl",
"type": "git",
"dir": null,
"ref": "228c1b5063911e2ad75cef6e3168ef0a4b9f9134",
"ref": "8306425c497766739510ab29e876059c96f77bd2",
"url": "https://github.com/FunkinCrew/openfl"
},
{
"name": "polymod",
"type": "git",
"dir": null,
"ref": "8553b800965f225bb14c7ab8f04bfa9cdec362ac",
"ref": "0fbdf27fe124549730accd540cec8a183f8652c0",
"url": "https://github.com/larsiusprime/polymod"
},
{
"name": "thx.core",
"type": "git",
"dir": null,
"ref": "22605ff44f01971d599641790d6bae4869f7d9f4",
"url": "https://github.com/FunkinCrew/thx.core"
"ref": "76d87418fadd92eb8e1b61f004cff27d656e53dd",
"url": "https://github.com/fponticelli/thx.core"
},
{
"name": "thx.semver",
"type": "git",
"dir": null,
"ref": "cf8d213589a2c7ce4a59b0fdba9e8ff36bc029fa",
"url": "https://github.com/FunkinCrew/thx.semver"
"ref": "bdb191fe7cf745c02a980749906dbf22719e200b",
"url": "https://github.com/fponticelli/thx.semver"
}
]
}
}
+1141
View File
File diff suppressed because it is too large Load Diff
+9 -10
View File
@@ -2,6 +2,7 @@ package;
import flixel.FlxGame;
import flixel.FlxState;
import funkin.Preferences;
import funkin.util.logging.CrashHandler;
import funkin.ui.debug.MemoryCounter;
import funkin.save.Save;
@@ -22,12 +23,6 @@ class Main extends Sprite
var gameHeight:Int = 720; // Height of the game in pixels (might be less / more in actual pixels depending on your zoom).
var initialState:Class<FlxState> = funkin.InitState; // The FlxState the game starts with.
var zoom:Float = -1; // If -1, zoom is automatically calculated to fit the window dimensions.
#if web
var framerate:Int = 60; // How many frames per second the game should run at.
#else
// TODO: This should probably be in the options menu?
var framerate:Int = 144; // How many frames per second the game should run at.
#end
var skipSplash:Bool = true; // Whether to skip the flixel splash screen that appears in release mode.
var startFullscreen:Bool = false; // Whether to start the game in fullscreen on desktop targets
@@ -66,6 +61,12 @@ class Main extends Sprite
function init(?event:Event):Void
{
#if web
// set this variable (which is a function) from the lime version at lime/_internal/backend/html5/HTML5Application.hx
// The framerate cap will more thoroughly initialize via Preferences in InitState.hx
funkin.Preferences.lockedFramerateFunction = untyped js.Syntax.code("window.requestAnimationFrame");
#end
if (hasEventListener(Event.ADDED_TO_STAGE))
{
removeEventListener(Event.ADDED_TO_STAGE, init);
@@ -103,7 +104,7 @@ class Main extends Sprite
// George recommends binding the save before FlxGame is created.
Save.load();
var game:FlxGame = new FlxGame(gameWidth, gameHeight, initialState, framerate, framerate, skipSplash, startFullscreen);
var game:FlxGame = new FlxGame(gameWidth, gameHeight, initialState, Preferences.framerate, Preferences.framerate, skipSplash, startFullscreen);
// FlxG.game._customSoundTray wants just the class, it calls new from
// create() in there, which gets called when it's added to stage
@@ -113,12 +114,10 @@ class Main extends Sprite
addChild(game);
#if debug
#if FEATURE_DEBUG_FUNCTIONS
game.debugger.interaction.addTool(new funkin.util.TrackerToolButtonUtil());
#end
addChild(fpsCounter);
#if hxcpp_debug_server
trace('hxcpp_debug_server is enabled! You can now connect to the game with a debugger.');
#else
+102
View File
@@ -0,0 +1,102 @@
/*
* Pulled from Tracey profiler PR
* @see https://github.com/HaxeFoundation/haxe/pull/11772
*/
package cpp.vm.tracy;
#if (!HXCPP_TRACY)
#error "This class cannot be used without -D HXCPP_TRACY"
#end
enum abstract PlotFormatType(cpp.UInt8) from cpp.UInt8 to cpp.UInt8
{
var Number = 0;
var Memory = 1;
var Percentage = 2;
}
@:include('hx/TelemetryTracy.h')
extern class Native_TracyProfiler
{
/**
Mark a frame. Call this at the end of each frame loop.
**/
@:native('::__hxcpp_tracy_framemark')
public static function frameMark():Void;
/**
Print a message into Tracy's log.
**/
@:native('::__hxcpp_tracy_message')
public static function message(_msg:String, ?_color:Int = 0x000000):Void;
/**
Tracy can collect additional information about the profiled application,
which will be available in the trace description.
This can include data such as the source repository revision,
the application’s environment (dev/prod), etc.
**/
@:native('::__hxcpp_tracy_message_app_info')
public static function messageAppInfo(_info:String):Void;
/**
Plot a named value to tracy. This will generate a graph in the profiler for you.
**/
@:native('::__hxcpp_tracy_plot')
public static function plot(_name:String, _val:cpp.Float32):Void;
/**
Configure how values are plotted and displayed.
**/
@:native('::__hxcpp_tracy_plot_config')
public static function plotConfig(_name:String, _format:PlotFormatType, ?_step:Bool = false, ?_fill:Bool = false, ?_color:Int = 0x000000):Void;
/**
Set a name for the current thread this function is called in. Supply an optional groupHint so threads become grouped in Tracy's UI.
**/
@:native('::__hxcpp_tracy_set_thread_name_and_group')
public static function setThreadName(_name:String, ?_groupHint:Int = 1):Void;
/**
Create a custom named scoped zone in your code.
**/
@:native('HXCPP_TRACY_ZONE')
public static function zoneScoped(_name:String):Void;
}
#if (scriptable || cppia)
class Cppia_TracyProfiler
{
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.frameMark)
public static function frameMark()
Native_TracyProfiler.frameMark();
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.message)
public static function message(_msg:String, ?_color:Int = 0x000000)
Native_TracyProfiler.message(_msg, _color);
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.messageAppInfo)
public static function messageAppInfo(_info:String)
Native_TracyProfiler.messageAppInfo(_info);
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.plot)
public static function plot(_name:String, _val:Float)
Native_TracyProfiler.plot(_name, _val);
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.plotConfig)
public static function plotConfig(_name:String, _format:PlotFormatType, ?_step:Bool = false, ?_fill:Bool = false, ?_color:Int = 0x000000)
Native_TracyProfiler.plotConfig(_name, _format, _step, _fill, _color);
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.setThreadName)
public static function setThreadName(_name:String, ?_groupHint:Int = 1)
Native_TracyProfiler.setThreadName(_name, _groupHint);
@:inheritDoc(cpp.vm.tracy.Native_TracyProfiler.zoneScoped)
public static function zoneScoped(_name:String)
Native_TracyProfiler.zoneScoped(_name);
}
typedef TracyProfiler = Cppia_TracyProfiler;
#else
typedef TracyProfiler = Native_TracyProfiler;
#end
+161
View File
@@ -0,0 +1,161 @@
package funkin;
import openfl.utils.Future;
/**
* A wrapper around `openfl.utils.Assets` which disallows access to the harmful functions.
* Later we'll add Funkin-specific caching to this.
*/
class Assets
{
/**
* Get the file system path for an asset
* @param path The asset path to load from, relative to the assets folder
* @return The path to the asset on the file system
*/
public static function getPath(path:String):String
{
return openfl.utils.Assets.getPath(path);
}
/**
* Load bytes from an asset
* May cause stutters or throw an error if the asset is not cached
* @param path The asset path to load from
* @return The byte contents of the file
*/
public static function getBytes(path:String):haxe.io.Bytes
{
return openfl.utils.Assets.getBytes(path);
}
/**
* Load bytes from an asset asynchronously
* @param path The asset path to load from
* @return A future which promises to return the byte contents of the file
*/
public static function loadBytes(path:String):Future<openfl.utils.ByteArray>
{
return openfl.utils.Assets.loadBytes(path);
}
/**
* Load text from an asset.
* May cause stutters or throw an error if the asset is not cached
* @param path The asset path to load from
* @return The text contents of the file
*/
public static function getText(path:String):String
{
return openfl.utils.Assets.getText(path);
}
/**
* Load text from an asset asynchronously
* @param path The asset path to load from
* @return A future which promises to return the text contents of the file
*/
public static function loadText(path:String):Future<String>
{
return openfl.utils.Assets.loadText(path);
}
/**
* Load a Sound file from an asset
* May cause stutters or throw an error if the asset is not cached
* @param path The asset path to load from
* @return The loaded sound
*/
public static function getSound(path:String):openfl.media.Sound
{
return openfl.utils.Assets.getSound(path);
}
/**
* Load a Sound file from an asset asynchronously
* @param path The asset path to load from
* @return A future which promises to return the loaded sound
*/
public static function loadSound(path:String):Future<openfl.media.Sound>
{
return openfl.utils.Assets.loadSound(path);
}
/**
* Load a Sound file from an asset, with optimizations specific to long-duration music
* May cause stutters or throw an error if the asset is not cached
* @param path The asset path to load from
* @return The loaded sound
*/
public static function getMusic(path:String):openfl.media.Sound
{
return openfl.utils.Assets.getMusic(path);
}
/**
* Load a Sound file from an asset asynchronously, with optimizations specific to long-duration music
* @param path The asset path to load from
* @return A future which promises to return the loaded sound
*/
public static function loadMusic(path:String):Future<openfl.media.Sound>
{
return openfl.utils.Assets.loadMusic(path);
}
/**
* Load a Bitmap from an asset
* May cause stutters or throw an error if the asset is not cached
* @param path The asset path to load from
* @return The loaded Bitmap image
*/
public static function getBitmapData(path:String):openfl.display.BitmapData
{
return openfl.utils.Assets.getBitmapData(path);
}
/**
* Load a Bitmap from an asset asynchronously
* @param path The asset path to load from
* @return The future which promises to return the loaded Bitmap image
*/
public static function loadBitmapData(path:String):Future<openfl.display.BitmapData>
{
return openfl.utils.Assets.loadBitmapData(path);
}
/**
* Determines whether the given asset of the given type exists.
* @param path The asset path to check
* @param type The asset type to check
* @return Whether the asset exists
*/
public static function exists(path:String, ?type:openfl.utils.AssetType):Bool
{
return openfl.utils.Assets.exists(path, type);
}
/**
* Retrieve a list of all assets of the given type
* @param type The asset type to check
* @return A list of asset paths
*/
public static function list(?type:openfl.utils.AssetType):Array<String>
{
return openfl.utils.Assets.list(type);
}
public static function hasLibrary(name:String):Bool
{
return openfl.utils.Assets.hasLibrary(name);
}
public static function getLibrary(name:String):lime.utils.AssetLibrary
{
return openfl.utils.Assets.getLibrary(name);
}
public static function loadLibrary(name:String):Future<openfl.utils.AssetLibrary>
{
return openfl.utils.Assets.loadLibrary(name);
}
}
+26 -6
View File
@@ -275,6 +275,13 @@ class Conductor
return Save.instance.options.audioVisualOffset;
}
public var combinedOffset(get, never):Float;
function get_combinedOffset():Float
{
return instrumentalOffset + audioVisualOffset + inputOffset;
}
/**
* The number of beats in a measure. May be fractional depending on the time signature.
*/
@@ -397,9 +404,12 @@ class Conductor
*/
public function update(?songPos:Float, applyOffsets:Bool = true, forceDispatch:Bool = false)
{
var currentTime:Float = (FlxG.sound.music != null) ? FlxG.sound.music.time : 0.0;
var currentLength:Float = (FlxG.sound.music != null) ? FlxG.sound.music.length : 0.0;
if (songPos == null)
{
songPos = (FlxG.sound.music != null) ? FlxG.sound.music.time : 0.0;
songPos = currentTime;
}
// Take into account instrumental and file format song offsets.
@@ -409,8 +419,17 @@ class Conductor
var oldBeat:Float = this.currentBeat;
var oldStep:Float = this.currentStep;
// If the song is playing, limit the song position to the length of the song or beginning of the song.
if (FlxG.sound.music != null && FlxG.sound.music.playing)
{
this.songPosition = Math.min(currentLength, Math.max(0, songPos));
}
else
{
this.songPosition = songPos;
}
// Set the song position we are at (for purposes of calculating note positions, etc).
this.songPosition = songPos;
currentTimeChange = timeChanges[0];
if (this.songPosition > 0.0)
@@ -430,7 +449,8 @@ class Conductor
else if (currentTimeChange != null && this.songPosition > 0.0)
{
// roundDecimal prevents representing 8 as 7.9999999
this.currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * 4) + (this.songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6);
this.currentStepTime = FlxMath.roundDecimal((currentTimeChange.beatTime * Constants.STEPS_PER_BEAT)
+ (this.songPosition - currentTimeChange.timeStamp) / stepLengthMs, 6);
this.currentBeatTime = currentStepTime / Constants.STEPS_PER_BEAT;
this.currentMeasureTime = currentStepTime / stepsPerMeasure;
this.currentStep = Math.floor(currentStepTime);
@@ -564,7 +584,7 @@ class Conductor
if (ms >= timeChange.timeStamp)
{
lastTimeChange = timeChange;
resultStep = lastTimeChange.beatTime * 4;
resultStep = lastTimeChange.beatTime * Constants.STEPS_PER_BEAT;
}
else
{
@@ -600,7 +620,7 @@ class Conductor
var lastTimeChange:SongTimeChange = timeChanges[0];
for (timeChange in timeChanges)
{
if (stepTime >= timeChange.beatTime * 4)
if (stepTime >= timeChange.beatTime * Constants.STEPS_PER_BEAT)
{
lastTimeChange = timeChange;
resultMs = lastTimeChange.timeStamp;
@@ -613,7 +633,7 @@ class Conductor
}
var lastStepLengthMs:Float = ((Constants.SECS_PER_MIN / lastTimeChange.bpm) * Constants.MS_PER_SEC) / timeSignatureNumerator;
resultMs += (stepTime - lastTimeChange.beatTime * 4) * lastStepLengthMs;
resultMs += (stepTime - lastTimeChange.beatTime * Constants.STEPS_PER_BEAT) * lastStepLengthMs;
return resultMs;
}
+50 -6
View File
@@ -1,5 +1,6 @@
package funkin;
import funkin.data.freeplay.player.PlayerRegistry;
import funkin.ui.debug.charting.ChartEditorState;
import funkin.ui.transition.LoadingState;
import flixel.FlxState;
@@ -18,6 +19,7 @@ import funkin.play.PlayStatePlaylist;
import openfl.display.BitmapData;
import funkin.data.story.level.LevelRegistry;
import funkin.data.notestyle.NoteStyleRegistry;
import funkin.data.freeplay.style.FreeplayStyleRegistry;
import funkin.data.event.SongEventRegistry;
import funkin.data.stage.StageRegistry;
import funkin.data.dialogue.conversation.ConversationRegistry;
@@ -26,13 +28,14 @@ import funkin.data.dialogue.speaker.SpeakerRegistry;
import funkin.data.freeplay.album.AlbumRegistry;
import funkin.data.song.SongRegistry;
import funkin.play.character.CharacterData.CharacterDataParser;
import funkin.play.notes.notekind.NoteKindManager;
import funkin.modding.module.ModuleHandler;
import funkin.ui.title.TitleState;
import funkin.util.CLIUtil;
import funkin.util.CLIUtil.CLIParams;
import funkin.util.TimerUtil;
import funkin.util.TrackerUtil;
#if discord_rpc
#if FEATURE_DISCORD_RPC
import Discord.DiscordClient;
#end
@@ -121,7 +124,7 @@ class InitState extends FlxState
//
// DISCORD API SETUP
//
#if discord_rpc
#if FEATURE_DISCORD_RPC
DiscordClient.initialize();
Application.current.onExit.add(function(exitCode) {
@@ -142,7 +145,7 @@ class InitState extends FlxState
// Plugins provide a useful interface for globally active Flixel objects,
// that receive update events regardless of the current state.
// TODO: Move scripted Module behavior to a Flixel plugin.
#if debug
#if FEATURE_DEBUG_FUNCTIONS
funkin.util.plugins.MemoryGCPlugin.initialize();
#end
funkin.util.plugins.EvacuateDebugPlugin.initialize();
@@ -164,9 +167,11 @@ class InitState extends FlxState
SongRegistry.instance.loadEntries();
LevelRegistry.instance.loadEntries();
NoteStyleRegistry.instance.loadEntries();
PlayerRegistry.instance.loadEntries();
ConversationRegistry.instance.loadEntries();
DialogueBoxRegistry.instance.loadEntries();
SpeakerRegistry.instance.loadEntries();
FreeplayStyleRegistry.instance.loadEntries();
AlbumRegistry.instance.loadEntries();
StageRegistry.instance.loadEntries();
@@ -174,6 +179,8 @@ class InitState extends FlxState
// Move it to use a BaseRegistry.
CharacterDataParser.loadCharacterCache();
NoteKindManager.loadScripts();
ModuleHandler.buildModuleCallbacks();
ModuleHandler.loadModuleCache();
ModuleHandler.callOnCreate();
@@ -214,6 +221,38 @@ class InitState extends FlxState
#elseif STAGEBUILD
// -DSTAGEBUILD
FlxG.switchState(() -> new funkin.ui.debug.stage.StageBuilderState());
#elseif RESULTS
// -DRESULTS
FlxG.switchState(() -> new funkin.play.ResultState(
{
storyMode: true,
title: "Cum Song Erect by Kawai Sprite",
songId: "cum",
characterId: "pico-playable",
difficultyId: "nightmare",
isNewHighscore: true,
scoreData:
{
score: 1_234_567,
tallies:
{
sick: 130,
good: 60,
bad: 69,
shit: 69,
missed: 69,
combo: 69,
maxCombo: 69,
totalNotesHit: 140,
totalNotes: 190
}
// 2400 total notes = 7% = LOSS
// 240 total notes = 79% = GOOD
// 230 total notes = 82% = GREAT
// 210 total notes = 91% = EXCELLENT
// 190 total notes = PERFECT
},
}));
#elseif ANIMDEBUG
// -DANIMDEBUG
FlxG.switchState(() -> new funkin.ui.debug.anim.DebugBoundingState());
@@ -337,11 +376,16 @@ class InitState extends FlxState
//
// FLIXEL DEBUG SETUP
//
#if (debug || FORCE_DEBUG_VERSION)
// Make errors and warnings less annoying.
// Forcing this always since I have never been happy to have the debugger to pop up
#if FEATURE_DEBUG_FUNCTIONS
trace('Initializing Flixel debugger...');
#if !debug
// Make errors less annoying on release builds.
LogStyle.ERROR.openConsole = false;
LogStyle.ERROR.errorSound = null;
#end
// Make errors and warnings less annoying.
LogStyle.WARNING.openConsole = false;
LogStyle.WARNING.errorSound = null;
+29 -7
View File
@@ -2,7 +2,6 @@ package funkin;
import flixel.graphics.frames.FlxAtlasFrames;
import openfl.utils.AssetType;
import openfl.utils.Assets as OpenFlAssets;
/**
* A core class which handles determining asset paths.
@@ -11,9 +10,16 @@ class Paths
{
static var currentLevel:Null<String> = null;
public static function setCurrentLevel(name:String):Void
public static function setCurrentLevel(name:Null<String>):Void
{
currentLevel = name.toLowerCase();
if (name == null)
{
currentLevel = null;
}
else
{
currentLevel = name.toLowerCase();
}
}
public static function stripLibrary(path:String):String
@@ -37,11 +43,11 @@ class Paths
if (currentLevel != null)
{
var levelPath:String = getLibraryPathForce(file, currentLevel);
if (OpenFlAssets.exists(levelPath, type)) return levelPath;
if (Assets.exists(levelPath, type)) return levelPath;
}
var levelPath:String = getLibraryPathForce(file, 'shared');
if (OpenFlAssets.exists(levelPath, type)) return levelPath;
if (Assets.exists(levelPath, type)) return levelPath;
return getPreloadPath(file);
}
@@ -123,9 +129,17 @@ class Paths
return 'songs:assets/songs/${song.toLowerCase()}/Voices$suffix.${Constants.EXT_SOUND}';
}
public static function inst(song:String, ?suffix:String = ''):String
/**
* Gets the path to an `Inst.mp3/ogg` song instrumental from songs:assets/songs/`song`/
* @param song name of the song to get instrumental for
* @param suffix any suffix to add to end of song name, used for `-erect` variants usually
* @param withExtension if it should return with the audio file extension `.mp3` or `.ogg`.
* @return String
*/
public static function inst(song:String, ?suffix:String = '', ?withExtension:Bool = true):String
{
return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix.${Constants.EXT_SOUND}';
var ext:String = withExtension ? '.${Constants.EXT_SOUND}' : '';
return 'songs:assets/songs/${song.toLowerCase()}/Inst$suffix$ext';
}
public static function image(key:String, ?library:String):String
@@ -153,3 +167,11 @@ class Paths
return FlxAtlasFrames.fromSpriteSheetPacker(image(key, library), file('images/$key.txt', library));
}
}
enum abstract PathsFunction(String)
{
var MUSIC;
var INST;
var VOICES;
var SOUND;
}
+82
View File
@@ -7,6 +7,35 @@ import funkin.save.Save;
*/
class Preferences
{
/**
* FPS
* @default `60`
*/
public static var framerate(get, set):Int;
static function get_framerate():Int
{
#if web
return 60;
#else
return Save?.instance?.options?.framerate ?? 60;
#end
}
static function set_framerate(value:Int):Int
{
#if web
return 60;
#else
var save:Save = Save.instance;
save.options.framerate = value;
save.flush();
FlxG.updateFramerate = value;
FlxG.drawFramerate = value;
return value;
#end
}
/**
* Whether some particularly fowl language is displayed.
* @default `true`
@@ -128,6 +157,48 @@ class Preferences
return value;
}
public static var unlockedFramerate(get, set):Bool;
static function get_unlockedFramerate():Bool
{
return Save?.instance?.options?.unlockedFramerate;
}
static function set_unlockedFramerate(value:Bool):Bool
{
if (value != Save.instance.options.unlockedFramerate)
{
#if web
toggleFramerateCap(value);
#end
}
var save:Save = Save.instance;
save.options.unlockedFramerate = value;
save.flush();
return value;
}
#if web
// We create a haxe version of this just for readability.
// We use these to override `window.requestAnimationFrame` in Javascript to uncap the framerate / "animation" request rate
// Javascript is crazy since u can just do stuff like that lol
public static function unlockedFramerateFunction(callback, element)
{
var currTime = Date.now().getTime();
var timeToCall = 0;
var id = js.Browser.window.setTimeout(function() {
callback(currTime + timeToCall);
}, timeToCall);
return id;
}
// Lime already implements their own little framerate cap, so we can just use that
// This also gets set in the init function in Main.hx, since we need to definitely override it
public static var lockedFramerateFunction = untyped js.Syntax.code("window.requestAnimationFrame");
#end
/**
* Loads the user's preferences from the save data and apply them.
*/
@@ -137,6 +208,17 @@ class Preferences
FlxG.autoPause = Preferences.autoPause;
// Apply the debugDisplay setting (enables the FPS and RAM display).
toggleDebugDisplay(Preferences.debugDisplay);
#if web
toggleFramerateCap(Preferences.unlockedFramerate);
#end
}
static function toggleFramerateCap(unlocked:Bool):Void
{
#if web
var framerateFunction = unlocked ? unlockedFramerateFunction : lockedFramerateFunction;
untyped js.Syntax.code("window.requestAnimationFrame = framerateFunction;");
#end
}
static function toggleDebugDisplay(show:Bool):Void
+2 -2
View File
@@ -1,13 +1,13 @@
package funkin.api.discord;
import Sys.sleep;
#if discord_rpc
#if FEATURE_DISCORD_RPC
import discord_rpc.DiscordRpc;
#end
class DiscordClient
{
#if discord_rpc
#if FEATURE_DISCORD_RPC
public function new()
{
trace("Discord Client starting...");
+3 -7
View File
@@ -1,9 +1,5 @@
package funkin.api.newgrounds;
import flixel.util.FlxSignal;
import flixel.util.FlxTimer;
import lime.app.Application;
import openfl.display.Stage;
#if newgrounds
import io.newgrounds.NG;
import io.newgrounds.NGLite;
@@ -28,7 +24,7 @@ class NGUnsafe
NG.core.calls.event.logEvent(event).send();
trace('should have logged: ' + event);
#else
#if debug
#if FEATURE_DEBUG_FUNCTIONS
trace('event:$event - not logged, missing NG.io lib');
#end
#end
@@ -43,7 +39,7 @@ class NGUnsafe
if (!medal.unlocked) medal.sendUnlock();
}
#else
#if debug
#if FEATURE_DEBUG_FUNCTIONS
trace('medal:$id - not unlocked, missing NG.io lib');
#end
#end
@@ -67,7 +63,7 @@ class NGUnsafe
}
}
#else
#if debug
#if FEATURE_DEBUG_FUNCTIONS
trace('Song:$song, Score:$score - not posted, missing NG.io lib');
#end
#end
+3 -11
View File
@@ -2,19 +2,11 @@ package funkin.api.newgrounds;
#if newgrounds
import flixel.util.FlxSignal;
import flixel.util.FlxTimer;
import io.newgrounds.NG;
import io.newgrounds.NGLite;
import io.newgrounds.components.ScoreBoardComponent.Period;
import io.newgrounds.objects.Error;
import io.newgrounds.objects.Medal;
import io.newgrounds.objects.Score;
import io.newgrounds.objects.ScoreBoard;
import io.newgrounds.objects.events.Response;
import io.newgrounds.objects.events.Result.GetCurrentVersionResult;
import io.newgrounds.objects.events.Result.GetVersionResult;
import lime.app.Application;
import openfl.display.Stage;
#end
/**
@@ -247,7 +239,7 @@ class NGio
NG.core.calls.event.logEvent(event).send();
trace('should have logged: ' + event);
#else
#if debug
#if FEATURE_DEBUG_FUNCTIONS
trace('event:$event - not logged, missing NG.io lib');
#end
#end
@@ -262,7 +254,7 @@ class NGio
if (!medal.unlocked) medal.sendUnlock();
}
#else
#if debug
#if FEATURE_DEBUG_FUNCTIONS
trace('medal:$id - not unlocked, missing NG.io lib');
#end
#end
@@ -286,7 +278,7 @@ class NGio
}
}
#else
#if debug
#if FEATURE_DEBUG_FUNCTIONS
trace('Song:$song, Score:$score - not posted, missing NG.io lib');
#end
#end
+156 -16
View File
@@ -11,10 +11,14 @@ import funkin.audio.waveform.WaveformDataParser;
import funkin.data.song.SongData.SongMusicData;
import funkin.data.song.SongRegistry;
import funkin.util.tools.ICloneable;
import funkin.util.flixel.sound.FlxPartialSound;
import funkin.Paths.PathsFunction;
import openfl.Assets;
import lime.app.Future;
import lime.app.Promise;
import openfl.media.SoundMixer;
#if (openfl >= "8.0.0")
import openfl.utils.AssetType;
#end
/**
@@ -223,12 +227,12 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
// already paused before we lost focus.
if (_lostFocus && !_alreadyPaused)
{
trace('Resuming audio (${this._label}) on focus!');
// trace('Resuming audio (${this._label}) on focus!');
resume();
}
else
{
trace('Not resuming audio (${this._label}) on focus!');
// trace('Not resuming audio (${this._label}) on focus!');
}
_lostFocus = false;
}
@@ -238,7 +242,7 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
*/
override function onFocusLost():Void
{
trace('Focus lost, pausing audio!');
// trace('Focus lost, pausing audio!');
_lostFocus = true;
_alreadyPaused = _paused;
pause();
@@ -336,29 +340,86 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
if (songMusicData != null)
{
Conductor.instance.mapTimeChanges(songMusicData.timeChanges);
if (songMusicData.looped != null && params.loop == null) params.loop = songMusicData.looped;
}
else
{
FlxG.log.warn('Tried and failed to find music metadata for $key');
}
}
var music = FunkinSound.load(Paths.music('$key/$key'), params?.startingVolume ?? 1.0, params.loop ?? true, false, true);
if (music != null)
var pathsFunction = params.pathsFunction ?? MUSIC;
var suffix = params.suffix ?? '';
var pathToUse = switch (pathsFunction)
{
FlxG.sound.music = music;
case MUSIC: Paths.music('$key/$key');
case INST: Paths.inst('$key', suffix);
default: Paths.music('$key/$key');
}
// Prevent repeat update() and onFocus() calls.
FlxG.sound.list.remove(FlxG.sound.music);
var shouldLoadPartial = params.partialParams?.loadPartial ?? false;
return true;
// 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);
if (music != null)
{
partialQueue.push(music);
@:nullSafety(Off)
music.future.onComplete(function(partialMusic:Null<FunkinSound>) {
FlxG.sound.music = partialMusic;
FlxG.sound.list.remove(FlxG.sound.music);
if (FlxG.sound.music != null && params.onLoad != null) params.onLoad();
});
return true;
}
else
{
return false;
}
}
else
{
return false;
var music = FunkinSound.load(pathToUse, params?.startingVolume ?? 1.0, params.loop ?? true, false, true, params.onComplete);
if (music != null)
{
FlxG.sound.music = music;
// Prevent repeat update() and onFocus() calls.
FlxG.sound.list.remove(FlxG.sound.music);
if (FlxG.sound.music != null && params.onLoad != null) params.onLoad();
return true;
}
else
{
return false;
}
}
}
public static function emptyPartialQueue():Void
{
while (partialQueue.length > 0)
{
@:nullSafety(Off)
partialQueue.pop().error("Cancel loading partial sound");
}
}
static var partialQueue:Array<Promise<Null<FunkinSound>>> = [];
/**
* Creates a new `FunkinSound` object synchronously.
*
@@ -415,6 +476,51 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
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)
public override function destroy():Void
{
@@ -433,21 +539,23 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
* Play a sound effect once, then destroy it.
* @param key
* @param volume
* @return static function construct():FunkinSound
* @return A `FunkinSound` object, or `null` if the sound could not be loaded.
*/
public static function playOnce(key:String, volume:Float = 1.0, ?onComplete:Void->Void, ?onLoad:Void->Void):Void
public static function playOnce(key:String, volume:Float = 1.0, ?onComplete:Void->Void, ?onLoad:Void->Void):Null<FunkinSound>
{
var result = FunkinSound.load(key, volume, false, true, true, onComplete, onLoad);
var result:Null<FunkinSound> = FunkinSound.load(key, volume, false, true, true, onComplete, onLoad);
return result;
}
/**
* Stop all sounds in the pool and allow them to be recycled.
*/
public static function stopAllAudio(musicToo:Bool = false):Void
public static function stopAllAudio(musicToo:Bool = false, persistToo:Bool = false):Void
{
for (sound in pool)
{
if (sound == null) continue;
if (!persistToo && sound.persist) continue;
if (!musicToo && sound == FlxG.sound.music) continue;
sound.destroy();
}
@@ -462,6 +570,14 @@ class FunkinSound extends FlxSound implements ICloneable<FunkinSound>
return sound;
}
/**
* Produces a string representation suitable for debugging.
*/
public override function toString():String
{
return 'FunkinSound(${this._label})';
}
}
/**
@@ -475,6 +591,12 @@ typedef FunkinSoundPlayMusicParams =
*/
var ?startingVolume:Float;
/**
* The suffix of the music file to play. Usually for "-erect" tracks when loading an INST file
* @default ``
*/
var ?suffix:String;
/**
* Whether to override music if a different track is already playing.
* @default `false`
@@ -498,4 +620,22 @@ typedef FunkinSoundPlayMusicParams =
* @default `true`
*/
var ?mapTimeChanges:Bool;
/**
* Which Paths function to use to load a song
* @default `MUSIC`
*/
var ?pathsFunction:PathsFunction;
var ?partialParams:PartialSoundParams;
var ?onComplete:Void->Void;
var ?onLoad:Void->Void;
}
typedef PartialSoundParams =
{
var loadPartial:Bool;
var start:Float;
var end:Float;
}
+5
View File
@@ -113,6 +113,11 @@ class SoundGroup extends FlxTypedGroup<FunkinSound>
public function play(forceRestart:Bool = false, startTime:Float = 0.0, ?endTime:Float)
{
forEachAlive(function(sound:FunkinSound) {
if (sound.length < startTime)
{
// trace('Queuing sound (${sound.toString()} past its length! Skipping...)');
return;
}
sound.play(forceRestart, startTime, endTime);
});
}
-2
View File
@@ -1,9 +1,7 @@
package funkin.audio;
import funkin.audio.FunkinSound;
import flixel.group.FlxGroup.FlxTypedGroup;
import funkin.audio.waveform.WaveformData;
import funkin.audio.waveform.WaveformDataParser;
class VoicesGroup extends SoundGroup
{
+16 -9
View File
@@ -1,13 +1,9 @@
package funkin.audio.visualize;
import funkin.audio.visualize.dsp.FFT;
import flixel.FlxSprite;
import flixel.addons.plugin.taskManager.FlxTask;
import flixel.graphics.frames.FlxAtlasFrames;
import flixel.group.FlxSpriteGroup.FlxTypedSpriteGroup;
import flixel.math.FlxMath;
import flixel.sound.FlxSound;
import funkin.util.MathUtil;
import funkin.vis.dsp.SpectralAnalyzer;
import funkin.vis.audioclip.frontends.LimeAudioClip;
@@ -58,8 +54,15 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
public function initAnalyzer()
{
@:privateAccess
analyzer = new SpectralAnalyzer(7, new LimeAudioClip(cast snd._channel.__source), 0.01, 30);
analyzer.maxDb = -35;
analyzer = new SpectralAnalyzer(snd._channel.__audioSource, 7, 0.1, 40);
#if desktop
// On desktop it uses FFT stuff that isn't as optimized as the direct browser stuff we use on HTML5
// So we want to manually change it!
analyzer.fftN = 256;
#end
// analyzer.maxDb = -35;
// analyzer.fftN = 2048;
}
@@ -83,9 +86,7 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
override function draw()
{
#if web
if (analyzer != null) drawFFT();
#end
super.draw();
}
@@ -94,12 +95,18 @@ class ABotVis extends FlxTypedSpriteGroup<FlxSprite>
*/
function drawFFT():Void
{
var levels = analyzer.getLevels(false);
var levels = analyzer.getLevels();
for (i in 0...min(group.members.length, levels.length))
{
var animFrame:Int = Math.round(levels[i].value * 5);
#if desktop
// Web version scales with the Flixel volume level.
// This line brings platform parity but looks worse.
// animFrame = Math.round(animFrame * FlxG.sound.volume);
#end
animFrame = Math.floor(Math.min(5, animFrame));
animFrame = Math.floor(Math.max(0, animFrame));
@@ -1,6 +1,5 @@
package funkin.audio.visualize;
import funkin.audio.visualize.PolygonSpectogram;
import flixel.group.FlxGroup.FlxTypedGroup;
import flixel.sound.FlxSound;
@@ -8,8 +8,6 @@ import flixel.sound.FlxSound;
import flixel.util.FlxColor;
import funkin.audio.visualize.PolygonSpectogram.VISTYPE;
import funkin.audio.visualize.VisShit.CurAudioInfo;
import funkin.audio.visualize.dsp.FFT;
import lime.system.ThreadPool;
import lime.utils.Int16Array;
using Lambda;
@@ -38,8 +36,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
lengthOfShit = amnt;
regenLineShit();
// makeGraphic(200, 200, FlxColor.BLACK);
}
public function regenLineShit():Void
@@ -89,8 +85,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
{
checkAndSetBuffer();
// vis.checkAndSetBuffer();
if (setBuffer)
{
var samplesToGen:Int = Std.int(sampleRate * seconds);
@@ -191,7 +185,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
// a value between 10hz and 100Khz
var hzPicker:Float = Math.pow(10, powedShit);
// var sampleApprox:Int = Std.int(FlxMath.remapToRange(i, 0, group.members.length, startingSample, startingSample + samplesToGen));
var remappedFreq:Int = Std.int(FlxMath.remapToRange(hzPicker, 0, 10000, 0, freqShit[0].length - 1));
group.members[i].x = prevLine.x;
@@ -211,8 +204,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
var line = FlxPoint.get(prevLine.x - group.members[i].x, prevLine.y - group.members[i].y);
// dont draw a line until i figure out a nicer way to view da spikes and shit idk lol!
// group.members[i].setGraphicSize(Std.int(Math.max(line.length, 1)), Std.int(1));
// group.members[i].angle = line.degrees;
}
}
}
@@ -261,9 +252,6 @@ class SpectogramSprite extends FlxTypedSpriteGroup<FlxSprite>
group.members[Std.int(remappedSample)].x = prevLine.x;
group.members[Std.int(remappedSample)].y = prevLine.y;
// group.members[0].y = prevLine.y;
// FlxSpriteUtil.drawLine(this, prevLine.x, prevLine.y, width * remappedSample, left * height / 2 + height / 2);
prevLine.x = (curAud.balanced * swagheight / 2 + swagheight / 2) + x;
prevLine.y = (Std.int(remappedSample) / lengthOfShit * daHeight) + y;
+1 -6
View File
@@ -3,7 +3,6 @@ package funkin.audio.visualize;
import flixel.math.FlxMath;
import flixel.sound.FlxSound;
import funkin.audio.visualize.dsp.FFT;
import lime.system.ThreadPool;
import lime.utils.Int16Array;
import funkin.util.MathUtil;
@@ -73,9 +72,6 @@ class VisShit
freqOutput.push([]);
// if (FlxG.keys.justPressed.M)
// trace(FFT.rfft(chunk).map(z -> z.scale(1 / fs).magnitude));
// find spectral peaks and their instantaneous frequencies
for (k => s in freqs)
{
@@ -91,7 +87,6 @@ class VisShit
if (freq < maxFreq) freqOutput[indexOfArray].push(power);
//
}
// haxe.Log.trace("", null);
indexOfArray++;
// move to next (overlapping) chunk
@@ -122,7 +117,7 @@ class VisShit
{
// Math.pow3
@:privateAccess
var buf = snd._channel.__source.buffer;
var buf = snd._channel.__audioSource.buffer;
// @:privateAccess
audioData = cast buf.data; // jank and hacky lol! kinda busted on HTML5 also!!
-2
View File
@@ -1,7 +1,5 @@
package funkin.audio.visualize.dsp;
import funkin.audio.visualize.dsp.Complex;
using funkin.audio.visualize.dsp.OffsetArray;
using funkin.audio.visualize.dsp.Signal;
@@ -1,7 +1,5 @@
package funkin.audio.waveform;
import funkin.util.MathUtil;
@:nullSafety
class WaveformData
{
@@ -16,7 +16,7 @@ class WaveformDataParser
// Method 1. This only works if the sound has been played before.
@:privateAccess
var soundBuffer:Null<lime.media.AudioBuffer> = sound?._channel?.__source?.buffer;
var soundBuffer:Null<lime.media.AudioBuffer> = sound?._channel?.__audioSource?.buffer;
if (soundBuffer == null)
{
@@ -1,7 +1,5 @@
package funkin.audio.waveform;
import funkin.audio.waveform.WaveformData;
import funkin.audio.waveform.WaveformDataParser;
import funkin.graphics.rendering.MeshRender;
import flixel.util.FlxColor;
+1 -1
View File
@@ -263,7 +263,7 @@ abstract class BaseRegistry<T:(IRegistryEntry<J> & Constructible<EntryConstructo
* @param version The entry's version (use `fetchEntryVersion(id)`).
* @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)
{
@@ -1,7 +1,5 @@
package funkin.data.dialogue.conversation;
import funkin.data.animation.AnimationData;
/**
* A type definition for the data for a specific conversation.
* It includes things like what dialogue boxes to use, what text to display, and what animations to play.
@@ -1,7 +1,6 @@
package funkin.data.dialogue.conversation;
import funkin.play.cutscene.dialogue.Conversation;
import funkin.data.dialogue.conversation.ConversationData;
import funkin.play.cutscene.dialogue.ScriptedConversation;
class ConversationRegistry extends BaseRegistry<Conversation, ConversationData>
@@ -46,7 +46,7 @@ class SongEventRegistry
if (event != null)
{
trace(' Loaded built-in song event: (${event.id})');
trace(' Loaded built-in song event: ${event.id}');
eventCache.set(event.id, event);
}
else
@@ -59,9 +59,9 @@ class SongEventRegistry
static function registerScriptedEvents()
{
var scriptedEventClassNames:Array<String> = ScriptedSongEvent.listScriptClasses();
trace('Instantiating ${scriptedEventClassNames.length} scripted song events...');
if (scriptedEventClassNames == null || scriptedEventClassNames.length == 0) return;
trace('Instantiating ${scriptedEventClassNames.length} scripted song events...');
for (eventCls in scriptedEventClassNames)
{
var event:SongEvent = ScriptedSongEvent.init(eventCls, "UKNOWN");
@@ -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.
@@ -0,0 +1,400 @@
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>;
/**
* The GF name to assign for this character.
*/
@:optional
public var gf:PlayerCharSelectGFData;
}
typedef PlayerCharSelectGFData =
{
@:optional
public var assetPath:String;
@:optional
public var animInfoPath:String;
@:optional
@:default(false)
public var visualizer:Bool;
}
typedef PlayerResultsData =
{
var music:PlayerResultsMusicData;
var perfectGold:Array<PlayerResultsAnimationData>;
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;
};
@@ -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;
});
}
}
@@ -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.
@@ -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;
}
@@ -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();
}
}
+31
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.
+95 -1
View File
@@ -74,6 +74,84 @@ typedef NoteStyleAssetsData =
*/
@:optional
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
var isPixel:Bool;
/**
* If true, animations will be played on the graphic.
* @default `false` to save performance.
*/
@:default(false)
@:optional
var animated:Bool;
/**
* The structure of this data depends on the asset.
*/
var data:T;
@:optional
var data:Null<T>;
}
typedef NoteStyleData_Note =
@@ -123,7 +210,14 @@ typedef NoteStyleData_Note =
var right:UnnamedAnimationData;
}
typedef NoteStyleData_Countdown =
{
var audioPath:String;
}
typedef NoteStyleData_HoldNote = {}
typedef NoteStyleData_Judgement = {}
typedef NoteStyleData_ComboNum = {}
/**
* Data on animations for each direction of the strumline.
@@ -11,9 +11,9 @@ class NoteStyleRegistry extends BaseRegistry<NoteStyle, NoteStyleData>
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateNoteStyleData()` function.
*/
public static final NOTE_STYLE_DATA_VERSION:thx.semver.Version = "1.0.0";
public static final NOTE_STYLE_DATA_VERSION:thx.semver.Version = "1.1.0";
public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.0.x";
public static final NOTE_STYLE_DATA_VERSION_RULE:thx.semver.VersionRule = "1.1.x";
public static var instance(get, never):NoteStyleRegistry;
static var _instance:Null<NoteStyleRegistry> = null;
+13
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/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.2.4]
### Added
- Added `playData.characters.opponentVocals` to specify which vocal track(s) to play for the opponent.
- If the value isn't present, it will use the `playData.characters.opponent`, but if it is present, it will be used (even if it's empty, in which case no vocals will be used for the opponent)
- Added `playData.characters.playerVocals` to specify which vocal track(s) to play for the player.
- If the value isn't present, it will use the `playData.characters.player`, but if it is present, it will be used (even if it's empty, in which case no vocals will be used for the player)
- Added `offsets.altVocals` field to apply vocal offsets when alternate instrumentals are used.
## [2.2.3]
### Added
- Added `charter` field to denote authorship of a chart.
## [2.2.2]
### Added
- Added `playData.previewStart` and `playData.previewEnd` fields to specify when in the song should the song's audio should be played as a preview in Freeplay.
+141 -37
View File
@@ -30,6 +30,9 @@ class SongMetadata implements ICloneable<SongMetadata>
@:default("Unknown")
public var artist:String;
@:optional
public var charter:Null<String> = null;
@:optional
@:default(96)
public var divisions:Null<Int>; // Optional field
@@ -53,6 +56,8 @@ class SongMetadata implements ICloneable<SongMetadata>
@:default(funkin.data.song.SongRegistry.DEFAULT_GENERATEDBY)
public var generatedBy:String;
@:optional
@:default(funkin.data.song.SongData.SongTimeFormat.MILLISECONDS)
public var timeFormat:SongTimeFormat;
public var timeChanges:Array<SongTimeChange>;
@@ -112,14 +117,23 @@ class SongMetadata implements ICloneable<SongMetadata>
*/
public function serialize(pretty:Bool = true):String
{
// Update generatedBy and version before writing.
updateVersionToLatest();
var ignoreNullOptionals = true;
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();
// output.variation = null; // Not sure how to make a field optional on the reader and ignored on the writer.
return writer.write(this, pretty ? ' ' : null);
}
public function updateVersionToLatest():Void
{
this.version = SongRegistry.SONG_METADATA_VERSION;
this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
}
/**
* Produces a string representation suitable for debugging.
*/
@@ -243,18 +257,27 @@ class SongOffsets implements ICloneable<SongOffsets>
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.
*/
@:optional
@:default([])
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.altInstrumentals = altInstrumentals == null ? new Map<String, Float>() : altInstrumentals;
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
@@ -279,11 +302,19 @@ class SongOffsets implements ICloneable<SongOffsets>
return value;
}
public function getVocalOffset(charId:String):Float
public function getVocalOffset(charId:String, ?instrumental:String):Float
{
if (!this.vocals.exists(charId)) return 0.0;
return this.vocals.get(charId);
if (instrumental == null)
{
if (!this.vocals.exists(charId)) return 0.0;
return this.vocals.get(charId);
}
else
{
if (!this.altVocals.exists(instrumental)) return 0.0;
if (!this.altVocals.get(instrumental).exists(charId)) return 0.0;
return this.altVocals.get(instrumental).get(charId);
}
}
public function setVocalOffset(charId:String, value:Float):Float
@@ -306,7 +337,7 @@ class SongOffsets implements ICloneable<SongOffsets>
*/
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;
}
public function updateVersionToLatest():Void
{
this.version = SongRegistry.SONG_MUSIC_DATA_VERSION;
this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
}
public function clone():SongMusicData
{
var result:SongMusicData = new SongMusicData(this.songName, this.artist, this.variation);
@@ -509,12 +546,26 @@ class SongCharacterData implements ICloneable<SongCharacterData>
@:default([])
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.girlfriend = girlfriend;
this.opponent = opponent;
this.instrumental = instrumental;
this.altInstrumentals = altInstrumentals;
this.opponentVocals = opponentVocals;
this.playerVocals = playerVocals;
if (opponentVocals == null) this.opponentVocals = [opponent];
if (playerVocals == null) this.playerVocals = [player];
}
public function clone():SongCharacterData
@@ -600,11 +651,20 @@ class SongChartData implements ICloneable<SongChartData>
*/
public function serialize(pretty:Bool = true):String
{
// Update generatedBy and version before writing.
updateVersionToLatest();
var ignoreNullOptionals = true;
var writer = new json2object.JsonWriter<SongChartData>(ignoreNullOptionals);
return writer.write(this, pretty ? ' ' : null);
}
public function updateVersionToLatest():Void
{
this.version = SongRegistry.SONG_CHART_DATA_VERSION;
this.generatedBy = SongRegistry.DEFAULT_GENERATEDBY;
}
public function clone():SongChartData
{
// We have to manually perform the deep clone here because Map.deepClone() doesn't work.
@@ -693,18 +753,6 @@ class SongEventDataRaw implements ICloneable<SongEventDataRaw>
{
return new SongEventDataRaw(this.time, this.eventKind, this.value);
}
}
/**
* Wrap SongEventData in an abstract so we can overload operators.
*/
@:forward(time, eventKind, value, activated, getStepTime, clone)
abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
{
public function new(time:Float, eventKind:String, value:Dynamic = null)
{
this = new SongEventDataRaw(time, eventKind, value);
}
public function valueAsStruct(?defaultKey:String = "key"):Dynamic
{
@@ -728,27 +776,27 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
}
}
public inline function getHandler():Null<SongEvent>
public function getHandler():Null<SongEvent>
{
return SongEventRegistry.getEvent(this.eventKind);
}
public inline function getSchema():Null<SongEventSchema>
public function getSchema():Null<SongEventSchema>
{
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);
}
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);
}
public inline function getInt(key:String):Null<Int>
public function getInt(key:String):Null<Int>
{
if (this.value == null) return null;
var result = Reflect.field(this.value, key);
@@ -758,7 +806,7 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return cast result;
}
public inline function getFloat(key:String):Null<Float>
public function getFloat(key:String):Null<Float>
{
if (this.value == null) return null;
var result = Reflect.field(this.value, key);
@@ -768,17 +816,17 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return cast result;
}
public inline function getString(key:String):String
public function getString(key:String):String
{
return this.value == null ? null : cast Reflect.field(this.value, key);
}
public inline function getArray(key:String):Array<Dynamic>
public function getArray(key:String):Array<Dynamic>
{
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);
}
@@ -810,6 +858,19 @@ abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataR
return result;
}
}
/**
* Wrap SongEventData in an abstract so we can overload operators.
*/
@:forward(time, eventKind, value, activated, getStepTime, clone, getHandler, getSchema, getDynamic, getBool, getInt, getFloat, getString, getArray,
getBoolArray, buildTooltip, valueAsStruct)
abstract SongEventData(SongEventDataRaw) from SongEventDataRaw to SongEventDataRaw
{
public function new(time:Float, eventKind:String, value:Dynamic = null)
{
this = new SongEventDataRaw(time, eventKind, value);
}
public function clone():SongEventData
{
@@ -922,12 +983,18 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
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.data = data;
this.length = length;
this.kind = kind;
this.params = params ?? [];
}
/**
@@ -1022,9 +1089,19 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
_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
{
return new SongNoteDataRaw(this.time, this.data, this.length, this.kind);
return new SongNoteDataRaw(this.time, this.data, this.length, this.kind, cloneParams());
}
public function toString():String
@@ -1040,9 +1117,9 @@ class SongNoteDataRaw implements ICloneable<SongNoteDataRaw>
@:forward
abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
{
public function new(time:Float, data:Int, length:Float = 0, kind:String = '')
public function new(time:Float, data:Int, length:Float = 0, kind:String = '', ?params:Array<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
@@ -1086,7 +1163,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
if (other.kind == '' || this.kind == null) return false;
}
return this.time == other.time && this.data == other.data && this.length == other.length;
return this.time == other.time && this.data == other.data && this.length == other.length && this.params == other.params;
}
@:op(A != B)
@@ -1105,7 +1182,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
if (other.kind == '') return true;
}
return this.time != other.time || this.data != other.data || this.length != other.length;
return this.time != other.time || this.data != other.data || this.length != other.length || this.params != other.params;
}
@:op(A > B)
@@ -1142,7 +1219,7 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
public function clone():SongNoteData
{
return new SongNoteData(this.time, this.data, this.length, this.kind);
return new SongNoteData(this.time, this.data, this.length, this.kind, this.params);
}
/**
@@ -1154,3 +1231,30 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw
+ (this.kind != '' ? ' [kind: ${this.kind}])' : ')');
}
}
class NoteParamData implements ICloneable<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})';
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ class SongRegistry extends BaseRegistry<Song, SongMetadata>
* Handle breaking changes by incrementing this value
* and adding migration to the `migrateStageData()` function.
*/
public static final SONG_METADATA_VERSION:thx.semver.Version = "2.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";
@@ -61,10 +61,18 @@ class ChartManifestData
*/
public function serialize(pretty:Bool = true):String
{
// Update generatedBy and version before writing.
updateVersionToLatest();
var writer = new json2object.JsonWriter<ChartManifestData>();
return writer.write(this, pretty ? ' ' : null);
}
public function updateVersionToLatest():Void
{
this.version = CHART_MANIFEST_DATA_VERSION;
}
public static function deserialize(contents:String):Null<ChartManifestData>
{
var parser = new json2object.JsonParser<ChartManifestData>();
@@ -36,7 +36,7 @@ class FNFLegacyImporter
{
trace('Migrating song metadata from FNF Legacy.');
var songMetadata:SongMetadata = new SongMetadata('Import', 'Kawai Sprite', 'default');
var songMetadata:SongMetadata = new SongMetadata('Import', Constants.DEFAULT_ARTIST, 'default');
var hadError:Bool = false;
@@ -65,7 +65,7 @@ class FNFLegacyImporter
songMetadata.timeChanges = rebuildTimeChanges(songData);
songMetadata.playData.characters = new SongCharacterData(songData?.song?.player1 ?? 'bf', 'gf', songData?.song?.player2 ?? 'dad', 'mom');
songMetadata.playData.characters = new SongCharacterData(songData?.song?.player1 ?? 'bf', 'gf', songData?.song?.player2 ?? 'dad');
return songMetadata;
}
@@ -199,6 +199,8 @@ class FNFLegacyImporter
{
// Handle the dumb logic for mustHitSection.
var noteData = note.data;
if (noteData < 0) continue; // Exclude Psych event notes.
if (noteData > (STRUMLINE_SIZE * 2)) noteData = noteData % (2 * STRUMLINE_SIZE); // Handle other engine event notes.
// Flip notes if mustHitSection is FALSE (not true lol).
if (!mustHitSection)
+12 -4
View File
@@ -58,9 +58,17 @@ class StageData
*/
public function serialize(pretty:Bool = true):String
{
// Update generatedBy and version before writing.
updateVersionToLatest();
var writer = new json2object.JsonWriter<StageData>();
return writer.write(this, pretty ? ' ' : null);
}
public function updateVersionToLatest():Void
{
this.version = StageRegistry.STAGE_DATA_VERSION;
}
}
typedef StageDataCharacters =
@@ -132,12 +140,12 @@ typedef StageDataProp =
* If not zero, this prop will play an animation every X beats of the song.
* This requires animations to be defined. If `danceLeft` and `danceRight` are defined,
* they will alternated between, otherwise the `idle` animation will be used.
*
* @default 0
* Supports up to 0.25 precision.
* @default 0.0
*/
@:default(0)
@:default(0.0)
@:optional
var danceEvery:Int;
var danceEvery:Float;
/**
* How much the prop scrolls relative to the camera. Used to create a parallax effect.
+2 -2
View File
@@ -93,8 +93,8 @@ class StageRegistry extends BaseRegistry<Stage, StageData>
public function listBaseGameStageIds():Array<String>
{
return [
"mainStage", "spookyMansion", "phillyTrain", "limoRide", "mallXmas", "mallEvil", "school", "schoolEvil", "tankmanBattlefield", "phillyStreets",
"phillyBlazin",
"mainStage", "mainStageErect", "spookyMansion", "phillyTrain", "phillyTrainErect", "limoRide", "limoRideErect", "mallXmas", "mallXmasErect", "mallEvil",
"school", "schoolEvil", "tankmanBattlefield", "phillyStreets", "phillyStreetsErect", "phillyBlazin",
];
}
+5 -3
View File
@@ -91,11 +91,13 @@ typedef LevelPropData =
/**
* The frequency to bop at, in beats.
* @default 1 = every beat, 2 = every other beat, etc.
* 1 = every beat, 2 = every other beat, etc.
* Supports up to 0.25 precision.
* @default 1.0
*/
@:default(1)
@:default(1.0)
@:optional
var danceEvery:Int;
var danceEvery:Float;
/**
* The offset on the position to render the prop at.
+40
View File
@@ -0,0 +1,40 @@
package funkin.effects;
import flixel.addons.effects.FlxTrail;
import funkin.play.stage.Bopper;
import flixel.FlxSprite;
import flixel.system.FlxAssets;
/**
* An offshoot of FlxTrail, but accomodates the way Funkin
* does offsets for characters. example, fixes Spirits trail
*/
class FunkTrail extends FlxTrail
{
/**
* Creates a new FunkTrail effect for a specific FlxSprite.
*
* @param Target The FlxSprite the trail is attached to.
* @param Graphic The image to use for the trailsprites. Optional, uses the sprite's graphic if null.
* @param Length The amount of trailsprites to create.
* @param Delay How often to update the trail. 0 updates every frame.
* @param Alpha The alpha value for the very first trailsprite.
* @param Diff How much lower the alpha of the next trailsprite is.
*/
public function new(Target:FlxSprite, ?Graphic:FlxGraphicAsset, Length:Int = 10, Delay:Int = 3, Alpha:Float = 0.4, Diff:Float = 0.05)
{
super(Target, Graphic, Length, Delay, Alpha, Diff);
}
override public function update(elapsed:Float):Void
{
if (target is Bopper)
{
var targ:Bopper = cast target;
@:privateAccess effectOffset.set((targ.animOffsets[0] - targ.globalOffsets[0]) * targ.scale.x,
(targ.animOffsets[1] - targ.globalOffsets[1]) * targ.scale.y);
}
super.update(elapsed);
}
}
+240
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() {}
}
+106
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);
}
}
+419
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;
}
}
+101
View File
@@ -7,6 +7,10 @@ import flixel.tweens.FlxTween;
import openfl.display3D.textures.TextureBase;
import funkin.graphics.framebuffer.FixedBitmapData;
import openfl.display.BitmapData;
import flixel.math.FlxRect;
import flixel.math.FlxPoint;
import flixel.graphics.frames.FlxFrame;
import flixel.FlxCamera;
/**
* An FlxSprite with additional functionality.
@@ -269,6 +273,103 @@ class FunkinSprite extends FlxSprite
return result;
}
@:access(flixel.FlxCamera)
override function getBoundingBox(camera:FlxCamera):FlxRect
{
getScreenPosition(_point, camera);
_rect.set(_point.x, _point.y, width, height);
_rect = camera.transformRect(_rect);
if (isPixelPerfectRender(camera))
{
_rect.width = _rect.width / this.scale.x;
_rect.height = _rect.height / this.scale.y;
_rect.x = _rect.x / this.scale.x;
_rect.y = _rect.y / this.scale.y;
_rect.floor();
_rect.x = _rect.x * this.scale.x;
_rect.y = _rect.y * this.scale.y;
_rect.width = _rect.width * this.scale.x;
_rect.height = _rect.height * this.scale.y;
}
return _rect;
}
/**
* Returns the screen position of this object.
*
* @param result Optional arg for the returning point
* @param camera The desired "screen" coordinate space. If `null`, `FlxG.camera` is used.
* @return The screen position of this object.
*/
public override function getScreenPosition(?result:FlxPoint, ?camera:FlxCamera):FlxPoint
{
if (result == null) result = FlxPoint.get();
if (camera == null) camera = FlxG.camera;
result.set(x, y);
if (pixelPerfectPosition)
{
_rect.width = _rect.width / this.scale.x;
_rect.height = _rect.height / this.scale.y;
_rect.x = _rect.x / this.scale.x;
_rect.y = _rect.y / this.scale.y;
_rect.round();
_rect.x = _rect.x * this.scale.x;
_rect.y = _rect.y * this.scale.y;
_rect.width = _rect.width * this.scale.x;
_rect.height = _rect.height * this.scale.y;
}
return result.subtract(camera.scroll.x * scrollFactor.x, camera.scroll.y * scrollFactor.y);
}
override function drawSimple(camera:FlxCamera):Void
{
getScreenPosition(_point, camera).subtractPoint(offset);
if (isPixelPerfectRender(camera))
{
_point.x = _point.x / this.scale.x;
_point.y = _point.y / this.scale.y;
_point.round();
_point.x = _point.x * this.scale.x;
_point.y = _point.y * this.scale.y;
}
_point.copyToFlash(_flashPoint);
camera.copyPixels(_frame, framePixels, _flashRect, _flashPoint, colorTransform, blend, antialiasing);
}
override function drawComplex(camera:FlxCamera):Void
{
_frame.prepareMatrix(_matrix, FlxFrameAngle.ANGLE_0, checkFlipX(), checkFlipY());
_matrix.translate(-origin.x, -origin.y);
_matrix.scale(scale.x, scale.y);
if (bakedRotationAngle <= 0)
{
updateTrig();
if (angle != 0) _matrix.rotateWithTrig(_cosAngle, _sinAngle);
}
getScreenPosition(_point, camera).subtractPoint(offset);
_point.add(origin.x, origin.y);
_matrix.translate(_point.x, _point.y);
if (isPixelPerfectRender(camera))
{
_matrix.tx = Math.round(_matrix.tx / this.scale.x) * this.scale.x;
_matrix.ty = Math.round(_matrix.ty / this.scale.y) * this.scale.y;
}
camera.drawPixels(_frame, framePixels, _matrix, colorTransform, blend, antialiasing, shader);
}
public override function destroy():Void
{
frames = null;
@@ -4,8 +4,11 @@ import flixel.util.FlxSignal.FlxTypedSignal;
import flxanimate.FlxAnimate;
import flxanimate.FlxAnimate.Settings;
import flxanimate.frames.FlxAnimateFrames;
import flixel.graphics.frames.FlxFrame;
import flixel.system.FlxAssets.FlxGraphicAsset;
import openfl.display.BitmapData;
import openfl.utils.Assets;
import flixel.math.FlxPoint;
import flxanimate.animate.FlxKeyFrame;
/**
* A sprite which provides convenience functions for rendering a texture atlas with animations.
@@ -18,16 +21,21 @@ class FlxAtlasSprite extends FlxAnimate
FrameRate: 24.0,
Reversed: false,
// ?OnComplete:Void -> Void,
ShowPivot: #if debug false #else false #end,
ShowPivot: false,
Antialiasing: true,
ScrollFactor: null,
// Offset: new FlxPoint(0, 0), // This is just FlxSprite.offset
};
/**
* Signal dispatched when an animation finishes playing.
* Signal dispatched when an animation advances to the next frame.
*/
public var onAnimationFinish: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;
@@ -42,19 +50,28 @@ class FlxAtlasSprite extends FlxAnimate
throw 'Null path specified for FlxAtlasSprite!';
}
// Validate asset path.
if (!Assets.exists('${path}/Animation.json'))
{
throw 'FlxAtlasSprite does not have an Animation.json file at the specified path (${path})';
}
super(x, y, path, settings);
if (this.anim.curInstance == null)
if (this.anim.stageInstance == null)
{
throw 'FlxAtlasSprite not initialized properly. Are you sure the path (${path}) exists?';
}
onAnimationFinish.add(cleanupAnimation);
onAnimationComplete.add(cleanupAnimation);
// This defaults the sprite to play the first animation in the atlas,
// then pauses it. This ensures symbols are intialized properly.
this.anim.play('');
this.anim.pause();
this.anim.onComplete.add(_onAnimationComplete);
this.anim.onFrame.add(_onAnimationFrame);
}
/**
@@ -62,9 +79,13 @@ class FlxAtlasSprite extends FlxAnimate
*/
public function listAnimations():Array<String>
{
if (this.anim == null) return [];
return this.anim.getFrameLabels();
// return [""];
var mainSymbol = this.anim.symbolDictionary[this.anim.stageInstance.symbol.name];
if (mainSymbol == null)
{
FlxG.log.error('FlxAtlasSprite does not have its main symbol!');
return [];
}
return mainSymbol.getFrameLabels().map(keyFrame -> keyFrame.name).filterNull();
}
/**
@@ -73,7 +94,7 @@ class FlxAtlasSprite extends FlxAnimate
*/
public function hasAnimation(id:String):Bool
{
return getLabelIndex(id) != -1;
return getLabelIndex(id) != -1 || anim.symbolDictionary.exists(id);
}
/**
@@ -84,22 +105,13 @@ class FlxAtlasSprite extends FlxAnimate
return this.currentAnimation;
}
/**
* `anim.finished` always returns false on looping animations,
* but this function will return true if we are on the last frame of the looping animation.
*/
public function isLoopFinished():Bool
{
if (this.anim == null) return false;
if (!this.anim.isPlaying) return false;
var _completeAnim:Bool = false;
// Reverse animation finished.
if (this.anim.reversed && this.anim.curFrame == 0) return true;
// Forward animation finished.
if (!this.anim.reversed && this.anim.curFrame >= (this.anim.length - 1)) return true;
var fr:FlxKeyFrame = null;
return false;
}
var looping:Bool = false;
public var ignoreExclusionPref:Array<String> = [];
/**
* Plays an animation.
@@ -107,61 +119,86 @@ class FlxAtlasSprite extends FlxAnimate
* @param restart Whether to restart the animation if it is already playing.
* @param ignoreOther Whether to ignore all other animation inputs, until this one is done playing
* @param loop Whether to loop the animation
* @param startFrame The frame to start the animation on
* NOTE: `loop` and `ignoreOther` are not compatible with each other!
*/
public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false, ?loop:Bool = false):Void
public function playAnimation(id:String, restart:Bool = false, ignoreOther:Bool = false, loop:Bool = false, startFrame:Int = 0):Void
{
if (loop == null) loop = false;
// Skip if not allowed to play animations.
if ((!canPlayOtherAnims && !ignoreOther)) return;
if ((!canPlayOtherAnims))
{
if (this.currentAnimation == id && restart) {}
else if (ignoreExclusionPref != null && ignoreExclusionPref.length > 0)
{
var detected:Bool = false;
for (entry in ignoreExclusionPref)
{
if (StringTools.startsWith(id, entry))
{
detected = true;
break;
}
}
if (!detected) return;
}
else
return;
}
if (anim == null) return;
if (id == null || id == '') id = this.currentAnimation;
if (this.currentAnimation == id && !restart)
{
if (anim.isPlaying)
if (!anim.isPlaying)
{
// Skip if animation is already playing.
return;
}
else
{
// Resume animation if it's paused.
anim.play('', false, false);
}
}
if (fr != null) anim.curFrame = fr.index + startFrame;
else
anim.curFrame = startFrame;
// Skip if the animation doesn't exist
if (!hasAnimation(id))
// Resume animation if it's paused.
anim.resume();
}
return;
}
else if (!hasAnimation(id))
{
// Skip if the animation doesn't exist
trace('Animation ' + id + ' not found');
return;
}
anim.callback = function(_, frame:Int) {
var offset = loop ? 0 : -1;
this.currentAnimation = id;
anim.onComplete.removeAll();
anim.onComplete.add(function() {
_onAnimationComplete();
});
var frameLabel = anim.getFrameLabel(id);
if (frame == (frameLabel.duration + offset) + frameLabel.index)
{
if (loop)
{
playAnimation(id, true, false, true);
}
else
{
onAnimationFinish.dispatch(id);
}
}
};
looping = loop;
// Prevent other animations from playing if `ignoreOther` is true.
if (ignoreOther) canPlayOtherAnims = false;
// Move to the first frame of the animation.
goToFrameLabel(id);
this.currentAnimation = id;
// goToFrameLabel(id);
// trace('Playing animation $id');
if ((id == null || id == "") || this.anim.symbolDictionary.exists(id) || (this.anim.getByName(id) != null))
{
this.anim.play(id, restart, false, startFrame);
this.currentAnimation = anim.curSymbol.name;
fr = null;
}
// Only call goToFrameLabel if there is a frame label with that name. This prevents annoying warnings!
if (getFrameLabelNames().indexOf(id) != -1)
{
goToFrameLabel(id);
fr = anim.getFrameLabel(id);
anim.curFrame += startFrame;
}
}
override public function update(elapsed:Float)
@@ -169,6 +206,29 @@ class FlxAtlasSprite extends FlxAnimate
super.update(elapsed);
}
/**
* Returns true if the animation has finished playing.
* Never true if animation is configured to loop.
*/
public function isAnimationFinished():Bool
{
return this.anim.finished;
}
/**
* Returns true if the animation has reached the last frame.
* Can be true even if animation is configured to loop.
*/
public function isLoopComplete():Bool
{
if (this.anim == null) return false;
if (!this.anim.isPlaying) return false;
if (fr != null) return (anim.reversed && anim.curFrame < fr.index || !anim.reversed && anim.curFrame >= (fr.index + fr.duration));
return (anim.reversed && anim.curFrame == 0 || !(anim.reversed) && (anim.curFrame) >= (anim.length - 1));
}
/**
* Stops the current animation.
*/
@@ -192,6 +252,18 @@ class FlxAtlasSprite extends FlxAnimate
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
{
return listAnimations()[(getLabelIndex(label) + 1) % listAnimations().length];
@@ -213,4 +285,95 @@ class FlxAtlasSprite extends FlxAnimate
// this.currentAnimation = null;
this.anim.pause();
}
function _onAnimationFrame(frame:Int):Void
{
if (currentAnimation != null)
{
onAnimationFrame.dispatch(currentAnimation, frame);
if (isLoopComplete())
{
anim.pause();
_onAnimationComplete();
if (looping)
{
anim.curFrame = (fr != null) ? fr.index : 0;
anim.resume();
}
else if (fr != null && anim.curFrame != anim.length - 1)
{
anim.curFrame--;
}
}
}
}
function _onAnimationComplete():Void
{
if (currentAnimation != null)
{
onAnimationComplete.dispatch(currentAnimation);
}
else
{
onAnimationComplete.dispatch('');
}
}
var prevFrames:Map<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();
}
}
@@ -0,0 +1,58 @@
package funkin.graphics.shaders;
import flixel.addons.display.FlxRuntimeShader;
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;
}
public override function toString():String
{
return 'AdjustColorShader(${this.hue}, ${this.saturation}, ${this.brightness}, ${this.contrast})';
}
}
+72 -19
View File
@@ -1,43 +1,96 @@
package funkin.graphics.shaders;
import flixel.system.FlxAssets.FlxShader;
import flixel.util.FlxColor;
class AngleMask extends FlxShader
{
public var extraColor(default, set):FlxColor = 0xFFFFFFFF;
function set_extraColor(value:FlxColor):FlxColor
{
extraTint.value = [value.redFloat, value.greenFloat, value.blueFloat];
this.extraColor = value;
return this.extraColor;
}
@:glFragmentSource('
#pragma header
uniform vec2 endPosition;
void main()
{
vec4 base = texture2D(bitmap, openfl_TextureCoordv);
#pragma header
vec2 uv = openfl_TextureCoordv.xy;
uniform vec3 extraTint;
uniform vec2 endPosition;
vec2 hash22(vec2 p) {
vec3 p3 = fract(vec3(p.xyx) * vec3(.1031, .1030, .0973));
p3 += dot(p3, p3.yzx + 33.33);
return fract((p3.xx + p3.yz) * p3.zy);
}
vec2 start = vec2(0.0, 0.0);
vec2 end = vec2(endPosition.x / openfl_TextureSize.x, 1.0);
// ====== 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));
}
float dx = end.x - start.x;
float dy = end.y - start.y;
vec4 mainPass(vec2 fragCoord) {
vec4 base = texture2D(bitmap, fragCoord);
float angle = atan(dy, dx);
vec2 uv = fragCoord.xy;
uv.x -= start.x;
uv.y -= start.y;
vec2 start = vec2(0.0, 0.0);
vec2 end = vec2(endPosition.x / openfl_TextureSize.x, 1.0);
float uvA = atan(uv.y, uv.x);
float dx = end.x - start.x;
float dy = end.y - start.y;
if (uvA < angle)
gl_FragColor = base;
else
gl_FragColor = vec4(0.0);
float angle = atan(dy, dx);
}')
uv.x -= start.x;
uv.y -= start.y;
float uvA = atan(uv.y, uv.x);
if (uvA < angle)
return base;
else
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()
{
super();
endPosition.value = [90, 100]; // 100 AS DEFAULT WORKS NICELY FOR FREEPLAY?
extraTint.value = [1, 1, 1];
}
}
@@ -1,8 +1,6 @@
package funkin.graphics.shaders;
import flixel.addons.display.FlxRuntimeShader;
import funkin.Paths;
import openfl.utils.Assets;
import openfl.display.BitmapData;
import openfl.display.ShaderInput;
@@ -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;
}
}
@@ -1,8 +1,6 @@
package funkin.graphics.shaders;
import flixel.addons.display.FlxRuntimeShader;
import funkin.Paths;
import openfl.utils.Assets;
/**
* Note... not actually gaussian!
@@ -1,8 +1,6 @@
package funkin.graphics.shaders;
import flixel.addons.display.FlxRuntimeShader;
import funkin.Paths;
import openfl.utils.Assets;
class Grayscale extends FlxRuntimeShader
{
@@ -1,8 +1,6 @@
package funkin.graphics.shaders;
import flixel.addons.display.FlxRuntimeShader;
import funkin.Paths;
import openfl.utils.Assets;
class HSVShader extends FlxRuntimeShader
{
@@ -0,0 +1,21 @@
package funkin.graphics.shaders;
import flixel.addons.display.FlxRuntimeShader;
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]);
}
}
@@ -2,7 +2,6 @@ package funkin.graphics.shaders;
import openfl.display.BitmapData;
import openfl.display.BlendMode;
import openfl.utils.Assets;
class RuntimeCustomBlendShader extends RuntimePostEffectShader
{
@@ -2,6 +2,7 @@ package funkin.graphics.shaders;
import flixel.FlxCamera;
import flixel.FlxG;
import flixel.graphics.frames.FlxFrame;
import flixel.addons.display.FlxRuntimeShader;
import lime.graphics.opengl.GLProgram;
import lime.utils.Log;
@@ -32,6 +33,9 @@ class RuntimePostEffectShader extends FlxRuntimeShader
// equals (camera.viewLeft, camera.viewTop, camera.viewRight, camera.viewBottom)
uniform vec4 uCameraBounds;
// equals (frame.left, frame.top, frame.right, frame.bottom)
uniform vec4 uFrameBounds;
// screen coord -> world coord conversion
// returns world coord in px
vec2 screenToWorld(vec2 screenCoord) {
@@ -56,6 +60,25 @@ class RuntimePostEffectShader extends FlxRuntimeShader
return (worldCoord - offset) / scale;
}
// screen coord -> frame coord conversion
// returns normalized frame coord
vec2 screenToFrame(vec2 screenCoord) {
float left = uFrameBounds.x;
float top = uFrameBounds.y;
float right = uFrameBounds.z;
float bottom = uFrameBounds.w;
float width = right - left;
float height = bottom - top;
float clampedX = clamp(screenCoord.x, left, right);
float clampedY = clamp(screenCoord.y, top, bottom);
return vec2(
(clampedX - left) / (width),
(clampedY - top) / (height)
);
}
// internally used to get the maximum `openfl_TextureCoordv`
vec2 bitmapCoordScale() {
return openfl_TextureCoordv / screenCoord;
@@ -80,6 +103,8 @@ class RuntimePostEffectShader extends FlxRuntimeShader
{
super(fragmentSource, null, glVersion);
uScreenResolution.value = [FlxG.width, FlxG.height];
uCameraBounds.value = [0, 0, FlxG.width, FlxG.height];
uFrameBounds.value = [0, 0, FlxG.width, FlxG.height];
}
// basically `updateViewInfo(FlxG.width, FlxG.height, FlxG.camera)` is good
@@ -89,6 +114,12 @@ class RuntimePostEffectShader extends FlxRuntimeShader
uCameraBounds.value = [camera.viewLeft, camera.viewTop, camera.viewRight, camera.viewBottom];
}
public function updateFrameInfo(frame:FlxFrame)
{
// NOTE: uv.width is actually the right pos and uv.height is the bottom pos
uFrameBounds.value = [frame.uv.x, frame.uv.y, frame.uv.width, frame.uv.height];
}
override function __createGLProgram(vertexSource:String, fragmentSource:String):GLProgram
{
try
@@ -4,7 +4,7 @@ import flixel.system.FlxAssets.FlxShader;
import openfl.display.BitmapData;
import openfl.display.ShaderParameter;
import openfl.display.ShaderParameterType;
import openfl.utils.Assets;
import flixel.util.FlxColor;
typedef Light =
{
@@ -32,6 +32,14 @@ class RuntimeRainShader extends RuntimePostEffectShader
return time = value;
}
public var spriteMode(default, set):Bool = false;
function set_spriteMode(value:Bool):Bool
{
this.setBool('uSpriteMode', value);
return spriteMode = value;
}
// The scale of the rain depends on the world coordinate system, so higher resolution makes
// the raindrops smaller. This parameter can be used to adjust the total scale of the scene.
// The size of the raindrops is proportional to the value of this parameter.
@@ -86,6 +94,14 @@ class RuntimeRainShader extends RuntimePostEffectShader
return mask = value;
}
public var rainColor(default, set):FlxColor;
function set_rainColor(color:FlxColor):FlxColor
{
this.setFloatArray("uRainColor", [color.red / 255, color.green / 255, color.blue / 255]);
return rainColor = color;
}
public var lightMap(default, set):BitmapData;
function set_lightMap(value:BitmapData):BitmapData
@@ -105,6 +121,7 @@ class RuntimeRainShader extends RuntimePostEffectShader
public function new()
{
super(Assets.getText(Paths.frag('rain')));
this.rainColor = 0xFF6680cc;
}
public function update(elapsed:Float):Void
@@ -40,8 +40,8 @@ class StrokeShader extends FlxShader
void main()
{
vec4 sample = flixel_texture2D(bitmap, openfl_TextureCoordv);
if (sample.a == 0.) {
vec4 gay = flixel_texture2D(bitmap, openfl_TextureCoordv);
if (gay.a == 0.) {
float w = size.x / openfl_TextureSize.x;
float h = size.y / openfl_TextureSize.y;
@@ -49,9 +49,9 @@ class StrokeShader extends FlxShader
|| flixel_texture2D(bitmap, vec2(openfl_TextureCoordv.x - w, openfl_TextureCoordv.y)).a != 0.
|| flixel_texture2D(bitmap, vec2(openfl_TextureCoordv.x, openfl_TextureCoordv.y + h)).a != 0.
|| flixel_texture2D(bitmap, vec2(openfl_TextureCoordv.x, openfl_TextureCoordv.y - h)).a != 0.)
sample = color;
gay = color;
}
gl_FragColor = sample;
gl_FragColor = gay;
}
')
public function new(color:FlxColor = 0xFFFFFFFF, width:Float = 1, height:Float = 1)
@@ -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;
}
}
@@ -0,0 +1,34 @@
package funkin.graphics.video;
#if hxCodec
import hxcodec.flixel.FlxVideoSprite;
/**
* Not to be confused with FlxVideo, this is a hxcodec based video class
* We override it simply to correct/control our volume easier.
*/
class FunkinVideoSprite extends FlxVideoSprite
{
public var volume(default, set):Float = 1;
public function new(x:Float = 0, y:Float = 0)
{
super(x, y);
set_volume(1);
}
override public function update(elapsed:Float):Void
{
super.update(elapsed);
set_volume(volume);
}
function set_volume(value:Float):Float
{
volume = value;
bitmap.volume = Std.int((FlxG.sound.muted ? 0 : 1) * (FlxG.sound.logToLinear(FlxG.sound.volume) * 100) * volume);
return volume;
}
}
#end

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